init commit

This commit is contained in:
2025-12-18 05:55:32 +09:00
commit a6817e487e
72 changed files with 13847 additions and 0 deletions

19
app/handlers/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
from .commands import start, help_command
from .callbacks import (
start_callback, manage_messages, manage_groups,
list_messages, list_groups
)
from .sender import send_message
from .group_manager import my_chat_member
__all__ = [
'start',
'help_command',
'start_callback',
'manage_messages',
'manage_groups',
'list_messages',
'list_groups',
'send_message',
'my_chat_member',
]

146
app/handlers/callbacks.py Normal file
View File

@@ -0,0 +1,146 @@
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository, MessageRepository, MessageGroupRepository
from app.utils.keyboards import (
get_main_keyboard, get_back_keyboard, get_message_actions_keyboard,
get_group_actions_keyboard, CallbackType
)
import logging
logger = logging.getLogger(__name__)
# Состояния для ConversationHandler
WAITING_MESSAGE_TEXT = 1
WAITING_MESSAGE_TITLE = 2
WAITING_GROUP_SELECTION = 3
WAITING_FOR_GROUP = 4
async def start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Главное меню"""
query = update.callback_query
await query.answer()
text = """🤖 <b>Автопостер - Главное меню</b>
Выберите, что вы хотите делать:"""
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=get_main_keyboard()
)
async def manage_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Меню управления сообщениями"""
query = update.callback_query
await query.answer()
text = """📨 <b>Управление сообщениями</b>
Выберите действие:"""
keyboard = [
[InlineKeyboardButton(" Новое сообщение", callback_data=CallbackType.CREATE_MESSAGE)],
[InlineKeyboardButton("📜 Список сообщений", callback_data=CallbackType.LIST_MESSAGES)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def manage_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Меню управления группами"""
query = update.callback_query
await query.answer()
text = """👥 <b>Управление группами</b>
Выберите действие:"""
keyboard = [
[InlineKeyboardButton("📜 Список групп", callback_data=CallbackType.LIST_GROUPS)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def list_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Список всех сообщений"""
query = update.callback_query
await query.answer()
async with AsyncSessionLocal() as session:
repo = MessageRepository(session)
messages = await repo.get_all_messages()
if not messages:
text = "📭 Нет сообщений"
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES)]]
else:
text = "📨 <b>Ваши сообщения:</b>\n\n"
keyboard = []
for msg in messages:
status = "" if msg.is_active else ""
text += f"{status} <b>{msg.title}</b> (ID: {msg.id})\n"
keyboard.append([
InlineKeyboardButton(f"📤 {msg.title}", callback_data=f"send_msg_{msg.id}"),
InlineKeyboardButton("🗑️", callback_data=f"delete_msg_{msg.id}")
])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES)])
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def list_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Список всех групп"""
query = update.callback_query
await query.answer()
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
groups = await repo.get_all_active_groups()
if not groups:
text = "👥 Нет групп в базе данных\n\nДобавьте бота в группы - они автоматически появятся здесь."
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS)]]
else:
text = "👥 <b>Группы в базе данных:</b>\n\n"
keyboard = []
for group in groups:
status = "" if group.is_active else ""
delay = f"⏱️ {group.slow_mode_delay}s" if group.slow_mode_delay > 0 else "🚀 нет"
text += f"{status} <b>{group.title}</b>\n"
text += f" ID: {group.chat_id}\n"
text += f" {delay}\n\n"
keyboard.append([
InlineKeyboardButton(f"📝 {group.title}", callback_data=f"group_messages_{group.id}"),
InlineKeyboardButton("🗑️", callback_data=f"delete_group_{group.id}")
])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS)])
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)

58
app/handlers/commands.py Normal file
View File

@@ -0,0 +1,58 @@
from telegram import Update
from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository, MessageRepository, MessageGroupRepository
from app.utils.keyboards import get_main_keyboard, get_groups_keyboard, get_messages_keyboard
import logging
logger = logging.getLogger(__name__)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /start"""
user = update.effective_user
text = f"""👋 Привет, {user.first_name}!
Я бот для автоматической рассылки сообщений в группы.
Что я умею:
• 📨 Создавать и управлять сообщениями
• 👥 Добавлять группы и управлять ими
• 📤 Отправлять сообщения со скоростью группы (slow mode)
• 📊 Отслеживать статус отправки
Выберите действие:"""
await update.message.reply_text(
text,
reply_markup=get_main_keyboard()
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /help"""
text = """📖 Справка по использованию:
<b>Основные команды:</b>
/start - Главное меню
/help - Эта справка
<b>Как работать с сообщениями:</b>
1. Перейдите в раздел "Сообщения"
2. Создайте новое сообщение
3. Введите текст сообщения
4. Выберите группы для отправки
<b>Как работать с группами:</b>
1. Бот автоматически обнаружит группы при добавлении
2. Для каждой группы можно настроить slow mode
3. Вы сможете отправлять разные сообщения в разные группы
<b>Slow mode:</b>
Это ограничение на скорость отправки сообщений в группу.
Бот автоматически учитывает это при отправке.
Нажмите /start для возврата в главное меню."""
await update.message.reply_text(text, parse_mode='HTML')

View File

@@ -0,0 +1,64 @@
from telegram import Update, ChatMember
from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository
import logging
logger = logging.getLogger(__name__)
async def my_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработчик изменения статуса бота в группах
Срабатывает когда бот добавлен или удален из группы
"""
my_chat_member_update = update.my_chat_member
if my_chat_member_update.new_chat_member.status == "member":
# Бот был добавлен в группу
chat = my_chat_member_update.chat
logger.info(f"Бот добавлен в группу: {chat.title} (ID: {chat.id})")
# Получаем информацию о slow mode
try:
chat_full = await context.bot.get_chat(chat.id)
slow_mode_delay = chat_full.slow_mode_delay or 0
async with AsyncSessionLocal() as session:
group_repo = GroupRepository(session)
existing = await group_repo.get_group_by_chat_id(str(chat.id))
if not existing:
# Добавляем новую группу
group = await group_repo.add_group(
chat_id=str(chat.id),
title=chat.title,
slow_mode_delay=slow_mode_delay
)
logger.info(f"Группа добавлена в БД: {group}")
# Уведомляем администратора (если это приватный чат)
# Этого функционала нет, т.к. нет ID администратора
else:
# Обновляем slow mode если он изменился
if existing.slow_mode_delay != slow_mode_delay:
await group_repo.update_group_slow_mode(
existing.id,
slow_mode_delay
)
logger.info(f"Slow mode обновлен для {existing.title}")
except Exception as e:
logger.error(f"Ошибка при обработке добавления в группу: {e}")
elif my_chat_member_update.new_chat_member.status == "left":
# Бот был удален из группы
chat = my_chat_member_update.chat
logger.info(f"Бот удален из группы: {chat.title} (ID: {chat.id})")
async with AsyncSessionLocal() as session:
group_repo = GroupRepository(session)
group = await group_repo.get_group_by_chat_id(str(chat.id))
if group:
await group_repo.deactivate_group(group.id)
logger.info(f"Группа деактивирована: {group}")

View File

@@ -0,0 +1,313 @@
import logging
import json
import re
from typing import List, Dict, Optional
from datetime import datetime
from telegram.ext import ContextTypes
from app.handlers.telethon_client import telethon_manager
from app.database.member_repository import GroupKeywordRepository, GroupStatisticsRepository
from app.database.repository import GroupRepository, MessageGroupRepository
logger = logging.getLogger(__name__)
class GroupParser:
"""Парсер для поиска и анализа групп по ключевым словам"""
def __init__(self, db_session, bot=None):
self.db_session = db_session
self.bot = bot
self.keyword_repo = GroupKeywordRepository(db_session)
self.stats_repo = GroupStatisticsRepository(db_session)
self.group_repo = GroupRepository(db_session)
async def parse_group_by_keywords(self, keywords: List[str], chat_id: int) -> Dict:
"""
Проанализировать группу и проверить совпадение с ключевыми словами
Args:
keywords: Список ключевых слов для поиска
chat_id: ID группы в Telegram
Returns:
dict: Результаты анализа группы
"""
if not telethon_manager.is_connected():
logger.warning("Telethon клиент не подключен, не могу получить информацию о группе")
return {'matched': False, 'keywords_found': []}
try:
chat_info = await telethon_manager.get_chat_info(chat_id)
if not chat_info:
return {'matched': False, 'keywords_found': []}
# Объединить название и описание для поиска
search_text = f"{chat_info.get('title', '')} {chat_info.get('description', '')}".lower()
# Найти совпадения ключевых слов
matched_keywords = []
for keyword in keywords:
if keyword.lower() in search_text:
matched_keywords.append(keyword)
result = {
'matched': len(matched_keywords) > 0,
'keywords_found': matched_keywords,
'chat_info': chat_info,
'match_count': len(matched_keywords),
'total_keywords': len(keywords),
'match_percentage': (len(matched_keywords) / len(keywords) * 100) if keywords else 0
}
logger.info(f"✅ Анализ группы {chat_id}: найдено {len(matched_keywords)} совпадений из {len(keywords)}")
return result
except Exception as e:
logger.error(f"❌ Ошибка при анализе группы {chat_id}: {e}")
return {'matched': False, 'keywords_found': []}
async def extract_keywords_from_text(self, text: str) -> List[str]:
"""
Извлечь ключевые слова из текста
Args:
text: Текст для извлечения ключевых слов
Returns:
List[str]: Список ключевых слов
"""
# Удалить спецсимволы и разбить на слова
words = re.findall(r'\b\w+\b', text.lower())
# Отфильтровать стоп-слова
stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
'я', 'ты', 'он', 'она', 'оно', 'мы', 'вы', 'они',
'и', 'или', 'но', 'в', 'на', 'к', 'по', 'с', 'о', 'об',
'что', 'как', 'где', 'когда', 'зачем', 'откуда', 'куда'}
keywords = [w for w in words if len(w) > 3 and w not in stop_words]
# Убрать дубликаты
return list(set(keywords))
async def parse_group_members(self, chat_id: int, member_repo,
limit: int = 100) -> Dict:
"""
Получить и сохранить список участников группы
Args:
chat_id: ID группы
member_repo: Репозиторий участников
limit: Максимум участников для загрузки
Returns:
dict: Статистика загруженных участников
"""
if not telethon_manager.is_connected():
logger.warning("Telethon клиент не подключен, не могу получить участников")
return {'success': False, 'members_added': 0}
try:
# Получить группу из БД
db_group = await self.group_repo.get_by_chat_id(str(chat_id))
if not db_group:
logger.warning(f"Группа {chat_id} не найдена в БД")
return {'success': False, 'members_added': 0}
# Получить участников
members = await telethon_manager.get_chat_members(chat_id, limit)
if not members:
return {'success': True, 'members_added': 0}
# Сохранить в БД
members_data = members # Уже в нужном формате из telethon_manager
# Очистить старых участников и добавить новых
await member_repo.clear_members(db_group.id)
added = await member_repo.bulk_add_members(db_group.id, members_data)
# Обновить статистику
admins = len([m for m in members_data if m.get('is_admin')])
bots = len([m for m in members_data if m.get('is_bot')])
await self.stats_repo.update_members_count(
db_group.id,
total=len(members_data),
admins=admins,
bots=bots
)
result = {
'success': True,
'members_added': len(added),
'admins_count': admins,
'bots_count': bots,
'users_count': len(members_data) - bots
}
logger.info(f"✅ Загружены участники группы {chat_id}: {result}")
return result
except Exception as e:
logger.error(f"❌ Ошибка при загрузке участников группы {chat_id}: {e}")
return {'success': False, 'members_added': 0}
async def search_groups_by_keywords(self, keywords: List[str],
group_ids: List[int] = None) -> Dict:
"""
Искать группы по ключевым словам из списка
Args:
keywords: Список ключевых слов для поиска
group_ids: Список ID групп для проверки (если None - проверить все)
Returns:
dict: Результаты поиска
"""
if not group_ids:
# Получить все активные группы
all_groups = await self.group_repo.get_active_groups()
group_ids = [g.id for g in all_groups]
results = {
'total_checked': len(group_ids),
'matched_groups': [],
'no_match': [],
'errors': []
}
for group_id in group_ids:
try:
# Получить группу
db_group = await self.group_repo.get_by_id(group_id)
if not db_group:
results['errors'].append({'group_id': group_id, 'error': 'Not found in DB'})
continue
# Анализировать
match_result = await self.parse_group_by_keywords(keywords, int(db_group.chat_id))
if match_result['matched']:
results['matched_groups'].append({
'group_id': group_id,
'chat_id': db_group.chat_id,
'title': db_group.title,
'keywords_found': match_result['keywords_found'],
'match_percentage': match_result['match_percentage']
})
else:
results['no_match'].append({
'group_id': group_id,
'chat_id': db_group.chat_id,
'title': db_group.title
})
except Exception as e:
logger.error(f"Ошибка при проверке группы {group_id}: {e}")
results['errors'].append({'group_id': group_id, 'error': str(e)})
logger.info(f"Поиск по ключевым словам завершен: найдено {len(results['matched_groups'])} групп")
return results
async def set_group_keywords(self, group_id: int, keywords: List[str],
description: str = None) -> bool:
"""
Установить ключевые слова для группы
Args:
group_id: ID группы в БД
keywords: Список ключевых слов
description: Описание для поиска
Returns:
bool: Успешность операции
"""
try:
# Сериализовать список в JSON
keywords_json = json.dumps(keywords)
# Проверить наличие записи
existing = await self.keyword_repo.get_keywords(group_id)
if existing:
await self.keyword_repo.update_keywords(group_id, keywords_json, description)
else:
await self.keyword_repo.add_keywords(group_id, keywords_json, description)
logger.info(f"Ключевые слова установлены для группы {group_id}: {keywords}")
return True
except Exception as e:
logger.error(f"Ошибка при установке ключевых слов: {e}")
return False
async def get_group_keywords(self, group_id: int) -> Optional[List[str]]:
"""
Получить ключевые слова для группы
Args:
group_id: ID группы в БД
Returns:
List[str]: Список ключевых слов или None
"""
try:
keyword_obj = await self.keyword_repo.get_keywords(group_id)
if not keyword_obj:
return None
return json.loads(keyword_obj.keywords)
except Exception as e:
logger.error(f"Ошибка при получении ключевых слов: {e}")
return None
async def format_group_info(self, group_id: int) -> str:
"""
Форматировать информацию о группе для вывода
Args:
group_id: ID группы в БД
Returns:
str: Отформатированная информация
"""
try:
group = await self.group_repo.get_by_id(group_id)
if not group:
return "Группа не найдена"
stats = await self.stats_repo.get_statistics(group_id)
keywords = await self.get_group_keywords(group_id)
info = f"<b>Группа:</b> {group.title}\n"
info += f"<b>Chat ID:</b> <code>{group.chat_id}</code>\n"
info += f"<b>Активна:</b> {'✅ Да' if group.is_active else '❌ Нет'}\n"
if stats:
info += f"\n<b>Статистика:</b>\n"
info += f" Участников: {stats.total_members}\n"
info += f" Администраторов: {stats.total_admins}\n"
info += f" Ботов: {stats.total_bots}\n"
info += f" Отправлено: {stats.messages_sent}\n"
info += f" Через клиент: {stats.messages_via_client}\n"
info += f" Может отправлять как бот: {'' if stats.can_send_as_bot else ''}\n"
info += f" Может отправлять как клиент: {'' if stats.can_send_as_client else ''}\n"
if keywords:
info += f"\n<b>Ключевые слова:</b>\n"
for kw in keywords:
info += f"{kw}\n"
return info
except Exception as e:
logger.error(f"Ошибка при форматировании информации: {e}")
return "Ошибка при получении информации"

