Harden bot against runtime stalls
This commit is contained in:
@@ -15,7 +15,8 @@ class AccountParticipationService:
|
||||
async def add_account_to_lottery(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
account_number: str
|
||||
account_number: str,
|
||||
commit: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Добавить счет в розыгрыш.
|
||||
@@ -91,7 +92,10 @@ class AccountParticipationService:
|
||||
account_id=account_record.id if account_record else None
|
||||
)
|
||||
session.add(participation)
|
||||
await session.commit()
|
||||
if commit:
|
||||
await session.commit()
|
||||
else:
|
||||
await session.flush()
|
||||
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
return {
|
||||
@@ -120,7 +124,7 @@ class AccountParticipationService:
|
||||
|
||||
for account in account_numbers:
|
||||
result = await AccountParticipationService.add_account_to_lottery(
|
||||
session, lottery_id, account
|
||||
session, lottery_id, account, commit=False
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
@@ -132,6 +136,8 @@ class AccountParticipationService:
|
||||
results["skipped_accounts"].append(account)
|
||||
results["errors"].append(result["message"])
|
||||
results["details"].append(f"❌ {result['message']}")
|
||||
|
||||
await session.commit()
|
||||
|
||||
return results
|
||||
|
||||
@@ -139,7 +145,8 @@ class AccountParticipationService:
|
||||
async def remove_account_from_lottery(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
account_number: str
|
||||
account_number: str,
|
||||
commit: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Удалить счет из розыгрыша"""
|
||||
formatted_account = format_account_number(account_number)
|
||||
@@ -164,7 +171,10 @@ class AccountParticipationService:
|
||||
}
|
||||
|
||||
await session.delete(participation)
|
||||
await session.commit()
|
||||
if commit:
|
||||
await session.commit()
|
||||
else:
|
||||
await session.flush()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -216,13 +226,21 @@ class AccountParticipationService:
|
||||
pattern: Паттерн поиска (например "11-22" или "33")
|
||||
limit: Максимальное количество результатов
|
||||
"""
|
||||
# Получаем все счета розыгрыша
|
||||
all_accounts = await AccountParticipationService.get_lottery_accounts(
|
||||
session, lottery_id
|
||||
digits_pattern = ''.join(c for c in pattern if c.isdigit())
|
||||
if not digits_pattern:
|
||||
return []
|
||||
|
||||
result = await session.execute(
|
||||
select(Participation.account_number)
|
||||
.where(
|
||||
Participation.lottery_id == lottery_id,
|
||||
Participation.account_number.isnot(None),
|
||||
func.replace(Participation.account_number, '-', '').contains(digits_pattern),
|
||||
)
|
||||
.order_by(Participation.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
# Ищем совпадения
|
||||
return search_accounts_by_pattern(pattern, all_accounts)[:limit]
|
||||
return [account for account in result.scalars().all() if account]
|
||||
|
||||
@staticmethod
|
||||
async def set_account_as_winner(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Расширенная админ-панель для управления розыгрышами
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import (
|
||||
@@ -24,6 +25,107 @@ from ..core.models import User, Lottery, Participation, Account, ChatMessage, Wi
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_users_export_file(users_data: list[dict]) -> bytes:
|
||||
from io import BytesIO
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Пользователи"
|
||||
|
||||
headers = [
|
||||
'Telegram ID', 'Username', 'Имя', 'Фамилия', 'Никнейм',
|
||||
'Телефон', 'Клубная карта', 'Зарегистрирован', 'Админ',
|
||||
'Код верификации', 'Дата создания', 'Последняя активность', 'Заблокирован в чате'
|
||||
]
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF")
|
||||
|
||||
for col_num, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col_num, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
for row_num, user in enumerate(users_data, 2):
|
||||
ws.cell(row=row_num, column=1, value=user["telegram_id"])
|
||||
ws.cell(row=row_num, column=2, value=user["username"] or '')
|
||||
ws.cell(row=row_num, column=3, value=user["first_name"] or '')
|
||||
ws.cell(row=row_num, column=4, value=user["last_name"] or '')
|
||||
ws.cell(row=row_num, column=5, value=user["nickname"] or '')
|
||||
ws.cell(row=row_num, column=6, value=user["phone"] or '')
|
||||
ws.cell(row=row_num, column=7, value=user["club_card_number"] or '')
|
||||
ws.cell(row=row_num, column=8, value='Да' if user["is_registered"] else 'Нет')
|
||||
ws.cell(row=row_num, column=9, value='Да' if user["is_admin"] else 'Нет')
|
||||
ws.cell(row=row_num, column=10, value=user["verification_code"] or '')
|
||||
ws.cell(row=row_num, column=11, value=user["created_at"] or '')
|
||||
ws.cell(row=row_num, column=12, value=user["last_activity"] or '')
|
||||
ws.cell(row=row_num, column=13, value='Да' if user["is_chat_banned"] else 'Нет')
|
||||
|
||||
for column in ws.columns:
|
||||
max_length = 0
|
||||
column_letter = column[0].column_letter
|
||||
for cell in column:
|
||||
try:
|
||||
max_length = max(max_length, len(str(cell.value)))
|
||||
except Exception:
|
||||
pass
|
||||
ws.column_dimensions[column_letter].width = min(max_length + 2, 50)
|
||||
|
||||
excel_file = BytesIO()
|
||||
wb.save(excel_file)
|
||||
return excel_file.getvalue()
|
||||
|
||||
|
||||
def _parse_users_xlsx(file_bytes: bytes) -> list[dict]:
|
||||
from io import BytesIO
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook(BytesIO(file_bytes), read_only=True)
|
||||
ws = wb.active
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
|
||||
if len(rows) < 2:
|
||||
return []
|
||||
|
||||
headers = [h if h else '' for h in rows[0]]
|
||||
telegram_id_idx = headers.index('Telegram ID')
|
||||
field_mapping = {
|
||||
'Username': 'username',
|
||||
'Имя': 'first_name',
|
||||
'Фамилия': 'last_name',
|
||||
'Никнейм': 'nickname',
|
||||
'Телефон': 'phone',
|
||||
'Клубная карта': 'club_card_number',
|
||||
'Зарегистрирован': 'is_registered',
|
||||
'Код верификации': 'verification_code'
|
||||
}
|
||||
|
||||
users_data = []
|
||||
for row in rows[1:]:
|
||||
if not row or len(row) <= telegram_id_idx or not row[telegram_id_idx]:
|
||||
continue
|
||||
|
||||
user_dict = {'telegram_id': row[telegram_id_idx]}
|
||||
for header_name, field_name in field_mapping.items():
|
||||
try:
|
||||
idx = headers.index(header_name)
|
||||
if idx < len(row):
|
||||
value = row[idx]
|
||||
user_dict[field_name] = (
|
||||
value in ['Да', 'Yes', 'True', True, 1]
|
||||
if field_name == 'is_registered'
|
||||
else value if value else None
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
user_dict[field_name] = None
|
||||
users_data.append(user_dict)
|
||||
|
||||
return users_data
|
||||
|
||||
|
||||
async def safe_edit_message(
|
||||
callback: CallbackQuery,
|
||||
text: str,
|
||||
@@ -980,12 +1082,10 @@ async def list_all_participants(callback: CallbackQuery):
|
||||
|
||||
async with async_session_maker() as session:
|
||||
users = await UserService.get_all_users(session, limit=50)
|
||||
|
||||
# Получаем статистику для каждого пользователя
|
||||
user_stats = []
|
||||
for user in users:
|
||||
stats = await ParticipationService.get_participant_stats(session, user.id)
|
||||
user_stats.append((user, stats))
|
||||
stats_by_user = await ParticipationService.get_participant_stats_bulk(
|
||||
session, [user.id for user in users]
|
||||
)
|
||||
user_stats = [(user, stats_by_user[user.id]) for user in users]
|
||||
|
||||
if not user_stats:
|
||||
await callback.message.edit_text(
|
||||
@@ -1116,6 +1216,9 @@ async def export_participants_data(callback: CallbackQuery):
|
||||
|
||||
async with async_session_maker() as session:
|
||||
users = await UserService.get_all_users(session)
|
||||
stats_by_user = await ParticipationService.get_participant_stats_bulk(
|
||||
session, [user.id for user in users]
|
||||
)
|
||||
|
||||
export_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
@@ -1124,7 +1227,7 @@ async def export_participants_data(callback: CallbackQuery):
|
||||
}
|
||||
|
||||
for user in users:
|
||||
stats = await ParticipationService.get_participant_stats(session, user.id)
|
||||
stats = stats_by_user[user.id]
|
||||
user_data = {
|
||||
"id": user.id,
|
||||
"telegram_id": user.telegram_id,
|
||||
@@ -1192,12 +1295,10 @@ async def process_search_participants(message: Message, state: FSMContext):
|
||||
|
||||
async with async_session_maker() as session:
|
||||
users = await UserService.search_users(session, search_term)
|
||||
|
||||
# Получаем статистику для найденных пользователей
|
||||
user_stats = []
|
||||
for user in users:
|
||||
stats = await ParticipationService.get_participant_stats(session, user.id)
|
||||
user_stats.append((user, stats))
|
||||
stats_by_user = await ParticipationService.get_participant_stats_bulk(
|
||||
session, [user.id for user in users]
|
||||
)
|
||||
user_stats = [(user, stats_by_user[user.id]) for user in users]
|
||||
|
||||
await state.clear()
|
||||
|
||||
@@ -3556,20 +3657,10 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
||||
await session.commit()
|
||||
logger.info(f"Изменения закоммичены для розыгрыша {lottery_id}")
|
||||
|
||||
# Отправляем уведомления победителям
|
||||
from ..utils.notifications import notify_winners_async
|
||||
try:
|
||||
await notify_winners_async(callback.bot, session, lottery_id)
|
||||
logger.info(f"Уведомления отправлены для розыгрыша {lottery_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомлений: {e}")
|
||||
|
||||
# Отправляем результаты розыгрыша всем участникам (кроме победителей)
|
||||
try:
|
||||
await _notify_all_participants_about_results(callback.bot, session, lottery_id, winners_dict)
|
||||
logger.info(f"Результаты розыгрыша разосланы всем участникам {lottery_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при рассылке результатов: {e}")
|
||||
asyncio.create_task(
|
||||
_send_draw_notifications_background(callback.bot, lottery_id, winners_dict)
|
||||
)
|
||||
logger.info(f"Фоновая рассылка итогов запущена для розыгрыша {lottery_id}")
|
||||
|
||||
# Получаем победителей из базы
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
@@ -4484,6 +4575,24 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
|
||||
logger.info(f"Результаты розыгрыша разосланы: {success_count} успешно, {fail_count} ошибок")
|
||||
|
||||
|
||||
async def _send_draw_notifications_background(bot, lottery_id: int, winners_dict: dict):
|
||||
"""Отправить уведомления по итогам розыгрыша вне callback handler."""
|
||||
async with async_session_maker() as session:
|
||||
from ..utils.notifications import notify_winners_async
|
||||
|
||||
try:
|
||||
await notify_winners_async(bot, session, lottery_id)
|
||||
logger.info(f"Уведомления отправлены для розыгрыша {lottery_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомлений: {e}", exc_info=True)
|
||||
|
||||
try:
|
||||
await _notify_all_participants_about_results(bot, session, lottery_id, winners_dict)
|
||||
logger.info(f"Результаты розыгрыша разосланы всем участникам {lottery_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при рассылке результатов: {e}", exc_info=True)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ЭКСПОРТ И ИМПОРТ ПОЛЬЗОВАТЕЛЕЙ
|
||||
# ============================================================================
|
||||
@@ -4498,87 +4607,45 @@ async def admin_export_users(callback: CallbackQuery):
|
||||
await callback.answer("⏳ Формирую файл...", show_alert=False)
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
from io import BytesIO
|
||||
from aiogram.types import BufferedInputFile
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем всех пользователей
|
||||
all_users = await UserService.get_all_users(session)
|
||||
|
||||
# Создаем Excel файл
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Пользователи"
|
||||
|
||||
# Заголовки
|
||||
headers = [
|
||||
'Telegram ID', 'Username', 'Имя', 'Фамилия', 'Никнейм',
|
||||
'Телефон', 'Клубная карта', 'Зарегистрирован', 'Админ',
|
||||
'Код верификации', 'Дата создания', 'Последняя активность', 'Заблокирован в чате'
|
||||
]
|
||||
|
||||
# Стиль для заголовков
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF")
|
||||
|
||||
for col_num, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col_num, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
# Данные пользователей
|
||||
for row_num, user in enumerate(all_users, 2):
|
||||
ws.cell(row=row_num, column=1, value=user.telegram_id)
|
||||
ws.cell(row=row_num, column=2, value=user.username or '')
|
||||
ws.cell(row=row_num, column=3, value=user.first_name or '')
|
||||
ws.cell(row=row_num, column=4, value=user.last_name or '')
|
||||
ws.cell(row=row_num, column=5, value=user.nickname or '')
|
||||
ws.cell(row=row_num, column=6, value=user.phone or '')
|
||||
ws.cell(row=row_num, column=7, value=user.club_card_number or '')
|
||||
ws.cell(row=row_num, column=8, value='Да' if user.is_registered else 'Нет')
|
||||
ws.cell(row=row_num, column=9, value='Да' if user.is_admin else 'Нет')
|
||||
ws.cell(row=row_num, column=10, value=user.verification_code or '')
|
||||
ws.cell(row=row_num, column=11, value=user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else '')
|
||||
ws.cell(row=row_num, column=12, value=user.last_activity.strftime('%d.%m.%Y %H:%M') if user.last_activity else '')
|
||||
ws.cell(row=row_num, column=13, value='Да' if user.is_chat_banned else 'Нет')
|
||||
|
||||
# Автоподбор ширины колонок
|
||||
for column in ws.columns:
|
||||
max_length = 0
|
||||
column_letter = column[0].column_letter
|
||||
for cell in column:
|
||||
try:
|
||||
if len(str(cell.value)) > max_length:
|
||||
max_length = len(str(cell.value))
|
||||
except:
|
||||
pass
|
||||
adjusted_width = min(max_length + 2, 50)
|
||||
ws.column_dimensions[column_letter].width = adjusted_width
|
||||
|
||||
# Сохраняем в BytesIO
|
||||
excel_file = BytesIO()
|
||||
wb.save(excel_file)
|
||||
excel_file.seek(0)
|
||||
|
||||
# Отправляем файл
|
||||
filename = f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
file = BufferedInputFile(excel_file.read(), filename=filename)
|
||||
|
||||
registered_count = len([u for u in all_users if u.is_registered])
|
||||
|
||||
await callback.message.answer_document(
|
||||
document=file,
|
||||
caption=(
|
||||
f"📥 <b>Экспорт пользователей</b>\n\n"
|
||||
f"📊 Всего пользователей: {len(all_users)}\n"
|
||||
f"✅ Зарегистрировано: {registered_count}\n"
|
||||
f"📅 Дата экспорта: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
users_data = [
|
||||
{
|
||||
"telegram_id": user.telegram_id,
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"nickname": user.nickname,
|
||||
"phone": user.phone,
|
||||
"club_card_number": user.club_card_number,
|
||||
"is_registered": user.is_registered,
|
||||
"is_admin": user.is_admin,
|
||||
"verification_code": user.verification_code,
|
||||
"created_at": user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else '',
|
||||
"last_activity": user.last_activity.strftime('%d.%m.%Y %H:%M') if user.last_activity else '',
|
||||
"is_chat_banned": user.is_chat_banned,
|
||||
}
|
||||
for user in all_users
|
||||
]
|
||||
|
||||
excel_bytes = await asyncio.to_thread(_build_users_export_file, users_data)
|
||||
filename = f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
file = BufferedInputFile(excel_bytes, filename=filename)
|
||||
registered_count = sum(1 for user in all_users if user.is_registered)
|
||||
|
||||
await callback.message.answer_document(
|
||||
document=file,
|
||||
caption=(
|
||||
f"📥 <b>Экспорт пользователей</b>\n\n"
|
||||
f"📊 Всего пользователей: {len(all_users)}\n"
|
||||
f"✅ Зарегистрировано: {registered_count}\n"
|
||||
f"📅 Дата экспорта: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Файл отправлен", show_alert=False)
|
||||
except Exception as e:
|
||||
@@ -4633,69 +4700,21 @@ async def admin_import_users_process(message: Message, state: FSMContext):
|
||||
status_msg = await message.answer("⏳ Загружаю файл...")
|
||||
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
from io import BytesIO
|
||||
|
||||
# Скачиваем файл
|
||||
file = await message.bot.get_file(message.document.file_id)
|
||||
file_content = await message.bot.download_file(file.file_path)
|
||||
|
||||
# Читаем Excel файл
|
||||
excel_file = BytesIO(file_content.read())
|
||||
wb = load_workbook(excel_file, read_only=True)
|
||||
ws = wb.active
|
||||
|
||||
# Читаем данные
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
|
||||
if len(rows) < 2:
|
||||
await status_msg.edit_text("❌ Файл пуст или не содержит данных.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
# Первая строка - заголовки
|
||||
headers = [h if h else '' for h in rows[0]]
|
||||
|
||||
# Находим индекс колонки Telegram ID
|
||||
|
||||
try:
|
||||
telegram_id_idx = headers.index('Telegram ID')
|
||||
users_data = await asyncio.to_thread(_parse_users_xlsx, file_content.read())
|
||||
except ValueError:
|
||||
await status_msg.edit_text("❌ Не найдена обязательная колонка 'Telegram ID'.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
# Создаем маппинг индексов для других полей
|
||||
field_mapping = {
|
||||
'Username': 'username',
|
||||
'Имя': 'first_name',
|
||||
'Фамилия': 'last_name',
|
||||
'Никнейм': 'nickname',
|
||||
'Телефон': 'phone',
|
||||
'Клубная карта': 'club_card_number',
|
||||
'Зарегистрирован': 'is_registered',
|
||||
'Код верификации': 'verification_code'
|
||||
}
|
||||
|
||||
users_data = []
|
||||
for row in rows[1:]: # Пропускаем заголовки
|
||||
if not row or len(row) <= telegram_id_idx or not row[telegram_id_idx]:
|
||||
continue
|
||||
|
||||
user_dict = {'telegram_id': row[telegram_id_idx]}
|
||||
|
||||
for header_name, field_name in field_mapping.items():
|
||||
try:
|
||||
idx = headers.index(header_name)
|
||||
if idx < len(row):
|
||||
value = row[idx]
|
||||
if field_name == 'is_registered':
|
||||
user_dict[field_name] = value in ['Да', 'Yes', 'True', True, 1]
|
||||
else:
|
||||
user_dict[field_name] = value if value else None
|
||||
except (ValueError, IndexError):
|
||||
user_dict[field_name] = None
|
||||
|
||||
users_data.append(user_dict)
|
||||
|
||||
if not users_data:
|
||||
await status_msg.edit_text("❌ Файл пуст или не содержит данных.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await status_msg.edit_text(
|
||||
f"📊 Найдено пользователей в файле: {len(users_data)}\n"
|
||||
@@ -4704,26 +4723,39 @@ async def admin_import_users_process(message: Message, state: FSMContext):
|
||||
|
||||
# Импортируем пользователей
|
||||
async with async_session_maker() as session:
|
||||
from sqlalchemy import select
|
||||
|
||||
added_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
|
||||
normalized_users = []
|
||||
for user_data in users_data:
|
||||
telegram_id = user_data.get('telegram_id')
|
||||
if not telegram_id:
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
user_data['telegram_id'] = int(telegram_id)
|
||||
except (ValueError, TypeError):
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
normalized_users.append(user_data)
|
||||
|
||||
existing_result = await session.execute(
|
||||
select(User).where(User.telegram_id.in_([u['telegram_id'] for u in normalized_users]))
|
||||
)
|
||||
existing_by_tg_id = {
|
||||
user.telegram_id: user
|
||||
for user in existing_result.scalars().all()
|
||||
}
|
||||
|
||||
for user_data in normalized_users:
|
||||
try:
|
||||
telegram_id = user_data.get('telegram_id')
|
||||
if not telegram_id:
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# Преобразуем telegram_id в int если это строка
|
||||
try:
|
||||
telegram_id = int(telegram_id)
|
||||
except (ValueError, TypeError):
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# Ищем существующего пользователя
|
||||
existing_user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
existing_user = existing_by_tg_id.get(telegram_id)
|
||||
|
||||
if existing_user:
|
||||
# Обновляем существующего
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Обработчики пользовательских сообщений в чате"""
|
||||
import logging
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.exceptions import TelegramRetryAfter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.filters import StateFilter, Command
|
||||
@@ -23,6 +25,9 @@ from src.core.database import async_session_maker
|
||||
from src.core.config import ADMIN_IDS
|
||||
from src.utils.account_utils import parse_accounts_from_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_RETRY_AFTER_SECONDS = 30
|
||||
|
||||
|
||||
class ChatStates(StatesGroup):
|
||||
"""Состояния для работы в чате"""
|
||||
@@ -478,7 +483,7 @@ async def broadcast_message_with_scheduler(
|
||||
return forwarded_ids, success_count, fail_count
|
||||
|
||||
|
||||
async def _send_message_to_user(message: Message, user_telegram_id: int) -> Optional[int]:
|
||||
async def _send_message_to_user(message: Message, user_telegram_id: int, retry: bool = True) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение конкретному пользователю.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
@@ -486,12 +491,24 @@ async def _send_message_to_user(message: Message, user_telegram_id: int) -> Opti
|
||||
try:
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
return sent_msg.message_id
|
||||
except TelegramRetryAfter as e:
|
||||
if retry and e.retry_after <= MAX_RETRY_AFTER_SECONDS:
|
||||
logger.warning(f"FloodWait при отправке {user_telegram_id}: {e.retry_after}с")
|
||||
await asyncio.sleep(e.retry_after + 1)
|
||||
return await _send_message_to_user(message, user_telegram_id, retry=False)
|
||||
logger.warning(f"Пропускаем отправку {user_telegram_id}: FloodWait {e.retry_after}с")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
logger.warning(f"Не удалось отправить сообщение {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_user_with_sender(message: Message, user_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
async def _send_message_to_user_with_sender(
|
||||
message: Message,
|
||||
user_telegram_id: int,
|
||||
sender_info: str,
|
||||
retry: bool = True,
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение обычному пользователю с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
@@ -565,12 +582,26 @@ async def _send_message_to_user_with_sender(message: Message, user_telegram_id:
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except TelegramRetryAfter as e:
|
||||
if retry and e.retry_after <= MAX_RETRY_AFTER_SECONDS:
|
||||
logger.warning(f"FloodWait при отправке {user_telegram_id}: {e.retry_after}с")
|
||||
await asyncio.sleep(e.retry_after + 1)
|
||||
return await _send_message_to_user_with_sender(
|
||||
message, user_telegram_id, sender_info, retry=False
|
||||
)
|
||||
logger.warning(f"Пропускаем отправку {user_telegram_id}: FloodWait {e.retry_after}с")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
logger.warning(f"Не удалось отправить сообщение {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
async def _send_message_to_admin_with_sender(
|
||||
message: Message,
|
||||
admin_telegram_id: int,
|
||||
sender_info: str,
|
||||
retry: bool = True,
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение админу с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
@@ -644,8 +675,17 @@ async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id
|
||||
sent_msg = await message.copy_to(admin_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except TelegramRetryAfter as e:
|
||||
if retry and e.retry_after <= MAX_RETRY_AFTER_SECONDS:
|
||||
logger.warning(f"FloodWait при отправке админу {admin_telegram_id}: {e.retry_after}с")
|
||||
await asyncio.sleep(e.retry_after + 1)
|
||||
return await _send_message_to_admin_with_sender(
|
||||
message, admin_telegram_id, sender_info, retry=False
|
||||
)
|
||||
logger.warning(f"Пропускаем отправку админу {admin_telegram_id}: FloodWait {e.retry_after}с")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Failed to send message with sender info to admin {admin_telegram_id}: {e}")
|
||||
logger.warning(f"Не удалось отправить сообщение админу {admin_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -656,7 +696,7 @@ async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, O
|
||||
sent_msg = await message.forward(channel_id)
|
||||
return True, sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to forward message to channel {channel_id}: {e}")
|
||||
logger.warning(f"Не удалось переслать сообщение в канал {channel_id}: {e}")
|
||||
return False, None
|
||||
|
||||
|
||||
@@ -992,7 +1032,7 @@ async def handle_sticker_message(message: Message, state: FSMContext):
|
||||
await message.answer("✅ Стикер переслан в канал")
|
||||
|
||||
|
||||
@router.message(F.voice)
|
||||
@router.message(F.voice, StateFilter(ChatStates.in_chat))
|
||||
async def handle_voice_message(message: Message):
|
||||
"""Обработчик голосовых сообщений - ЗАБЛОКИРОВАНО"""
|
||||
await message.answer(
|
||||
@@ -1002,7 +1042,7 @@ async def handle_voice_message(message: Message):
|
||||
return
|
||||
|
||||
|
||||
@router.message(F.audio)
|
||||
@router.message(F.audio, StateFilter(ChatStates.in_chat))
|
||||
async def handle_audio_message(message: Message):
|
||||
"""Обработчик аудиофайлов (музыка, аудиозаписи) - ЗАБЛОКИРОВАНО"""
|
||||
await message.answer(
|
||||
|
||||
@@ -7,6 +7,7 @@ from src.filters.case_insensitive import CaseInsensitiveCommand
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from src.core.p2p_services import P2PMessageService
|
||||
@@ -124,9 +125,13 @@ async def select_recipient(callback: CallbackQuery, state: FSMContext):
|
||||
await callback.answer()
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем всех зарегистрированных пользователей кроме себя
|
||||
users = await UserService.get_all_users(session)
|
||||
users = [u for u in users if u.telegram_id != callback.from_user.id and u.is_registered]
|
||||
result = await session.execute(
|
||||
select(User)
|
||||
.where(User.telegram_id != callback.from_user.id, User.is_registered == True)
|
||||
.order_by(User.created_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
users = result.scalars().all()
|
||||
|
||||
if not users:
|
||||
await callback.message.edit_text("❌ Нет доступных пользователей для общения")
|
||||
@@ -134,7 +139,7 @@ async def select_recipient(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
# Создаём кнопки с пользователями (по 1 на строку)
|
||||
buttons = []
|
||||
for user in users[:20]: # Ограничение 20 пользователей на странице
|
||||
for user in users:
|
||||
display_name = user.nickname or f"@{user.username}" or user.first_name or "Unknown"
|
||||
if user.club_card_number:
|
||||
display_name += f" (карта: {user.club_card_number})"
|
||||
|
||||
Reference in New Issue
Block a user