Initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from aiogram import Router
|
||||
|
||||
|
||||
from .import start
|
||||
from .import subscription
|
||||
from .import referral
|
||||
from .import promo_user
|
||||
from .import trial_handler
|
||||
|
||||
|
||||
user_router_aggregate =Router (name ="user_router_aggregate")
|
||||
|
||||
|
||||
|
||||
|
||||
user_router_aggregate .include_router (promo_user .router )
|
||||
user_router_aggregate .include_router (trial_handler .router )
|
||||
user_router_aggregate .include_router (start .router )
|
||||
user_router_aggregate .include_router (subscription .router )
|
||||
user_router_aggregate .include_router (referral .router )
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import logging
|
||||
import json
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from datetime import datetime ,timezone
|
||||
from typing import Optional ,Dict ,Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from yookassa .domain .notification import WebhookNotification
|
||||
from yookassa .domain .models import Amount
|
||||
|
||||
from db .database import get_db_connection_manager ,_setup_db_connection ,get_user
|
||||
from bot .services .subscription_service import SubscriptionService
|
||||
from bot .services .referral_service import ReferralService
|
||||
from bot .services .panel_api_service import PanelApiService
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from config .settings import Settings
|
||||
from bot .services .payment_service import YooKassaService
|
||||
|
||||
payment_processing_lock =asyncio .Lock ()
|
||||
|
||||
YOOKASSA_EVENT_PAYMENT_SUCCEEDED ='payment.succeeded'
|
||||
YOOKASSA_EVENT_PAYMENT_CANCELED ='payment.canceled'
|
||||
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE ='payment.waiting_for_capture'
|
||||
YOOKASSA_EVENT_REFUND_SUCCEEDED ='refund.succeeded'
|
||||
|
||||
async def process_successful_payment (
|
||||
bot :Bot ,payment_info_from_webhook :dict ,i18n :JsonI18n ,
|
||||
settings :Settings ,panel_service :PanelApiService ,yk_service :YooKassaService ,
|
||||
subscription_service :SubscriptionService ,
|
||||
referral_service :ReferralService
|
||||
):
|
||||
metadata =payment_info_from_webhook .get ("metadata",{})
|
||||
user_id_str =metadata .get ("user_id")
|
||||
subscription_months_str =metadata .get ("subscription_months")
|
||||
promo_code_id_str =metadata .get ("promo_code_id")
|
||||
payment_db_id_str =metadata .get ("payment_db_id")
|
||||
|
||||
if not user_id_str or not subscription_months_str or not payment_db_id_str :
|
||||
logging .error (f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}")
|
||||
return
|
||||
try :
|
||||
user_id =int (user_id_str );subscription_months =int (subscription_months_str )
|
||||
payment_db_id =int (payment_db_id_str )
|
||||
promo_code_id =int (promo_code_id_str )if promo_code_id_str and promo_code_id_str .isdigit ()else None
|
||||
amount_data =payment_info_from_webhook .get ("amount",{});
|
||||
payment_value =float (amount_data .get ("value",0.0 ))
|
||||
except (TypeError ,ValueError )as e :
|
||||
logging .error (f"Invalid metadata format for payment processing: {metadata} - {e}")
|
||||
return
|
||||
|
||||
final_end_date_for_user :Optional [datetime ]=None
|
||||
applied_referee_bonus_days :Optional [int ]=None
|
||||
base_subscription_end_date :Optional [datetime ]=None
|
||||
|
||||
async with get_db_connection_manager ()as db :
|
||||
await _setup_db_connection (db )
|
||||
try :
|
||||
await db .execute ("UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",(payment_info_from_webhook .get ("status","succeeded"),payment_db_id ,payment_info_from_webhook .get ("id")))
|
||||
|
||||
new_sub_details =await subscription_service .activate_subscription (user_id ,subscription_months ,payment_value ,payment_db_id ,db_conn =db ,promo_code_id =promo_code_id )
|
||||
|
||||
if new_sub_details and new_sub_details .get ('end_date'):
|
||||
base_subscription_end_date =new_sub_details ['end_date']
|
||||
final_end_date_for_user =base_subscription_end_date
|
||||
|
||||
referral_bonus_info =await referral_service .apply_referral_bonuses_for_payment (user_id ,subscription_months ,db_conn =db )
|
||||
|
||||
if referral_bonus_info and referral_bonus_info .get ("referee_new_end_date"):
|
||||
final_end_date_for_user =referral_bonus_info ["referee_new_end_date"]
|
||||
applied_referee_bonus_days =referral_bonus_info .get ("referee_bonus_applied_days")
|
||||
|
||||
await db .commit ()
|
||||
|
||||
user_lang =await subscription_service .get_user_language (user_id )
|
||||
_ =lambda key ,**kwargs :i18n .gettext (user_lang ,key ,**kwargs )
|
||||
|
||||
success_message =""
|
||||
if applied_referee_bonus_days and final_end_date_for_user :
|
||||
referee_user_data =await get_user (user_id )
|
||||
inviter_name_for_msg =_ ("friend_placeholder")
|
||||
|
||||
if referee_user_data and referee_user_data ['referred_by_id']is not None :
|
||||
inviter_user_data_for_msg =await get_user (referee_user_data ['referred_by_id'])
|
||||
if inviter_user_data_for_msg and inviter_user_data_for_msg ['first_name']:
|
||||
inviter_name_for_msg =inviter_user_data_for_msg ['first_name']
|
||||
|
||||
success_message =_ ("payment_successful_with_referral_bonus",
|
||||
months =subscription_months ,
|
||||
base_end_date =base_subscription_end_date .strftime ('%Y-%m-%d')if base_subscription_end_date else "N/A",
|
||||
bonus_days =applied_referee_bonus_days ,
|
||||
final_end_date =final_end_date_for_user .strftime ('%Y-%m-%d'),
|
||||
inviter_name =inviter_name_for_msg
|
||||
)
|
||||
elif final_end_date_for_user :
|
||||
success_message =_ ("payment_successful",
|
||||
months =subscription_months ,
|
||||
end_date =final_end_date_for_user .strftime ('%Y-%m-%d')
|
||||
)
|
||||
else :
|
||||
logging .error (f"Critical error: final_end_date_for_user is None for user {user_id}")
|
||||
success_message =_ ("payment_successful_error_details")
|
||||
|
||||
try :await bot .send_message (user_id ,success_message )
|
||||
except Exception as e :logging .error (f"Failed to send final payment success message to user {user_id}: {e}")
|
||||
else :
|
||||
logging .error (f"Failed to activate subscription for user {user_id} after payment {payment_info_from_webhook.get('id')}")
|
||||
await db .rollback ()
|
||||
except Exception as e :
|
||||
logging .error (f"Error during process_successful_payment transaction for user {user_id}: {e}",exc_info =True )
|
||||
await db .rollback ()
|
||||
try :
|
||||
user_lang_for_error =await subscription_service .get_user_language (user_id )
|
||||
_err =lambda key ,**kwargs :i18n .gettext (user_lang_for_error ,key ,**kwargs )
|
||||
await bot .send_message (user_id ,_err ("error_processing_your_payment"))
|
||||
except Exception as notify_err :logging .error (f"Failed to send error notification to user {user_id}: {notify_err}")
|
||||
|
||||
|
||||
async def process_cancelled_payment (bot :Bot ,payment_info_from_webhook :dict ,i18n :JsonI18n ,settings :Settings ):
|
||||
metadata =payment_info_from_webhook .get ("metadata",{})
|
||||
user_id_str =metadata .get ("user_id");payment_db_id_str =metadata .get ("payment_db_id")
|
||||
if not user_id_str or not payment_db_id_str :logging .warning (f"Missing metadata in cancelled payment: {payment_info_from_webhook.get('id')}");return
|
||||
try :user_id =int (user_id_str );payment_db_id =int (payment_db_id_str )
|
||||
except ValueError :logging .error (f"Invalid metadata in cancelled payment: {metadata}");return
|
||||
async with get_db_connection_manager ()as db :
|
||||
await _setup_db_connection (db )
|
||||
await db .execute ("UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",(payment_info_from_webhook .get ("status","canceled"),payment_db_id ,payment_info_from_webhook .get ("id")))
|
||||
await db .commit ()
|
||||
user_lang =getattr (settings ,'DEFAULT_LANGUAGE','en')
|
||||
_ =lambda key ,**kwargs :i18n .gettext (user_lang ,key ,**kwargs )
|
||||
try :await bot .send_message (user_id ,_ ("payment_failed"))
|
||||
except Exception as e :logging .error (f"Failed to send payment cancellation message to user {user_id}: {e}")
|
||||
|
||||
async def yookassa_webhook_route (request :web .Request ):
|
||||
logging .info (f"YooKassa Webhook Route: Available keys in request.app: {list(request.app.keys())}")
|
||||
try :
|
||||
bot :Bot =request .app ['bot'];i18n_instance :JsonI18n =request .app ['i18n'];settings :Settings =request .app ['settings']
|
||||
yk_service :YooKassaService =request .app ['yookassa_service'];panel_service :PanelApiService =request .app ['panel_service']
|
||||
subscription_service :SubscriptionService =request .app ['subscription_service']
|
||||
referral_service :ReferralService =request .app ['referral_service']
|
||||
except KeyError as e :logging .error (f"KeyError accessing app context in yookassa_webhook_route: {e}.",exc_info =True );return web .Response (status =500 ,text ="Internal Server Error: Missing app context")
|
||||
try :
|
||||
event_json =await request .json ();notification_object =WebhookNotification (event_json )
|
||||
payment_data_from_notification =notification_object .object
|
||||
logging .info (f"YooKassa Webhook Parsed: Event='{notification_object.event}', PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'")
|
||||
if not payment_data_from_notification or not hasattr (payment_data_from_notification ,'metadata')or payment_data_from_notification .metadata is None :
|
||||
logging .error (f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata.");return web .Response (status =200 ,text ="ok_error_no_metadata")
|
||||
payment_dict_for_processing ={}
|
||||
if hasattr (payment_data_from_notification ,'model_dump'):
|
||||
payment_dict_for_processing =payment_data_from_notification .model_dump (exclude_none =True )
|
||||
if 'amount'in payment_dict_for_processing and isinstance (payment_dict_for_processing ['amount'],Amount ):amount_obj =payment_dict_for_processing ['amount'];payment_dict_for_processing ['amount']={"value":str (amount_obj .value ),"currency":str (amount_obj .currency )}
|
||||
elif 'amount'in payment_dict_for_processing and not isinstance (payment_dict_for_processing ['amount'],dict ):amount_obj_original =payment_data_from_notification .amount ;payment_dict_for_processing ['amount']={"value":str (amount_obj_original .value ),"currency":str (amount_obj_original .currency )}if hasattr (amount_obj_original ,'value')and hasattr (amount_obj_original ,'currency')else {"value":"0.0","currency":"RUB"}
|
||||
elif hasattr (payment_data_from_notification ,'amount')and hasattr (payment_data_from_notification .amount ,'value')and hasattr (payment_data_from_notification .amount ,'currency'):amount_obj =payment_data_from_notification .amount ;payment_dict_for_processing ={"id":str (payment_data_from_notification .id ),"status":str (payment_data_from_notification .status ),"paid":bool (payment_data_from_notification .paid ),"amount":{"value":str (amount_obj .value ),"currency":str (amount_obj .currency )},"metadata":dict (payment_data_from_notification .metadata )if payment_data_from_notification .metadata else {},"description":str (payment_data_from_notification .description )if payment_data_from_notification .description else None }
|
||||
else :logging .error (f"Could not serialize payment_data for payment {payment_data_from_notification.id}");return web .Response (status =200 ,text ="ok_error_serialization")
|
||||
async with payment_processing_lock :
|
||||
if notification_object .event ==YOOKASSA_EVENT_PAYMENT_SUCCEEDED :
|
||||
if payment_dict_for_processing .get ("paid")and payment_dict_for_processing .get ("status")=="succeeded":
|
||||
await process_successful_payment (bot ,payment_dict_for_processing ,i18n_instance ,settings ,panel_service ,yk_service ,subscription_service ,referral_service )
|
||||
else :logging .warning (f"Payment Succeeded event for {payment_dict_for_processing.get('id')} but data not ok: status='{payment_dict_for_processing.get('status')}', paid='{payment_dict_for_processing.get('paid')}'")
|
||||
elif notification_object .event ==YOOKASSA_EVENT_PAYMENT_CANCELED :
|
||||
await process_cancelled_payment (bot ,payment_dict_for_processing ,i18n_instance ,settings )
|
||||
return web .Response (status =200 ,text ="ok")
|
||||
except json .JSONDecodeError :logging .error ("YooKassa Webhook: Invalid JSON.");return web .Response (status =200 ,text ="ok_invalid_json")
|
||||
except KeyError as e :logging .error (f"KeyError in yookassa_webhook_route after initial context access: {e}.",exc_info =True );return web .Response (status =500 ,text ="Internal Server Error: Context error post-access")
|
||||
except Exception as e :logging .error (f"YooKassa Webhook processing error: {e}",exc_info =True );return web .Response (status =200 ,text ="ok_internal_error")
|
||||
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
import re
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .fsm .context import FSMContext
|
||||
from typing import Optional
|
||||
|
||||
from config .settings import Settings
|
||||
from bot .states .user_states import UserPromoStates
|
||||
from bot .services .promo_code_service import PromoCodeService
|
||||
from bot .services .subscription_service import SubscriptionService
|
||||
from bot .keyboards .inline .user_keyboards import get_back_to_main_menu_markup
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from aiogram .utils .markdown import hcode
|
||||
|
||||
from .start import send_main_menu
|
||||
|
||||
router =Router (name ="user_promo_router")
|
||||
|
||||
SUSPICIOUS_SQL_KEYWORDS_REGEX =re .compile (
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|;\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
re .IGNORECASE
|
||||
)
|
||||
SUSPICIOUS_CHARS_REGEX =re .compile (r"(--|#\s|;|\*\/|\/\*)")
|
||||
|
||||
async def prompt_promo_code_input (
|
||||
callback :types .CallbackQuery ,
|
||||
state :FSMContext ,
|
||||
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 :await callback .answer ("Language error.",show_alert =True );return
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
|
||||
if not callback .message :
|
||||
logging .error ("CallbackQuery has no message in prompt_promo_code_input")
|
||||
await callback .answer (_ ("error_occurred_processing_request"),show_alert =True )
|
||||
return
|
||||
|
||||
try :
|
||||
await callback .message .edit_text (
|
||||
text =_ (key ="promo_code_prompt"),
|
||||
reply_markup =get_back_to_main_menu_markup (current_lang ,i18n )
|
||||
)
|
||||
except Exception as e :
|
||||
logging .warning (f"Failed to edit message for promo prompt: {e}")
|
||||
await callback .message .answer (text =_ (key ="promo_code_prompt"),reply_markup =get_back_to_main_menu_markup (current_lang ,i18n ))
|
||||
|
||||
await callback .answer ()
|
||||
await state .set_state (UserPromoStates .waiting_for_promo_code )
|
||||
logging .info (f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. FSM state: {await state.get_state()}")
|
||||
|
||||
|
||||
@router .message (UserPromoStates .waiting_for_promo_code ,F .text )
|
||||
async def process_promo_code_input (
|
||||
message :types .Message ,
|
||||
state :FSMContext ,
|
||||
settings :Settings ,
|
||||
i18n_data :dict ,
|
||||
promo_code_service :PromoCodeService ,
|
||||
bot :Bot
|
||||
):
|
||||
logging .info (f"Processing promo code input from user {message.from_user.id} in state {await state.get_state()}: '{message.text}'")
|
||||
|
||||
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
|
||||
if not i18n or not promo_code_service :
|
||||
logging .error ("Deps missing in process_promo_code_input")
|
||||
await message .reply ("Service error. Please try again.")
|
||||
await state .clear ();return
|
||||
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
code_input =message .text .strip ();user =message .from_user
|
||||
is_suspicious =False
|
||||
if SUSPICIOUS_SQL_KEYWORDS_REGEX .search (code_input )or SUSPICIOUS_CHARS_REGEX .search (code_input )or len (code_input )>100 :
|
||||
is_suspicious =True
|
||||
logging .warning (f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'")
|
||||
|
||||
response_to_user_text =""
|
||||
if is_suspicious :
|
||||
admin_notify_key ="admin_suspicious_promo_attempt_notification_no_username"if not user .username else "admin_suspicious_promo_attempt_notification"
|
||||
admin_lang =settings .DEFAULT_LANGUAGE
|
||||
_admin =lambda k ,**kw :i18n .gettext (admin_lang ,k ,**kw )
|
||||
admin_notification_text =_admin (admin_notify_key ,user_id =user .id ,user_username =user .username or "N/A",user_first_name =user .first_name or "N/A",promo_code_input =hcode (code_input ))
|
||||
try :await bot .send_message (settings .ADMIN_ID ,admin_notification_text ,parse_mode ="HTML")
|
||||
except Exception as e_admin_notify :logging .error (f"Failed to send suspicious promo notification to admin: {e_admin_notify}")
|
||||
response_to_user_text =_ ("promo_code_not_found",code =code_input .upper ())
|
||||
else :
|
||||
success ,response_text_from_service =await promo_code_service .apply_promo_code (user .id ,code_input ,current_lang )
|
||||
response_to_user_text =response_text_from_service
|
||||
|
||||
await message .answer (response_to_user_text ,reply_markup =get_back_to_main_menu_markup (current_lang ,i18n ))
|
||||
await state .clear ()
|
||||
logging .info (f"Promo code '{code_input}' processing finished for user {message.from_user.id}. State cleared.")
|
||||
|
||||
|
||||
|
||||
@router .callback_query (F .data =="main_action:back_to_main",UserPromoStates .waiting_for_promo_code )
|
||||
async def cancel_promo_input_via_button (
|
||||
callback :types .CallbackQuery ,
|
||||
state :FSMContext ,
|
||||
settings :Settings ,
|
||||
i18n_data :dict ,
|
||||
subscription_service :SubscriptionService
|
||||
):
|
||||
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 cancel_promo_input_via_button")
|
||||
await callback .answer ("Language error",show_alert =True )
|
||||
return
|
||||
|
||||
logging .info (f"User {callback.from_user.id} cancelled promo code input via button from state {await state.get_state()}. Clearing state.")
|
||||
await state .clear ()
|
||||
logging .info (f"State after clear for user {callback.from_user.id}: {await state.get_state()}")
|
||||
|
||||
if callback .message :
|
||||
show_trial_button_on_back =False
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ):
|
||||
show_trial_button_on_back =True
|
||||
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_on_back ,is_edit =True )
|
||||
else :
|
||||
|
||||
await callback .answer ("Promo code input cancelled.",show_alert =False )
|
||||
@@ -0,0 +1,81 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .filters import Command
|
||||
from typing import Optional ,Dict
|
||||
|
||||
from config .settings import Settings
|
||||
from bot .services .referral_service import ReferralService
|
||||
from db .database import get_db_connection_manager
|
||||
from bot .keyboards .inline .user_keyboards import get_referral_link_keyboard ,get_back_to_main_menu_markup
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
|
||||
router =Router (name ="user_referral_router")
|
||||
|
||||
async def referral_command_handler (
|
||||
event :types .Message |types .CallbackQuery ,
|
||||
settings :Settings ,
|
||||
i18n_data :dict ,
|
||||
referral_service :ReferralService ,
|
||||
bot :Bot
|
||||
):
|
||||
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
|
||||
target_message =event .message if isinstance (event ,types .CallbackQuery )else event
|
||||
if not target_message :
|
||||
logging .error ("Target message is None in referral_command_handler from callback.")
|
||||
if isinstance (event ,types .CallbackQuery ):await event .answer ("Error displaying referral info.")
|
||||
return
|
||||
|
||||
if not i18n or not referral_service :
|
||||
logging .error ("Deps missing in referral_command_handler")
|
||||
await target_message .answer ("Service error."if isinstance (event ,types .Message )else "Service error.",parse_mode =None )
|
||||
if isinstance (event ,types .CallbackQuery ):await event .answer ()
|
||||
return
|
||||
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
|
||||
bot_info =await bot .get_me ()
|
||||
bot_username =bot_info .username
|
||||
inviter_user_id =event .from_user .id
|
||||
referral_link =referral_service .generate_referral_link (bot_username ,inviter_user_id )
|
||||
bonus_info_parts =[]
|
||||
|
||||
if hasattr (settings ,'subscription_options')and settings .subscription_options :
|
||||
for months_period in sorted (settings .subscription_options .keys ()):
|
||||
inv_bonus =settings .referral_bonus_inviter .get (months_period )
|
||||
ref_bonus =settings .referral_bonus_referee .get (months_period )
|
||||
if inv_bonus is not None or ref_bonus is not None :
|
||||
bonus_info_parts .append (
|
||||
_ ("referral_bonus_per_period",
|
||||
months =months_period ,
|
||||
inviter_bonus_days =inv_bonus if inv_bonus is not None else _ ("no_bonus_days"),
|
||||
referee_bonus_days =ref_bonus if ref_bonus is not None else _ ("no_bonus_days")
|
||||
)
|
||||
)
|
||||
bonus_details_str ="\n".join (bonus_info_parts )if bonus_info_parts else _ ("referral_no_bonuses_configured")
|
||||
text =_ ("referral_program_info_new",referral_link =referral_link ,bonus_details =bonus_details_str )
|
||||
|
||||
|
||||
|
||||
reply_markup_val =get_back_to_main_menu_markup (current_lang ,i18n )
|
||||
|
||||
if isinstance (event ,types .Message ):
|
||||
await event .answer (text ,reply_markup =reply_markup_val ,disable_web_page_preview =True )
|
||||
elif isinstance (event ,types .CallbackQuery ):
|
||||
try :
|
||||
await event .message .edit_text (text ,reply_markup =reply_markup_val ,disable_web_page_preview =True )
|
||||
except Exception as e :
|
||||
logging .warning (f"Failed to edit message for referral info: {e}")
|
||||
|
||||
await event .message .answer (text ,reply_markup =reply_markup_val ,disable_web_page_preview =True )
|
||||
await event .answer ()
|
||||
|
||||
|
||||
@router .callback_query (F .data =="copy_referral_link_ack")
|
||||
async def copy_referral_link_ack_callback_handler (callback :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 :return await callback .answer ("Language service error.",show_alert =True )
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
await callback .answer (text =_ (key ="referral_link_for_copying_reminder"),show_alert =False )
|
||||
@@ -0,0 +1,165 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .filters import CommandStart ,Command
|
||||
from aiogram .fsm .context import FSMContext
|
||||
from typing import Optional ,Dict ,Any ,Callable ,Awaitable
|
||||
from datetime import datetime ,timezone ,timedelta
|
||||
|
||||
from db .database import add_user_if_not_exists ,update_user_language_code
|
||||
from bot .keyboards .inline .user_keyboards import get_main_menu_inline_keyboard ,get_language_selection_keyboard
|
||||
from bot .services .subscription_service import SubscriptionService
|
||||
from bot .services .panel_api_service import PanelApiService
|
||||
from bot .services .referral_service import ReferralService
|
||||
from bot .services .promo_code_service import PromoCodeService
|
||||
from config .settings import Settings
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from aiogram .types import InlineKeyboardMarkup
|
||||
|
||||
router =Router (name ="user_start_router")
|
||||
|
||||
async def send_main_menu (message_or_callback :types .Message |types .CallbackQuery ,settings :Settings ,i18n_data :dict ,show_trial_button_flag :bool ,is_edit :bool =False ):
|
||||
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
answered_callback_internally =False
|
||||
|
||||
if not i18n :
|
||||
logging .error ("i18n_instance missing in send_main_menu")
|
||||
target_mc_for_error =message_or_callback if isinstance (message_or_callback ,types .Message )else message_or_callback .message
|
||||
error_text_fallback ="Error: Language service unavailable."
|
||||
if target_mc_for_error :
|
||||
try :await target_mc_for_error .answer (error_text_fallback )
|
||||
except Exception as e_ans :logging .error (f"Failed to send error message in send_main_menu: {e_ans}")
|
||||
if isinstance (message_or_callback ,types .CallbackQuery ):await message_or_callback .answer ();answered_callback_internally =True
|
||||
return
|
||||
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
user_full_name =message_or_callback .from_user .full_name
|
||||
text =_ (key ="main_menu_greeting",user_name =user_full_name )
|
||||
reply_markup :Optional [InlineKeyboardMarkup ]=get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_button_flag )
|
||||
target_message :Optional [types .Message ]=None
|
||||
if isinstance (message_or_callback ,types .Message ):target_message =message_or_callback
|
||||
elif isinstance (message_or_callback ,types .CallbackQuery ):target_message =message_or_callback .message
|
||||
|
||||
if not target_message :
|
||||
logging .error (f"send_main_menu: target_message is None for event from user {message_or_callback.from_user.id}.")
|
||||
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally :await message_or_callback .answer ("Error displaying menu.");answered_callback_internally =True
|
||||
return
|
||||
|
||||
try :
|
||||
if is_edit :
|
||||
await target_message .edit_text (text ,reply_markup =reply_markup )
|
||||
else :
|
||||
await target_message .answer (text ,reply_markup =reply_markup )
|
||||
|
||||
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally :
|
||||
await message_or_callback .answer ()
|
||||
answered_callback_internally =True
|
||||
except Exception as e_send_edit :
|
||||
logging .warning (f"Failed to send/edit main menu (user: {message_or_callback.from_user.id}): {e_send_edit}.")
|
||||
if is_edit :
|
||||
try :await target_message .answer (text ,reply_markup =reply_markup )
|
||||
except Exception as e_send_new :logging .error (f"Also failed to send new main menu message: {e_send_new}")
|
||||
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally :
|
||||
await message_or_callback .answer ()
|
||||
answered_callback_internally =True
|
||||
|
||||
|
||||
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally :
|
||||
logging .warning (f"Callback {message_or_callback.id} was not answered in send_main_menu main logic paths.")
|
||||
await message_or_callback .answer ()
|
||||
|
||||
|
||||
@router .message (CommandStart ())
|
||||
async def start_command_handler (message :types .Message ,state :FSMContext ,settings :Settings ,i18n_data :dict ,subscription_service :SubscriptionService ,bot :Bot ):
|
||||
|
||||
await state .clear ();current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
if not i18n :logging .error ("i18n_instance not found");await message .answer ("Language service error.");return
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
user_id =message .from_user .id ;referred_by_user_id :Optional [int ]=None ;args =message .text .split ()
|
||||
if len (args )>1 and args [0 ]=="/start":
|
||||
try :
|
||||
referral_param =args [1 ]
|
||||
if referral_param .startswith ("ref_")and referral_param .split ("_")[1 ].isdigit ():
|
||||
potential_referrer_id =int (referral_param .split ("_")[1 ])
|
||||
if potential_referrer_id !=user_id :referred_by_user_id =potential_referrer_id
|
||||
except (ValueError ,IndexError )as e :logging .warning (f"Could not parse referral: '{args[1]}' - {e}")
|
||||
db_op_success ,was_new_bot_user =await add_user_if_not_exists (user_id =user_id ,username =message .from_user .username ,first_name =message .from_user .first_name ,last_name =message .from_user .last_name ,lang_code =current_lang ,referred_by_id =referred_by_user_id )
|
||||
if not db_op_success :await message .answer (_ ("error_occurred_processing_request"));return
|
||||
if referred_by_user_id :logging .info (f"User {user_id} started with referral from {referred_by_user_id}.")
|
||||
await message .answer (_ (key ="welcome",user_name =message .from_user .full_name ))
|
||||
show_trial_button_in_menu =False
|
||||
if settings .TRIAL_ENABLED :
|
||||
if not await subscription_service .has_had_any_subscription (user_id ):
|
||||
show_trial_button_in_menu =True
|
||||
logging .info (f"User {user_id} is eligible for a trial button.")
|
||||
|
||||
else :logging .info (f"User {user_id} not eligible for trial button (already had a subscription).")
|
||||
else :logging .info (f"Trial period is disabled in settings. No trial button.")
|
||||
await send_main_menu (message ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_in_menu )
|
||||
|
||||
|
||||
@router .message (Command ("language"))
|
||||
@router .callback_query (F .data =="main_action:language")
|
||||
async def language_command_handler (event :types .Message |types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
|
||||
|
||||
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE );i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
target_message_for_reply :Optional [types .Message ]=None ;is_callback =isinstance (event ,types .CallbackQuery );answered_callback =False
|
||||
if is_callback :await event .answer ();answered_callback =True ;target_message_for_reply =event .message
|
||||
else :target_message_for_reply =event
|
||||
if not i18n :
|
||||
logging .error ("i18n instance is missing in language_command_handler.");error_message_text ="Language service error."
|
||||
if target_message_for_reply :await target_message_for_reply .answer (error_message_text )
|
||||
return
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );text_to_send =_ (key ="choose_language");reply_markup_to_send =get_language_selection_keyboard (i18n ,current_lang )
|
||||
if not target_message_for_reply :logging .warning ("language_command_handler: No target message.");return
|
||||
if is_callback :
|
||||
try :await target_message_for_reply .edit_text (text_to_send ,reply_markup =reply_markup_to_send )
|
||||
except Exception as e :logging .info (f"Could not edit for lang selection: {e}. Sending new.");await target_message_for_reply .answer (text_to_send ,reply_markup =reply_markup_to_send )
|
||||
else :await target_message_for_reply .answer (text_to_send ,reply_markup =reply_markup_to_send )
|
||||
|
||||
|
||||
@router .callback_query (F .data .startswith ("set_lang_"))
|
||||
async def select_language_callback_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,subscription_service :SubscriptionService ):
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
if not i18n or not callback .message :
|
||||
await callback .answer ("Language service error.",show_alert =True );return
|
||||
|
||||
lang_code =callback .data .split ("_")[2 ];user_id =callback .from_user .id
|
||||
await update_user_language_code (user_id ,lang_code );i18n_data ["current_language"]=lang_code
|
||||
_ =lambda key ,**kwargs :i18n .gettext (lang_code ,key ,**kwargs )
|
||||
|
||||
|
||||
await callback .answer (_ (key ="language_set_alert"))
|
||||
|
||||
show_trial_button_after_lang_change =False
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (user_id ):
|
||||
show_trial_button_after_lang_change =True
|
||||
|
||||
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_after_lang_change ,is_edit =True )
|
||||
|
||||
|
||||
@router .callback_query (F .data .startswith ("main_action:"))
|
||||
async def main_action_callback_handler (
|
||||
|
||||
callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ,bot :Bot ,
|
||||
subscription_service :SubscriptionService ,referral_service :ReferralService ,
|
||||
panel_service :PanelApiService ,promo_code_service :PromoCodeService
|
||||
):
|
||||
|
||||
action =callback .data .split (":")[1 ];from .import subscription as user_subscription_handlers ;from .import referral as user_referral_handlers ;from .import promo_user as user_promo_handlers ;from .import trial_handler as user_trial_handlers
|
||||
if not callback .message :logging .error (f"Callback {callback.id} no message for {action}");await callback .answer ("Error.");return
|
||||
|
||||
if action =="subscribe":await user_subscription_handlers .display_subscription_options (callback ,i18n_data ,settings )
|
||||
elif action =="my_subscription":await user_subscription_handlers .my_subscription_command_handler (callback ,i18n_data ,settings ,panel_service ,subscription_service )
|
||||
elif action =="referral":await user_referral_handlers .referral_command_handler (callback ,settings ,i18n_data ,referral_service ,bot )
|
||||
elif action =="apply_promo":await user_promo_handlers .prompt_promo_code_input (callback ,state ,i18n_data ,settings )
|
||||
elif action =="request_trial":await user_trial_handlers .request_trial_confirmation_handler (callback ,settings ,i18n_data ,subscription_service )
|
||||
elif action =="language":await language_command_handler (callback ,i18n_data ,settings )
|
||||
elif action =="back_to_main":
|
||||
show_trial_button_on_back =False
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ):
|
||||
show_trial_button_on_back =True
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_on_back ,is_edit =True )
|
||||
else :await callback .answer ("Unknown action.",show_alert =True )
|
||||
@@ -0,0 +1,207 @@
|
||||
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 )
|
||||
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
|
||||
from typing import Optional ,Dict ,Any
|
||||
from datetime import datetime ,timedelta ,timezone
|
||||
|
||||
from config .settings import Settings
|
||||
from bot .services .subscription_service import SubscriptionService
|
||||
from bot .services .panel_api_service import PanelApiService
|
||||
from bot .keyboards .inline .user_keyboards import get_trial_confirmation_keyboard ,get_main_menu_inline_keyboard
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from .start import send_main_menu
|
||||
|
||||
router =Router (name ="user_trial_router")
|
||||
|
||||
|
||||
async def request_trial_confirmation_handler (callback :types .CallbackQuery ,settings :Settings ,i18n_data :dict ,subscription_service :SubscriptionService ):
|
||||
user_id =callback .from_user .id ;current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE );i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
if not i18n or not callback .message :await callback .answer ("Error.",show_alert =True );return
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
show_trial_btn_if_fail =False ;
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (user_id ):show_trial_btn_if_fail =True
|
||||
if not settings .TRIAL_ENABLED :await callback .message .edit_text (_ ("trial_feature_disabled"),reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_btn_if_fail ));await callback .answer ();return
|
||||
if await subscription_service .has_had_any_subscription (user_id ):await callback .message .edit_text (_ ("trial_already_had_subscription_or_trial"),reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,False ));await callback .answer ();return
|
||||
traffic_gb_display =str (settings .TRIAL_TRAFFIC_LIMIT_GB )if settings .TRIAL_TRAFFIC_LIMIT_GB and settings .TRIAL_TRAFFIC_LIMIT_GB >0 else _ ("traffic_unlimited")
|
||||
await callback .message .edit_text (text =_ ("trial_confirm_prompt",days =settings .TRIAL_DURATION_DAYS ,traffic_gb =traffic_gb_display ),reply_markup =get_trial_confirmation_keyboard (current_lang ,i18n ));await callback .answer ()
|
||||
|
||||
|
||||
@router .callback_query (F .data =="trial_action:confirm_activate")
|
||||
async def confirm_activate_trial_handler (
|
||||
callback :types .CallbackQuery ,
|
||||
settings :Settings ,
|
||||
i18n_data :dict ,
|
||||
subscription_service :SubscriptionService ,
|
||||
panel_service :PanelApiService ,
|
||||
bot :Bot
|
||||
):
|
||||
user_id =callback .from_user .id
|
||||
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
|
||||
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
|
||||
|
||||
if not i18n or not callback .message :await callback .answer ("Error.",show_alert =True );return
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
|
||||
show_trial_button_after_action =False
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (user_id ):
|
||||
show_trial_button_after_action =True
|
||||
|
||||
|
||||
if not settings .TRIAL_ENABLED :
|
||||
await callback .answer (_ ("trial_feature_disabled"),show_alert =True )
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =False ,is_edit =True )
|
||||
return
|
||||
if await subscription_service .has_had_any_subscription (user_id ):
|
||||
await callback .answer (_ ("trial_already_had_subscription_or_trial"),show_alert =True )
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =False ,is_edit =True )
|
||||
return
|
||||
|
||||
activation_result =await subscription_service .activate_trial_subscription (user_id )
|
||||
final_message_text_in_chat =""
|
||||
|
||||
if activation_result and activation_result .get ("activated"):
|
||||
await callback .answer (_ ("trial_activated_alert"),show_alert =True )
|
||||
|
||||
end_date =activation_result .get ("end_date")
|
||||
config_link_for_trial =_ ("config_link_not_available")
|
||||
|
||||
|
||||
|
||||
if activation_result .get ("subscription_url"):
|
||||
config_link_for_trial =activation_result ["subscription_url"]
|
||||
elif activation_result .get ("panel_short_uuid"):
|
||||
link =await panel_service .get_subscription_link (activation_result ["panel_short_uuid"])
|
||||
if link :config_link_for_trial =link
|
||||
|
||||
traffic_gb_val =activation_result .get ("traffic_gb",settings .TRIAL_TRAFFIC_LIMIT_GB )
|
||||
traffic_display =f"{traffic_gb_val} GB"if traffic_gb_val and traffic_gb_val >0 else _ ("traffic_unlimited")
|
||||
|
||||
final_message_text_in_chat =_ (
|
||||
"trial_activated_details_message",
|
||||
days =activation_result .get ("days",settings .TRIAL_DURATION_DAYS ),
|
||||
end_date =end_date .strftime ('%Y-%m-%d')if isinstance (end_date ,datetime )else "N/A",
|
||||
config_link =config_link_for_trial ,
|
||||
traffic_gb =traffic_display
|
||||
)
|
||||
show_trial_button_after_action =False
|
||||
else :
|
||||
message_key =activation_result .get ("message_key","trial_activation_failed")if activation_result else "trial_activation_failed"
|
||||
final_message_text_in_chat =_ (message_key )
|
||||
await callback .answer (final_message_text_in_chat ,show_alert =True )
|
||||
|
||||
|
||||
if callback .message :
|
||||
try :
|
||||
await callback .message .edit_text (final_message_text_in_chat ,
|
||||
parse_mode ="HTML",
|
||||
reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_button_after_action ),
|
||||
disable_web_page_preview =True )
|
||||
except Exception as e_edit :
|
||||
logging .warning (f"Could not edit trial result message: {e_edit}")
|
||||
await callback .message .answer (final_message_text_in_chat ,
|
||||
parse_mode ="HTML",
|
||||
reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_button_after_action ),
|
||||
disable_web_page_preview =True )
|
||||
|
||||
|
||||
|
||||
@router .callback_query (F .data =="main_action:cancel_trial")
|
||||
async def cancel_trial_activation (callback :types .CallbackQuery ,settings :Settings ,i18n_data :dict ,subscription_service :SubscriptionService ):
|
||||
show_trial_button_on_back =False
|
||||
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ):
|
||||
show_trial_button_on_back =True
|
||||
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_on_back ,is_edit =True )
|
||||
Reference in New Issue
Block a user