MpakoPhone/bot.py

356 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 <user_id> [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 <user_id>")
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())