Compare commits

..

17 Commits

280 changed files with 537 additions and 2111 deletions

1
lottery/.gitignore vendored
View File

@@ -12,3 +12,4 @@ var/mysql/
.db
.png
db_data/

View File

@@ -3,7 +3,7 @@ from django.urls import path
from django.shortcuts import redirect
from django.contrib import messages
from .models import BotConfig
from .models import BotConfig, WelcomeMessage
from .models import BotConfig, WelcomeMessage, BotMessage, BotEventMessageConfig
from .tasks import restart_bot_container
@admin.register(BotConfig)
@@ -35,4 +35,12 @@ class BotConfigAdmin(admin.ModelAdmin):
@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")
@admin.register(BotMessage)
class BotMessageAdmin(admin.ModelAdmin):
list_display = ("name", "text", "image", "buttons_json")
@admin.register(BotEventMessageConfig)
class BotEventMessageConfigAdmin(admin.ModelAdmin):
list_display = ("event", "message", "enabled")
list_filter = ("event", "enabled")

View File

@@ -93,7 +93,7 @@ async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE)
BINDING_PENDING.add(telegram_chat_id)
await update.message.reply_text("Введите номер вашей клубной карты (КК):")
await send_event_message("binding_started", update, context)
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message or not update.message.text:
@@ -124,7 +124,7 @@ async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAU
message_text = "💳 *Все счета:*\n"
if invoices:
for inv in invoices:
message_text += f" • ID {inv.id} (*{inv.sum}*)\n"
message_text += f" • ID {inv.ext_id} (*{inv.sum}*)\n"
else:
message_text += " _Нет счетов_\n"

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.6 on 2025-08-03 05:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0007_botconfig_active_welcome_botconfig_is_active_and_more'),
]
operations = [
migrations.CreateModel(
name='BotMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название сообщения')),
('text', models.TextField(blank=True, verbose_name='Текст')),
('image', models.ImageField(blank=True, null=True, upload_to='bot_messages/', verbose_name='Картинка')),
('buttons_json', models.JSONField(blank=True, null=True, verbose_name='Кнопки (JSON-формат)')),
],
),
migrations.CreateModel(
name='BotEventMessageConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя')], max_length=50, unique=True, verbose_name='Событие')),
('enabled', models.BooleanField(default=True, verbose_name='Активно')),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_configs', to='bot.botmessage')),
],
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.1.6 on 2025-08-03 05:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0008_botmessage_boteventmessageconfig'),
]
operations = [
migrations.AlterModelOptions(
name='boteventmessageconfig',
options={'verbose_name': 'Событие сообщения', 'verbose_name_plural': 'События сообщений'},
),
migrations.AlterModelOptions(
name='botmessage',
options={'verbose_name': 'Сообщение', 'verbose_name_plural': 'Сообщения'},
),
migrations.AlterField(
model_name='boteventmessageconfig',
name='event',
field=models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя'), ('guest_binding', 'Подтверждение гостя')], max_length=50, unique=True, verbose_name='Событие'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-08-03 11:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0009_alter_boteventmessageconfig_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='welcomemessage',
name='welcome_image',
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='welcome_messages/', verbose_name='Приветственное изображение'),
),
]

View File