View File

@@ -0,0 +1,248 @@
import logging
import asyncio
from typing import Optional, Tuple
from telegram.error import TelegramError, BadRequest, Forbidden
from telethon.errors import FloodWaitError, UserDeactivatedError, ChatAdminRequiredError
from app.handlers.telethon_client import telethon_manager
from app.handlers.sender import MessageSender
from app.database.member_repository import GroupStatisticsRepository
from app.settings import Config
logger = logging.getLogger(__name__)
class HybridMessageSender:
"""
Гибридный отправитель сообщений.
Пытается отправить как бот, при ошибке переключается на Pyrogram клиента.
"""
def __init__(self, bot, db_session):
self.bot = bot
self.db_session = db_session
self.message_sender = MessageSender(bot, db_session)
self.stats_repo = GroupStatisticsRepository(db_session)
async def send_message(self, chat_id: str, message_text: str,
group_id: int = None,
parse_mode: str = "HTML",
disable_web_page_preview: bool = True) -> Tuple[bool, Optional[str]]:
"""
Отправить сообщение с гибридной логикой.
Сначала пытается отправить как бот, если ошибка - переходит на клиент.
Returns:
Tuple[bool, Optional[str]]: (успешность, метод_отправки)
Методы: 'bot', 'client', None если оба способа не работают
"""
# Попытка 1: отправить как бот
try:
logger.info(f"Попытка отправить сообщение как бот в {chat_id}")
await self.message_sender.send_message(
chat_id=chat_id,
message_text=message_text,
parse_mode=parse_mode,
disable_web_page_preview=disable_web_page_preview
)
if group_id:
await self.stats_repo.update_send_capabilities(group_id, can_bot=True, can_client=False)
logger.info(f"Сообщение успешно отправлено ботом в {chat_id}")
return True, "bot"
except (BadRequest, Forbidden) as e:
# Ошибки которые означают что бот не может писать
logger.warning(f"Бот не может отправить сообщение в {chat_id}: {e}")
if group_id:
await self.stats_repo.update_send_capabilities(group_id, can_bot=False, can_client=False)
# Если Telethon отключен или не инициализирован - выходим
if not Config.USE_TELETHON or not telethon_manager.is_connected():
logger.error(f"Telethon недоступен, не удалось отправить сообщение в {chat_id}")
return False, None
# Попытка 2: отправить как клиент
return await self._send_via_telethon(chat_id, message_text, group_id)
except TelegramError as e:
logger.error(f"Ошибка Telegram при отправке в {chat_id}: {e}")
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
return False, None
except Exception as e:
logger.error(f"Неожиданная ошибка при отправке в {chat_id}: {e}")
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
return False, None
async def _send_via_telethon(self, chat_id: str, message_text: str,
group_id: int = None) -> Tuple[bool, Optional[str]]:
"""Отправить сообщение через Telethon клиент"""
if not telethon_manager.is_connected():
logger.error("Telethon клиент не инициализирован")
return False, None
try:
# Конвертировать chat_id в int для Telethon
try:
numeric_chat_id = int(chat_id)
except ValueError:
# Если это строка типа "-100123456789"
numeric_chat_id = int(chat_id)
logger.info(f"Попытка отправить сообщение через Telethon в {numeric_chat_id}")
message_id = await telethon_manager.send_message(
chat_id=numeric_chat_id,
text=message_text,
parse_mode="html",
disable_web_page_preview=True
)
if message_id:
if group_id:
await self.stats_repo.increment_sent_messages(group_id, via_client=True)
await self.stats_repo.update_send_capabilities(group_id, can_bot=False, can_client=True)
logger.info(f"✅ Сообщение успешно отправлено через Telethon в {numeric_chat_id}")
return True, "client"
else:
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
return False, None
except FloodWaitError as e:
logger.warning(f"⏳ FloodWait от Telethon: нужно ждать {e.seconds} сек")
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
# Ожидание и повторная попытка
await asyncio.sleep(min(e.seconds, Config.TELETHON_FLOOD_WAIT_MAX))
return await self._send_via_telethon(chat_id, message_text, group_id)
except (ChatAdminRequiredError, UserDeactivatedError):
logger.error(f"❌ Telethon клиент не администратор в {chat_id}")
if group_id:
await self.stats_repo.update_send_capabilities(group_id, can_bot=False, can_client=False)
return False, None
except Exception as e:
logger.error(f"❌ Ошибка Telethon при отправке в {chat_id}: {e}")
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
return False, None
async def send_message_with_retry(self, chat_id: str, message_text: str,
group_id: int = None,
max_retries: int = None) -> Tuple[bool, Optional[str]]:
"""
Отправить сообщение с повторными попытками
Args:
chat_id: ID чата
message_text: Текст сообщения
group_id: ID группы в БД (для отслеживания статистики)
max_retries: Максимум повторов (по умолчанию из Config)
Returns:
Tuple[bool, Optional[str]]: (успешность, метод_отправки)
"""
if max_retries is None:
max_retries = Config.MAX_RETRIES
for attempt in range(max_retries):
try:
success, method = await self.send_message(
chat_id=chat_id,
message_text=message_text,
group_id=group_id,
parse_mode="HTML"
)
if success:
return True, method
# Ждать перед повторной попыткой
if attempt < max_retries - 1:
wait_time = Config.RETRY_DELAY * (attempt + 1)
logger.info(f"Повтор попытки {attempt + 1}/{max_retries} через {wait_time}с")
await asyncio.sleep(wait_time)
except Exception as e:
logger.error(f"Ошибка при попытке {attempt + 1}: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(Config.RETRY_DELAY)
logger.error(f"Не удалось отправить сообщение в {chat_id} после {max_retries} попыток")
if group_id:
await self.stats_repo.increment_failed_messages(group_id)
return False, None
async def bulk_send(self, chat_ids: list, message_text: str,
group_ids: list = None,
use_slow_mode: bool = False) -> dict:
"""
Массовая отправка сообщений
Returns:
dict: {
'total': количество чатов,
'success': успешно отправлено,
'failed': ошибок,
'via_bot': через бот,
'via_client': через клиент
}
"""
results = {
'total': len(chat_ids),
'success': 0,
'failed': 0,
'via_bot': 0,
'via_client': 0
}
for idx, chat_id in enumerate(chat_ids):
group_id = group_ids[idx] if group_ids else None
success, method = await self.send_message_with_retry(
chat_id=str(chat_id),
message_text=message_text,
group_id=group_id
)
if success:
results['success'] += 1
if method == 'bot':
results['via_bot'] += 1
elif method == 'client':
results['via_client'] += 1
else:
results['failed'] += 1
# Slow mode
if use_slow_mode and idx < len(chat_ids) - 1:
await asyncio.sleep(Config.MIN_SEND_INTERVAL)
logger.info(f"Массовая отправка завершена: {results}")
return results

View File

@@ -0,0 +1,198 @@
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from app.database import AsyncSessionLocal
from app.database.repository import (
GroupRepository, MessageRepository, MessageGroupRepository
)
from app.utils.keyboards import (
get_back_keyboard, get_main_keyboard, CallbackType
)
import logging
logger = logging.getLogger(__name__)
# Состояния для ConversationHandler
CREATE_MSG_TITLE = 1
CREATE_MSG_TEXT = 2
SELECT_GROUPS = 3
WAITING_GROUP_INPUT = 4
async def create_message_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Начало создания нового сообщения"""
query = update.callback_query
await query.answer()
text = "📝 Введите название сообщения (короткое описание):"
await query.edit_message_text(text)
return CREATE_MSG_TITLE
async def create_message_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Получаем название и просим текст"""
message = update.message
title = message.text.strip()
if len(title) > 100:
await message.reply_text("❌ Название слишком длинное (макс 100 символов)")
return CREATE_MSG_TITLE
context.user_data['message_title'] = title
text = """✏️ Теперь введите текст сообщения.
Вы можете использовать HTML форматирование:
<b>жирный</b>
<i>курсив</i>
<u>подчеркивание</u>
<code>код</code>
Введите /cancel для отмены"""
await message.reply_text(text, parse_mode='HTML')
return CREATE_MSG_TEXT
async def create_message_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Получаем текст и показываем выбор групп"""
message = update.message
if message.text == '/cancel':
await message.reply_text("❌ Отменено", reply_markup=get_main_keyboard())
return ConversationHandler.END
text = message.text.strip()
if len(text) > 4096:
await message.reply_text("❌ Текст слишком длинный (макс 4096 символов)")
return CREATE_MSG_TEXT
context.user_data['message_text'] = text
# Сохраняем сообщение в БД
async with AsyncSessionLocal() as session:
msg_repo = MessageRepository(session)
msg = await msg_repo.add_message(
text=text,
title=context.user_data['message_title']
)
context.user_data['message_id'] = msg.id
# Теперь показываем список групп для выбора
async with AsyncSessionLocal() as session:
group_repo = GroupRepository(session)
groups = await group_repo.get_all_active_groups()
if not groups:
await message.reply_text(
"❌ Нет активных групп. Сначала добавьте бота в группы.",
reply_markup=get_main_keyboard()
)
return ConversationHandler.END
# Создаем клавиатуру с группами
keyboard = []
for group in groups:
callback = f"select_group_{group.id}"
keyboard.append([InlineKeyboardButton(
f"{group.title} (delay: {group.slow_mode_delay}s)",
callback_data=callback
)])
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=f"{CallbackType.MAIN_MENU}")])
text = f"""✅ Сообщение создано: <b>{context.user_data['message_title']}</b>
Выберите группы для отправки (нажмите на каждую):"""
await message.reply_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
context.user_data['selected_groups'] = []
return SELECT_GROUPS
async def select_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Выбор групп для отправки"""
query = update.callback_query
callback_data = query.data
if callback_data == "done_groups":
# Подтверждаем выбор
selected = context.user_data.get('selected_groups', [])
if not selected:
await query.answer("❌ Выберите хотя бы одну группу", show_alert=True)
return SELECT_GROUPS
# Добавляем сообщение в выбранные группы
message_id = context.user_data['message_id']
async with AsyncSessionLocal() as session:
mg_repo = MessageGroupRepository(session)
for group_id in selected:
await mg_repo.add_message_to_group(message_id, group_id)
text = f"""✅ <b>Сообщение готово!</b>
Название: {context.user_data['message_title']}
Групп выбрано: {len(selected)}
Теперь вы можете отправить сообщение нажав кнопку "Отправить" в списке сообщений."""
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_main_keyboard())
return ConversationHandler.END
elif callback_data.startswith("select_group_"):
group_id = int(callback_data.split("_")[2])
selected = context.user_data.get('selected_groups', [])
if group_id in selected:
selected.remove(group_id)
else:
selected.append(group_id)
context.user_data['selected_groups'] = selected
# Обновляем клавиатуру
async with AsyncSessionLocal() as session:
group_repo = GroupRepository(session)
groups = await group_repo.get_all_active_groups()
keyboard = []
for group in groups:
callback = f"select_group_{group.id}"
is_selected = group.id in selected
prefix = "" if is_selected else ""
keyboard.append([InlineKeyboardButton(
f"{prefix} {group.title} (delay: {group.slow_mode_delay}s)",
callback_data=callback
)])
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=f"{CallbackType.MAIN_MENU}")])
await query.edit_message_text(
f"Выбрано групп: {len(selected)}",
reply_markup=InlineKeyboardMarkup(keyboard)
)
await query.answer()
return SELECT_GROUPS
elif callback_data == CallbackType.MAIN_MENU:
# Отмена
await query.answer()
await query.edit_message_text(
"❌ Создание сообщения отменено",
reply_markup=get_main_keyboard()
)
return ConversationHandler.END
await query.answer()
return SELECT_GROUPS

