207 lines
12 KiB
Python
207 lines
12 KiB
Python
import logging
|
|
import aiosqlite
|
|
from aiogram import Router ,F ,types ,Bot
|
|
from aiogram .filters import Command
|
|
from aiogram .fsm .context import FSMContext
|
|
from typing import Optional ,Dict ,Any
|
|
from datetime import datetime ,timezone
|
|
from aiogram .utils .keyboard import InlineKeyboardBuilder
|
|
from aiogram .types import InlineKeyboardMarkup
|
|
|
|
from config .settings import Settings
|
|
from db .database import add_payment_record ,get_db_connection_manager ,_setup_db_connection
|
|
from bot .keyboards .inline .user_keyboards import (
|
|
get_subscription_options_keyboard ,
|
|
get_confirm_subscription_keyboard ,
|
|
get_payment_url_keyboard ,
|
|
get_back_to_main_menu_markup
|
|
)
|
|
from bot .services .payment_service import YooKassaService
|
|
from bot .services .subscription_service import SubscriptionService
|
|
from bot .services .panel_api_service import PanelApiService
|
|
from bot .middlewares .i18n import JsonI18n
|
|
|
|
router =Router (name ="user_subscription_router")
|
|
|
|
async def display_subscription_options (message_or_callback :types .Message |types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
|
|
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
|
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
|
if not i18n :
|
|
logging .error ("i18n missing in display_subscription_options")
|
|
|
|
target_msg =message_or_callback .message if isinstance (message_or_callback ,types .CallbackQuery )else message_or_callback
|
|
if target_msg :await target_msg .answer ("Language service error.")
|
|
if isinstance (message_or_callback ,types .CallbackQuery ):await message_or_callback .answer ()
|
|
return
|
|
|
|
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
|
currency_symbol_val =settings .DEFAULT_CURRENCY_SYMBOL
|
|
|
|
text =get_translation ("select_subscription_period")if settings .subscription_options else get_translation ("no_subscription_options_available")
|
|
reply_markup =get_subscription_options_keyboard (settings .subscription_options ,currency_symbol_val ,current_lang ,i18n )if settings .subscription_options else None
|
|
|
|
target_message =message_or_callback .message if isinstance (message_or_callback ,types .CallbackQuery )else message_or_callback
|
|
answered_callback =False
|
|
|
|
if isinstance (message_or_callback ,types .CallbackQuery ):
|
|
|
|
await message_or_callback .answer ()
|
|
answered_callback =True
|
|
|
|
if target_message :
|
|
if isinstance (message_or_callback ,types .CallbackQuery ):
|
|
try :
|
|
await target_message .edit_text (text ,reply_markup =reply_markup )
|
|
except Exception :
|
|
await target_message .answer (text ,reply_markup =reply_markup )
|
|
else :
|
|
await target_message .answer (text ,reply_markup =reply_markup )
|
|
elif isinstance (message_or_callback ,types .Message ):
|
|
await message_or_callback .answer (text ,reply_markup =reply_markup )
|
|
|
|
|
|
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback :
|
|
await message_or_callback .answer ()
|
|
|
|
|
|
@router .callback_query (F .data .startswith ("subscribe_period:"))
|
|
async def select_subscription_period_callback_handler (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ):
|
|
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
|
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
|
if not i18n :
|
|
logging .error ("i18n missing in select_subscription_period_callback_handler")
|
|
await callback .answer ("Service error. Please try again.",show_alert =True )
|
|
return
|
|
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
|
|
|
try :months =int (callback .data .split (":")[-1 ])
|
|
except ValueError :
|
|
logging .error (f"Invalid sub period: {callback.data}")
|
|
await callback .answer (get_translation ("error_try_again"),show_alert =True );return
|
|
|
|
price =settings .subscription_options .get (months )
|
|
if price is None :
|
|
logging .error (f"Price not found for {months} months subscription.")
|
|
await callback .answer (get_translation ("error_try_again"),show_alert =True );return
|
|
|
|
currency_symbol_val =settings .DEFAULT_CURRENCY_SYMBOL
|
|
confirmation_text =get_translation ("confirm_subscription_prompt",months =months ,price =price ,currency_symbol =currency_symbol_val )
|
|
reply_markup =get_confirm_subscription_keyboard (months ,price ,currency_symbol_val ,current_lang ,i18n )
|
|
|
|
if callback .message :
|
|
try :await callback .message .edit_text (confirmation_text ,reply_markup =reply_markup )
|
|
except Exception as e :logging .warning (f"Edit failed: {e}");await callback .message .answer (confirmation_text ,reply_markup =reply_markup )
|
|
await callback .answer ()
|
|
|
|
|
|
@router .callback_query (F .data .startswith ("confirm_sub:"))
|
|
async def confirm_subscription_callback_handler (
|
|
callback :types .CallbackQuery ,state :FSMContext ,
|
|
settings :Settings ,i18n_data :dict ,yookassa_service :YooKassaService
|
|
):
|
|
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
|
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
|
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return
|
|
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
|
if not yookassa_service or not yookassa_service .configured :
|
|
logging .error ("YooKassa service missing or not configured")
|
|
await callback .message .edit_text (get_translation ("payment_service_unavailable"))if callback .message else None
|
|
await callback .answer (get_translation ("payment_service_unavailable"),show_alert =True );return
|
|
try :_ ,data_payload =callback .data .split (":",1 );months_str ,price_str =data_payload .split (":");months =int (months_str );price =float (price_str )
|
|
except ValueError :logging .error (f"Invalid confirm data: {callback.data}");await callback .answer (get_translation ("error_try_again"),show_alert =True );return
|
|
|
|
user_id =callback .from_user .id
|
|
description =get_translation ("payment_description_subscription",months =months )
|
|
currency =settings .DEFAULT_CURRENCY_SYMBOL
|
|
payment_metadata ={"user_id":str (user_id ),"subscription_months":str (months ),"description":description }
|
|
payment_db_id =await add_payment_record (user_id ,None ,None ,price ,currency ,"pending_creation",description ,months ,None )
|
|
if not payment_db_id :
|
|
if callback .message :await callback .message .edit_text (get_translation ("error_creating_payment_record"))
|
|
await callback .answer (show_alert =True );return
|
|
payment_metadata ["payment_db_id"]=str (payment_db_id )
|
|
payment_response =await yookassa_service .create_payment (price ,currency ,description ,payment_metadata )
|
|
|
|
if callback .message :
|
|
if payment_response and payment_response .get ("confirmation_url"):
|
|
async with get_db_connection_manager ()as db :await _setup_db_connection (db );await db .execute ("UPDATE payments SET yookassa_payment_id = ?, idempotence_key = ?, status = ? WHERE payment_id = ?",(payment_response ["id"],payment_response .get ("idempotence_key"),payment_response ["status"],payment_db_id ));await db .commit ()
|
|
await callback .message .edit_text (get_translation (key ="payment_link_message",months =months ),reply_markup =get_payment_url_keyboard (payment_response ["confirmation_url"],current_lang ,i18n ),disable_web_page_preview =False )
|
|
else :
|
|
async with get_db_connection_manager ()as db :await _setup_db_connection (db );await db .execute ("UPDATE payments SET status = ? WHERE payment_id = ?",("failed_creation",payment_db_id ));await db .commit ()
|
|
await callback .message .edit_text (get_translation ("error_payment_gateway"))
|
|
await callback .answer ()
|
|
|
|
|
|
@router .callback_query (F .data =="main_action:subscribe")
|
|
async def reshow_subscription_options_callback (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
|
|
await display_subscription_options (callback ,i18n_data ,settings )
|
|
|
|
|
|
|
|
async def my_subscription_command_handler (
|
|
message_event :types .Message |types .CallbackQuery ,
|
|
i18n_data :dict ,
|
|
settings :Settings ,
|
|
panel_service :PanelApiService ,
|
|
subscription_service :SubscriptionService
|
|
):
|
|
target_message =message_event .message if isinstance (message_event ,types .CallbackQuery )else message_event
|
|
user =message_event .from_user
|
|
if isinstance (message_event ,types .CallbackQuery ):await message_event .answer ()
|
|
|
|
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
|
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
|
if not i18n :logging .error ("i18n missing");await target_message .answer ("Lang error");return
|
|
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
|
if not panel_service or not subscription_service :logging .error ("Services missing");await target_message .answer (get_translation ("error_service_unavailable"));return
|
|
|
|
active_sub =await subscription_service .get_active_subscription (user .id )
|
|
sub_info_text =""
|
|
if active_sub :
|
|
end_date_obj =active_sub .get ('end_date')
|
|
if isinstance (end_date_obj ,str ):
|
|
try :end_date_obj =datetime .fromisoformat (end_date_obj .replace ("Z","+00:00"))
|
|
except ValueError :logging .warning (f"Could not parse date string '{end_date_obj}'.");end_date_obj =datetime .now (timezone .utc )
|
|
|
|
if not isinstance (end_date_obj ,datetime ):end_date_obj =datetime .now (timezone .utc )
|
|
if end_date_obj .tzinfo is None :end_date_obj =end_date_obj .replace (tzinfo =timezone .utc )
|
|
|
|
|
|
today_date_utc =datetime .now (timezone .utc ).date ()
|
|
end_date_only =end_date_obj .date ()
|
|
days_left =(end_date_only -today_date_utc ).days
|
|
|
|
actual_config_link =get_translation ("config_link_not_available");panel_user_uuid =active_sub .get ('panel_user_uuid')
|
|
if panel_user_uuid :
|
|
panel_user_data =await panel_service .get_user_by_uuid (panel_user_uuid )
|
|
if panel_user_data :
|
|
if panel_user_data .get ('subscriptionUrl'):actual_config_link =panel_user_data ['subscriptionUrl']
|
|
elif panel_user_data .get ('shortUuid'):
|
|
link =await panel_service .get_subscription_link (panel_user_data ['shortUuid'])
|
|
if link :actual_config_link =link
|
|
|
|
traffic_limit_gb =get_translation ("traffic_unlimited");traffic_used_gb =get_translation ("traffic_na")
|
|
if active_sub .get ('traffic_limit_bytes')and active_sub ['traffic_limit_bytes']>0 :traffic_limit_gb =f"{active_sub['traffic_limit_bytes'] / (1024**3):.2f} GB"
|
|
if active_sub .get ('traffic_used_bytes')is not None :traffic_used_gb =f"{active_sub['traffic_used_bytes'] / (1024**3):.2f} GB"
|
|
|
|
sub_info_text =get_translation ("my_subscription_details",end_date =end_date_obj .strftime ("%Y-%m-%d"),days_left =max (0 ,days_left ),status =active_sub .get ('status_from_panel',get_translation ('status_active')).capitalize (),config_link =actual_config_link ,traffic_limit =traffic_limit_gb ,traffic_used =traffic_used_gb )
|
|
else :
|
|
sub_info_text =get_translation ("subscription_not_active")
|
|
|
|
reply_markup_val =get_back_to_main_menu_markup (current_lang ,i18n )
|
|
if isinstance (message_event ,types .CallbackQuery )and message_event .message :
|
|
try :await message_event .message .edit_text (sub_info_text ,reply_markup =reply_markup_val ,parse_mode ="HTML",disable_web_page_preview =True )
|
|
except Exception as e :logging .warning (f"Edit 'my_sub' failed: {e}");await target_message .answer (sub_info_text ,reply_markup =reply_markup_val ,parse_mode ="HTML",disable_web_page_preview =True )
|
|
else :await target_message .answer (sub_info_text ,reply_markup =reply_markup_val ,parse_mode ="HTML",disable_web_page_preview =True )
|
|
|
|
|
|
@router .message (Command ("connect"))
|
|
async def connect_command_handler (
|
|
message :types .Message ,
|
|
i18n_data :dict ,
|
|
settings :Settings ,
|
|
panel_service :PanelApiService ,
|
|
subscription_service :SubscriptionService
|
|
):
|
|
"""Handles the /connect command, showing subscription info."""
|
|
logging .info (f"User {message.from_user.id} used /connect command.")
|
|
await my_subscription_command_handler (message ,i18n_data ,settings ,panel_service ,subscription_service ) |