Code Refactoring

This commit is contained in:
machka
2025-05-14 08:43:23 +00:00
parent 7f94d794d6
commit d406baf0c1
33 changed files with 6015 additions and 3571 deletions
+274 -151
View File
@@ -1,171 +1,294 @@
import logging
import asyncio
from aiogram import Bot ,Dispatcher ,Router ,F
from aiogram .enums import ParseMode
from aiogram .filters import CommandStart ,Command
from aiogram .client .default import DefaultBotProperties
from aiogram .webhook .aiohttp_server import SimpleRequestHandler ,setup_application
from aiogram .fsm .storage .memory import MemoryStorage
from aiogram import Bot, Dispatcher, Router, F
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart, Command
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 apscheduler .schedulers .asyncio import AsyncIOScheduler
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from config.settings import Settings, get_settings
from .middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n
from .middlewares.ban_check_middleware import BanCheckMiddleware
from .middlewares.action_logger_middleware import ActionLoggerMiddleware
from .handlers.user import user_router_aggregate
from .handlers.user import payment as user_payment_webhook_module
from .handlers.admin import admin_router_aggregate
from .filters.admin_filter import AdminFilter
from db.database import get_db_connection_manager
from .services.notification_service import schedule_subscription_notifications
from .services.payment_service import YooKassaService
from .services.panel_api_service import PanelApiService
from .services.subscription_service import SubscriptionService
from .services.referral_service import ReferralService
from .services.promo_code_service import PromoCodeService
from config .settings import Settings ,get_settings
from .middlewares .i18n import I18nMiddleware ,get_i18n_instance ,JsonI18n
from .middlewares .ban_check_middleware import BanCheckMiddleware
from .middlewares .action_logger_middleware import ActionLoggerMiddleware
from .handlers .user import user_router_aggregate
from .handlers .user import payment as user_payment_webhook_module
from .handlers .admin import admin_router_aggregate
from .filters .admin_filter import AdminFilter
from db .database import get_db_connection_manager
from .services .notification_service import schedule_subscription_notifications
from .services .payment_service import YooKassaService
from .services .panel_api_service import PanelApiService
from .services .subscription_service import SubscriptionService
from .services .referral_service import ReferralService
from .services .promo_code_service import PromoCodeService
async def register_all_routers(dp: Dispatcher, settings: Settings):
dp.include_router(user_router_aggregate)
admin_filtered_router_wrapper = Router(
name="admin_filtered_router_wrapper")
admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS)
admin_filtered_router_wrapper.message.filter(admin_filter_instance)
admin_filtered_router_wrapper.callback_query.filter(admin_filter_instance)
admin_filtered_router_wrapper.include_router(admin_router_aggregate)
dp.include_router(admin_filtered_router_wrapper)
logging.info("All application routers registered.")
async def register_all_routers (dp :Dispatcher ,settings :Settings ):
dp .include_router (user_router_aggregate )
admin_filtered_router_wrapper =Router (name ="admin_filtered_router_wrapper")
admin_filter_instance =AdminFilter (admin_ids =settings .ADMIN_IDS )
admin_filtered_router_wrapper .message .filter (admin_filter_instance )
admin_filtered_router_wrapper .callback_query .filter (admin_filter_instance )
admin_filtered_router_wrapper .include_router (admin_router_aggregate )
dp .include_router (admin_filtered_router_wrapper )
logging .info ("All application routers registered.")
async def on_startup_configured(dispatcher: Dispatcher):
async def on_startup_configured (dispatcher :Dispatcher ):
bot :Bot =dispatcher ["bot_instance"];settings :Settings =dispatcher ["settings"];i18n_instance :JsonI18n =dispatcher ["i18n_instance"]
logging .info ("STARTUP: on_startup_configured executing...")
scheduler =AsyncIOScheduler (timezone ="UTC")
try :await schedule_subscription_notifications (bot ,settings ,i18n_instance ,scheduler );scheduler .start ();dispatcher ["scheduler"]=scheduler ;logging .info ("STARTUP: APScheduler started.")
except Exception as e :logging .error (f"STARTUP: Failed to start APScheduler: {e}",exc_info =True )
telegram_webhook_url_to_set =getattr (settings ,'TELEGRAM_WEBHOOK_BASE_URL',None )
if telegram_webhook_url_to_set :
if settings .BOT_TOKEN in telegram_webhook_url_to_set :logging .error (f"CRITICAL: Bot token in TELEGRAM_WEBHOOK_BASE_URL ('{telegram_webhook_url_to_set}').");full_telegram_webhook_url ="ERROR_URL"
else :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}")
try :
current_webhook_info_before =await bot .get_webhook_info ();logging .info (f"STARTUP: Current webhook info BEFORE: {current_webhook_info_before.model_dump_json(exclude_none=True, indent=2)}")
if full_telegram_webhook_url !="ERROR_URL":
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: Webhook info AFTER: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}")
if not new_webhook_info .url :logging .error ("STARTUP: CRITICAL - Webhook URL EMPTY after set attempt.")
else :logging .error ("STARTUP: Skipped setting webhook due to URL config error.")
except Exception as e_setwebhook :logging .error (f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",exc_info =True )
else :
logging .info ("STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set. Attempting to delete webhook.");await bot .delete_webhook (drop_pending_updates =True )
logging .info ("STARTUP: Bot on_startup_configured completed.")
bot: Bot = dispatcher["bot_instance"]
settings: Settings = dispatcher["settings"]
i18n_instance: JsonI18n = dispatcher["i18n_instance"]
logging.info("STARTUP: on_startup_configured executing...")
scheduler = AsyncIOScheduler(timezone="UTC")
try:
await schedule_subscription_notifications(bot, settings, i18n_instance,
scheduler)
scheduler.start()
dispatcher["scheduler"] = scheduler
logging.info("STARTUP: APScheduler started.")
except Exception as e:
logging.error(f"STARTUP: Failed to start APScheduler: {e}",
exc_info=True)
telegram_webhook_url_to_set = getattr(settings,
'TELEGRAM_WEBHOOK_BASE_URL', None)
if telegram_webhook_url_to_set:
if settings.BOT_TOKEN in telegram_webhook_url_to_set:
logging.error(
f"CRITICAL: Bot token in TELEGRAM_WEBHOOK_BASE_URL ('{telegram_webhook_url_to_set}')."
)
full_telegram_webhook_url = "ERROR_URL"
else:
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}"
)
try:
current_webhook_info_before = await bot.get_webhook_info()
logging.info(
f"STARTUP: Current webhook info BEFORE: {current_webhook_info_before.model_dump_json(exclude_none=True, indent=2)}"
)
if full_telegram_webhook_url != "ERROR_URL":
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: Webhook info AFTER: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
)
if not new_webhook_info.url:
logging.error(
"STARTUP: CRITICAL - Webhook URL EMPTY after set attempt."
)
else:
logging.error(
"STARTUP: Skipped setting webhook due to URL config error."
)
except Exception as e_setwebhook:
logging.error(
f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",
exc_info=True)
else:
logging.info(
"STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set. Attempting to delete webhook."
)
await bot.delete_webhook(drop_pending_updates=True)
logging.info("STARTUP: Bot on_startup_configured completed.")
async def on_shutdown_configured (dispatcher :Dispatcher ):
async def on_shutdown_configured(dispatcher: Dispatcher):
logging .warning ("SHUTDOWN: on_shutdown_configured executing...");scheduler :AsyncIOScheduler =dispatcher .get ("scheduler")
if scheduler and scheduler .running :
try :scheduler .shutdown (wait =False );logging .info ("SHUTDOWN: APScheduler shut down.")
except Exception as e :logging .error (f"SHUTDOWN: Error APScheduler: {e}",exc_info =True )
panel_service :PanelApiService =dispatcher .get ("panel_service")
if panel_service and panel_service ._session and not panel_service ._session .closed :await panel_service .close_session ()
bot :Bot =dispatcher .get ("bot_instance")
if bot and bot .session and not bot .session .closed :await bot .session .close ()
logging .info ("SHUTDOWN: Bot on_shutdown_configured completed.")
logging.warning("SHUTDOWN: on_shutdown_configured executing...")
scheduler: AsyncIOScheduler = dispatcher.get("scheduler")
if scheduler and scheduler.running:
try:
scheduler.shutdown(wait=False)
logging.info("SHUTDOWN: APScheduler shut down.")
except Exception as e:
logging.error(f"SHUTDOWN: Error APScheduler: {e}", exc_info=True)
panel_service: PanelApiService = dispatcher.get("panel_service")
if panel_service and panel_service._session and not panel_service._session.closed:
await panel_service.close_session()
bot: Bot = dispatcher.get("bot_instance")
if bot and bot.session and not bot.session.closed:
await bot.session.close()
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 )
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)
dp =Dispatcher (storage =storage ,settings =settings_param ,bot_instance =bot )
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: @{actual_bot_username}")
except Exception as e :
logging .error (f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}")
actual_bot_username = "your_bot_username"
try:
bot_info = await bot.get_me()
actual_bot_username = bot_info.username
logging.info(f"Bot username: @{actual_bot_username}")
except Exception as e:
logging.error(
f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}"
)
default_lang =settings_param .DEFAULT_LANGUAGE
i18n_instance =get_i18n_instance (path ="locales",default =default_lang )
default_lang = settings_param.DEFAULT_LANGUAGE
i18n_instance = get_i18n_instance(path="locales", default=default_lang)
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=actual_bot_username,
settings_obj=settings_param)
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 =actual_bot_username ,
settings_obj =settings_param
panel_service = PanelApiService(settings_param)
subscription_service = SubscriptionService(get_db_connection_manager,
settings_param, panel_service)
referral_service = ReferralService(get_db_connection_manager,
settings_param, subscription_service,
bot, i18n_instance)
promo_code_service = PromoCodeService(get_db_connection_manager,
settings_param, subscription_service,
bot, i18n_instance)
dp["i18n_instance"] = i18n_instance
dp["i18n_data"] = {
"i18n_instance": i18n_instance,
"current_language": default_lang
}
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.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)
dp.shutdown.register(on_shutdown_configured)
await register_all_routers(dp, settings_param)
tg_webhook_base = getattr(settings_param, 'TELEGRAM_WEBHOOK_BASE_URL',
None)
yk_webhook_base = getattr(settings_param, 'YOOKASSA_WEBHOOK_BASE_URL',
None)
logging.info(f"--- Determining Run Mode ---")
logging.info(f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}'")
logging.info(f"Configured YOOKASSA_WEBHOOK_BASE_URL: '{yk_webhook_base}'")
should_run_aiohttp = bool(
yk_webhook_base
and settings_param.yookassa_webhook_path) or bool(tg_webhook_base)
telegram_uses_webhook = bool(tg_webhook_base)
telegram_should_poll = not telegram_uses_webhook
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp}")
logging.info(f"Decision: Telegram uses webhook: {telegram_uses_webhook}")
logging.info(f"Decision: Telegram should poll: {telegram_should_poll}")
logging.info(f"--- End Run Mode Decision ---")
web_app_runner = None
main_tasks_to_await = []
if should_run_aiohttp:
app = web.Application()
app['bot'] = bot
app['dp'] = dp
app['settings'] = settings_param
app['i18n'] = i18n_instance
app['yookassa_service'] = yookassa_service
app['panel_service'] = panel_service
app['subscription_service'] = subscription_service
app['referral_service'] = referral_service
app['promo_code_service'] = promo_code_service
setup_application(app, dp, bot=bot)
logging.info(
"AIOHTTP app context populated and dispatcher lifecycle linked.")
if telegram_uses_webhook:
telegram_webhook_path = f"/{settings_param.BOT_TOKEN}"
app.router.add_post(telegram_webhook_path,
SimpleRequestHandler(dispatcher=dp, bot=bot))
logging.info(f"Telegram webhook route: {telegram_webhook_path}.")
if yk_webhook_base and settings_param.yookassa_webhook_path:
app.router.add_post(
settings_param.yookassa_webhook_path,
user_payment_webhook_module.yookassa_webhook_route)
logging.info(
f"YK webhook route: {settings_param.yookassa_webhook_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_wrapper():
await site.start()
logging.info(
f"AIOHTTP server started on {settings_param.WEB_SERVER_HOST}:{settings_param.WEB_SERVER_PORT}."
)
await asyncio.Event().wait(
) if not telegram_should_poll else await asyncio.sleep(31536000)
main_tasks_to_await.append(
asyncio.create_task(web_server_task_wrapper(),
name="AIOHTTPServerWrapperTask"))
if telegram_should_poll:
logging.info("TG polling task created.")
main_tasks_to_await.append(
asyncio.create_task(dp.start_polling(
bot, allowed_updates=dp.resolve_used_update_types()),
name="TelegramPollingTask"))
if not main_tasks_to_await:
logging.error("Bot not configured for any mode.")
await dp.emit_shutdown()
return
logging.info(
f"Starting bot with main tasks: {[task.get_name() for task in main_tasks_to_await]}"
)
panel_service =PanelApiService (settings_param )
subscription_service =SubscriptionService (get_db_connection_manager ,settings_param ,panel_service )
referral_service =ReferralService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance )
promo_code_service =PromoCodeService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance )
dp ["i18n_instance"]=i18n_instance
dp ["i18n_data"]={"i18n_instance":i18n_instance ,"current_language":default_lang }
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 .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 )
dp .shutdown .register (on_shutdown_configured )
await register_all_routers (dp ,settings_param )
tg_webhook_base =getattr (settings_param ,'TELEGRAM_WEBHOOK_BASE_URL',None );yk_webhook_base =getattr (settings_param ,'YOOKASSA_WEBHOOK_BASE_URL',None )
logging .info (f"--- Determining Run Mode ---");logging .info (f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}'");logging .info (f"Configured YOOKASSA_WEBHOOK_BASE_URL: '{yk_webhook_base}'")
should_run_aiohttp =bool (yk_webhook_base and settings_param .yookassa_webhook_path )or bool (tg_webhook_base )
telegram_uses_webhook =bool (tg_webhook_base );telegram_should_poll =not telegram_uses_webhook
logging .info (f"Decision: Run AIOHTTP server: {should_run_aiohttp}");logging .info (f"Decision: Telegram uses webhook: {telegram_uses_webhook}");logging .info (f"Decision: Telegram should poll: {telegram_should_poll}");logging .info (f"--- End Run Mode Decision ---")
web_app_runner =None ;main_tasks_to_await =[]
if should_run_aiohttp :
app =web .Application ();app ['bot']=bot ;app ['dp']=dp ;app ['settings']=settings_param ;app ['i18n']=i18n_instance
app ['yookassa_service']=yookassa_service ;app ['panel_service']=panel_service ;app ['subscription_service']=subscription_service
app ['referral_service']=referral_service ;app ['promo_code_service']=promo_code_service
setup_application (app ,dp ,bot =bot );logging .info ("AIOHTTP app context populated and dispatcher lifecycle linked.")
if telegram_uses_webhook :telegram_webhook_path =f"/{settings_param.BOT_TOKEN}";app .router .add_post (telegram_webhook_path ,SimpleRequestHandler (dispatcher =dp ,bot =bot ));logging .info (f"Telegram webhook route: {telegram_webhook_path}.")
if yk_webhook_base and settings_param .yookassa_webhook_path :app .router .add_post (settings_param .yookassa_webhook_path ,user_payment_webhook_module .yookassa_webhook_route );logging .info (f"YK webhook route: {settings_param.yookassa_webhook_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_wrapper ():await site .start ();logging .info (f"AIOHTTP server started on {settings_param.WEB_SERVER_HOST}:{settings_param.WEB_SERVER_PORT}.");await asyncio .Event ().wait ()if not telegram_should_poll else await asyncio .sleep (31536000 )
main_tasks_to_await .append (asyncio .create_task (web_server_task_wrapper (),name ="AIOHTTPServerWrapperTask"))
if telegram_should_poll :logging .info ("TG polling task created.");main_tasks_to_await .append (asyncio .create_task (dp .start_polling (bot ,allowed_updates =dp .resolve_used_update_types ()),name ="TelegramPollingTask"))
if not main_tasks_to_await :logging .error ("Bot not configured for any mode.");await dp .emit_shutdown ();return
logging .info (f"Starting bot with main tasks: {[task.get_name() for task in main_tasks_to_await]}")
try :
if main_tasks_to_await :await asyncio .gather (*main_tasks_to_await )
except (KeyboardInterrupt ,SystemExit ,asyncio .CancelledError )as e :logging .info (f"Main loop interrupted: {type(e).__name__}")
finally :
logging .info ("Initiating final shutdown sequence...");
for task in main_tasks_to_await :
if task and not task .done ():task .cancel ();
try :await task
except asyncio .CancelledError :logging .info (f"Task '{task.get_name()}' cancelled.")
except Exception as e_cancel :logging .error (f"Error cancelling task '{task.get_name()}': {e_cancel}",exc_info =True )
if web_app_runner :await web_app_runner .cleanup ();logging .info ("AIOHTTP AppRunner cleaned up.")
if not (telegram_should_poll and len (main_tasks_to_await )==1 and any (t .get_name ()=="TelegramPollingTask"for t in main_tasks_to_await if t and not t .done ())):
logging .info ("Explicitly calling dp.emit_shutdown() in run_bot finally.")
await dp .emit_shutdown ()
if bot .session and not bot .session .closed :await bot .session .close ()
logging .info ("Bot run_bot function finished.")
try:
if main_tasks_to_await: await asyncio.gather(*main_tasks_to_await)
except (KeyboardInterrupt, SystemExit, asyncio.CancelledError) as e:
logging.info(f"Main loop interrupted: {type(e).__name__}")
finally:
logging.info("Initiating final shutdown sequence...")
for task in main_tasks_to_await:
if task and not task.done(): task.cancel()
try:
await task
except asyncio.CancelledError:
logging.info(f"Task '{task.get_name()}' cancelled.")
except Exception as e_cancel:
logging.error(
f"Error cancelling task '{task.get_name()}': {e_cancel}",
exc_info=True)
if web_app_runner:
await web_app_runner.cleanup()
logging.info("AIOHTTP AppRunner cleaned up.")
if not (telegram_should_poll and len(main_tasks_to_await) == 1
and any(t.get_name() == "TelegramPollingTask"
for t in main_tasks_to_await if t and not t.done())):
logging.info(
"Explicitly calling dp.emit_shutdown() in run_bot finally.")
await dp.emit_shutdown()
if bot.session and not bot.session.closed: await bot.session.close()
logging.info("Bot run_bot function finished.")