View File

@@ -0,0 +1,227 @@
import logging
from typing import List, Optional, Dict
from pyrogram import Client
from pyrogram.types import Message, ChatMember
from pyrogram.errors import (
FloodWait, UserDeactivated, ChatAdminRequired,
PeerIdInvalid, ChannelInvalid, UserNotParticipant
)
from app.settings import Config
logger = logging.getLogger(__name__)
class PyrogramClientManager:
"""Менеджер для работы с Pyrogram клиентом"""
def __init__(self):
self.client: Optional[Client] = None
self.is_initialized = False
async def initialize(self) -> bool:
"""Инициализировать Pyrogram клиент"""
try:
if not Config.USE_PYROGRAM:
logger.warning("Pyrogram отключен в конфигурации")
return False
if not (Config.PYROGRAM_API_ID and Config.PYROGRAM_API_HASH):
logger.error("PYROGRAM_API_ID или PYROGRAM_API_HASH не установлены")
return False
self.client = Client(
name="tg_autoposter",
api_id=Config.PYROGRAM_API_ID,
api_hash=Config.PYROGRAM_API_HASH,
phone_number=Config.PYROGRAM_PHONE
)
await self.client.start()
self.is_initialized = True
me = await self.client.get_me()
logger.info(f"Pyrogram клиент инициализирован: {me.first_name}")
return True
except Exception as e:
logger.error(f"Ошибка при инициализации Pyrogram: {e}")
return False
async def shutdown(self):
"""Остановить Pyrogram клиент"""
if self.client and self.is_initialized:
try:
await self.client.stop()
self.is_initialized = False
logger.info("Pyrogram клиент остановлен")
except Exception as e:
logger.error(f"Ошибка при остановке Pyrogram: {e}")
async def send_message(self, chat_id: int, text: str,
parse_mode: str = "html",
disable_web_page_preview: bool = True) -> Optional[Message]:
"""Отправить сообщение в чат"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return None
try:
message = await self.client.send_message(
chat_id=chat_id,
text=text,
parse_mode=parse_mode,
disable_web_page_preview=disable_web_page_preview
)
logger.info(f"Сообщение отправлено в чат {chat_id} (клиент)")
return message
except FloodWait as e:
logger.warning(f"FloodWait: нужно ждать {e.value} секунд")
raise
except (ChatAdminRequired, UserNotParticipant):
logger.error(f"Клиент не администратор или не участник чата {chat_id}")
return None
except PeerIdInvalid:
logger.error(f"Неверный ID чата: {chat_id}")
return None
except Exception as e:
logger.error(f"Ошибка при отправке сообщения: {e}")
return None
async def get_chat_members(self, chat_id: int, limit: int = None) -> List[ChatMember]:
"""Получить список участников чата"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return []
try:
members = []
async for member in self.client.get_chat_members(chat_id):
members.append(member)
if limit and len(members) >= limit:
break
logger.info(f"Получено {len(members)} участников из чата {chat_id}")
return members
except (ChatAdminRequired, UserNotParticipant):
logger.error(f"Нет прав получить участников чата {chat_id}")
return []
except Exception as e:
logger.error(f"Ошибка при получении участников: {e}")
return []
async def get_chat_info(self, chat_id: int) -> Optional[Dict]:
"""Получить информацию о чате"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return None
try:
chat = await self.client.get_chat(chat_id)
return {
'id': chat.id,
'title': chat.title,
'description': getattr(chat, 'description', None),
'members_count': getattr(chat, 'members_count', None),
'is_supergroup': chat.is_supergroup,
'linked_chat': getattr(chat, 'linked_chat_id', None)
}
except Exception as e:
logger.error(f"Ошибка при получении информации о чате {chat_id}: {e}")
return None
async def join_chat(self, chat_link: str) -> Optional[int]:
"""Присоединиться к чату по ссылке"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return None
try:
chat = await self.client.join_chat(chat_link)
logger.info(f"Присоединился к чату: {chat.id}")
return chat.id
except Exception as e:
logger.error(f"Ошибка при присоединении к чату: {e}")
return None
async def leave_chat(self, chat_id: int) -> bool:
"""Покинуть чат"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return False
try:
await self.client.leave_chat(chat_id)
logger.info(f"Покинул чат: {chat_id}")
return True
except Exception as e:
logger.error(f"Ошибка при выходе из чата: {e}")
return False
async def edit_message(self, chat_id: int, message_id: int, text: str) -> Optional[Message]:
"""Отредактировать сообщение"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return None
try:
message = await self.client.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=text,
parse_mode="html"
)
logger.info(f"Сообщение отредактировано: {chat_id}/{message_id}")
return message
except Exception as e:
logger.error(f"Ошибка при редактировании сообщения: {e}")
return None
async def delete_message(self, chat_id: int, message_id: int) -> bool:
"""Удалить сообщение"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return False
try:
await self.client.delete_messages(chat_id, message_id)
logger.info(f"Сообщение удалено: {chat_id}/{message_id}")
return True
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
return False
async def search_messages(self, chat_id: int, query: str, limit: int = 100) -> List[Message]:
"""Искать сообщения в чате"""
if not self.is_initialized:
logger.error("Pyrogram клиент не инициализирован")
return []
try:
messages = []
async for message in self.client.search_messages(chat_id, query=query, limit=limit):
messages.append(message)
logger.info(f"Найдено {len(messages)} сообщений по запросу '{query}'")
return messages
except Exception as e:
logger.error(f"Ошибка при поиске сообщений: {e}")
return []
def is_connected(self) -> bool:
"""Проверить, подключен ли клиент"""
return self.is_initialized and self.client is not None
# Глобальный экземпляр менеджера
pyrogram_manager = PyrogramClientManager()

139
app/handlers/schedule.py Normal file
View File

@@ -0,0 +1,139 @@
"""
Обработчик команд для управления расписанием рассылок
"""
import logging
from telegram import Update
from telegram.ext import ContextTypes
from app.scheduler import broadcast_scheduler, schedule_broadcast, cancel_broadcast, list_broadcasts
from app.database.repository import MessageRepository
from app.database import AsyncSessionLocal
logger = logging.getLogger(__name__)
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда для управления расписанием"""
if not update.message:
return
user_id = update.message.from_user.id
# Только администратор может управлять расписанием
# (это нужно добавить в конфигурацию)
try:
# /schedule list - показать все расписания
if context.args and context.args[0] == 'list':
schedules = await list_broadcasts()
if not schedules:
await update.message.reply_text("📋 Нет активных расписаний")
return
text = "📅 Активные расписания:\n\n"
for idx, sched in enumerate(schedules, 1):
text += f"{idx}. {sched['name']}\n"
text += f" ID: `{sched['id']}`\n"
text += f" Расписание: {sched['trigger']}\n"
text += f" Следующее выполнение: {sched['next_run_time']}\n\n"
await update.message.reply_text(text, parse_mode='Markdown')
# /schedule add message_id group_id cron_expr
elif context.args and context.args[0] == 'add':
if len(context.args) < 4:
await update.message.reply_text(
"❌ Использование: /schedule add <message_id> <group_id> <cron_expr>\n\n"
"Пример: /schedule add 1 10 '0 9 * * *'\n\n"
"Cron формат: minute hour day month day_of_week\n"
"0 9 * * * - ежедневно в 9:00 UTC"
)
return
try:
message_id = int(context.args[1])
group_id = int(context.args[2])
cron_expr = ' '.join(context.args[3:])
# Проверить, что сообщение существует
async with AsyncSessionLocal() as session:
message_repo = MessageRepository(session)
message = await message_repo.get_by_id(message_id)
if not message:
await update.message.reply_text(f"❌ Сообщение с ID {message_id} не найдено")
return
job_id = await schedule_broadcast(
message_id=message_id,
group_ids=[group_id],
cron_expr=cron_expr
)
await update.message.reply_text(
f"✅ Расписание создано!\n\n"
f"ID: `{job_id}`\n"
f"Сообщение: {message_id}\n"
f"Группа: {group_id}\n"
f"Расписание: {cron_expr}"
)
except ValueError as e:
await update.message.reply_text(f"❌ Ошибка: {e}")
except Exception as e:
logger.error(f"Ошибка при создании расписания: {e}")
await update.message.reply_text(f"❌ Ошибка: {e}")
# /schedule remove job_id
elif context.args and context.args[0] == 'remove':
if len(context.args) < 2:
await update.message.reply_text(
"❌ Использование: /schedule remove <job_id>"
)
return
job_id = context.args[1]
success = await cancel_broadcast(job_id)
if success:
await update.message.reply_text(f"✅ Расписание удалено: {job_id}")
else:
await update.message.reply_text(f"❌ Расписание не найдено: {job_id}")
else:
await update.message.reply_text(
"📅 Управление расписанием\n\n"
"Команды:\n"
"/schedule list - показать все расписания\n"
"/schedule add <msg_id> <group_id> <cron> - добавить расписание\n"
"/schedule remove <job_id> - удалить расписание\n\n"
"Примеры cron:\n"
"0 9 * * * - ежедневно в 9:00 UTC\n"
"0 9 * * MON - по понедельникам в 9:00\n"
"*/30 * * * * - каждые 30 минут"
)
except Exception as e:
logger.error(f"Ошибка в команде schedule: {e}")
await update.message.reply_text(f"❌ Ошибка: {e}")
async def initialize_scheduler(context: ContextTypes.DEFAULT_TYPE):
"""Инициализировать планировщик при запуске бота"""
try:
await broadcast_scheduler.initialize()
broadcast_scheduler.start()
await broadcast_scheduler.add_maintenance_schedules()
logger.info("✅ Планировщик инициализирован и запущен")
except Exception as e:
logger.error(f"❌ Ошибка при инициализации планировщика: {e}")
async def shutdown_scheduler(context: ContextTypes.DEFAULT_TYPE):
"""Завершить планировщик при остановке бота"""
try:
await broadcast_scheduler.shutdown()
logger.info("✅ Планировщик завершен")
except Exception as e:
logger.error(f"❌ Ошибка при завершении планировщика: {e}")