@@ -1,6 +1,7 @@
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 канала/чата, куда бот будет отправлять сообщения")
@@ -44,7 +45,7 @@ class WelcomeMessage(models.Model):
help_text="Текст, который будет отправлен при запуске команды /start"
)
welcome_image = models.ImageField(
upload_to='static/upload_image/',
upload_to='welcome_messages/',
verbose_name="Приветственное изображение",
blank=True,
null=True,
@@ -98,3 +99,38 @@ class WelcomeMessage(models.Model):
def __str__(self):
return f"Приветствие для {self.bot}"
class BotMessage(models.Model):
name = models.CharField("Название сообщения", max_length=100)
text = models.TextField("Текст", blank=True)
image = models.ImageField("Картинка", upload_to="bot_messages/", blank=True, null=True)
buttons_json = models.JSONField("Кнопки (JSON-формат)", blank=True, null=True)
class Meta:
verbose_name = "Сообщение"
verbose_name_plural = "Сообщения"
def __str__(self):
return self.name
class BotEventMessageConfig(models.Model):
EVENT_CHOICES = [
('welcome', 'Приветствие'),
('draw_started', 'Розыгрыш начат'),
('draw_finished', 'Розыгрыш завершен'),
('winner_announced', 'Объявление победителя'),
('guest_binding', 'Подтверждение гостя'),
# и другие
]
event = models.CharField("Событие", max_length=50, choices=EVENT_CHOICES, unique=True)
message = models.ForeignKey(BotMessage, on_delete=models.CASCADE, related_name="event_configs")
enabled = models.BooleanField("Активно", default=True)
class Meta:
verbose_name = "Событие сообщения"
verbose_name_plural = "События сообщений"
def __str__(self):
return f"{self.get_event_display()}{self.message.name}"

View File

@@ -1,105 +1,131 @@
# notifications.py
import logging
from asgiref.sync import sync_to_async
from bot.utils import send_event_message_async
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class NotificationService:
def __init__(self, bot):
"""
Инициализация сервиса уведомлений.
:param bot: экземпляр telegram.Bot, используемый для отправки сообщений.
"""
self.bot = bot
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger("NotificationService")
self.logger.setLevel(logging.DEBUG)
if not self.logger.handlers:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
self.logger.addHandler(handler)
@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"]))
clients = list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"]))
self.logger.info(f"Найдено клиентов для уведомления: {len(clients)}")
return clients
@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"
self.logger.debug(f"Отправка 'draw_started' клиенту {client.telegram_id}")
await send_event_message_async(
event="draw_started",
bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"lottery": lottery, "client": client}
)
except Exception as e:
self.logger.error(f"Ошибка отправки уведомления о запуске розыгрыша пользователю {client.telegram_id}: {e}")
self.logger.error(f"Ошибка отправки draw_started: {e}")
async def notify_draw_results(self, lottery, results):
"""
Отправляет результаты розыгрыша всем клиентам.
:param lottery: объект розыгрыша (Lottery)
:param results: QuerySet с результатами розыгрыша (например, объекты DrawResult)
"""
# Принудительно загружаем связанные объекты
# async def notify_draw_results(self, lottery, results):
# results_list = await self._prepare_results(results)
# clients = await self._get_all_clients()
# for client in clients:
# if not client.telegram_id:
# continue
# try:
# self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}")
# await send_event_message_async(
# event="draw_finished",
# bot=self.bot,
# chat_id=int(client.telegram_id),
# context_vars={"lottery": lottery, "client": client, "results": results_list}
# )
# except Exception as e:
# self.logger.error(f"Ошибка отправки draw_finished: {e}")
async def notify_draw_results(self, lottery, results, context_vars=None):
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()
# 🔢 Подготовка суммы и списка победителей
total_reward = sum(
r.prize.reward for r in results_list if r.prize and r.prize.reward
)
winners_lines = []
for r in results_list:
try:
account_id = r.participant.invoice.ext_id
prize_amount = r.prize.reward
winners_lines.append(f"• Счёт {account_id}{prize_amount:,.2f}")
except Exception:
continue
winners_list = "\n".join(winners_lines)
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"
self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}")
await send_event_message_async(
event="draw_finished",
bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={
**(context_vars or {}),
"lottery": lottery,
"client": client,
"results": results_list,
"total_reward": f"{total_reward:,.2f}",
"winners_list": winners_list
}
)
except Exception as e:
self.logger.error(f"Ошибка отправки результатов розыгрыша пользователю {client.telegram_id}: {e}")
self.logger.error(f"Ошибка отправки draw_finished: {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"
self.logger.debug(f"Отправка 'winner_announced' клиенту {client.telegram_id}")
await send_event_message_async(
event="winner_announced",
bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"client": client, "result": result, "prize": result.prize}
)
except Exception as e:
self.logger.error(f"Ошибка отправки обновления статуса приза пользователю {client.telegram_id}: {e}")
self.logger.error(f"Ошибка отправки winner_announced: {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"
self.logger.debug(f"Отправка 'binding_complete' клиенту {client.telegram_id}")
await send_event_message_async(
event="binding_complete",
bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"client": client}
)
except Exception as e:
self.logger.error(f"Ошибка отправки уведомления о привязке пользователю {client.telegram_id}: {e}")
self.logger.error(f"Ошибка отправки binding_complete: {e}")

