init commit
This commit is contained in:
0
lottery/bot/__init__.py
Normal file
0
lottery/bot/__init__.py
Normal file
13
lottery/bot/admin.py
Normal file
13
lottery/bot/admin.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
from .models import BotConfig, WelcomeMessage
|
||||
|
||||
|
||||
@admin.register(BotConfig)
|
||||
class BotConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ("bot_name", "channel_id", "bot_token")
|
||||
search_fields = ("bot_name", "channel_id", "bot_token")
|
||||
|
||||
@admin.register(WelcomeMessage)
|
||||
class WWelcomeMessageAdmin(admin.ModelAdmin):
|
||||
list_display = ("bot", "welcome_message", "welcome_image", "admin_contact", "channel_link", "group_link", "custom_link1_name", "custom_link1_url")
|
||||
|
||||
7
lottery/bot/apps.py
Normal file
7
lottery/bot/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BotConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bot'
|
||||
verbose_name='Telegram Бот'
|
||||
92
lottery/bot/chat.py
Normal file
92
lottery/bot/chat.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# chat.py
|
||||
import logging
|
||||
from telegram import Update, InputMediaPhoto
|
||||
from telegram.ext import ContextTypes
|
||||
from asgiref.sync import sync_to_async
|
||||
from bot.state import BINDING_PENDING
|
||||
|
||||
class ChatHandler:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@sync_to_async
|
||||
def get_bot_settings(self):
|
||||
from bot.models import BotConfig
|
||||
settings = BotConfig.objects.first()
|
||||
if settings is None:
|
||||
settings = BotConfig.objects.create(chat_enabled=True)
|
||||
return settings
|
||||
|
||||
@sync_to_async
|
||||
def get_client_by_telegram_id(self, telegram_id: int):
|
||||
from webapp.models import Client
|
||||
try:
|
||||
return Client.objects.get(telegram_id=str(telegram_id))
|
||||
except Client.DoesNotExist:
|
||||
return None
|
||||
|
||||
@sync_to_async
|
||||
def get_all_clients(self):
|
||||
from webapp.models import Client
|
||||
return list(Client.objects.all())
|
||||
|
||||
async def broadcast_message(self, sender, message_text: str, photo_media: InputMediaPhoto = None):
|
||||
clients = await self.get_all_clients()
|
||||
for client in clients:
|
||||
if client.telegram_id == sender.telegram_id:
|
||||
continue
|
||||
try:
|
||||
if photo_media:
|
||||
await self.bot.send_photo(
|
||||
chat_id=client.telegram_id,
|
||||
photo=photo_media.media,
|
||||
caption=photo_media.caption,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
await self.bot.send_message(
|
||||
chat_id=client.telegram_id,
|
||||
text=message_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки сообщения пользователю {client.telegram_id}: {e}")
|
||||
|
||||
async def handle_chat_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not update.message:
|
||||
return
|
||||
|
||||
bot_settings = await self.get_bot_settings()
|
||||
if not bot_settings.chat_enabled:
|
||||
await update.message.reply_text("🚫 Чат временно отключён администрацией.")
|
||||
return
|
||||
|
||||
chat_id = update.message.chat_id
|
||||
sender = await self.get_client_by_telegram_id(chat_id)
|
||||
# Если пользователь не привязан, но находится в pending, обрабатываем как ввод КК
|
||||
if not sender and chat_id in BINDING_PENDING:
|
||||
# Импортируем функцию process_binding_input из bot.handlers
|
||||
from bot.handlers import process_binding_input
|
||||
await process_binding_input(update, context)
|
||||
return
|
||||
# Если пользователь не привязан и не находится в pending, просим привязаться
|
||||
if not sender:
|
||||
await update.message.reply_text("🚫 Для участия в чате привяжите свою клиентскую карту с помощью /bind")
|
||||
return
|
||||
|
||||
# Если у пользователя отключена возможность отправки сообщений
|
||||
if getattr(sender, "chat_disabled", False):
|
||||
await update.message.reply_text("🚫 Вы не можете отправлять сообщения. Обратитесь к администратору.")
|
||||
return
|
||||
|
||||
sender_name = sender.name if sender else "Аноним"
|
||||
broadcast_text = f"💬 *{sender_name} пишет:*\n{update.message.text or ''}"
|
||||
|
||||
photo_media = None
|
||||
if update.message.photo:
|
||||
photo = update.message.photo[-1]
|
||||
caption = update.message.caption or broadcast_text
|
||||
photo_media = InputMediaPhoto(photo.file_id, caption=caption)
|
||||
|
||||
await self.broadcast_message(sender, broadcast_text, photo_media)
|
||||
140
lottery/bot/handlers.py
Normal file
140
lottery/bot/handlers.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# bot/handlers.py
|
||||
import re
|
||||
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from telegram.ext import ContextTypes
|
||||
from asgiref.sync import sync_to_async
|
||||
from webapp.models import BindingRequest, Client, Invoice
|
||||
from draw.models import Lottery, DrawResult, LotteryParticipant
|
||||
from bot.state import BINDING_PENDING
|
||||
|
||||
@sync_to_async
|
||||
def get_client_by_telegram_id(telegram_id: int):
|
||||
try:
|
||||
return Client.objects.get(telegram_id=str(telegram_id))
|
||||
except Client.DoesNotExist:
|
||||
return None
|
||||
|
||||
@sync_to_async
|
||||
def get_invoices_for_client(client):
|
||||
return list(Invoice.objects.filter(client_club_card_number=client.club_card_number))
|
||||
|
||||
@sync_to_async
|
||||
def get_draws_for_client(client):
|
||||
return list(
|
||||
DrawResult.objects.filter(participant__invoice__client_club_card_number=client.club_card_number)
|
||||
.select_related('lottery', 'prize', 'participant__invoice')
|
||||
)
|
||||
|
||||
@sync_to_async
|
||||
def create_binding_request(telegram_chat_id, client_card):
|
||||
return BindingRequest.objects.create(
|
||||
telegram_chat_id=str(telegram_chat_id),
|
||||
client_card=client_card
|
||||
)
|
||||
|
||||
async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not update.message:
|
||||
return
|
||||
chat_id = update.message.chat_id
|
||||
client = await get_client_by_telegram_id(chat_id)
|
||||
if not client:
|
||||
await update.message.reply_text(
|
||||
"❌ Пользователь не найден. Возможно, Вы ещё не привязали свою клубную карту."
|
||||
)
|
||||
return
|
||||
|
||||
invoices = await get_invoices_for_client(client)
|
||||
draws = await get_draws_for_client(client)
|
||||
|
||||
message_text = f"👤 *Профиль клиента:*\n" \
|
||||
f"• *Имя:* {client.name}\n" \
|
||||
f"• *Клубная карта:* {client.club_card_number}\n\n"
|
||||
|
||||
message_text += "💳 *Счета:*\n"
|
||||
if invoices:
|
||||
show_all = len(invoices) > 5
|
||||
displayed_invoices = invoices[:5] if show_all else invoices
|
||||
for inv in displayed_invoices:
|
||||
invoice_str = str(inv).split('/')[0].strip()
|
||||
message_text += f" • {invoice_str} (*{inv.sum}*)\n"
|
||||
else:
|
||||
message_text += " _Нет счетов_\n"
|
||||
|
||||
message_text += "\n🎉 *Розыгрыши и выигранные призы:*\n"
|
||||
if draws:
|
||||
for draw in draws:
|
||||
lottery_name = draw.lottery.name if draw.lottery else "неизвестно"
|
||||
invoice_info = (draw.participant.invoice
|
||||
if draw.participant and hasattr(draw.participant, 'invoice')
|
||||
else "неизвестно")
|
||||
prize_info = (draw.prize.reward
|
||||
if draw.prize and hasattr(draw.prize, 'reward')
|
||||
else "неизвестно")
|
||||
message_text += f" • Лотерея: *{lottery_name}*\n Счет: _{invoice_info}_\n Выигрыш: *{prize_info}*\n\n"
|
||||
else:
|
||||
message_text += " _Нет результатов розыгрышей_\n"
|
||||
|
||||
reply_markup = None
|
||||
if invoices and len(invoices) > 5:
|
||||
reply_markup = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Показать все счета", callback_data="show_all_invoices")]
|
||||
])
|
||||
|
||||
await update.message.reply_text(message_text, parse_mode="Markdown", reply_markup=reply_markup)
|
||||
|
||||
async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Обработчик команды /bind.
|
||||
После ввода команды бот запрашивает ввод номера КК и ожидает сообщение с кодом.
|
||||
Здесь мы просто добавляем ID пользователя в pending.
|
||||
"""
|
||||
if not update.message:
|
||||
return
|
||||
telegram_chat_id = update.message.chat_id
|
||||
# Если пользователь уже привязан, сообщаем об этом.
|
||||
client = await get_client_by_telegram_id(telegram_chat_id)
|
||||
if client:
|
||||
await update.message.reply_text("✅ Вы уже привязаны!")
|
||||
return
|
||||
|
||||
# Добавляем пользователя в список pending и просим ввести номер КК.
|
||||
BINDING_PENDING.add(telegram_chat_id)
|
||||
await update.message.reply_text("Введите номер вашей клубной карты (КК):")
|
||||
|
||||
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Обрабатывает ввод номера КК от пользователя, который находится в состоянии ожидания.
|
||||
"""
|
||||
if not update.message or not update.message.text:
|
||||
return
|
||||
client_card = update.message.text.strip()
|
||||
telegram_chat_id = update.message.chat_id
|
||||
|
||||
if not re.fullmatch(r'\d{3,}', client_card):
|
||||
await update.message.reply_text("❌ Неверный формат клиентской карты. Пожалуйста, введите корректный код (минимум 3 цифры).")
|
||||
return
|
||||
|
||||
await create_binding_request(telegram_chat_id, client_card)
|
||||
if telegram_chat_id in BINDING_PENDING:
|
||||
BINDING_PENDING.remove(telegram_chat_id)
|
||||
await update.message.reply_text("✅ Заявка отправлена. После подтверждения вы сможете участвовать в чате.")
|
||||
|
||||
async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
chat_id = query.message.chat_id
|
||||
client = await get_client_by_telegram_id(chat_id)
|
||||
if not client:
|
||||
await query.edit_message_text("❌ Пользователь не найден. Возможно, Вы ещё не привязали свою клубную карту.")
|
||||
return
|
||||
|
||||
invoices = await get_invoices_for_client(client)
|
||||
message_text = "💳 *Все счета:*\n"
|
||||
if invoices:
|
||||
for inv in invoices:
|
||||
invoice_str = str(inv).split('/')[0].strip()
|
||||
message_text += f" • {invoice_str} (*{inv.sum}*)\n"
|
||||
else:
|
||||
message_text += " _Нет счетов_\n"
|
||||
|
||||
await query.edit_message_text(message_text, parse_mode="Markdown")
|
||||
33
lottery/bot/management/commands/runbot.py
Normal file
33
lottery/bot/management/commands/runbot.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# bot/management/commands/runbot.py
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from bot.utils import create_bot_instance
|
||||
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, CallbackQueryHandler
|
||||
from bot.handlers import whoami, show_all_invoices_callback, handle_client_card
|
||||
from bot.chat import ChatHandler
|
||||
from bot.welcome import WelcomeHandler
|
||||
class Command(BaseCommand):
|
||||
help = 'Запускает Telegram-бота'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
bot = create_bot_instance()
|
||||
except Exception as e:
|
||||
raise CommandError(f"Ошибка при создании бота: {e}")
|
||||
|
||||
application = ApplicationBuilder().token(bot.token).build()
|
||||
|
||||
async def start_command(update, context):
|
||||
await update.message.reply_text("Здравствуйте! Используйте команду /bind для привязки клиентской карты.")
|
||||
|
||||
welcome_handler = WelcomeHandler(bot=application.bot)
|
||||
application.add_handler(CommandHandler("start", welcome_handler.send_welcome))
|
||||
application.add_handler(CommandHandler("bind", handle_client_card))
|
||||
application.add_handler(CommandHandler("whoami", whoami))
|
||||
application.add_handler(CallbackQueryHandler(show_all_invoices_callback, pattern="^show_all_invoices$"))
|
||||
|
||||
chat_handler = ChatHandler(bot=application.bot)
|
||||
application.add_handler(MessageHandler(filters.TEXT | filters.PHOTO, chat_handler.handle_chat_message))
|
||||
|
||||
self.stdout.write("Бот запущен. Ожидание обновлений...")
|
||||
application.run_polling()
|
||||
self.stdout.write("Бот остановлен.")
|
||||
23
lottery/bot/migrations/0001_initial.py
Normal file
23
lottery/bot/migrations/0001_initial.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-21 03:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BotConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('bot_token', models.CharField(help_text='Токен для подключения к Telegram API', max_length=255)),
|
||||
('channel_id', models.CharField(help_text='ID канала/чата, куда бот будет отправлять сообщения', max_length=100)),
|
||||
('bot_name', models.CharField(help_text='Отображаемое имя бота', max_length=100)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-21 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bot', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='botconfig',
|
||||
name='chat_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Если выключено, то глобально блокируется рассылка сообщений в чате.', verbose_name='Чат включён'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='botconfig',
|
||||
name='global_block_message',
|
||||
field=models.TextField(default='', help_text='Сообщение, которое показывается пользователям, если чат отключён.', verbose_name='Сообщение о глобальном блоке'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-23 01:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bot', '0002_botconfig_chat_enabled_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='botconfig',
|
||||
options={'verbose_name': 'Настройка бота', 'verbose_name_plural': 'Настройки Ботов'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WelcomeMessage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('welcome_message', models.TextField(blank=True, help_text='Текст, который будет отправлен при запуске команды /start', null=True, verbose_name='Приветственное сообщение')),
|
||||
('welcome_image', models.URLField(blank=True, help_text='URL изображения для приветствия', null=True, verbose_name='Ссылка на приветственное изображение')),
|
||||
('admin_contact', models.CharField(blank=True, help_text='Ссылка или контакт для связи с администратором', max_length=255, null=True, verbose_name='Контакт администратора')),
|
||||
('channel_link', models.URLField(blank=True, help_text='URL канала бота', null=True, verbose_name='Ссылка на канал')),
|
||||
('group_link', models.URLField(blank=True, help_text='URL группы бота', null=True, verbose_name='Ссылка на группу')),
|
||||
('custom_link1_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Название кастомной ссылки 1')),
|
||||
('custom_link1_url', models.URLField(blank=True, null=True, verbose_name='URL кастомной ссылки 1')),
|
||||
('custom_link2_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Название кастомной ссылки 2')),
|
||||
('custom_link2_url', models.URLField(blank=True, null=True, verbose_name='URL кастомной ссылки 2')),
|
||||
('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bot.botconfig', verbose_name='Бот')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Приветственное сообщение',
|
||||
'verbose_name_plural': 'Приветственные сообщения',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-23 01:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bot', '0003_alter_botconfig_options_welcomemessage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='welcomemessage',
|
||||
name='welcome_image',
|
||||
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='welcome_images/', verbose_name='Приветственное изображение'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-23 01:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bot', '0004_alter_welcomemessage_welcome_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='welcomemessage',
|
||||
name='welcome_image',
|
||||
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='', verbose_name='Приветственное изображение'),
|
||||
),
|
||||
]
|
||||
0
lottery/bot/migrations/__init__.py
Normal file
0
lottery/bot/migrations/__init__.py
Normal file
87
lottery/bot/models.py
Normal file
87
lottery/bot/models.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.db import models
|
||||
|
||||
class BotConfig(models.Model):
|
||||
bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API")
|
||||
channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения")
|
||||
bot_name = models.CharField(max_length=100, help_text="Отображаемое имя бота")
|
||||
chat_enabled = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Чат включён",
|
||||
help_text="Если выключено, то глобально блокируется рассылка сообщений в чате."
|
||||
)
|
||||
global_block_message = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Сообщение о глобальном блоке",
|
||||
help_text="Сообщение, которое показывается пользователям, если чат отключён."
|
||||
)
|
||||
def __str__(self):
|
||||
return self.bot_name
|
||||
class Meta:
|
||||
verbose_name = "Настройка бота"
|
||||
verbose_name_plural = "Настройки Ботов"
|
||||
|
||||
|
||||
class WelcomeMessage(models.Model):
|
||||
bot = models.ForeignKey(BotConfig, on_delete=models.CASCADE, verbose_name="Бот")
|
||||
welcome_message = models.TextField(
|
||||
verbose_name="Приветственное сообщение",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Текст, который будет отправлен при запуске команды /start"
|
||||
)
|
||||
welcome_image = models.ImageField(
|
||||
upload_to='static/upload_image/',
|
||||
verbose_name="Приветственное изображение",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Загрузите изображение для приветствия"
|
||||
)
|
||||
admin_contact = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name="Контакт администратора",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Ссылка или контакт для связи с администратором"
|
||||
)
|
||||
channel_link = models.URLField(
|
||||
verbose_name="Ссылка на канал",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="URL канала бота"
|
||||
)
|
||||
group_link = models.URLField(
|
||||
verbose_name="Ссылка на группу",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="URL группы бота"
|
||||
)
|
||||
custom_link1_name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Название кастомной ссылки 1",
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
custom_link1_url = models.URLField(
|
||||
verbose_name="URL кастомной ссылки 1",
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
custom_link2_name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Название кастомной ссылки 2",
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
custom_link2_url = models.URLField(
|
||||
verbose_name="URL кастомной ссылки 2",
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Приветственное сообщение"
|
||||
verbose_name_plural = "Приветственные сообщения"
|
||||
|
||||
def __str__(self):
|
||||
return f"Приветствие для {self.bot}"
|
||||
105
lottery/bot/notifications.py
Normal file
105
lottery/bot/notifications.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# notifications.py
|
||||
import logging
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
class NotificationService:
|
||||
def __init__(self, bot):
|
||||
"""
|
||||
Инициализация сервиса уведомлений.
|
||||
:param bot: экземпляр telegram.Bot, используемый для отправки сообщений.
|
||||
"""
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@sync_to_async
|
||||
def _get_all_clients(self):
|
||||
from webapp.models import Client
|
||||
# Возвращаем только клиентов, у которых telegram_id заполнен корректно
|
||||
return list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"]))
|
||||
|
||||
@sync_to_async
|
||||
def _prepare_results(self, results):
|
||||
# Принудительно загружаем связанные объекты через select_related
|
||||
return list(results.select_related('prize', 'participant__invoice'))
|
||||
|
||||
async def notify_draw_start(self, lottery):
|
||||
"""
|
||||
Уведомляет всех клиентов о запуске розыгрыша.
|
||||
:param lottery: объект розыгрыша (например, модель Lottery)
|
||||
"""
|
||||
message_text = f"🎉 *Розыгрыш '{lottery.name}' начался!* Следите за обновлениями."
|
||||
clients = await self._get_all_clients()
|
||||
for client in clients:
|
||||
if not client.telegram_id:
|
||||
continue
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id=client.telegram_id,
|
||||
text=message_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки уведомления о запуске розыгрыша пользователю {client.telegram_id}: {e}")
|
||||
|
||||
async def notify_draw_results(self, lottery, results):
|
||||
"""
|
||||
Отправляет результаты розыгрыша всем клиентам.
|
||||
:param lottery: объект розыгрыша (Lottery)
|
||||
:param results: QuerySet с результатами розыгрыша (например, объекты DrawResult)
|
||||
"""
|
||||
# Принудительно загружаем связанные объекты
|
||||
results_list = await self._prepare_results(results)
|
||||
message_text = f"🎉 *Результаты розыгрыша '{lottery.name}':*\n"
|
||||
for result in results_list:
|
||||
status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения"
|
||||
prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно"
|
||||
account = (str(result.participant.invoice).split('/')[0].strip()
|
||||
if result.participant and hasattr(result.participant, "invoice") else "неизвестно")
|
||||
message_text += f"• Приз: *{prize}*, Счет: _{account}_, Статус: *{status}*\n"
|
||||
clients = await self._get_all_clients()
|
||||
for client in clients:
|
||||
if not client.telegram_id:
|
||||
continue
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id=client.telegram_id,
|
||||
text=message_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки результатов розыгрыша пользователю {client.telegram_id}: {e}")
|
||||
|
||||
async def notify_prize_status_update(self, client, result):
|
||||
"""
|
||||
Отправляет конкретному пользователю обновление по статусу подтверждения приза.
|
||||
:param client: объект клиента (Client)
|
||||
:param result: объект результата розыгрыша (DrawResult)
|
||||
"""
|
||||
status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения"
|
||||
prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно"
|
||||
message_text = f"🎉 *Обновление розыгрыша:*\nВаш приз *{prize}* имеет статус: *{status}*."
|
||||
try:
|
||||
if client.telegram_id:
|
||||
await self.bot.send_message(
|
||||
chat_id=client.telegram_id,
|
||||
text=message_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки обновления статуса приза пользователю {client.telegram_id}: {e}")
|
||||
|
||||
async def notify_binding_complete(self, client):
|
||||
"""
|
||||
Уведомляет пользователя об окончании привязки клубной карты к Telegram ID.
|
||||
:param client: объект клиента (Client)
|
||||
"""
|
||||
message_text = "✅ *Привязка завершена!* Ваша клубная карта успешно привязана к Telegram. Теперь вы можете участвовать в розыгрышах и чате."
|
||||
try:
|
||||
if client.telegram_id:
|
||||
await self.bot.send_message(
|
||||
chat_id=client.telegram_id,
|
||||
text=message_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки уведомления о привязке пользователю {client.telegram_id}: {e}")
|
||||
2
lottery/bot/state.py
Normal file
2
lottery/bot/state.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# bot/state.py
|
||||
BINDING_PENDING = set()
|
||||
9
lottery/bot/storage.py
Normal file
9
lottery/bot/storage.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
# Кастомное хранилище для загрузки изображений в /static/upload_image
|
||||
static_storage = FileSystemStorage(
|
||||
location=os.path.join(settings.BASE_DIR, 'static', 'upload_image'),
|
||||
base_url='/static/upload_image/'
|
||||
)
|
||||
3
lottery/bot/tests.py
Normal file
3
lottery/bot/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
39
lottery/bot/utils.py
Normal file
39
lottery/bot/utils.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# bot/utils.py
|
||||
from telegram import Bot
|
||||
from django.conf import settings
|
||||
from telegram import Bot
|
||||
from bot.models import BotConfig
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
def create_bot_instance():
|
||||
"""
|
||||
Получает настройки бота из БД и создаёт экземпляр Telegram Bot.
|
||||
"""
|
||||
config = BotConfig.objects.first() # предполагается, что конфигурация одна
|
||||
if not config:
|
||||
raise ImproperlyConfigured("Настройки бота (BotConfig) не сконфигурированы в базе данных.")
|
||||
|
||||
# Можно дополнительно использовать config.channel_id и config.bot_name при необходимости.
|
||||
bot = Bot(token=config.bot_token)
|
||||
return bot
|
||||
|
||||
|
||||
def notify_user(binding_request, approved=True):
|
||||
bot_token = settings.BOT_CONFIG.bot_token # можно использовать модель BotConfig или настройку из settings
|
||||
bot = Bot(token=bot_token)
|
||||
message = "Ваша заявка на привязку успешно подтверждена!" if approved else "Ваша заявка на привязку отклонена."
|
||||
try:
|
||||
bot.send_message(chat_id=binding_request.telegram_chat_id, text=message)
|
||||
except Exception as e:
|
||||
# Обработка ошибок отправки сообщения
|
||||
print(f"Ошибка уведомления: {e}")
|
||||
|
||||
|
||||
# Пример использования в точке входа приложения:
|
||||
if __name__ == '__main__':
|
||||
# Предполагается, что Django уже настроен (например, через manage.py shell или management command)
|
||||
bot = create_bot_instance()
|
||||
# Теперь можно использовать объект bot для отправки сообщений, обработки обновлений и т.д.
|
||||
print(f"Бот {bot.name} успешно создан!")
|
||||
|
||||
|
||||
3
lottery/bot/views.py
Normal file
3
lottery/bot/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
82
lottery/bot/welcome.py
Normal file
82
lottery/bot/welcome.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# bot/welcome.py
|
||||
import logging
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import ContextTypes
|
||||
from asgiref.sync import sync_to_async
|
||||
from bot.models import WelcomeMessage
|
||||
|
||||
class WelcomeHandler:
|
||||
def __init__(self, bot):
|
||||
"""
|
||||
Инициализация обработчика приветственных сообщений.
|
||||
:param bot: экземпляр telegram.Bot для отправки сообщений.
|
||||
"""
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@sync_to_async
|
||||
def get_welcome_config(self):
|
||||
"""
|
||||
Получает первую запись настроек приветствия.
|
||||
"""
|
||||
try:
|
||||
return WelcomeMessage.objects.select_related("bot").first()
|
||||
except WelcomeMessage.DoesNotExist:
|
||||
return None
|
||||
|
||||
async def send_welcome(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Отправляет приветственное сообщение с inline-кнопками, расположенными в 3 строки:
|
||||
1 кнопка, затем 2 и затем 2.
|
||||
"""
|
||||
if not update.message:
|
||||
return
|
||||
|
||||
config = await self.get_welcome_config()
|
||||
if not config:
|
||||
await update.message.reply_text("Добро пожаловать!")
|
||||
return
|
||||
|
||||
# Формируем список всех кнопок
|
||||
buttons = []
|
||||
if config.admin_contact:
|
||||
buttons.append(InlineKeyboardButton("📞 Связаться с администратором", url=config.admin_contact))
|
||||
if config.channel_link:
|
||||
buttons.append(InlineKeyboardButton(" 📢 Канал", url=config.channel_link))
|
||||
if config.group_link:
|
||||
buttons.append(InlineKeyboardButton("👥 Группа", url=config.group_link))
|
||||
if config.custom_link1_name and config.custom_link1_url:
|
||||
buttons.append(InlineKeyboardButton(config.custom_link1_name, url=config.custom_link1_url))
|
||||
if config.custom_link2_name and config.custom_link2_url:
|
||||
buttons.append(InlineKeyboardButton(config.custom_link2_name, url=config.custom_link2_url))
|
||||
|
||||
# Распределяем кнопки по рядам: первая строка 1 кнопка, вторая – 2, третья – 2.
|
||||
row1 = buttons[0:1] if len(buttons) >= 1 else []
|
||||
row2 = buttons[1:3] if len(buttons) >= 3 else buttons[1:] if len(buttons) > 1 else []
|
||||
row3 = buttons[3:5] if len(buttons) >= 5 else []
|
||||
|
||||
# Собираем список рядов, исключая пустые строки
|
||||
keyboard = []
|
||||
if row1:
|
||||
keyboard.append(row1)
|
||||
if row2:
|
||||
keyboard.append(row2)
|
||||
if row3:
|
||||
keyboard.append(row3)
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard) if keyboard else None
|
||||
|
||||
# Отправка сообщения с изображением, если задано
|
||||
if config.welcome_image:
|
||||
await update.message.reply_photo(
|
||||
photo=config.welcome_image,
|
||||
caption=config.welcome_message,
|
||||
parse_mode="Markdown",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
else:
|
||||
await update.message.reply_text(
|
||||
text=config.welcome_message,
|
||||
parse_mode="Markdown",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
Reference in New Issue
Block a user