124
app/handlers/sender.py Normal file
View File

@@ -0,0 +1,124 @@
from telegram import Update
from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository, MessageRepository, MessageGroupRepository
from app.utils.keyboards import get_back_keyboard, CallbackType
from app.utils import can_send_message
from datetime import datetime, timedelta
import logging
import asyncio
logger = logging.getLogger(__name__)
async def send_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Отправить сообщение в группы с учетом slow mode"""
query = update.callback_query
# Парсим callback: send_msg_<message_id>
callback_data = query.data
if callback_data.startswith("send_msg_"):
message_id = int(callback_data.split("_")[2])
else:
await query.answer("❌ Ошибка обработки", show_alert=True)
return
async with AsyncSessionLocal() as session:
msg_repo = MessageRepository(session)
group_repo = GroupRepository(session)
mg_repo = MessageGroupRepository(session)
message = await msg_repo.get_message(message_id)
if not message:
await query.answer("❌ Сообщение не найдено", show_alert=True)
return
# Получить группы, куда нужно отправить
message_groups = await mg_repo.get_message_groups_to_send(message_id)
if not message_groups:
await query.answer("✅ Сообщение уже отправлено во все группы", show_alert=True)
return
await query.answer()
await query.edit_message_text(
f"📤 Начинаю отправку '{message.title}' в {len(message_groups)} групп(ы)...\n\n"
"⏳ Это может занять некоторое время в зависимости от slow mode."
)
# Отправляем в каждую группу
sent_count = 0
failed_count = 0
total_wait = 0
for mg in message_groups:
try:
# Проверяем slow mode
can_send, wait_time = await can_send_message(mg.group)
if not can_send:
# Ждем
await query.edit_message_text(
f"📤 Отправка '{message.title}'...\n\n"
f"✅ Отправлено: {sent_count}\n"
f"❌ Ошибок: {failed_count}\n"
f"⏳ Ожидание {wait_time}s перед отправкой в {mg.group.title}..."
)
await asyncio.sleep(wait_time)
total_wait += wait_time
# Отправляем сообщение
await context.bot.send_message(
chat_id=mg.group.chat_id,
text=message.text,
parse_mode=message.parse_mode
)
# Отмечаем как отправленное
async with AsyncSessionLocal() as session:
mg_repo = MessageGroupRepository(session)
await mg_repo.mark_as_sent(mg.id)
group_repo = GroupRepository(session)
await group_repo.update_last_message_time(mg.group.id)
sent_count += 1
except Exception as e:
logger.error(f"Ошибка при отправке в группу {mg.group.chat_id}: {e}")
async with AsyncSessionLocal() as session:
mg_repo = MessageGroupRepository(session)
await mg_repo.mark_as_sent(mg.id, error=str(e))
failed_count += 1
# Обновляем сообщение каждые 5 отправок
if sent_count % 5 == 0:
await query.edit_message_text(
f"📤 Отправка '{message.title}'...\n\n"
f"✅ Отправлено: {sent_count}\n"
f"❌ Ошибок: {failed_count}"
)
# Финальное сообщение
final_text = f"✅ <b>Отправка завершена</b>\n\n"
final_text += f"✅ Успешно: {sent_count}\n"
final_text += f"❌ Ошибок: {failed_count}\n"
if total_wait > 0:
final_text += f"Всего ожидалось: {total_wait}s"
await query.edit_message_text(
final_text,
parse_mode='HTML',
reply_markup=get_back_keyboard()
)
async def discover_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обнаружить все группы, в которых есть бот
Этот метод вызывается при запуске или по команде
"""
# Получить список всех чатов, в которых есть бот
# NOTE: python-telegram-bot не имеет встроенного способа получить все чаты
# Это нужно реализовать через webhook или polling с сохранением информации о новых группах
logger.info("Функция обнаружения групп - необходимо добавить обработчик my_chat_member")

