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()