"""Telegram-бот + Mini App HTTP (+ Telegram webhook).""" from __future__ import annotations import asyncio import logging import signal from typing import Any, Awaitable, Callable from aiogram import BaseMiddleware, Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import MenuButtonWebApp, TelegramObject, WebAppInfo from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application from config import Settings, get_settings from filters import IsAdmin from handlers_admin import router as admin_router from handlers_admin import setup_admin_router from handlers_shop import router as shop_router from handlers_tickets import router as tickets_router from handlers_user import router as user_router from remnawave_client import create_sdk_ready, probe_users_write_access from services.anypay import Anypay from services.cryptopay import CryptoPay from services.db import Database from services.digiseller import Digiseller from services.heleket import Heleket from services.lava import Lava from services.extras_billing import extras_billing_worker from services.subscription_renewal import subscription_renewal_worker from services.vpn_configs_expiry import vpn_configs_expiry_worker from services.tariffs import load_tariffs from webapp_server import create_webapp, start_webapp_site logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger(__name__) class InjectMiddleware(BaseMiddleware): """Проброс settings, SDK, DB, тарифов и платёжек в хендлеры.""" def __init__( self, settings: Settings, sdk, db: Database, tariffs, cryptopay: CryptoPay, heleket: Heleket, digiseller: Digiseller, lava: Lava, anypay: Anypay, ) -> None: self.settings = settings self.sdk = sdk self.db = db self.tariffs = tariffs self.cryptopay = cryptopay self.heleket = heleket self.digiseller = digiseller self.lava = lava self.anypay = anypay async def __call__( self, handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, data: dict[str, Any], ) -> Any: data["settings"] = self.settings data["sdk"] = self.sdk data["db"] = self.db data["tariffs"] = self.tariffs data["cryptopay"] = self.cryptopay data["heleket"] = self.heleket data["digiseller"] = self.digiseller data["lava"] = self.lava data["anypay"] = self.anypay # Бан: режем обычные апдейты (админам можно) user = getattr(event, "from_user", None) or getattr( getattr(event, "message", None), "from_user", None ) if user and user.id not in self.settings.admin_ids: bot_user = self.db.get_user(user.id) if bot_user and bot_user.banned: from aiogram.types import CallbackQuery, Message try: if isinstance(event, CallbackQuery): await event.answer("🚫 Аккаунт заблокирован.", show_alert=True) elif isinstance(event, Message): await event.answer("🚫 Аккаунт заблокирован.") except Exception: # noqa: BLE001 pass return None return await handler(event, data) async def _idle_until_stop() -> None: """Держим процесс живым в режиме webhook (вместо polling).""" stop = asyncio.Event() loop = asyncio.get_running_loop() def _stop(*_: Any) -> None: stop.set() for sig in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(sig, _stop) except (NotImplementedError, RuntimeError): pass await stop.wait() async def main() -> None: settings = get_settings(require_bot_token=True) if not settings.admin_ids: logger.warning("ADMIN_IDS пуст — админка будет недоступна никому") if settings.web_admin_enabled: logger.info( "Web admin /admin OK (login=%s, pass_len=%s)", settings.web_admin_login, len(settings.web_admin_password), ) else: logger.warning( "Web admin /admin выключен — задайте WEB_ADMIN_LOGIN и WEB_ADMIN_PASSWORD в .env" ) tariffs = load_tariffs(settings.tariffs_path) logger.info("Loaded %s tariffs from %s", len(tariffs), settings.tariffs_path) logger.info( "Telegram Stars: %s (1⭐ ≈ %s ₽)", "ON" if settings.stars_enabled else "OFF", settings.stars_rub_per_star, ) cryptopay = CryptoPay( settings.crypto_bot_token, testnet=settings.crypto_pay_testnet, ) if cryptopay.enabled: try: me_cp = await cryptopay.get_me() logger.info("Crypto Pay OK: %s", me_cp) except Exception as exc: # noqa: BLE001 logger.error("Crypto Pay token invalid: %s", exc) else: logger.warning("CRYPTO_BOT_TOKEN пуст — Crypto Bot выключен") heleket = Heleket( settings.heleket_merchant_id, settings.heleket_api_key, callback_url=settings.heleket_callback_url, currency=settings.heleket_currency, ) if heleket.enabled: logger.info( "Heleket OK (merchant=%s… callback=%s currency=%s)", settings.heleket_merchant_id[:8], settings.heleket_callback_url or "—", settings.heleket_currency, ) else: logger.warning("Heleket не настроен — резервная крипта выключена") digiseller = Digiseller( settings.digiseller_seller_id, settings.digiseller_api_key, product_ids=settings.digiseller_product_ids, pay_url=settings.digiseller_pay_url, credit_from=settings.digiseller_credit_from, ) if digiseller.enabled: logger.info( "Digiseller OK (seller=%s products=%s credit=%s callback=%s)", settings.digiseller_seller_id, settings.digiseller_product_ids or "any", settings.digiseller_credit_from, settings.digiseller_callback_url or "—", ) else: logger.warning("Digiseller не настроен — пополнение по уник. коду выключено") lava = Lava( settings.lava_shop_id, settings.lava_secret_key, additional_key=settings.lava_additional_key, hook_url=settings.lava_hook_url, success_url=settings.lava_success_url, fail_url=settings.lava_fail_url, expire_minutes=settings.lava_expire_minutes, ) if lava.enabled: logger.info( "Lava OK (shop=%s… hook=%s success=%s)", settings.lava_shop_id[:8], settings.lava_hook_url or "—", settings.lava_success_url or "—", ) else: logger.warning("Lava не настроен — карта/СБП выключены") anypay = Anypay( settings.anypay_api_id, settings.anypay_api_key, settings.anypay_project_id, currency=settings.anypay_currency, method=settings.anypay_method, success_url=settings.anypay_success_url, fail_url=settings.anypay_fail_url, default_email=settings.anypay_email or "pay@anypay.local", check_ip=settings.anypay_check_ip, ) if anypay.enabled: logger.info( "AnyPay OK (api_id=%s… project=%s notify=%s/api/payments/anypay)", settings.anypay_api_id[:6], settings.anypay_project_id, settings.webapp_url or "", ) else: logger.warning("AnyPay не настроен — ANYPAY_API_ID / KEY / PROJECT_ID") db = Database(settings.database_url) sdk = await create_sdk_ready(settings) ok_write, write_msg = await probe_users_write_access(sdk) if ok_write: logger.info("Remnawave write access: %s", write_msg) else: logger.error("Remnawave write access FAILED:\n%s", write_msg) bot = Bot( token=settings.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML), ) me = await bot.get_me() bot_username = me.username or "" dp = Dispatcher(storage=MemoryStorage()) inject = InjectMiddleware( settings, sdk, db, tariffs, cryptopay, heleket, digiseller, lava, anypay ) dp.update.middleware(inject) setup_admin_router(IsAdmin(settings)) dp.include_router(admin_router) dp.include_router(shop_router) dp.include_router(tickets_router) dp.include_router(user_router) web_runner = None web_app = create_webapp( settings=settings, db=db, sdk=sdk, tariffs=tariffs, cryptopay=cryptopay, heleket=heleket, digiseller=digiseller, lava=lava, anypay=anypay, bot=bot, bot_username=bot_username, ) web_app["inject"] = inject use_webhook = bool(settings.telegram_webhook_enabled) webhook_url = settings.effective_telegram_webhook_url webhook_path = settings.effective_telegram_webhook_path if use_webhook: if not webhook_url.startswith("https://"): logger.error( "TELEGRAM_WEBHOOK включён, но нет HTTPS URL (WEBAPP_URL / TELEGRAM_WEBHOOK_URL). " "Fallback на long polling." ) use_webhook = False else: SimpleRequestHandler( dispatcher=dp, bot=bot, secret_token=settings.telegram_webhook_secret or None, ).register(web_app, path=webhook_path) setup_application(web_app, dp, bot=bot) web_runner = await start_webapp_site( web_app, settings.webapp_host, settings.webapp_port ) if settings.webapp_url: try: await bot.set_chat_menu_button( menu_button=MenuButtonWebApp( text="Кабинет", web_app=WebAppInfo(url=settings.webapp_url), ) ) logger.info("Menu button WebApp -> %s", settings.webapp_url) except Exception as exc: # noqa: BLE001 logger.warning("Не удалось поставить Menu Button: %s", exc) else: logger.warning( "WEBAPP_URL пуст — кнопка Mini App скрыта. " "Нужен публичный HTTPS (Caddy/nginx) на порт %s", settings.webapp_port, ) logger.info("Bot started as @%s (id=%s)", me.username, me.id) asyncio.create_task( extras_billing_worker(db, sdk, bot), name="extras-billing", ) asyncio.create_task( subscription_renewal_worker(db, sdk, bot, tariffs), name="subscription-renewal", ) asyncio.create_task( vpn_configs_expiry_worker(db, sdk), name="vpn-configs-expiry", ) try: if use_webhook: await bot.set_webhook( url=webhook_url, secret_token=settings.telegram_webhook_secret or None, drop_pending_updates=True, allowed_updates=dp.resolve_used_update_types(), ) info = await bot.get_webhook_info() logger.info( "Telegram webhook ON → %s (pending=%s)", info.url or webhook_url, info.pending_update_count, ) await _idle_until_stop() else: await bot.delete_webhook(drop_pending_updates=False) logger.info("Telegram long polling ON (webhook выключен)") await dp.start_polling(bot) finally: db.close() if web_runner is not None: await web_runner.cleanup() await bot.session.close() if __name__ == "__main__": asyncio.run(main())