This commit is contained in:
@@ -6,16 +6,16 @@ BOT_TOKEN=8300330445:AAFyxAqtmWsgtSPa_nb-lH3Q4ovmn9Ei6rA
|
||||
|
||||
# PostgreSQL настройки для внешней БД
|
||||
# Замените на данные вашего внешнего PostgreSQL сервера
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_HOST=192.168.0.102
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=bot_db
|
||||
POSTGRES_DB=lottery_bot
|
||||
POSTGRES_USER=trevor
|
||||
POSTGRES_PASSWORD=Cl0ud_1985!
|
||||
|
||||
# Database URL для бота
|
||||
# Формат: postgresql+asyncpg://user:password@host:port/database
|
||||
# Для внешнего сервера укажите его IP или домен вместо localhost
|
||||
DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@localhost:5432/bot_db
|
||||
DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@192.168.0.102:5432/lottery_bot
|
||||
|
||||
# ID администраторов (через запятую)
|
||||
ADMIN_IDS=556399210,6639865742
|
||||
|
||||
9
export_20260208_174031.json
Normal file
9
export_20260208_174031.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"export_date": "2026-02-08T17:40:31.898764",
|
||||
"statistics": {
|
||||
"users": 3,
|
||||
"lotteries": 1,
|
||||
"participations": 1,
|
||||
"winners": 0
|
||||
}
|
||||
}
|
||||
9
export_20260208_174208.json
Normal file
9
export_20260208_174208.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"export_date": "2026-02-08T17:42:08.014799",
|
||||
"statistics": {
|
||||
"users": 3,
|
||||
"lotteries": 1,
|
||||
"participations": 1,
|
||||
"winners": 0
|
||||
}
|
||||
}
|
||||
9
export_20260208_174221.json
Normal file
9
export_20260208_174221.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"export_date": "2026-02-08T17:42:21.844218",
|
||||
"statistics": {
|
||||
"users": 3,
|
||||
"lotteries": 1,
|
||||
"participations": 1,
|
||||
"winners": 0
|
||||
}
|
||||
}
|
||||
9
main.py
9
main.py
@@ -129,12 +129,13 @@ async def main():
|
||||
dp.include_router(redraw_router) # Повторные розыгрыши
|
||||
dp.include_router(p2p_chat_router) # P2P чат между пользователями
|
||||
|
||||
# 3. Chat router для broadcast (ловит все необработанные сообщения)
|
||||
dp.include_router(chat_router) # Пользовательский чат (broadcast всем)
|
||||
|
||||
# 4. Account router ПОСЛЕДНИМ (обнаружение счетов для админов)
|
||||
# 3. Account router ПЕРЕД chat_router (обнаружение счетов для админов)
|
||||
dp.include_router(account_router) # Пользовательские счета + обнаружение для админов
|
||||
|
||||
# 4. Chat router для broadcast (ловит все необработанные сообщения)
|
||||
# chat_router пропускает сообщения со счетами от админов
|
||||
dp.include_router(chat_router) # Пользовательский чат (broadcast всем)
|
||||
|
||||
# Запускаем polling
|
||||
try:
|
||||
logger.info("Бот запущен")
|
||||
|
||||
@@ -18,7 +18,7 @@ from ..core.database import async_session_maker
|
||||
from ..core.services import UserService, LotteryService, ParticipationService
|
||||
from ..core.chat_services import ChatMessageService
|
||||
from ..core.config import ADMIN_IDS
|
||||
from ..core.models import User, Lottery, Participation, Account, ChatMessage
|
||||
from ..core.models import User, Lottery, Participation, Account, ChatMessage, Winner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -3052,7 +3052,8 @@ async def export_data(callback: CallbackQuery):
|
||||
text += f"🏆 Победителей: {winners_count}\n\n"
|
||||
text += f"✅ Данные экспортированы в файл:\n{filename}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
await safe_edit_message(
|
||||
callback,
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Экспортировать снова", callback_data="admin_export_data")],
|
||||
|
||||
@@ -3,7 +3,9 @@ from aiogram import Router, F
|
||||
from aiogram.types import Message
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import asyncio
|
||||
from typing import List, Dict, Optional
|
||||
from typing import List, Dict, Optional, Set
|
||||
from collections import deque
|
||||
import time
|
||||
|
||||
from src.core.chat_services import (
|
||||
ChatSettingsService,
|
||||
@@ -14,6 +16,7 @@ from src.core.chat_services import (
|
||||
from src.core.services import UserService
|
||||
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
|
||||
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
@@ -21,22 +24,54 @@ def is_admin(user_id: int) -> bool:
|
||||
return user_id in ADMIN_IDS
|
||||
|
||||
|
||||
def _contains_account_numbers(text: str) -> bool:
|
||||
"""Проверка содержит ли текст номера счетов"""
|
||||
if not text:
|
||||
return False
|
||||
accounts = parse_accounts_from_message(text)
|
||||
return len(accounts) > 0
|
||||
|
||||
|
||||
router = Router(name='chat_router')
|
||||
|
||||
# Настройки для планировщика рассылки
|
||||
BATCH_SIZE = 20 # Количество сообщений в пакете
|
||||
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
|
||||
|
||||
# Защита от дубликатов сообщений (храним последние 100 message_id)
|
||||
_processed_messages: deque = deque(maxlen=100)
|
||||
|
||||
|
||||
def _is_message_processed(message_id: int) -> bool:
|
||||
"""Проверка, было ли сообщение уже обработано"""
|
||||
if message_id in _processed_messages:
|
||||
return True
|
||||
_processed_messages.append(message_id)
|
||||
return False
|
||||
|
||||
|
||||
async def get_all_active_users(session: AsyncSession) -> List:
|
||||
"""Получить всех зарегистрированных пользователей для рассылки"""
|
||||
"""Получить всех пользователей для рассылки (зарегистрированные + админы)"""
|
||||
users = await UserService.get_all_users(session)
|
||||
return [u for u in users if u.is_registered] # Используем is_registered вместо is_active
|
||||
# Рассылаем зарегистрированным пользователям И админам (даже если они не зарегистрированы)
|
||||
return [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
|
||||
|
||||
|
||||
async def broadcast_message_with_scheduler(message: Message, exclude_user_id: Optional[int] = None) -> tuple[Dict[str, int], int, int]:
|
||||
async def broadcast_message_with_scheduler(
|
||||
message: Message,
|
||||
exclude_user_id: Optional[int] = None,
|
||||
admin_only: bool = False,
|
||||
sender_info: Optional[str] = None
|
||||
) -> tuple[Dict[str, int], int, int]:
|
||||
"""
|
||||
Разослать сообщение всем пользователям с планировщиком (пакетная отправка).
|
||||
|
||||
Args:
|
||||
message: Сообщение для рассылки
|
||||
exclude_user_id: ID пользователя для исключения
|
||||
admin_only: Рассылать только админам
|
||||
sender_info: Информация об отправителе (для показа админам)
|
||||
|
||||
Возвращает: (forwarded_ids, success_count, fail_count)
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
@@ -45,6 +80,10 @@ async def broadcast_message_with_scheduler(message: Message, exclude_user_id: Op
|
||||
if exclude_user_id:
|
||||
users = [u for u in users if u.telegram_id != exclude_user_id]
|
||||
|
||||
# Если только для админов - фильтруем
|
||||
if admin_only:
|
||||
users = [u for u in users if u.telegram_id in ADMIN_IDS]
|
||||
|
||||
forwarded_ids = {}
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
@@ -56,7 +95,11 @@ async def broadcast_message_with_scheduler(message: Message, exclude_user_id: Op
|
||||
# Отправляем пакет
|
||||
tasks = []
|
||||
for user in batch:
|
||||
tasks.append(_send_message_to_user(message, user.telegram_id))
|
||||
# Для админов добавляем информацию об отправителе, если сообщение от обычного пользователя
|
||||
if sender_info and user.telegram_id in ADMIN_IDS:
|
||||
tasks.append(_send_message_to_admin_with_sender(message, user.telegram_id, sender_info))
|
||||
else:
|
||||
tasks.append(_send_message_to_user(message, user.telegram_id))
|
||||
|
||||
# Ждем завершения пакета
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
@@ -91,6 +134,85 @@ async def _send_message_to_user(message: Message, user_telegram_id: int) -> Opti
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение админу с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>Сообщение от {sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
admin_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
admin_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
admin_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
admin_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
admin_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(admin_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
admin_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(admin_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(admin_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message with sender info to admin {admin_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, Optional[int]]:
|
||||
"""Переслать сообщение в канал/группу"""
|
||||
try:
|
||||
@@ -107,8 +229,21 @@ async def handle_text_message(message: Message):
|
||||
"""Обработчик текстовых сообщений"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Защита от дубликатов - если сообщение уже обработано, пропускаем
|
||||
if _is_message_processed(message.message_id):
|
||||
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
|
||||
return
|
||||
|
||||
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
|
||||
|
||||
# ПРОВЕРКА СЧЕТОВ: Если админ отправил сообщение с номерами счетов - НЕ рассылаем
|
||||
# Это сообщение будет обработано account_router для добавления в розыгрыш
|
||||
if is_admin(message.from_user.id) and message.text and not message.text.startswith('/'):
|
||||
if _contains_account_numbers(message.text):
|
||||
logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем рассылку (обработает account_router)")
|
||||
return # Пропускаем - обработает account_router
|
||||
|
||||
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-"
|
||||
if message.reply_to_message and is_admin(message.from_user.id):
|
||||
if message.text and message.text.lower().strip() in ['удалить', 'del', '-']:
|
||||
@@ -229,8 +364,20 @@ async def handle_text_message(message: Message):
|
||||
# Обрабатываем в зависимости от режима
|
||||
if settings.mode == 'broadcast':
|
||||
# Режим рассылки с планировщиком
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
# Сохраняем сообщение в историю
|
||||
await ChatMessageService.save_message(
|
||||
@@ -277,6 +424,10 @@ async def handle_text_message(message: Message):
|
||||
@router.message(F.photo)
|
||||
async def handle_photo_message(message: Message):
|
||||
"""Обработчик фото"""
|
||||
# Защита от дубликатов
|
||||
if _is_message_processed(message.message_id):
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
can_send, reason = await ChatPermissionService.can_send_message(
|
||||
session,
|
||||
@@ -298,11 +449,19 @@ async def handle_photo_message(message: Message):
|
||||
photo = message.photo[-1]
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Отправляем только админам
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем фото - ВСЕГДА исключаем отправителя
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
admin_only=True
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
@@ -317,7 +476,7 @@ async def handle_photo_message(message: Message):
|
||||
|
||||
# Показываем статистику только админам
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer(f"✅ Фото отправлено админам: {success}")
|
||||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
||||
|
||||
elif settings.mode == 'forward':
|
||||
if settings.forward_chat_id:
|
||||
@@ -339,6 +498,10 @@ async def handle_photo_message(message: Message):
|
||||
@router.message(F.video)
|
||||
async def handle_video_message(message: Message):
|
||||
"""Обработчик видео"""
|
||||
# Защита от дубликатов
|
||||
if _is_message_processed(message.message_id):
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
can_send, reason = await ChatPermissionService.can_send_message(
|
||||
session,
|
||||
@@ -357,8 +520,19 @@ async def handle_video_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -394,6 +568,10 @@ async def handle_video_message(message: Message):
|
||||
@router.message(F.document)
|
||||
async def handle_document_message(message: Message):
|
||||
"""Обработчик документов"""
|
||||
# Защита от дубликатов
|
||||
if _is_message_processed(message.message_id):
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
can_send, reason = await ChatPermissionService.can_send_message(
|
||||
session,
|
||||
@@ -412,8 +590,19 @@ async def handle_document_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -449,6 +638,10 @@ async def handle_document_message(message: Message):
|
||||
@router.message(F.animation)
|
||||
async def handle_animation_message(message: Message):
|
||||
"""Обработчик GIF анимаций"""
|
||||
# Защита от дубликатов
|
||||
if _is_message_processed(message.message_id):
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
can_send, reason = await ChatPermissionService.can_send_message(
|
||||
session,
|
||||
@@ -467,8 +660,19 @@ async def handle_animation_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -504,6 +708,10 @@ async def handle_animation_message(message: Message):
|
||||
@router.message(F.sticker)
|
||||
async def handle_sticker_message(message: Message):
|
||||
"""Обработчик стикеров"""
|
||||
# Защита от дубликатов
|
||||
if _is_message_processed(message.message_id):
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
can_send, reason = await ChatPermissionService.can_send_message(
|
||||
session,
|
||||
@@ -522,8 +730,19 @@ async def handle_sticker_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
|
||||
Reference in New Issue
Block a user