Initial commit: Batch Bot - Telegram Comment Bot
0.0.1 Features: - Multi-account support via session files - AI comments generation via Ollama (local LLM) - Telegram bot for moderation (approve/reject/regenerate) - Docker support (controller + worker) - Auto-join public groups - Comment regeneration on group re-add - Statistics tracking Tech stack: - Python 3.11 - Telethon 1.34 (Telegram user client) - Aiogram 3.4 (Telegram bot framework) - SQLite (Database) - Docker & Docker Compose - Ollama (Local LLM)
This commit is contained in:
262
bot/session_manager.py
Normal file
262
bot/session_manager.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from telethon import TelegramClient
|
||||
from telethon.sessions import StringSession
|
||||
from bot.config import API_ID, API_HASH, SESSIONS_DIR
|
||||
from bot.db import save_session
|
||||
|
||||
logger = logging.getLogger('session_manager')
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""Менеджер сессий Telegram"""
|
||||
|
||||
def __init__(self):
|
||||
self.sessions_dir = SESSIONS_DIR
|
||||
self.sessions_dir.mkdir(exist_ok=True)
|
||||
self.clients: dict[str, TelegramClient] = {}
|
||||
self.session_info: dict[str, dict] = {}
|
||||
|
||||
def get_session_files(self) -> list[str]:
|
||||
"""Получение списка файлов сессий"""
|
||||
session_files = list(self.sessions_dir.glob("*.session"))
|
||||
return [f.name for f in session_files]
|
||||
|
||||
def load_session_string(self, session_file: str) -> str | None:
|
||||
"""Загрузка строки сессии из файла"""
|
||||
try:
|
||||
session_path = self.sessions_dir / session_file
|
||||
if not session_path.exists():
|
||||
logger.error(f"Файл сессии не найден: {session_file}")
|
||||
return None
|
||||
|
||||
session_string = session_path.read_text(encoding='utf-8').strip()
|
||||
if not session_string:
|
||||
logger.error(f"Файл сессии пуст: {session_file}")
|
||||
return None
|
||||
|
||||
return session_string
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке сессии {session_file}: {e}")
|
||||
return None
|
||||
|
||||
async def create_client(self, session_file: str) -> TelegramClient | None:
|
||||
"""Создание клиента Telegram для сессии"""
|
||||
try:
|
||||
session_string = self.load_session_string(session_file)
|
||||
if not session_string:
|
||||
return None
|
||||
|
||||
# Проверяем, не подключён ли уже клиент
|
||||
if session_file in self.clients:
|
||||
if self.clients[session_file].is_connected():
|
||||
logger.info(f"Клиент для {session_file} уже подключён")
|
||||
return self.clients[session_file]
|
||||
|
||||
# Создаём новую сессию из строки
|
||||
session = StringSession(session_string)
|
||||
|
||||
client = TelegramClient(
|
||||
session,
|
||||
API_ID,
|
||||
API_HASH,
|
||||
device_model="comment_bot",
|
||||
system_version="Linux",
|
||||
app_version="1.0",
|
||||
lang_code="ru"
|
||||
)
|
||||
|
||||
await client.connect()
|
||||
|
||||
if not await client.is_user_authorized():
|
||||
logger.warning(f"Сессия {session_file} не авторизована")
|
||||
await client.disconnect()
|
||||
return None
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
me = await client.get_me()
|
||||
user_info = {
|
||||
'user_id': me.id,
|
||||
'username': me.username or '',
|
||||
'first_name': me.first_name or '',
|
||||
'last_name': me.last_name or '',
|
||||
'phone': me.phone or ''
|
||||
}
|
||||
|
||||
# Сохраняем информацию в БД
|
||||
save_session(session_file, user_info)
|
||||
|
||||
# Сохраняем клиента и информацию
|
||||
self.clients[session_file] = client
|
||||
self.session_info[session_file] = user_info
|
||||
|
||||
logger.info(f"Клиент подключён: {session_file} (@{user_info['username'] or 'no_username'})")
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании клиента для {session_file}: {e}")
|
||||
return None
|
||||
|
||||
async def create_all_clients(self) -> dict[str, TelegramClient]:
|
||||
"""Создание клиентов для всех сессий"""
|
||||
session_files = self.get_session_files()
|
||||
logger.info(f"Найдено сессий: {len(session_files)}")
|
||||
|
||||
tasks = [self.create_client(sf) for sf in session_files]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Фильтруем успешные результаты
|
||||
for session_file, result in zip(session_files, results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Ошибка при подключении {session_file}: {result}")
|
||||
|
||||
return {sf: client for sf, client in zip(session_files, results)
|
||||
if isinstance(client, TelegramClient)}
|
||||
|
||||
async def get_client(self, session_file: str) -> TelegramClient | None:
|
||||
"""Получение клиента для сессии"""
|
||||
if session_file in self.clients and self.clients[session_file].is_connected():
|
||||
return self.clients[session_file]
|
||||
|
||||
return await self.create_client(session_file)
|
||||
|
||||
async def disconnect_client(self, session_file: str):
|
||||
"""Отключение клиента"""
|
||||
if session_file in self.clients:
|
||||
try:
|
||||
await self.clients[session_file].disconnect()
|
||||
logger.info(f"Клиент {session_file} отключён")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отключении клиента {session_file}: {e}")
|
||||
finally:
|
||||
del self.clients[session_file]
|
||||
if session_file in self.session_info:
|
||||
del self.session_info[session_file]
|
||||
|
||||
async def disconnect_all(self):
|
||||
"""Отключение всех клиентов"""
|
||||
tasks = [self.disconnect_client(sf) for sf in list(self.clients.keys())]
|
||||
await asyncio.gather(*tasks)
|
||||
logger.info("Все клиенты отключены")
|
||||
|
||||
def get_session_info(self, session_file: str) -> dict | None:
|
||||
"""Получение информации о сессии"""
|
||||
return self.session_info.get(session_file)
|
||||
|
||||
def get_all_info(self) -> list[dict]:
|
||||
"""Получение информации о всех сессиях"""
|
||||
return [
|
||||
{
|
||||
'session_file': sf,
|
||||
**self.session_info.get(sf, {})
|
||||
}
|
||||
for sf in self.get_session_files()
|
||||
]
|
||||
|
||||
async def join_channel(self, session_file: str, channel_id: int | str) -> tuple[bool, str]:
|
||||
"""Вступление сессии в канал/группу (по ID или username)"""
|
||||
try:
|
||||
client = await self.get_client(session_file)
|
||||
if not client:
|
||||
return False, "Не удалось подключить сессию"
|
||||
|
||||
# Пытаемся получить сущность по ID или username
|
||||
entity = await client.get_entity(channel_id)
|
||||
|
||||
# Проверяем, участник ли уже
|
||||
from telethon.tl.functions.channels import GetParticipantRequest
|
||||
|
||||
try:
|
||||
await client(GetParticipantRequest(entity, await client.get_me()))
|
||||
return True, "Уже участник"
|
||||
except:
|
||||
# Не участник, вступаем
|
||||
pass
|
||||
|
||||
# Вступаем в канал/группу
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
|
||||
if hasattr(entity, 'megagroup') or hasattr(entity, 'broadcast'):
|
||||
# Канал или супергруппа
|
||||
await client(JoinChannelRequest(entity))
|
||||
else:
|
||||
# Обычная группа
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
await client(AddChatUserRequest(entity.id, 0, fwd_limit=0))
|
||||
|
||||
return True, "Вступил успешно"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "PRIVATE" in error_msg.upper() or "CHANNEL_PRIVATE" in error_msg:
|
||||
return False, "Частный канал/группа"
|
||||
elif "INVITE" in error_msg.upper():
|
||||
return False, "Нужно приглашение"
|
||||
elif "Could not find" in error_msg:
|
||||
return False, "Группа не найдена"
|
||||
else:
|
||||
return False, f"Ошибка: {error_msg[:50]}"
|
||||
|
||||
async def join_all_sessions(self, channel_id: int | str) -> dict[str, str]:
|
||||
"""Все сессии вступают в канал/группу (по ID или username)"""
|
||||
results = {}
|
||||
for session_file in self.get_session_files():
|
||||
success, message = await self.join_channel(session_file, channel_id)
|
||||
results[session_file] = f"{'✅' if success else '❌'} {message}"
|
||||
return results
|
||||
|
||||
async def leave_channel(self, session_file: str, channel_id: int | str) -> tuple[bool, str]:
|
||||
"""Выход сессии из канала/группы"""
|
||||
try:
|
||||
client = await self.get_client(session_file)
|
||||
if not client:
|
||||
return False, "Не удалось подключить сессию"
|
||||
|
||||
# Пробуем разные форматы ID
|
||||
entity = None
|
||||
for try_id in [channel_id, str(channel_id), str(channel_id).lstrip('-')]:
|
||||
try:
|
||||
entity = await client.get_entity(try_id)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not entity:
|
||||
# Если не нашли, пробуем получить из диалогов
|
||||
async for dialog in client.iter_dialogs():
|
||||
if str(dialog.id) == str(channel_id) or str(dialog.id).lstrip('-') == str(channel_id).lstrip('-'):
|
||||
entity = dialog.entity
|
||||
break
|
||||
|
||||
if not entity:
|
||||
return False, "Группа не найдена в диалогах"
|
||||
|
||||
# Выходим из канала/группы
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
await client(LeaveChannelRequest(entity))
|
||||
|
||||
return True, "Вышел успешно"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "PRIVATE" in error_msg.upper():
|
||||
return False, "Частный канал/группа"
|
||||
elif "BOT" in error_msg.upper() or "ADMIN" in error_msg.upper():
|
||||
return False, "Нельзя выйти (бот/админ)"
|
||||
elif "Cannot find" in error_msg or "not found" in error_msg.lower():
|
||||
return True, "Уже не в группе" # Считаем успешным - уже вышел
|
||||
else:
|
||||
return False, f"Ошибка: {error_msg[:50]}"
|
||||
|
||||
async def leave_all_sessions(self, channel_id: int | str) -> dict[str, str]:
|
||||
"""Все сессии выходят из канала/группы"""
|
||||
results = {}
|
||||
for session_file in self.get_session_files():
|
||||
success, message = await self.leave_channel(session_file, channel_id)
|
||||
results[session_file] = f"{'✅' if success else '❌'} {message}"
|
||||
return results
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
session_manager = SessionManager()
|
||||
Reference in New Issue
Block a user