View File

@@ -0,0 +1,281 @@
import logging
import os
from typing import List, Optional, Dict
from telethon import TelegramClient, events
from telethon.tl.types import ChatMember, User
from telethon.errors import (
FloodWaitError, UserDeactivatedError, ChatAdminRequiredError,
PeerIdInvalidError, ChannelInvalidError, UserNotParticipantError
)
from app.settings import Config
logger = logging.getLogger(__name__)
class TelethonClientManager:
"""Менеджер для работы с Telethon клиентом"""
def __init__(self):
self.client: Optional[TelegramClient] = None
self.is_initialized = False
async def initialize(self) -> bool:
"""Инициализировать Telethon клиент"""
try:
if not Config.USE_TELETHON:
logger.warning("Telethon отключен в конфигурации")
return False
if not (Config.TELETHON_API_ID and Config.TELETHON_API_HASH):
logger.error("TELETHON_API_ID или TELETHON_API_HASH не установлены")
return False
# Получить путь для сессии
session_dir = os.path.join(os.path.dirname(__file__), '..', 'sessions')
os.makedirs(session_dir, exist_ok=True)
session_path = os.path.join(session_dir, 'telethon_session')
self.client = TelegramClient(
session_path,
api_id=Config.TELETHON_API_ID,
api_hash=Config.TELETHON_API_HASH
)
await self.client.connect()
# Проверить авторизацию
if not await self.client.is_user_authorized():
logger.error("Telethon клиент не авторизован. Требуется повторный вход")
return False
self.is_initialized = True
me = await self.client.get_me()
logger.info(f"✅ Telethon клиент инициализирован: {me.first_name}")
return True
except Exception as e:
logger.error(f"Ошибка при инициализации Telethon: {e}")
return False
async def shutdown(self):
"""Остановить Telethon клиент"""
if self.client and self.is_initialized:
try:
await self.client.disconnect()
self.is_initialized = False
logger.info("✅ Telethon клиент остановлен")
except Exception as e:
logger.error(f"Ошибка при остановке Telethon: {e}")
async def send_message(self, chat_id: int, text: str,
parse_mode: str = "html",
disable_web_page_preview: bool = True) -> Optional[int]:
"""
Отправить сообщение в чат
Returns:
Optional[int]: ID отправленного сообщения или None при ошибке
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return None
try:
message = await self.client.send_message(
chat_id,
text,
parse_mode=parse_mode,
link_preview=not disable_web_page_preview
)
logger.info(f"✅ Сообщение отправлено в чат {chat_id} (Telethon)")
return message.id
except FloodWaitError as e:
logger.warning(f"⏳ FloodWait: нужно ждать {e.seconds} секунд")
raise
except (ChatAdminRequiredError, UserNotParticipantError):
logger.error(f"❌ Клиент не администратор или не участник чата {chat_id}")
return None
except PeerIdInvalidError:
logger.error(f"❌ Неверный ID чата: {chat_id}")
return None
except Exception as e:
logger.error(f"❌ Ошибка при отправке сообщения: {e}")
return None
async def get_chat_members(self, chat_id: int, limit: int = None) -> List[Dict]:
"""
Получить список участников чата
Returns:
List[Dict]: Список участников с информацией
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return []
try:
members = []
async for member in self.client.iter_participants(chat_id, limit=limit):
member_info = {
'user_id': str(member.id),
'username': member.username,
'first_name': member.first_name,
'last_name': member.last_name,
'is_bot': member.bot,
'is_admin': member.is_self, # self-check для упрощения
}
members.append(member_info)
logger.info(f"✅ Получено {len(members)} участников из чата {chat_id}")
return members
except (ChatAdminRequiredError, UserNotParticipantError):
logger.error(f"❌ Нет прав получить участников чата {chat_id}")
return []
except Exception as e:
logger.error(f"❌ Ошибка при получении участников: {e}")
return []
async def get_chat_info(self, chat_id: int) -> Optional[Dict]:
"""Получить информацию о чате"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return None
try:
chat = await self.client.get_entity(chat_id)
members_count = None
if hasattr(chat, 'participants_count'):
members_count = chat.participants_count
return {
'id': chat.id,
'title': chat.title if hasattr(chat, 'title') else str(chat.id),
'description': chat.about if hasattr(chat, 'about') else None,
'members_count': members_count,
'is_supergroup': hasattr(chat, 'megagroup') and chat.megagroup,
'is_channel': hasattr(chat, 'broadcast'),
'is_group': hasattr(chat, 'gigagroup')
}
except Exception as e:
logger.error(f"❌ Ошибка при получении информации о чате {chat_id}: {e}")
return None
async def join_chat(self, chat_link: str) -> Optional[int]:
"""
Присоединиться к чату по ссылке
Returns:
Optional[int]: ID чата или None при ошибке
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return None
try:
# Попытаться присоединиться
result = await self.client(ImportChatInviteRequest(hash))
chat_id = result.chats[0].id
logger.info(f"✅ Присоединился к чату: {chat_id}")
return chat_id
except Exception as e:
logger.error(f"❌ Ошибка при присоединении к чату: {e}")
return None
async def leave_chat(self, chat_id: int) -> bool:
"""Покинуть чат"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return False
try:
await self.client.delete_dialog(chat_id, revoke=True)
logger.info(f"✅ Покинул чат: {chat_id}")
return True
except Exception as e:
logger.error(f"❌ Ошибка при выходе из чата: {e}")
return False
async def edit_message(self, chat_id: int, message_id: int, text: str) -> Optional[int]:
"""
Отредактировать сообщение
Returns:
Optional[int]: ID отредактированного сообщения или None при ошибке
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return None
try:
message = await self.client.edit_message(
chat_id,
message_id,
text,
parse_mode="html"
)
logger.info(f"✅ Сообщение отредактировано: {chat_id}/{message_id}")
return message.id
except Exception as e:
logger.error(f"❌ Ошибка при редактировании сообщения: {e}")
return None
async def delete_message(self, chat_id: int, message_id: int) -> bool:
"""Удалить сообщение"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return False
try:
await self.client.delete_messages(chat_id, message_id)
logger.info(f"✅ Сообщение удалено: {chat_id}/{message_id}")
return True
except Exception as e:
logger.error(f"❌ Ошибка при удалении сообщения: {e}")
return False
async def search_messages(self, chat_id: int, query: str, limit: int = 100) -> List[Dict]:
"""
Искать сообщения в чате
Returns:
List[Dict]: Список найденных сообщений
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return []
try:
messages = []
async for message in self.client.iter_messages(chat_id, search=query, limit=limit):
messages.append({
'id': message.id,
'text': message.text,
'date': message.date
})
logger.info(f"✅ Найдено {len(messages)} сообщений по запросу '{query}'")
return messages
except Exception as e:
logger.error(f"❌ Ошибка при поиске сообщений: {e}")
return []
def is_connected(self) -> bool:
"""Проверить, подключен ли клиент"""
return self.is_initialized and self.client is not None
# Глобальный экземпляр менеджера
telethon_manager = TelethonClientManager()