import os import asyncio from functools import wraps from dotenv import load_dotenv from aiogram import Bot, Dispatcher, types from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo from aiogram.filters import Command from aiohttp import web import database as db from phone_utils import normalize_phone from qr_utils import decode_qr_from_image, generate_qr_code # Загружаем переменные из .env load_dotenv() # ========== КОНФИГУРАЦИЯ ИЗ .ENV ========== BOT_TOKEN = os.getenv("BOT_TOKEN") WEBAPP_HOST = os.getenv("WEBAPP_HOST", "0.0.0.0") WEBAPP_PORT = int(os.getenv("WEBAPP_PORT", "8085")) ADMIN_ID = int(os.getenv("ADMIN_ID")) DOMAIN = os.getenv("DOMAIN") # Проверяем, что все переменные загружены if not BOT_TOKEN: raise ValueError("BOT_TOKEN не найден в .env файле") if not ADMIN_ID: raise ValueError("ADMIN_ID не найден в .env файле") if not DOMAIN: raise ValueError("DOMAIN не найден в .env файле") bot = Bot(token=BOT_TOKEN) dp = Dispatcher() # ========== ДЕКОРАТОР ПРОВЕРКИ ДОСТУПА ========== def allowed_only(func): @wraps(func) async def wrapper(message: Message, *args, **kwargs): if not db.is_user_allowed(message.from_user.id) and message.from_user.id != ADMIN_ID: await message.answer("⛔ Доступ запрещён, брат. Обратись к админу.") return return await func(message, *args, **kwargs) return wrapper # ========== ОСНОВНЫЕ КОМАНДЫ ========== @dp.message(Command("start")) async def cmd_start(message: Message): user_id = message.from_user.id if not db.is_user_allowed(user_id) and user_id != ADMIN_ID: await message.answer("⛔ Тебя нет в белом списке.") return keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="📋 Панель управления", web_app=WebAppInfo(url=f"https://{DOMAIN}/get_webapp"))] ]) await message.answer( "🌑 *МракоЗвон*\n\n" "📥 Отправь номер телефона — сохраню.\n" "🃏 Отправь QR-код бонусной карты — привяжу.\n" "📎 Отправь номер + QR вместе — свяжу их.\n\n" "🎲 /get — случайный номер из базы\n" "📊 /stats — статистика\n" "🗑 /del <номер> — удалить номер\n\n" "🌐 Кнопка ниже — веб-морда", reply_markup=keyboard, parse_mode="Markdown" ) @dp.message(Command("get")) @allowed_only async def cmd_get(message: Message): result = db.get_random_from_oldest() if not result: await message.answer("❌ База пуста, брат. Добавь номера!") return phone = result['phone'] bonus_card = result.get('bonus_card') if bonus_card: await message.answer_photo( photo=bonus_card, caption=f"🎲 *МракоЗвон*\n" f"📞 Номер: `{phone}`\n" f"🃏 Бонусная карта привязана\n" f"🔄 Использован: {result['usage_count']} раз", parse_mode="Markdown" ) else: await message.answer( f"🎲 *МракоЗвон*\n" f"📞 `{phone}`\n" f"🔄 Использован: {result['usage_count']} раз\n" f"❌ Бонусная карта отсутствует", parse_mode="Markdown" ) @dp.message(Command("stats")) @allowed_only async def cmd_stats(message: Message): all_nums = db.get_all_numbers() total = len(all_nums) used_total = sum(n["usage_count"] for n in all_nums) with_card = sum(1 for n in all_nums if n.get("bonus_card")) await message.answer( f"📊 *Статистика МракоЗвона*\n\n" f"📞 Всего номеров: {total}\n" f"🃏 Из них с бонусными картами: {with_card}\n" f"🔄 Всего использований: {used_total}", parse_mode="Markdown" ) @dp.message(Command("del")) @allowed_only async def cmd_delete_number(message: Message): args = message.text.split() if len(args) < 2: await message.answer("ℹ️ Использование: /del <номер>\nПример: /del 89231234567") return normalized = normalize_phone(args[1]) if not normalized: await message.answer("⚠️ Некорректный формат номера") return all_nums = db.get_all_numbers() for num in all_nums: if num["phone"] == normalized: db.delete_number(num["id"]) await message.answer(f"✅ Номер {normalized} удалён") return await message.answer(f"❌ Номер {normalized} не найден") # ========== АДМИН-КОМАНДЫ ========== @dp.message(Command("adduser")) async def cmd_add_user(message: Message): if message.from_user.id != ADMIN_ID: await message.answer("⛔ Только админ.") return args = message.text.split() if len(args) < 2: await message.answer("ℹ️ /adduser [username]\nПример: /adduser 123456789 брат") return try: user_id = int(args[1]) username = args[2] if len(args) > 2 else None if db.add_allowed_user(user_id, username, str(message.from_user.id)): await message.answer(f"✅ Пользователь {user_id} добавлен в белый список!") else: await message.answer(f"⚠️ Пользователь {user_id} уже есть в списке.") except ValueError: await message.answer("❌ user_id должен быть числом.") @dp.message(Command("removeuser")) async def cmd_remove_user(message: Message): if message.from_user.id != ADMIN_ID: await message.answer("⛔ Только админ.") return args = message.text.split() if len(args) < 2: await message.answer("ℹ️ /removeuser ") return try: user_id = int(args[1]) if user_id == ADMIN_ID: await message.answer("❌ Нельзя удалить админа") return if db.remove_allowed_user(user_id): await message.answer(f"✅ Пользователь {user_id} удалён из белого списка") else: await message.answer("⚠️ Пользователь не найден") except ValueError: await message.answer("❌ user_id должен быть числом.") @dp.message(Command("whitelist")) async def cmd_whitelist(message: Message): if message.from_user.id != ADMIN_ID: await message.answer("⛔ Только админ.") return users = db.get_allowed_users() if not users: await message.answer("📭 Белый список пуст") return text = "📋 *Белый список пользователей:*\n\n" for u in users: text += f"• ID: `{u['user_id']}`" if u['username']: text += f" (@{u['username']})" text += f"\n Добавлен: {u['added_at'][:19]}\n\n" if len(text) > 4000: await message.answer("📋 Слишком много записей, смотри в БД.") else: await message.answer(text, parse_mode="Markdown") # ========== ОБРАБОТЧИК СООБЩЕНИЙ (НОМЕРА + QR) ========== @dp.message() @allowed_only async def handle_message(message: Message): phone = None qr_raw_data = None # 1. Если есть фото - пытаемся распознать QR if message.photo: photo = message.photo[-1] file = await bot.get_file(photo.file_id) file_bytes = await bot.download_file(file.file_path) qr_raw_data = decode_qr_from_image(file_bytes.read()) # 2. Если есть текст - пробуем извлечь номер if message.text: phone = normalize_phone(message.text.strip()) # 3. Если нет ничего if not phone and not qr_raw_data: await message.answer( "⚠️ Не распознал.\n\n" "Отправь:\n" "• номер телефона текстом\n" "• бонусную карту (QR-код)\n" "• номер + карту вместе" ) return # 4. Только номер if phone and not qr_raw_data: if db.add_number(phone): await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown") else: await message.answer(f"⚠️ Номер `{phone}` уже есть в базе", parse_mode="Markdown") return # 5. Только QR (без номера) if qr_raw_data and not phone: await message.answer( f"🃏 Распознана бонусная карта:\n" f"`{qr_raw_data[:50]}{'...' if len(qr_raw_data) > 50 else ''}`\n\n" f"⚠️ Чтобы привязать карту к номеру, отправь и номер тоже.\n" f"Пример: номер и карту в одном сообщении" ) return # 6. Номер + QR вместе if phone and qr_raw_data: qr_base64 = generate_qr_code(qr_raw_data) if db.add_number(phone, qr_base64): await message.answer( f"✅ Сохранено:\n" f"📞 Номер: `{phone}`\n" f"🃏 Бонусная карта привязана", parse_mode="Markdown" ) else: # Если номер уже есть - обновляем карту if db.update_bonus_card(phone, qr_base64): await message.answer( f"🔄 Обновлена бонусная карта для номера `{phone}`", parse_mode="Markdown" ) else: await message.answer(f"⚠️ Ошибка сохранения", parse_mode="Markdown") return # ========== ВЕБ-СЕРВЕР ========== routes = web.RouteTableDef() @routes.get("/get_webapp") async def serve_webapp(request): try: with open("web_app.html", "r", encoding="utf-8") as f: html = f.read() return web.Response(text=html, content_type="text/html") except FileNotFoundError: return web.Response(text="web_app.html not found", status=404) @routes.get("/get_numbers") async def get_numbers(request): numbers = db.get_all_numbers() safe_numbers = [] for num in numbers: safe_numbers.append({ "id": num["id"], "phone": num["phone"], "has_bonus_card": bool(num.get("bonus_card")), "created_at": num["created_at"], "used_at": num["used_at"], "usage_count": num["usage_count"] }) return web.json_response({"numbers": safe_numbers}) @routes.post("/delete_number") async def delete_number(request): data = await request.json() db.delete_number(data["id"]) return web.json_response({"status": "ok"}) @routes.get("/get_bonus_card/{record_id}") async def get_bonus_card(request): record_id = int(request.match_info.get('record_id', 0)) bonus_card = db.get_bonus_card(record_id) if bonus_card: return web.json_response({"bonus_card": bonus_card}) return web.json_response({"bonus_card": None}) async def start_webapp(): app = web.Application() app.add_routes(routes) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, WEBAPP_HOST, WEBAPP_PORT) await site.start() print(f"✅ Веб-морда запущена на порту {WEBAPP_PORT}") # ========== ЗАПУСК ========== async def main(): db.init_db() db.add_allowed_user(ADMIN_ID, "admin", "system") asyncio.create_task(start_webapp()) print(f"🚀 МракоЗвон запущен!") print(f"📱 Домен: https://{DOMAIN}") print(f"🔌 Порт: {WEBAPP_PORT}") await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())