import base64 import os import asyncio import sqlite3 from functools import wraps from dotenv import load_dotenv from aiogram import Bot, Dispatcher, types from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo, MenuButtonWebApp, \ BufferedInputFile 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_base64, generate_qr_image # Загружаем переменные из .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): record = db.get_random_record() if not record: await message.answer("❌ База пуста, брат!") return record_type = record['type'] phone = record.get('phone') qr_code_base64 = record.get('qr_code') qr_data = record.get('qr_data') if (record_type == 'both' or record_type == 'qr') and qr_code_base64: # Конвертируем base64 в байты для отправки if qr_code_base64.startswith('data:image/png;base64,'): img_base64 = qr_code_base64.replace('data:image/png;base64,', '') img_bytes = base64.b64decode(img_base64) photo_file = BufferedInputFile(img_bytes, filename="qr.png") if record_type == 'both': await message.answer_photo( photo=photo_file, caption=f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🃏 Бонусная карта\n🔄 Использован: {record['usage_count']} раз", parse_mode="Markdown" ) else: short_data = qr_data[:50] + '...' if qr_data and len(qr_data) > 50 else (qr_data or '—') await message.answer_photo( photo=photo_file, caption=f"🎲 *МракоЗвон*\n🃏 Бонусная карта\n📝 Данные: `{short_data}`\n🔄 Использована: {record['usage_count']} раз", parse_mode="Markdown" ) elif record_type == 'phone': await message.answer( f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🔄 Использован: {record['usage_count']} раз", parse_mode="Markdown" ) @dp.message(Command("stats")) @allowed_only async def cmd_stats(message: Message): records = db.get_all_records() total = len(records) phones = sum(1 for r in records if r['type'] in ('phone', 'both')) qrs = sum(1 for r in records if r['type'] in ('qr', 'both')) both = sum(1 for r in records if r['type'] == 'both') await message.answer( f"📊 *Статистика МракоЗвона*\n\n" f"📝 Всего записей: {total}\n" f"📞 Из них номеров: {phones}\n" f"🃏 Из них карт: {qrs}\n" f"🔗 Связок номер+карта: {both}", 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 # Ищем запись с таким номером records = db.get_all_records() for record in records: if record.get('phone') == normalized: db.delete_record(record['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") @dp.message(Command("update_menu")) @allowed_only async def cmd_update_menu(message: Message): menu_button = MenuButtonWebApp( text="📱 Открыть панель", web_app=WebAppInfo(url=f"https://{DOMAIN}/get_webapp") ) await bot.set_chat_menu_button(chat_id=message.from_user.id, menu_button=menu_button) await message.answer("✅ Кнопка меню обновлена!") # ========== ОБРАБОТЧИК СООБЩЕНИЙ ========== @dp.message() @allowed_only async def handle_message(message: Message): phone = None qr_raw_data = None qr_image_bytes = None qr_image_base64 = 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()) if qr_raw_data: # Генерируем новый чистый QR из распознанных данных qr_image_bytes = generate_qr_image(qr_raw_data) qr_image_base64 = generate_qr_base64(qr_raw_data) # 2. Извлекаем номер из текста if message.text: phone = normalize_phone(message.text.strip()) # 3. Определяем тип записи и сохраняем if phone and qr_raw_data: # Сохраняем base64 для веб-морды и байты для Telegram db.add_record('both', phone=phone, qr_code=qr_image_base64, qr_data=qr_raw_data) # Отправляем сгенерированный QR в ответ photo_file = BufferedInputFile(qr_image_bytes, filename="qr.png") await message.answer_photo( photo=photo_file, caption=f"✅ Сохранено: номер `{phone}` + бонусная карта", parse_mode="Markdown" ) elif phone: db.add_record('phone', phone=phone) await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown") elif qr_raw_data: db.add_record('qr', qr_code=qr_image_base64, qr_data=qr_raw_data) photo_file = BufferedInputFile(qr_image_bytes, filename="qr.png") await message.answer_photo( photo=photo_file, caption=f"🃏 Сохранена бонусная карта\n📝 Данные: `{qr_raw_data[:50]}...`", parse_mode="Markdown" ) else: await message.answer("⚠️ Не распознано. Отправь номер, QR-код или оба вместе.") # ========== ВЕБ-СЕРВЕР ========== 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_records") async def get_records(request): records = db.get_all_records() # Убираем qr_code из списка для экономии (будем подгружать отдельно) safe_records = [] for r in records: safe_records.append({ "id": r["id"], "type": r["type"], "phone": r.get("phone"), "qr_data": r.get("qr_data"), "created_at": r["created_at"], "used_at": r.get("used_at"), "usage_count": r["usage_count"] }) return web.json_response({"records": safe_records}) @routes.get("/get_qr_code/{record_id}") async def get_qr_code(request): record_id = int(request.match_info.get('record_id', 0)) with sqlite3.connect(db.DB_NAME) as conn: conn.row_factory = sqlite3.Row result = conn.execute("SELECT qr_code FROM records WHERE id = ?", (record_id,)).fetchone() if result and result['qr_code']: return web.json_response({"qr_code": result['qr_code']}) return web.json_response({"qr_code": None}) @routes.post("/delete_record") async def delete_record(request): data = await request.json() db.delete_record(data["id"]) return web.json_response({"status": "ok"}) 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())