MpakoPhone/bot.py

363 lines
13 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 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 <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")
@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_image_bytes = generate_qr_image(qr_raw_data)
qr_image_base64 = generate_qr_base64(qr_raw_data)
# 2. Извлекаем номер из текста ИЛИ из подписи к фото
text = message.text or message.caption
if text:
phone = normalize_phone(text.strip())
# 3. Определяем тип записи и сохраняем
if phone and qr_raw_data:
db.add_record('both', phone=phone, 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"✅ Сохранено: номер `{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()
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"),
"has_qr": bool(r.get("qr_code")),
"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())