View File

@@ -1,39 +1,99 @@
# bot/utils.py
from telegram import Bot
from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton
from django.conf import settings
from telegram import Bot
from bot.models import BotConfig
from django.core.exceptions import ImproperlyConfigured
from urllib.parse import urljoin
from bot.models import BotConfig, BotEventMessageConfig
import logging
from asgiref.sync import sync_to_async
from telegram import InputFile
import os
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def create_bot_instance():
"""
Получает настройки бота из БД и создаёт экземпляр Telegram Bot.
"""
config = BotConfig.objects.first() # предполагается, что конфигурация одна
config = BotConfig.objects.first()
if not config:
raise ImproperlyConfigured("Настройки бота (BotConfig) не сконфигурированы в базе данных.")
raise ImproperlyConfigured("BotConfig не задан.")
# Можно дополнительно использовать config.channel_id и config.bot_name при необходимости.
bot = Bot(token=config.bot_token)
return bot
logger.debug("Создан экземпляр бота с токеном.")
return Bot(token=config.bot_token)
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 "Ваша заявка на привязку отклонена."
def get_event_message_sync(event_key: str, context_vars: dict = None):
logger.debug(f"get_event_message_sync для события: {event_key}")
try:
bot.send_message(chat_id=binding_request.telegram_chat_id, text=message)
config = BotEventMessageConfig.objects.select_related("message").get(event=event_key, enabled=True)
msg = config.message
context_vars = context_vars or {}
text = msg.text.format(**context_vars) if msg.text else None
image_name = msg.image.name if msg.image else None # только имя файла
keyboard = None
if msg.buttons_json:
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton(btn["text"], url=btn["url"])]
for btn in msg.buttons_json
])
return {
"text": text,
"image": image_name,
"keyboard": keyboard
}
except Exception as e:
# Обработка ошибок отправки сообщения
print(f"Ошибка уведомления: {e}")
logger.error(f"Ошибка get_event_message_sync для события '{event_key}': {e}")
return None
# Пример использования в точке входа приложения:
if __name__ == '__main__':
# Предполагается, что Django уже настроен (например, через manage.py shell или management command)
bot = create_bot_instance()
# Теперь можно использовать объект bot для отправки сообщений, обработки обновлений и т.д.
print(f"Бот {bot.name} успешно создан!")
def send_event_message_sync(event: str, bot: Bot, chat_id: int, context_vars: dict = None):
msg = get_event_message_sync(event, context_vars)
if not msg:
logger.warning(f"Пропущена отправка: шаблон события '{event}' не найден.")
return
try:
if msg["image"]:
bot.send_photo(chat_id=chat_id, photo=msg["image"], caption=msg["text"], reply_markup=msg["keyboard"])
elif msg["text"]:
bot.send_message(chat_id=chat_id, text=msg["text"], reply_markup=msg["keyboard"])
logger.info(f"Сообщение '{event}' успешно отправлено пользователю {chat_id}")
except Exception as e:
logger.error(f"Ошибка при отправке сообщения '{event}' пользователю {chat_id}: {e}")
@sync_to_async
def get_event_message_async(event_key: str, context_vars: dict = None):
return get_event_message_sync(event_key, context_vars)
async def send_event_message_async(event: str, bot: Bot, chat_id: int, context_vars: dict = None):
msg = await get_event_message_async(event, context_vars)
if not msg:
logger.warning(f"[ASYNC] Пропущена отправка: шаблон события '{event}' не найден.")
return
try:
if msg["image"]:
# локальный путь к файлу
from django.conf import settings
file_path = os.path.join(settings.MEDIA_ROOT, msg["image"])
logger.debug(f"[ASYNC] Отправка фото из {file_path}")
with open(file_path, "rb") as photo:
await bot.send_photo(
chat_id=chat_id,
photo=InputFile(photo),
caption=msg["text"],
reply_markup=msg["keyboard"]
)
elif msg["text"]:
await bot.send_message(
chat_id=chat_id,
text=msg["text"],
reply_markup=msg["keyboard"]
)
logger.info(f"[ASYNC] Сообщение '{event}' отправлено пользователю {chat_id}")
except Exception as e:
logger.error(f"[ASYNC] Ошибка Telegram при отправке '{event}' пользователю {chat_id}: {e}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -1,6 +0,0 @@
[mariadb-client]
port=3306
socket=/run/mysqld/mysqld.sock
user=healthcheck
password=9hOHOe^WXq`lsXwRba/@]gB3"|[mOFe{

Binary file not shown.

Binary file not shown.

View File

@@ -1,269 +0,0 @@
59,5
59,4
59,3
59,2
59,1
59,0
56,4
56,3
56,2
56,1
56,0
55,4
55,3
55,2
55,1
55,0
53,4
53,3
53,2
53,1
53,0
52,5
52,4
52,3
52,2
52,1
52,0
51,5
51,4
51,3
51,2
51,1
51,0
50,6
50,5
50,4
50,3
50,2
50,1
50,0
37,3
37,2
37,1
37,0
36,4
36,3
36,2
36,1
36,0
35,4
35,3
35,2
35,1
35,0
34,3
34,2
34,1
34,0
32,4
32,3
32,2
32,1
32,0
28,4
28,3
28,2
28,1
28,0
27,4
27,3
27,2
27,1
27,0
26,5
26,4
26,3
26,2
26,1
26,0
25,5
25,4
25,3
25,2
25,1
25,0
22,5
22,4
22,3
22,2
22,1
22,0
20,5
20,4
20,3
20,2
20,1
20,0
18,5
18,4
18,3
18,2
18,1
18,0
11,4
11,3
11,2
11,1
11,0
8,3
8,2
8,1
8,0
7,3
5,5
5,4
5,3
5,2
5,0
4,3
3,2
2,2
1,2
0,9
0,2
1,45
3,44
2,44
1,44
3,43
2,43
1,43
3,42
2,42
1,42
3,41
2,41
1,41
3,40
2,40
1,40
3,39
2,39
1,39
3,38
2,38
1,38
3,37
2,37
1,37
3,36
2,36
1,36
3,35
2,35
1,35
3,34
2,34
1,34
3,33
2,33
1,33
3,32
2,32
1,32
3,31
2,31
1,31
3,30
2,30
1,30
3,29
2,29
1,29
3,28
2,28
1,28
3,27
2,27
1,27
3,26
2,26
1,26
3,25
2,25
1,25
3,24
2,24
1,24
3,23
2,23
1,23
3,22
2,22
1,22
3,21
2,21
1,21
3,20
2,20
1,20
3,19
2,19
1,19
3,18
2,18
1,18
3,17
2,17
1,17
3,16
2,16
1,16
3,15
2,15
1,15
3,14
2,14
1,14
3,13
2,13
1,13
3,12
2,12
1,12
3,11
2,11
1,11
3,10
2,10
1,10
3,9
2,9
1,9
3,8
2,8
1,8
3,7
2,7
1,7
3,6
2,6
1,6
3,5
2,5
1,5
3,4
2,4
1,4
3,3
3,0
2,3
2,0
1,3
1,0
0,6
0,0
0,47
0,46
0,49
0,48
0,45
0,12
0,10
0,8
0,11
0,5
0,7
0,4
0,3

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

View File

@@ -1 +0,0 @@
11.6.2-MariaDB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More