Files
remnawave-minishop/bot/main_bot.py
T
machka-pasla 18f65ea493 Refactor notification handling and streamline router registration
- Replaced legacy notification functions with a unified NotificationService for better maintainability and clarity.
- Updated the main bot router registration to utilize a root router, simplifying the inclusion of user and admin routes.
- Removed unused middleware and helper functions to enhance code cleanliness and focus on essential components.
- Improved localization by adding new error messages for user interactions.
2025-08-07 23:30:32 +03:00

485 lines
19 KiB
Python

import logging
import asyncio
from typing import Dict, Any, Optional
from aiogram import Bot, Dispatcher
from aiogram.types import (MenuButtonDefault, MenuButtonWebApp, WebAppInfo, BotCommand)
from aiogram.enums import ParseMode
from aiogram.client.default import DefaultBotProperties
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiogram.fsm.storage.memory import MemoryStorage
from aiohttp import web
from bot.services.panel_webhook_service import PanelWebhookService, panel_webhook_route
from sqlalchemy.orm import sessionmaker
from config.settings import Settings
from db.database_setup import init_db_connection
from bot.middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n
from bot.middlewares.db_session import DBSessionMiddleware
from bot.middlewares.ban_check_middleware import BanCheckMiddleware
from bot.middlewares.action_logger_middleware import ActionLoggerMiddleware
from bot.routers import build_root_router
from bot.services.yookassa_service import YooKassaService
from bot.services.panel_api_service import PanelApiService
from bot.services.subscription_service import SubscriptionService
from bot.services.referral_service import ReferralService
from bot.services.promo_code_service import PromoCodeService
from bot.services.stars_service import StarsService
from bot.services.tribute_service import TributeService, tribute_webhook_route
from bot.services.crypto_pay_service import CryptoPayService, cryptopay_webhook_route
from bot.handlers.user import payment as user_payment_webhook_module
from bot.handlers.admin.sync_admin import perform_sync
from bot.utils.message_queue import init_queue_manager
async def register_all_routers(dp: Dispatcher, settings: Settings):
dp.include_router(build_root_router(settings))
logging.info("All application routers registered.")
async def on_startup_configured(dispatcher: Dispatcher):
bot: Bot = dispatcher["bot_instance"]
settings: Settings = dispatcher["settings"]
i18n_instance: JsonI18n = dispatcher["i18n_instance"]
panel_service: PanelApiService = dispatcher["panel_service"]
async_session_factory: sessionmaker = dispatcher["async_session_factory"]
logging.info("STARTUP: on_startup_configured executing...")
telegram_webhook_url_to_set = settings.WEBHOOK_BASE_URL
if telegram_webhook_url_to_set:
full_telegram_webhook_url = (
f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
)
logging.info(
f"STARTUP: Attempting to set Telegram webhook to: {full_telegram_webhook_url if full_telegram_webhook_url != 'ERROR_URL_TOKEN_DETECTED' else 'HIDDEN DUE TO TOKEN'}"
)
if full_telegram_webhook_url != "ERROR_URL_TOKEN_DETECTED":
try:
current_webhook_info = await bot.get_webhook_info()
logging.info(
f"STARTUP: Current Telegram webhook info BEFORE setting: {current_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
)
set_success = await bot.set_webhook(
url=full_telegram_webhook_url,
drop_pending_updates=True,
allowed_updates=dispatcher.resolve_used_update_types(),
)
if set_success:
logging.info(
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True)."
)
else:
logging.error(
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False)."
)
new_webhook_info = await bot.get_webhook_info()
logging.info(
f"STARTUP: Telegram Webhook info AFTER setting: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
)
if not new_webhook_info.url:
logging.error(
"STARTUP: CRITICAL - Telegram Webhook URL is EMPTY after set attempt. Check bot token and URL validity."
)
except Exception as e_setwebhook:
logging.error(
f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",
exc_info=True,
)
else:
logging.error(
"STARTUP: Skipped setting Telegram webhook due to security or configuration error."
)
else:
logging.info(
"STARTUP: WEBHOOK_BASE_URL not set in environment. Running in polling mode and clearing any existing webhook."
)
await bot.delete_webhook(drop_pending_updates=True)
if settings.SUBSCRIPTION_MINI_APP_URL:
try:
menu_text = i18n_instance.gettext(
settings.DEFAULT_LANGUAGE,
"menu_my_subscription_inline",
)
await bot.set_chat_menu_button(
menu_button=MenuButtonWebApp(
text=menu_text,
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
)
)
await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
logging.info(
"STARTUP: Mini app domain registered and default menu button restored."
)
except Exception as e:
logging.error(
f"STARTUP: Failed to register mini app domain: {e}", exc_info=True
)
if settings.START_COMMAND_DESCRIPTION:
try:
await bot.set_my_commands([
BotCommand(command="start", description=settings.START_COMMAND_DESCRIPTION)
])
logging.info("STARTUP: /start command description set.")
except Exception as e:
logging.error(f"STARTUP: Failed to set bot commands: {e}", exc_info=True)
# Initialize message queue manager
try:
queue_manager = init_queue_manager(bot)
dispatcher["queue_manager"] = queue_manager
logging.info("STARTUP: Message queue manager initialized")
except Exception as e:
logging.error(f"STARTUP: Failed to initialize message queue manager: {e}", exc_info=True)
# Automatic sync on startup
try:
logging.info("STARTUP: Running automatic panel sync...")
async with async_session_factory() as session:
sync_result = await perform_sync(
panel_service=panel_service,
session=session,
settings=settings,
i18n_instance=i18n_instance
)
if sync_result.get("status") == "completed":
logging.info(f"STARTUP: Automatic sync completed successfully. Details: {sync_result.get('details', 'N/A')}")
else:
logging.warning(f"STARTUP: Automatic sync completed with issues. Status: {sync_result.get('status', 'unknown')}")
except Exception as e:
logging.error(f"STARTUP: Failed to run automatic sync: {e}", exc_info=True)
logging.info("STARTUP: Bot on_startup_configured completed.")
async def on_shutdown_configured(dispatcher: Dispatcher):
logging.warning("SHUTDOWN: on_shutdown_configured executing...")
async def close_service(key: str) -> None:
service = dispatcher.get(key)
if not service:
return
close_coro = getattr(service, "close", None)
if callable(close_coro):
try:
await close_coro()
logging.info(f"{key} closed on shutdown.")
except Exception as e:
logging.warning(f"Failed to close {key}: {e}")
else:
close_session = getattr(service, "close_session", None)
if callable(close_session):
try:
await close_session()
logging.info(f"{key} session closed on shutdown.")
except Exception as e:
logging.warning(f"Failed to close session for {key}: {e}")
for service_key in (
"panel_service",
"cryptopay_service",
"tribute_service",
"panel_webhook_service",
"yookassa_service",
"promo_code_service",
"stars_service",
"subscription_service",
"referral_service",
):
await close_service(service_key)
bot: Bot = dispatcher["bot_instance"]
if bot and bot.session:
try:
await bot.session.close()
logging.info("SHUTDOWN: Aiogram Bot session closed.")
except Exception as e:
logging.warning(f"SHUTDOWN: Failed to close bot session: {e}")
from db.database_setup import async_engine as global_async_engine
if global_async_engine:
logging.info("SHUTDOWN: Disposing SQLAlchemy engine...")
await global_async_engine.dispose()
logging.info("SHUTDOWN: SQLAlchemy engine disposed.")
logging.info("SHUTDOWN: Bot on_shutdown_configured completed.")
async def run_bot(settings_param: Settings):
storage = MemoryStorage()
default_props = DefaultBotProperties(parse_mode=ParseMode.HTML)
bot = Bot(token=settings_param.BOT_TOKEN, default=default_props)
local_async_session_factory = init_db_connection(settings_param)
if local_async_session_factory is None:
logging.critical(
"Failed to initialize database connection and session factory. Exiting."
)
return
dp = Dispatcher(storage=storage, settings=settings_param, bot_instance=bot)
actual_bot_username = "your_bot_username"
try:
bot_info = await bot.get_me()
actual_bot_username = bot_info.username
logging.info(f"Bot username resolved: @{actual_bot_username}")
except Exception as e:
logging.error(
f"Failed to get bot info (e.g., for YooKassa default URL): {e}. Using fallback: {actual_bot_username}"
)
i18n_instance = get_i18n_instance(
path="locales", default=settings_param.DEFAULT_LANGUAGE
)
yookassa_service = YooKassaService(
shop_id=settings_param.YOOKASSA_SHOP_ID,
secret_key=settings_param.YOOKASSA_SECRET_KEY,
configured_return_url=settings_param.YOOKASSA_RETURN_URL,
bot_username_for_default_return=actual_bot_username,
settings_obj=settings_param,
)
panel_service = PanelApiService(settings_param)
subscription_service = SubscriptionService(
settings_param, panel_service, bot, i18n_instance
)
referral_service = ReferralService(
settings_param, subscription_service, bot, i18n_instance
)
promo_code_service = PromoCodeService(
settings_param, subscription_service, bot, i18n_instance
)
stars_service = StarsService(
bot, settings_param, i18n_instance, subscription_service, referral_service
)
cryptopay_service = CryptoPayService(
settings_param.CRYPTOPAY_TOKEN,
settings_param.CRYPTOPAY_NETWORK,
bot,
settings_param,
i18n_instance,
local_async_session_factory,
subscription_service,
referral_service,
)
tribute_service = TributeService(
bot,
settings_param,
i18n_instance,
local_async_session_factory,
panel_service,
subscription_service,
referral_service,
)
panel_webhook_service = PanelWebhookService(
bot,
settings_param,
i18n_instance,
local_async_session_factory,
)
dp["i18n_instance"] = i18n_instance
dp["yookassa_service"] = yookassa_service
dp["panel_service"] = panel_service
dp["subscription_service"] = subscription_service
dp["referral_service"] = referral_service
dp["promo_code_service"] = promo_code_service
dp["stars_service"] = stars_service
dp["cryptopay_service"] = cryptopay_service
dp["tribute_service"] = tribute_service
dp["panel_webhook_service"] = panel_webhook_service
dp["async_session_factory"] = local_async_session_factory
dp.update.outer_middleware(DBSessionMiddleware(local_async_session_factory))
dp.update.outer_middleware(
I18nMiddleware(i18n=i18n_instance, settings=settings_param)
)
dp.update.outer_middleware(
BanCheckMiddleware(settings=settings_param, i18n_instance=i18n_instance)
)
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param))
dp.startup.register(on_startup_configured)
# Register shutdown callback directly so Dispatcher instance is provided
dp.shutdown.register(on_shutdown_configured)
await register_all_routers(dp, settings_param)
tg_webhook_base = settings_param.WEBHOOK_BASE_URL
yk_webhook_base = settings_param.WEBHOOK_BASE_URL
should_run_aiohttp_server = bool(tg_webhook_base) or (
bool(yk_webhook_base) and bool(settings_param.yookassa_webhook_path)
)
telegram_uses_webhook_mode = bool(tg_webhook_base)
run_telegram_polling = not telegram_uses_webhook_mode
logging.info(f"--- Bot Run Mode Decision ---")
logging.info(
f"Configured WEBHOOK_BASE_URL: '{tg_webhook_base}' -> Telegram Webhook Mode: {telegram_uses_webhook_mode}"
)
logging.info(
f"YooKassa webhook path: '{settings_param.yookassa_webhook_path}'"
)
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp_server}")
logging.info(f"Decision: Run Telegram Polling: {run_telegram_polling}")
logging.info(f"--- End Bot Run Mode Decision ---")
web_app_runner = None
main_tasks = []
if should_run_aiohttp_server:
app = web.Application()
app["bot"] = bot
app["dp"] = dp
app["settings"] = settings_param
app["i18n"] = i18n_instance
app["async_session_factory"] = local_async_session_factory
app["yookassa_service"] = yookassa_service
app["subscription_service"] = subscription_service
app["referral_service"] = referral_service
app["panel_service"] = panel_service
app["stars_service"] = stars_service
app["cryptopay_service"] = cryptopay_service
app["tribute_service"] = tribute_service
app["panel_webhook_service"] = panel_webhook_service
setup_application(app, dp, bot=bot)
if telegram_uses_webhook_mode:
telegram_webhook_path = f"/{settings_param.BOT_TOKEN}"
if not telegram_webhook_path.startswith("/"):
telegram_webhook_path = "/" + telegram_webhook_path
app.router.add_post(
telegram_webhook_path, SimpleRequestHandler(dispatcher=dp, bot=bot)
)
logging.info(
f"Telegram webhook route configured at: [POST] {telegram_webhook_path} (relative to base URL)"
)
if yk_webhook_base and settings_param.yookassa_webhook_path:
yk_path = settings_param.yookassa_webhook_path
if not yk_path or not isinstance(yk_path, str):
logging.error(
f"YooKassa webhook path is invalid or not configured in settings: {yk_path}. Skipping YooKassa webhook setup."
)
elif not yk_path.startswith("/"):
logging.error(
f"CRITICAL: YooKassa webhook path '{yk_path}' from settings does not start with '/'. Correct settings.py or .env. Skipping YooKassa webhook."
)
else:
app.router.add_post(
yk_path, user_payment_webhook_module.yookassa_webhook_route
)
logging.info(f"YooKassa webhook route configured at: [POST] {yk_path}")
tribute_path = settings_param.tribute_webhook_path
if tribute_path.startswith("/"):
app.router.add_post(tribute_path, tribute_webhook_route)
logging.info(f"Tribute webhook route configured at: [POST] {tribute_path}")
cp_path = settings_param.cryptopay_webhook_path
if cp_path.startswith("/"):
app.router.add_post(cp_path, cryptopay_webhook_route)
logging.info(f"CryptoPay webhook route configured at: [POST] {cp_path}")
panel_path = settings_param.panel_webhook_path
if panel_path.startswith("/"):
app.router.add_post(panel_path, panel_webhook_route)
logging.info(f"Panel webhook route configured at: [POST] {panel_path}")
web_app_runner = web.AppRunner(app)
await web_app_runner.setup()
site = web.TCPSite(
web_app_runner,
host=settings_param.WEB_SERVER_HOST,
port=settings_param.WEB_SERVER_PORT,
)
async def web_server_task():
await site.start()
logging.info(
f"AIOHTTP server started on http://{settings_param.WEB_SERVER_HOST}:{settings_param.WEB_SERVER_PORT}"
)
(
await asyncio.Event().wait()
if not run_telegram_polling
else await asyncio.sleep(31536000)
)
main_tasks.append(
asyncio.create_task(web_server_task(), name="AIOHTTPServerTask")
)
if run_telegram_polling:
logging.info("Starting bot in Telegram Polling mode...")
main_tasks.append(
asyncio.create_task(
dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()),
name="TelegramPollingTask",
)
)
if not main_tasks:
logging.error(
"Bot is not configured for any run mode (neither Webhook nor Polling). Exiting."
)
await dp.emit_shutdown()
return
logging.info(
f"Starting bot with main tasks: {[task.get_name() for task in main_tasks]}"
)
try:
await asyncio.gather(*main_tasks)
except (KeyboardInterrupt, SystemExit, asyncio.CancelledError) as e:
logging.info(f"Main bot loop interrupted/cancelled: {type(e).__name__} - {e}")
finally:
logging.info("Initiating final bot shutdown sequence...")
for task in main_tasks:
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
logging.info(
f"Task '{task.get_name()}' was cancelled successfully."
)
except Exception as e_task_cancel:
logging.error(
f"Error during cancellation of task '{task.get_name()}': {e_task_cancel}",
exc_info=True,
)
if web_app_runner:
await web_app_runner.cleanup()
logging.info("AIOHTTP AppRunner cleaned up.")
await dp.emit_shutdown()
logging.info("Dispatcher shutdown sequence emitted.")
logging.info("Bot run_bot function finished.")