Final release: Multi-session comment bot with filtering

Features:
- Multi-account support (session files)
- AI comments via Ollama
- Telegram bot moderation
- Filter by sessions and groups
- Docker support
- Auto-join groups
- Log notifications
- DB migration script

Bug fixes:
- Fixed comment_to for proper post targeting
- Fixed entity lookup with multiple ID formats
- Fixed callback handlers for filtering
- Added auto-join before entity lookup
This commit is contained in:
2026-02-28 01:44:40 +03:00
parent a18ad30961
commit bb27161524
8 changed files with 537 additions and 99 deletions

View File

@@ -26,9 +26,10 @@ logger = logging.getLogger('worker')
class CommentWorker:
"""Воркер для отправки комментариев от имени пользователей"""
def __init__(self):
self.clients: dict[str, TelegramClient] = {}
self.log_client: TelegramClient = None # Клиент для отправки логов
self.running = False
async def start(self):
@@ -46,14 +47,37 @@ class CommentWorker:
logger.warning("Нет активных сессий. Добавьте файлы в sessions/")
return
# Создаём клиента для отправки логов (используем первую сессию)
if self.clients:
first_session = list(self.clients.keys())[0]
self.log_client = self.clients[first_session]
logger.info(f"Лог-клиент: {first_session}")
self.running = True
# Отправляем уведомление о запуске
await self.send_log_message("🚀 Worker запущен")
# Запускаем задачи параллельно
await asyncio.gather(
self.listen_for_new_posts(),
self.monitor_approved_comments()
)
async def send_log_message(self, message: str):
"""Отправка сообщения в лог-группу"""
if not self.log_client or not LOG_GROUP_ID:
return
try:
await self.log_client.send_message(
int(LOG_GROUP_ID),
message,
parse_mode='HTML'
)
except Exception as e:
logger.debug(f"Не удалось отправить лог: {e}")
async def stop(self):
"""Остановка воркера"""
logger.info("Остановка Comment Worker...")
@@ -164,45 +188,59 @@ class CommentWorker:
while self.running:
current_time = datetime.now().timestamp()
# Проверяем новые группы каждые 10 секунд
if current_time - last_groups_check > 10:
target_groups = get_target_groups()
new_group_ids = [str(g['group_id']) for g in target_groups]
# Проверяем, есть ли группы которые были удалены и добавлены заново
# Добавляем обработчики для новых групп
for group_id in new_group_ids:
if group_id not in group_handlers:
logger.info(f"Добавлена группа для мониторинга: {group_id}")
logger.info(f" Добавлена группа для мониторинга: {group_id}")
# Сканируем последние 10 сообщений новой группы
# Всегда сканируем при добавлении, даже если уже было
await self.scan_group(group_id, limit=10)
scanned_groups.add(group_id)
# Регистрируем обработчик для каждой сессии
for session_file, client in self.clients.items():
# Проверяем что клиент подключён
if not client.is_connected():
logger.warning(f"Клиент {session_file} не подключён")
continue
# Регистрируем обработчик
@client.on(events.NewMessage(chats=group_id))
async def handle_new_post(event):
message = event.message
logger.info(f"Новый пост в группе {group_id}: {message.id}")
logger.info(f"📬 Новый пост в группе {group_id}: {message.id}")
# Проверяем что это не бот
if message.from_id and message.from_id.user_id == (await client.get_me()).id:
return
# Отправляем уведомление в лог-группу
post_preview = message.text[:100] + "..." if len(message.text or "") > 100 else (message.text or "Без текста")
log_message = (
f"📬 <b>Новый пост</b>\n\n"
f"📢 Группа: <code>{group_id}</code>\n"
f"🔗 Пост: <a href='https://t.me/c/{str(group_id).lstrip('-')}/{message.id}'>{message.id}</a>\n"
f"📄 Текст: <i>{post_preview}</i>"
)
await self.send_log_message(log_message)
await self.process_message(message, session_file, client, group_id)
group_handlers[group_id] = True
else:
# Группа уже обрабатывается, но проверяем не была ли она удалена и добавлена снова
# Если была - очищаем scanned_groups для этой группы
if group_id in scanned_groups:
# Проверяем что группа всё ещё в БД
group_exists = any(str(g['group_id']) == group_id for g in target_groups)
if not group_exists:
scanned_groups.discard(group_id)
del group_handlers[group_id]
logger.info(f"Группа {group_id} удалена, сброс кэша")
logger.info(f"✅ Обработчик зарегистрирован для {group_id}")
# Удаляем обработчики для удалённых групп
for group_id in list(group_handlers.keys()):
if group_id not in new_group_ids:
logger.info(f"❌ Группа {group_id} удалена из мониторинга")
del group_handlers[group_id]
scanned_groups.discard(group_id)
logger.info(f"Группа {group_id} удалена из мониторинга")
last_groups_check = current_time
@@ -332,8 +370,27 @@ class CommentWorker:
logger.info(f"Отправка комментария {comment['id']}: message_id={message_id_in_channel}, comments_group={comments_group_id}, channel={channel_id}")
comments_group = await client.get_entity(PeerChannel(comments_group_id))
logger.info(f"Группа комментариев: {comments_group.title} (ID: {comments_group_id})")
# Вступаем в группу комментариев ПЕРЕД тем как получать сущность
try:
await session_manager.join_channel(session_file, comments_group_id)
logger.info(f"✅ Вступил в группу комментариев: {comments_group_id}")
except Exception as e:
logger.debug(f"Уже в группе: {e}")
# Теперь получаем сущность группы комментариев
comments_group = None
for try_id in [comments_group_id, abs(int(comments_group_id)), -abs(int(comments_group_id))]:
try:
comments_group = await client.get_entity(PeerChannel(abs(int(try_id))))
logger.info(f"Группа комментариев: {comments_group.title} (ID: {try_id})")
break
except Exception as e:
logger.debug(f"Не удалось получить сущность {try_id}: {e}")
continue
if not comments_group:
logger.error(f"Не удалось найти группу комментариев {comments_group_id}")
return
from telethon.tl.functions.channels import GetFullChannelRequest
channel_full = await client(GetFullChannelRequest(comments_group))
@@ -346,25 +403,24 @@ class CommentWorker:
logger.info(f"Linked chat ID: {linked_chat_id}")
# Получаем сообщение в КАНАЛЕ
target_message_in_channel = await client.get_messages(
PeerChannel(abs(int(linked_chat_id))),
ids=message_id_in_channel
)
target_message_in_channel = None
for try_id in [linked_chat_id, abs(int(linked_chat_id)), -abs(int(linked_chat_id))]:
try:
target_message_in_channel = await client.get_messages(
PeerChannel(abs(int(try_id))),
ids=message_id_in_channel
)
if target_message_in_channel:
logger.info(f"Найдено сообщение в канале: {target_message_in_channel.id}")
break
except Exception as e:
logger.debug(f"Не удалось получить сообщение {try_id}/{message_id_in_channel}: {e}")
continue
if not target_message_in_channel:
logger.error(f"Сообщение {message_id_in_channel} не найдено в канале {linked_chat_id}")
return
logger.info(f"Найдено сообщение в канале: {target_message_in_channel.id}")
# Вступаем в группу комментариев (если ещё не вступили)
# Это требуется для некоторых каналов
try:
await session_manager.join_channel(session_file, comments_group_id)
logger.info(f"Вступил в группу комментариев: {comments_group_id}")
except Exception as e:
logger.warning(f"Не удалось вступить в группу комментариев: {e}")
# ОТПРАВЛЯЕМ В КАНАЛ с comment_to= (как в старом проекте)
# Telegram автоматически направит комментарий в группу комментариев
delay = random.uniform(COMMENT_DELAY_MIN, COMMENT_DELAY_MAX)