Initial commit

This commit is contained in:
machka
2025-05-14 08:11:59 +00:00
commit 77584b5b51
41 changed files with 5205 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
from aiogram import Router
from .common import router as common_admin_router
from .promo_codes import router as promo_codes_admin_router
from .user_management import router as user_management_admin_router
from .broadcast import router as broadcast_admin_router
from .statistics import router as statistics_admin_router
from .sync_admin import router as sync_admin_router
from .logs_admin import router as logs_admin_router
admin_router_aggregate =Router (name ="admin_router_aggregate")
admin_router_aggregate .include_router (common_admin_router )
admin_router_aggregate .include_router (promo_codes_admin_router )
admin_router_aggregate .include_router (user_management_admin_router )
admin_router_aggregate .include_router (broadcast_admin_router )
admin_router_aggregate .include_router (statistics_admin_router )
admin_router_aggregate .include_router (sync_admin_router )
admin_router_aggregate .include_router (logs_admin_router )
+172
View File
@@ -0,0 +1,172 @@
import logging
import asyncio
from aiogram import Router ,F ,types ,Bot
from aiogram .filters import Command
from aiogram .fsm .context import FSMContext
from typing import Optional
from config .settings import Settings
from db .database import get_all_users_for_broadcast ,log_user_action
from bot .states .admin_states import AdminStates
from bot .keyboards .inline .admin_keyboards import get_broadcast_confirmation_keyboard ,get_back_to_admin_panel_keyboard
from bot .middlewares .i18n import JsonI18n
router =Router (name ="admin_broadcast_router")
async def broadcast_message_prompt_handler (
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 :
logging .error ("i18n missing in broadcast_message_prompt_handler")
await callback .answer ("Language service error.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
prompt_text =_ ("admin_broadcast_enter_message")
if callback .message :
try :
await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
except Exception as e :
logging .warning (f"Could not edit message for broadcast prompt: {e}")
await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ()
await state .set_state (AdminStates .waiting_for_broadcast_message )
@router .message (AdminStates .waiting_for_broadcast_message ,F .text )
async def process_broadcast_message_handler (
message :types .Message ,
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 :
logging .error ("i18n missing in process_broadcast_message_handler")
await message .reply ("Language service error.")
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
broadcast_message_text =message .html_text
await state .update_data (broadcast_message =broadcast_message_text )
preview_snippet =broadcast_message_text [:200 ]+"..."if len (broadcast_message_text )>200 else broadcast_message_text
confirmation_prompt =_ ("admin_broadcast_confirm_prompt",message_preview =preview_snippet )
await message .answer (
confirmation_prompt ,
reply_markup =get_broadcast_confirmation_keyboard (current_lang ,i18n ),
parse_mode ="HTML"
)
await state .set_state (AdminStates .confirming_broadcast )
@router .callback_query (F .data =="admin_action:main",AdminStates .waiting_for_broadcast_message )
async def cancel_broadcast_at_prompt_stage (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ):
from .common import admin_panel_actions_callback_handler
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 callback .answer ("Language error.",show_alert =True );return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
if callback .message :
try :await callback .message .edit_text (_ ("admin_broadcast_cancelled"),reply_markup =None )
except Exception :await callback .message .answer (_ ("admin_broadcast_cancelled"))
await callback .answer (_ ("admin_broadcast_cancelled"))
await state .clear ()
callback .data ="admin_action:main"
from bot .keyboards .inline .admin_keyboards import get_admin_panel_keyboard
if callback .message :
await callback .message .answer (_ ("admin_panel_title"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang ))
@router .callback_query (F .data .startswith ("broadcast_final_action:"),AdminStates .confirming_broadcast )
async def confirm_broadcast_callback_handler (
callback :types .CallbackQuery ,
state :FSMContext ,
i18n_data :dict ,
bot :Bot ,
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");await callback .answer ("Language error.",show_alert =True );return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
action =callback .data .split (":")[1 ]
user_fsm_data =await state .get_data ()
broadcast_message =user_fsm_data .get ("broadcast_message")
if not callback .message :
await callback .answer ("Error: message context lost.",show_alert =True )
await state .clear ();return
if action =="send":
if not broadcast_message :
await callback .message .edit_text (_ ("admin_broadcast_error_no_message"))
await state .clear ();await callback .answer (show_alert =True );return
await callback .message .edit_text (_ ("admin_broadcast_sending_started"),reply_markup =None )
await callback .answer ()
users_to_broadcast =await get_all_users_for_broadcast ()
sent_count =0 ;failed_count =0
logging .info (f"Starting broadcast: '{broadcast_message[:50]}...' to {len(users_to_broadcast)} users.")
admin_user =callback .from_user
for user_row in users_to_broadcast :
user_id =user_row ['user_id']
try :
await bot .send_message (user_id ,broadcast_message ,parse_mode ="HTML")
sent_count +=1
await log_user_action (
user_id =admin_user .id ,
telegram_username =admin_user .username ,
telegram_first_name =admin_user .first_name ,
event_type ="admin_broadcast_sent",
content =f"To user {user_id}: {broadcast_message[:70]}...",
is_admin_event =True ,
target_user_id =user_id
)
except Exception as e :
failed_count +=1
logging .warning (f"Failed to send broadcast to user {user_id}: {type(e).__name__} - {e}")
await log_user_action (
user_id =admin_user .id ,
telegram_username =admin_user .username ,
telegram_first_name =admin_user .first_name ,
event_type ="admin_broadcast_failed",
content =f"For user {user_id}: {type(e).__name__} - {str(e)[:70]}...",
is_admin_event =True ,
target_user_id =user_id
)
await asyncio .sleep (0.1 )
result_message =_ ("admin_broadcast_finished_stats",sent_count =sent_count ,failed_count =failed_count )
await callback .message .answer (result_message ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
elif action =="cancel":
await callback .message .edit_text (_ ("admin_broadcast_cancelled"),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ()
await state .clear ()
+96
View File
@@ -0,0 +1,96 @@
import logging
from aiogram import Router ,F ,types ,Bot
from aiogram .filters import Command
from aiogram .fsm .context import FSMContext
from typing import Optional
from config .settings import Settings
from bot .keyboards .inline .admin_keyboards import get_admin_panel_keyboard
from bot .middlewares .i18n import JsonI18n
from bot .services .panel_api_service import PanelApiService
from .import broadcast as admin_broadcast_handlers
from .import promo_codes as admin_promo_handlers
from .import user_management as admin_user_mgmnt_handlers
from .import statistics as admin_stats_handlers
from .import sync_admin as admin_sync_handlers
from .import logs_admin as admin_logs_handlers
router =Router (name ="admin_common_router")
@router .message (Command ("admin"))
async def admin_panel_command_handler (message :types .Message ,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 admin_panel_command_handler")
await message .answer ("Language service error.")
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
await message .answer (
_ (key ="admin_panel_title"),
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings )
)
@router .callback_query (F .data .startswith ("admin_action:"))
async def admin_panel_actions_callback_handler (
callback :types .CallbackQuery ,
state :FSMContext ,
settings :Settings ,
i18n_data :dict ,
bot :Bot ,
panel_service :PanelApiService
):
action_parts =callback .data .split (":")
action =action_parts [1 ]
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 admin_panel_actions_callback_handler")
await callback .answer ("Language error.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
if not callback .message :
logging .error (f"CallbackQuery {callback.id} from {callback.from_user.id} has no message for action {action}")
await callback .answer ("Error processing action: message context lost.",show_alert =True )
return
if action =="stats":
await admin_stats_handlers .show_statistics_handler (callback ,i18n_data ,settings )
elif action =="broadcast":
await admin_broadcast_handlers .broadcast_message_prompt_handler (callback ,state ,i18n_data ,settings )
elif action =="create_promo":
await admin_promo_handlers .create_promo_prompt_handler (callback ,state ,i18n_data ,settings )
elif action =="view_promos":
await admin_promo_handlers .view_promo_codes_handler (callback ,i18n_data ,settings )
elif action =="ban_user_prompt":
await admin_user_mgmnt_handlers .ban_user_prompt_handler (callback ,state ,i18n_data ,settings )
elif action =="unban_user_prompt":
await admin_user_mgmnt_handlers .unban_user_prompt_handler (callback ,state ,i18n_data ,settings )
elif action =="view_banned":
await admin_user_mgmnt_handlers .view_banned_users_handler (callback ,i18n_data ,settings ,state )
elif action =="view_logs_menu":
await admin_logs_handlers .display_logs_menu (callback ,i18n_data ,settings )
elif action =="sync_panel":
await admin_sync_handlers .sync_command_handler (callback .message ,bot =bot ,settings =settings ,i18n_data =i18n_data ,panel_service =panel_service )
await callback .answer (_ ("admin_sync_initiated_from_panel"))
elif action =="main":
try :
await callback .message .edit_text (
_ (key ="admin_panel_title"),
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings )
)
except Exception :
await callback .message .answer (
_ (key ="admin_panel_title"),
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings )
)
await callback .answer ()
else :
logging .warning (f"Unknown admin_action received: {action} from callback {callback.data}")
await callback .answer (_ ("admin_unknown_action"),show_alert =True )
+233
View File
@@ -0,0 +1,233 @@
import logging
import math
import re
from aiogram import Router ,F ,types ,Bot
from aiogram .fsm .context import FSMContext
from typing import Optional ,List ,Dict ,Any
import aiosqlite
from config .settings import Settings
from db .database import (
get_all_message_logs_paginated ,count_all_message_logs ,
get_user_message_logs_paginated ,count_user_message_logs ,
get_user ,get_user_by_telegram_username
)
from bot .states .admin_states import AdminStates
from bot .keyboards .inline .admin_keyboards import (
get_logs_menu_keyboard ,
get_logs_pagination_keyboard ,
get_back_to_admin_panel_keyboard ,
get_admin_panel_keyboard
)
from bot .middlewares .i18n import JsonI18n
router =Router (name ="admin_logs_router")
USERNAME_REGEX =re .compile (r"^[a-zA-Z0-9_]{5,32}$")
async def display_logs_menu (callback :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")
if not i18n :
logging .error ("i18n_instance missing in display_logs_menu")
await callback .answer ("Language service error.",show_alert =True )
return
if not callback .message :
logging .error ("CallbackQuery has no message in display_logs_menu")
await callback .answer ("Error processing request.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
try :
await callback .message .edit_text (
text =_ (key ="admin_logs_menu_title"),
reply_markup =get_logs_menu_keyboard (i18n ,current_lang )
)
except Exception as e :
logging .warning (f"Failed to edit message for logs menu: {e}")
await callback .message .answer (
text =_ (key ="admin_logs_menu_title"),
reply_markup =get_logs_menu_keyboard (i18n ,current_lang )
)
await callback .answer ()
async def _display_formatted_logs (
target_message :types .Message ,
logs :List [aiosqlite .Row ],
total_logs :int ,
current_page :int ,
settings :Settings ,
title_key :str ,
base_pagination_callback_data :str ,
i18n :JsonI18n ,
current_lang :str ,
title_kwargs :Optional [Dict [str ,Any ]]=None
):
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
page_size =settings .LOGS_PAGE_SIZE
actual_title_kwargs =title_kwargs or {}
if not logs and total_logs ==0 :
text =_ (title_key ,current_page =1 ,total_pages =1 ,**actual_title_kwargs )+"\n\n"+_ ("admin_no_logs_found")
reply_markup =get_logs_pagination_keyboard (current_page ,1 ,base_pagination_callback_data ,i18n ,current_lang ,back_to_logs_menu =True )
else :
total_pages =math .ceil (total_logs /page_size )if page_size >0 else 1
text =_ (title_key ,current_page =current_page +1 ,total_pages =max (1 ,total_pages ),**actual_title_kwargs )+"\n"
log_entries_text =[]
for log_entry in logs :
user_display_parts =[]
telegram_first_name =log_entry ['telegram_first_name']if 'telegram_first_name'in log_entry .keys ()and log_entry ['telegram_first_name']else None
telegram_username =log_entry ['telegram_username']if 'telegram_username'in log_entry .keys ()and log_entry ['telegram_username']else None
user_id_from_log =log_entry ['user_id']if 'user_id'in log_entry .keys ()and log_entry ['user_id']else None
if telegram_first_name :user_display_parts .append (telegram_first_name )
if telegram_username :user_display_parts .append (f"(@{telegram_username})")
user_display =" ".join (user_display_parts ).strip ()
if not user_display :
user_display =_ ("system_or_unknown_user")if not user_id_from_log else f"ID: {user_id_from_log}"
user_id_display =str (user_id_from_log )if user_id_from_log is not None else "N/A"
content_raw =log_entry ['content']if 'content'in log_entry .keys ()and log_entry ['content']else ""
content_preview =(content_raw [:100 ]+"...")if len (content_raw )>100 else (content_raw or "N/A")
log_entries_text .append (
_ ("admin_log_entry_format",
timestamp_str =log_entry ['timestamp_str']if 'timestamp_str'in log_entry .keys ()else 'N/A',
user_display =user_display ,
user_id =user_id_display ,
event_type =log_entry ['event_type']if 'event_type'in log_entry .keys ()else 'N/A',
content_preview =content_preview
).replace ("\n","\n ")
)
text +="\n\n".join (log_entries_text )
reply_markup =get_logs_pagination_keyboard (current_page ,total_pages ,base_pagination_callback_data ,i18n ,current_lang ,back_to_logs_menu =True )
try :
await target_message .edit_text (text ,reply_markup =reply_markup ,parse_mode ="HTML",disable_web_page_preview =True )
except Exception as e :
logging .warning (f"Failed to edit message for logs display: {e}. Content length: {len(text)}")
chunk_size =4000
for i in range (0 ,len (text ),chunk_size ):
chunk =text [i :i +chunk_size ];is_last_chunk =(i +chunk_size )>=len (text )
await target_message .answer (chunk ,reply_markup =reply_markup if is_last_chunk else None ,parse_mode ="HTML",disable_web_page_preview =True )
@router .callback_query (F .data .startswith ("admin_logs:view_all"))
async def view_all_logs_handler (callback :types .CallbackQuery ,settings :Settings ,i18n_data :dict ):
page =0
parts =callback .data .split (":")
if len (parts )==3 :
try :
page =int (parts [2 ])
except ValueError :
page =0
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
if not i18n or not callback .message :
await callback .answer ("Error processing request.",show_alert =True )
return
logs ,total_logs =await get_all_message_logs_paginated (settings .LOGS_PAGE_SIZE ,page *settings .LOGS_PAGE_SIZE ),await count_all_message_logs ()
await _display_formatted_logs (
target_message =callback .message ,logs =logs ,total_logs =total_logs ,
current_page =page ,settings =settings ,
title_key ="admin_all_logs_title",base_pagination_callback_data ="admin_logs:view_all",
i18n =i18n ,current_lang =current_lang
)
await callback .answer ()
@router .callback_query (F .data =="admin_logs:prompt_user")
async def prompt_user_for_logs_handler (callback :types .CallbackQuery ,state :FSMContext ,i18n_data :dict ,settings :Settings ):
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
if not i18n or not callback .message :await callback .answer ("Error");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
await callback .message .edit_text (
text =_ ("admin_prompt_for_user_id_or_username_logs"),
reply_markup =get_logs_menu_keyboard (i18n ,current_lang )
)
await state .set_state (AdminStates .waiting_for_user_id_for_logs )
await callback .answer ()
@router .message (AdminStates .waiting_for_user_id_for_logs ,F .text )
async def process_user_id_for_logs_handler (message :types .Message ,state :FSMContext ,settings :Settings ,i18n_data :dict ):
current_state_fsm =await state .get_state ()
logging .info (f"Processing user input for logs in state {current_state_fsm}: '{message.text}'")
await state .clear ()
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
if not i18n :await message .reply ("Language error.");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
input_text =message .text .strip ()
user_data_for_logs :Optional [aiosqlite .Row ]=None
if input_text .isdigit ():
try :user_data_for_logs =await get_user (int (input_text ))
except ValueError :pass
elif input_text .startswith ("@")and USERNAME_REGEX .match (input_text [1 :]):
user_data_for_logs =await get_user_by_telegram_username (input_text [1 :])
elif USERNAME_REGEX .match (input_text ):
user_data_for_logs =await get_user_by_telegram_username (input_text )
if not user_data_for_logs :
await message .answer (_ ("admin_log_user_not_found",input =input_text ))
return
target_user_id =user_data_for_logs ['user_id']
user_display =user_data_for_logs ['first_name']or (f"@{user_data_for_logs['username']}"if user_data_for_logs .get ('username')else f"ID {target_user_id}")
logs ,total_logs =await get_user_message_logs_paginated (target_user_id ,settings .LOGS_PAGE_SIZE ,0 ),await count_user_message_logs (target_user_id )
await _display_formatted_logs (
target_message =message ,logs =logs ,total_logs =total_logs ,
current_page =0 ,settings =settings ,
title_key ="admin_user_logs_title",
base_pagination_callback_data =f"admin_logs:view_user:{target_user_id}",
i18n =i18n ,current_lang =current_lang ,
title_kwargs ={"user_display":user_display }
)
@router .callback_query (F .data .startswith ("admin_logs:view_user:"))
async def view_user_logs_paginated_handler (callback :types .CallbackQuery ,settings :Settings ,i18n_data :dict ):
try :
parts =callback .data .split (":")
target_user_id =int (parts [2 ])
page =int (parts [3 ])
except (IndexError ,ValueError ):
await callback .answer ("Invalid log request.",show_alert =True );return
i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance")
current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE )
if not i18n or not callback .message :await callback .answer ("Error");return
user_data_for_logs =await get_user (target_user_id )
if not user_data_for_logs :
await callback .message .edit_text ("User not found for logs.");await callback .answer ();return
user_display =user_data_for_logs ['first_name']or (f"@{user_data_for_logs['username']}"if user_data_for_logs .get ('username')else f"ID {target_user_id}")
logs ,total_logs =await get_user_message_logs_paginated (target_user_id ,settings .LOGS_PAGE_SIZE ,page *settings .LOGS_PAGE_SIZE ),await count_user_message_logs (target_user_id )
await _display_formatted_logs (
target_message =callback .message ,logs =logs ,total_logs =total_logs ,
current_page =page ,settings =settings ,
title_key ="admin_user_logs_title",
base_pagination_callback_data =f"admin_logs:view_user:{target_user_id}",
i18n =i18n ,current_lang =current_lang ,
title_kwargs ={"user_display":user_display }
)
await callback .answer ()
@router .callback_query (F .data =="admin_action:view_logs_menu",AdminStates .waiting_for_user_id_for_logs )
async def cancel_log_user_input_state_to_menu (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ):
await state .clear ()
await display_logs_menu (callback ,i18n_data ,settings )
+157
View File
@@ -0,0 +1,157 @@
import logging
from aiogram import Router ,F ,types ,Bot
from aiogram .filters import Command
from aiogram .fsm .context import FSMContext
from datetime import datetime ,timedelta ,timezone
from typing import Optional
from config .settings import Settings
from db .database import create_promo_code_db ,get_promo_codes_db
from bot .states .admin_states import AdminStates
from bot .keyboards .inline .admin_keyboards import get_back_to_admin_panel_keyboard
from bot .middlewares .i18n import JsonI18n
router =Router (name ="admin_promo_codes_router")
async def create_promo_prompt_handler (
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 :
logging .error ("i18n missing in create_promo_prompt_handler")
await callback .answer ("Language service error.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
prompt_text =_ ("admin_promo_create_prompt",example_format ="MYPROMO20 7 100 30")
if callback .message :
try :
await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
except Exception as e :
logging .warning (f"Could not edit message for promo prompt: {e}")
await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ()
await state .set_state (AdminStates .waiting_for_promo_details )
@router .message (AdminStates .waiting_for_promo_details ,F .text )
async def process_promo_code_details_handler (
message :types .Message ,
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 :
logging .error ("i18n missing in process_promo_code_details_handler")
await message .reply ("Language service error.")
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
parts =message .text .strip ().split ()
if not (3 <=len (parts )<=4 ):
await message .answer (_ ("admin_promo_invalid_format"))
return
try :
code =parts [0 ].upper ()
if not (3 <=len (code )<=30 and code .isalnum ()):
raise ValueError ("Promo code must be 3-30 alphanumeric characters.")
bonus_days =int (parts [1 ]);max_activations =int (parts [2 ])
valid_until_date :Optional [datetime ]=None
valid_until_str_display =_ ("admin_promo_valid_indefinitely")
if len (parts )==4 :
valid_days_from_now =int (parts [3 ])
if valid_days_from_now <=0 :raise ValueError ("Validity days (if provided) must be positive.")
valid_until_date =datetime .now (timezone .utc )+timedelta (days =valid_days_from_now )
valid_until_str_display =_ ("admin_promo_valid_until_display",date =valid_until_date .strftime ('%Y-%m-%d'))
if bonus_days <=0 or max_activations <=0 :raise ValueError ("Bonus days and max activations must be positive.")
except ValueError as e :
await message .answer (_ ("admin_promo_invalid_values",error =str (e )))
return
admin_id =message .from_user .id
promo_id =await create_promo_code_db (code ,bonus_days ,max_activations ,admin_id ,valid_until_date )
if promo_id :
success_text =_ ("admin_promo_created_success",code =code ,bonus_days =bonus_days ,max_activations =max_activations ,valid_until_str =valid_until_str_display )
await message .answer (success_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
else :
await message .answer (_ ("admin_promo_creation_failed"))
await state .clear ()
async def view_promo_codes_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 :
logging .error ("i18n missing in view_promo_codes_handler")
await callback .answer ("Language error.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
promos =await get_promo_codes_db (is_active_only =True ,limit =20 )
if not callback .message :
await callback .answer ("Error: message context lost.",show_alert =True );return
if not promos :
await callback .message .edit_text (_ ("admin_no_active_promos"),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ();return
response_text_parts =[f"<b>{_('admin_active_promos_list_header')}</b>\n"]
for promo in promos :
valid_until_display_text =_ ("admin_promo_valid_indefinitely")
if promo ['valid_until']:
try :
valid_until_dt :Optional [datetime ]=None
if 'T'in promo ['valid_until']:
valid_until_dt =datetime .fromisoformat (promo ['valid_until'].replace ("Z","+00:00"))
else :
valid_until_dt =datetime .strptime (promo ['valid_until'],'%Y-%m-%d %H:%M:%S')
if valid_until_dt and valid_until_dt .tzinfo is None :
valid_until_dt =valid_until_dt .replace (tzinfo =timezone .utc )
valid_until_display_text =valid_until_dt .strftime ('%Y-%m-%d')
except ValueError as e :
logging .warning (f"Could not parse valid_until date string '{promo['valid_until']}' for promo code {promo['code']}: {e}")
valid_until_display_text =promo ['valid_until']
response_text_parts .append (
_ ("admin_promo_list_item",code =promo ['code'],bonus =promo ['bonus_days'],
current =promo ['current_activations'],max =promo ['max_activations'],
valid_until =valid_until_display_text )
)
final_text ="\n".join (response_text_parts )
try :
await callback .message .edit_text (final_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ),parse_mode ="HTML")
except Exception as e :
logging .warning (f"Failed to edit message for promo list: {e}")
if callback .message :
await callback .message .answer (final_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ),parse_mode ="HTML")
await callback .answer ()
+101
View File
@@ -0,0 +1,101 @@
import logging
from aiogram import Router ,F ,types
from typing import Optional ,Dict
from datetime import datetime
from config .settings import Settings
from db .database import get_user_count_stats ,get_payment_logs ,get_message_logs_db ,get_last_sync_status
from bot .keyboards .inline .admin_keyboards import get_back_to_admin_panel_keyboard
from bot .middlewares .i18n import JsonI18n
router =Router (name ="admin_statistics_router")
async def show_statistics_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 :
logging .error ("i18n missing in show_statistics_handler")
await callback .answer ("Language service error.",show_alert =True )
return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
await callback .answer ()
stats_text_parts =[f"<b>{_('admin_stats_header')}</b>"]
user_stats =await get_user_count_stats ()
stats_text_parts .append (
_ ("admin_stats_users",
total_users =user_stats .get ("total_users",0 ),
banned_users =user_stats .get ("banned_users",0 ),
active_subs =user_stats .get ("users_with_active_subscriptions",0 )
)
)
last_payments =await get_payment_logs (limit =5 )
if last_payments :
stats_text_parts .append (f"\n<b>{_('admin_stats_recent_payments_header')}</b>")
for payment in last_payments :
status_emoji =""if payment ['status']=='succeeded'else (""if payment ['status']=='pending'else "")
user_info =f"User {payment['user_id']}"+(f" (@{payment['username']})"if payment ['username']else "")
payment_date_str =payment ['created_at']
if isinstance (payment_date_str ,str )and len (payment_date_str )>10 :
payment_date_str =payment_date_str [:10 ]
stats_text_parts .append (
_ ("admin_stats_payment_item",
status_emoji =status_emoji ,amount =payment ['amount'],currency =payment ['currency'],
user_info =user_info ,p_status =payment ['status'],p_date =payment_date_str
)
)
else :
stats_text_parts .append (f"\n{_('admin_stats_no_payments_found')}")
sync_status =await get_last_sync_status ()
if sync_status :
stats_text_parts .append (f"\n<b>{_('admin_stats_last_sync_header')}</b>")
sync_time_val =sync_status ['last_sync_time']
sync_time_str ="N/A"
if isinstance (sync_time_val ,datetime ):
sync_time_str =sync_time_val .strftime ('%Y-%m-%d %H:%M:%S UTC')
elif isinstance (sync_time_val ,str ):
sync_time_str =sync_time_val [:19 ]if len (sync_time_val )>19 else sync_time_val
details_val =sync_status ['details']
details_str =(details_val [:100 ]+"...")if details_val and len (details_val )>100 else (details_val or "N/A")
stats_text_parts .append (f" {_('admin_stats_sync_time')}: {sync_time_str}")
stats_text_parts .append (f" {_('admin_stats_sync_status')}: {sync_status['status']}")
stats_text_parts .append (f" {_('admin_stats_sync_users_processed')}: {sync_status['users_processed_from_panel']}")
stats_text_parts .append (f" {_('admin_stats_sync_subs_synced')}: {sync_status['subscriptions_synced']}")
stats_text_parts .append (f" {_('admin_stats_sync_details_label')}: {details_str}")
else :
stats_text_parts .append (f"\n{_('admin_sync_status_never_run')}")
final_text ="\n".join (stats_text_parts )
if callback .message :
try :
await callback .message .edit_text (
final_text ,
reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ),
parse_mode ="HTML"
)
except Exception as e :
logging .error (f"Error editing message for statistics: {e}",exc_info =True )
for chunk in [final_text [i :i +4000 ]for i in range (0 ,len (final_text ),4000 )]:
await callback .message .answer (
chunk ,
reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )if chunk ==final_text [-len (chunk ):]else None ,
parse_mode ="HTML"
)
else :
logging .error ("Cannot send statistics, callback.message is None.")
+126
View File
@@ -0,0 +1,126 @@
import logging
from aiogram import Router ,types ,Bot
from aiogram .filters import Command
from typing import Optional
from config .settings import Settings
from bot .services .panel_api_service import PanelApiService
from db .database import update_sync_status ,get_last_sync_status ,sync_panel_user_data
from bot .middlewares .i18n import JsonI18n
from bot .keyboards .inline .admin_keyboards import get_back_to_admin_panel_keyboard
router =Router (name ="admin_sync_router")
@router .message (Command ("sync"))
async def sync_command_handler (
message :types .Message ,
bot :Bot ,
settings :Settings ,
i18n_data :dict ,
panel_service :PanelApiService
):
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 message .answer ("Language error.");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
await message .answer (_ ("sync_started"))
logging .info ("Admin triggered panel sync.")
users_processed_count =0
users_synced_successfully =0
sync_errors =[]
try :
panel_users_data =await panel_service .get_all_panel_users ()
if panel_users_data is None :
error_msg ="Failed to fetch users from panel or panel API issue."
sync_errors .append (error_msg )
await update_sync_status ("failed",error_msg )
await message .answer (_ ("sync_failed",details =error_msg ))
return
if not panel_users_data :
status_msg ="No users found in the panel to sync."
await update_sync_status ("success",status_msg ,0 ,0 )
await message .answer (_ ("sync_completed",status ="Success",details =status_msg ))
return
total_panel_users =len (panel_users_data )
logging .info (f"Starting sync for {total_panel_users} panel users.")
for panel_user_dict in panel_users_data :
users_processed_count +=1
telegram_id_from_panel =panel_user_dict .get ('telegramId')
panel_uuid =panel_user_dict .get ('uuid')
if not telegram_id_from_panel :
logging .info (f"Panel user {panel_uuid} (username: {panel_user_dict.get('username')}) has no 'telegramId'. Skipping TG ID based sync.")
continue
if not panel_uuid :
logging .warning (f"Panel user (TG ID: {telegram_id_from_panel}) missing 'uuid'. Skipping.")
sync_errors .append (f"Panel user data for TG ID {telegram_id_from_panel} missing UUID.")
continue
if await sync_panel_user_data (panel_user_dict ):
users_synced_successfully +=1
else :
sync_errors .append (f"Sync issue for panel user: {panel_uuid} (TG ID: {telegram_id_from_panel})")
if users_processed_count %20 ==0 :
logging .info (f"Sync progress: {users_processed_count}/{total_panel_users} users processed.")
status_msg =f"Panel users checked: {total_panel_users}. Users/Subscriptions synced via TG ID: {users_synced_successfully}."
if sync_errors :
status_msg +=f" Errors encountered: {len(sync_errors)}. See logs for details."
error_preview ="\n".join (sync_errors [:3 ])
await update_sync_status ("partial_success",status_msg +" "+error_preview ,total_panel_users ,users_synced_successfully )
await message .answer (_ ("sync_completed",status ="Partial Success",details =status_msg ))
else :
await update_sync_status ("success",status_msg ,total_panel_users ,users_synced_successfully )
await message .answer (_ ("sync_completed",status ="Success",details =status_msg ))
except Exception as e :
logging .error (f"Error during /sync command: {e}",exc_info =True )
error_detail =f"An unexpected error occurred during sync: {str(e)}"
await update_sync_status ("failed",error_detail ,users_processed_count ,users_synced_successfully )
await message .answer (_ ("sync_failed",details =error_detail ))
@router .message (Command ("syncstatus"))
async def sync_status_command_handler (message :types .Message ,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 message .answer ("Language error.");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );status_record =await get_last_sync_status ();response_text =""
if status_record :
last_time_val =status_record ['last_sync_time'];last_time_str =last_time_val .strftime ('%Y-%m-%d %H:%M:%S UTC')if isinstance (last_time_val ,datetime )else str (last_time_val )
if isinstance (last_time_str ,str )and len (last_time_str )>19 :last_time_str =last_time_str [:19 ]
details_val =status_record ['details'];details_str =(details_val [:200 ]+"...")if details_val and len (details_val )>200 else (details_val or "N/A")
response_text =(f"<b>{_('admin_stats_last_sync_header')}</b>\n"
f" {_('admin_stats_sync_time')}: {last_time_str}\n"
f" {_('admin_stats_sync_status')}: {status_record['status']}\n"
f" {_('admin_stats_sync_users_processed')}: {status_record['users_processed_from_panel']}\n"
f" {_('admin_stats_sync_subs_synced')}: {status_record['subscriptions_synced']}\n"
f" {_('admin_stats_sync_details_label')}: {details_str}")
else :response_text =_ ("admin_sync_status_never_run")
await message .answer (response_text ,parse_mode ="HTML")
+220
View File
@@ -0,0 +1,220 @@
import logging
import re
from aiogram import Router ,F ,types ,Bot
from aiogram .fsm .context import FSMContext
from typing import Optional ,Tuple ,List ,Any
import math
import aiosqlite
from datetime import datetime
from config .settings import Settings
from db .database import (
get_user ,set_user_ban_status_db ,get_user_by_telegram_username ,
get_banned_users_list_paginated ,get_user_active_subscription_end_date
)
from bot .services .panel_api_service import PanelApiService
from bot .states .admin_states import AdminStates
from bot .keyboards .inline .admin_keyboards import (
get_back_to_admin_panel_keyboard ,get_user_card_keyboard ,
get_banned_users_keyboard ,get_confirmation_keyboard ,get_admin_panel_keyboard
)
from bot .middlewares .i18n import JsonI18n
router =Router (name ="admin_user_management_router")
USERNAME_REGEX =re .compile (r"^[a-zA-Z0-9_]{5,32}$")
async def ban_user_prompt_handler (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 :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );prompt_text =_ ("admin_ban_user_prompt")
if callback .message :
try :await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
except Exception as e :logging .warning (f"Edit failed: {e}");await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ();await state .set_state (AdminStates .waiting_for_user_id_to_ban )
@router .message (AdminStates .waiting_for_user_id_to_ban ,F .text )
async def process_user_input_to_ban_handler (message :types .Message ,state :FSMContext ,i18n_data :dict ,settings :Settings ,panel_service :PanelApiService ):
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 message .reply ("Language error.");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );input_text =message .text .strip ();user_to_ban_data :Optional [aiosqlite .Row ]=None ;user_id_or_username_for_msg =input_text
if input_text .isdigit ():
try :user_id_to_ban_val =int (input_text );user_to_ban_data =await get_user (user_id_to_ban_val )
except ValueError :await message .answer (_ ("admin_invalid_user_id_format"));return
elif input_text .startswith ("@")and USERNAME_REGEX .match (input_text [1 :]):user_to_ban_data =await get_user_by_telegram_username (input_text [1 :])
elif USERNAME_REGEX .match (input_text ):user_to_ban_data =await get_user_by_telegram_username (input_text )
else :await message .answer (_ ("admin_invalid_user_id_format")+" "+_ ("admin_invalid_username_format"));return
if not user_to_ban_data :await message .answer (_ ("admin_user_not_found_in_bot_db",user_id =input_text ));await state .clear ();return
user_id_to_ban =user_to_ban_data ['user_id'];user_id_or_username_for_msg =f"@{user_to_ban_data['username']}"if user_to_ban_data .get ('username')else str (user_id_to_ban )
if user_id_to_ban ==message .from_user .id or user_id_to_ban in settings .ADMIN_IDS :await message .answer (_ ("admin_cannot_ban_self_or_admin"));await state .clear ();return
if user_to_ban_data ['is_banned']:await message .answer (_ ("admin_user_already_banned",user_id_or_username =user_id_or_username_for_msg ));await state .clear ();return
panel_user_uuid =user_to_ban_data ['panel_user_uuid']if user_to_ban_data and 'panel_user_uuid'in user_to_ban_data .keys ()and user_to_ban_data ['panel_user_uuid']else None
await set_user_ban_status_db (user_id_to_ban ,is_banned =True )
reply_markup_val =get_back_to_admin_panel_keyboard (current_lang ,i18n )
if panel_user_uuid :
panel_ban_success =await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =False )
if panel_ban_success :await message .answer (_ ("admin_user_banned_success_panel_too",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
else :await message .answer (_ ("admin_user_banned_local_panel_fail",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
else :await message .answer (_ ("admin_user_banned_local_no_panel_uuid",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
await state .clear ()
async def unban_user_prompt_handler (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 :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );prompt_text =_ ("admin_unban_user_prompt")
if callback .message :
try :await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
except :await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
await callback .answer ();await state .set_state (AdminStates .waiting_for_user_id_to_unban )
@router .message (AdminStates .waiting_for_user_id_to_unban ,F .text )
async def process_user_input_to_unban_handler (message :types .Message ,state :FSMContext ,i18n_data :dict ,settings :Settings ,panel_service :PanelApiService ):
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 message .reply ("Language error.");return
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );input_text =message .text .strip ();user_to_unban_data :Optional [aiosqlite .Row ]=None ;user_id_or_username_for_msg =input_text
if input_text .isdigit ():
try :user_id_to_unban_val =int (input_text );user_to_unban_data =await get_user (user_id_to_unban_val )
except ValueError :await message .answer (_ ("admin_invalid_user_id_format"));return
elif input_text .startswith ("@")and USERNAME_REGEX .match (input_text [1 :]):user_to_unban_data =await get_user_by_telegram_username (input_text [1 :])
elif USERNAME_REGEX .match (input_text ):user_to_unban_data =await get_user_by_telegram_username (input_text )
else :await message .answer (_ ("admin_invalid_user_id_format")+" "+_ ("admin_invalid_username_format"));return
if not user_to_unban_data :await message .answer (_ ("admin_user_not_found_in_bot_db",user_id =input_text ));await state .clear ();return
user_id_to_unban =user_to_unban_data ['user_id'];user_id_or_username_for_msg =f"@{user_to_unban_data['username']}"if user_to_unban_data .get ('username')else str (user_id_to_unban )
if not user_to_unban_data ['is_banned']:await message .answer (_ ("admin_user_not_banned",user_id_or_username =user_id_or_username_for_msg ));await state .clear ();return
panel_user_uuid =user_to_unban_data ['panel_user_uuid']if user_to_unban_data and 'panel_user_uuid'in user_to_unban_data .keys ()and user_to_unban_data ['panel_user_uuid']else None
await set_user_ban_status_db (user_id_to_unban ,is_banned =False )
reply_markup_val =get_back_to_admin_panel_keyboard (current_lang ,i18n )
if panel_user_uuid :
panel_unban_success =await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =True )
if panel_unban_success :await message .answer (_ ("admin_user_unbanned_success_panel_too",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
else :await message .answer (_ ("admin_user_unbanned_local_panel_fail",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
else :await message .answer (_ ("admin_user_unbanned_local_no_panel_uuid",user_id_or_username =user_id_or_username_for_msg ),reply_markup =reply_markup_val )
await state .clear ()
async def view_banned_users_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,state :FSMContext ):
await state .clear ();current_page =0 ;
if ":"in callback .data and callback .data .count (":")==2 :
try :current_page =int (callback .data .split (":")[-1 ])
except ValueError :current_page =0
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 )
banned_users ,total_banned =await get_banned_users_list_paginated (limit =settings .LOGS_PAGE_SIZE ,offset =current_page *settings .LOGS_PAGE_SIZE )
if not callback .message :await callback .answer ("Error.");return
if total_banned ==0 :
await callback .message .edit_text (_ ("admin_no_banned_users"),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ))
else :
total_pages =math .ceil (total_banned /settings .LOGS_PAGE_SIZE )if settings .LOGS_PAGE_SIZE >0 else 1
await callback .message .edit_text (text =_ ("admin_banned_list_title",current_page =current_page +1 ,total_pages =max (1 ,total_pages )),reply_markup =get_banned_users_keyboard (banned_users ,current_page ,total_banned ,i18n ,current_lang ,settings ))
await callback .answer ()
@router .callback_query (F .data .startswith ("admin_user_card:"))
async def show_user_card_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,panel_service :PanelApiService ,state :FSMContext ,force_user_id :Optional [int ]=None ,force_page :Optional [int ]=None ):
await state .clear ();user_id_to_show =0 ;banned_list_page_to_return =0
if force_user_id is not None and force_page is not None :user_id_to_show =force_user_id ;banned_list_page_to_return =force_page
else :
try :parts =callback .data .split (":");user_id_to_show =int (parts [1 ]);banned_list_page_to_return =int (parts [2 ])if len (parts )>2 else 0
except (IndexError ,ValueError ):await callback .answer ("Invalid user card data.",show_alert =True );return
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 )
user_data =await get_user (user_id_to_show )
if not callback .message :await callback .answer ("Error.");return
if not user_data :await callback .message .edit_text (_ ("admin_user_not_found_in_bot_db",user_id =user_id_to_show ),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ));await callback .answer ();return
user_display_name =user_data ['first_name']or (f"@{user_data['username']}"if user_data .get ('username')else f"ID: {user_id_to_show}")
sub_end_date_str =await get_user_active_subscription_end_date (user_id_to_show )or _ ("user_card_sub_na")
reg_date_from_db =user_data ['registration_date_str']if 'registration_date_str'in user_data .keys ()else "N/A"
reg_date_display =reg_date_from_db [:10 ]if reg_date_from_db and reg_date_from_db !="N/A"else "N/A"
card_text =_ ("user_card_info",user_id =user_data ['user_id'],username =user_data .get ('username',"N/A"),first_name =user_data .get ('first_name',""),last_name =user_data .get ('last_name',""),language_code =user_data .get ('language_code',"N/A"),panel_user_uuid =user_data .get ('panel_user_uuid',"N/A"),ban_status =_ (key ="user_card_banned")if user_data ['is_banned']else _ (key ="user_card_active"),reg_date =reg_date_display ,sub_end_date =sub_end_date_str )
await callback .message .edit_text (text =f"{_('admin_user_card_title', user_display=user_display_name)}\n\n{card_text}",reply_markup =get_user_card_keyboard (user_id_to_show ,bool (user_data ['is_banned']),i18n ,current_lang ,banned_list_page_to_return ),parse_mode ="HTML")
if force_user_id is None :await callback .answer ()
@router .callback_query (F .data .startswith ("admin_unban_confirm:"))
async def confirm_unban_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id =int (user_id_str );banned_list_page =int (page_str )
except :await callback .answer ("Invalid data",show_alert =True );return
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 )
user_data =await get_user (user_id );user_display =(user_data ['first_name']or (f"@{user_data['username']}"if user_data .get ('username')else f"ID {user_id}"))if user_data else f"ID {user_id}"
if callback .message :await callback .message .edit_text (text =_ ("admin_confirm_unban_prompt",user_display =user_display ,user_id =user_id ),reply_markup =get_confirmation_keyboard (yes_callback_data =f"admin_unban_do:{user_id}:{banned_list_page}",no_callback_data =f"admin_user_card:{user_id}:{banned_list_page}",i18n_instance =i18n ,lang =current_lang ),parse_mode ="HTML")
await callback .answer ()
@router .callback_query (F .data .startswith ("admin_ban_confirm:"))
async def confirm_ban_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id =int (user_id_str );banned_list_page =int (page_str )
except :await callback .answer ("Invalid data",show_alert =True );return
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 )
user_data =await get_user (user_id );user_display =(user_data ['first_name']or (f"@{user_data['username']}"if user_data .get ('username')else f"ID {user_id}"))if user_data else f"ID {user_id}"
if callback .message :
await callback .message .edit_text (text =_ ("admin_confirm_ban_prompt",user_display =user_display ,user_id =user_id ),reply_markup =get_confirmation_keyboard (yes_callback_data =f"admin_ban_do:{user_id}:{banned_list_page}",no_callback_data =f"admin_user_card:{user_id}:{banned_list_page}",i18n_instance =i18n ,lang =current_lang ),parse_mode ="HTML")
await callback .answer ()
@router .callback_query (F .data .startswith ("admin_unban_do:"))
async def do_unban_user_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,panel_service :PanelApiService ,state :FSMContext ):
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id_to_unban =int (user_id_str );banned_list_page =int (page_str )
except :await callback .answer ("Invalid data",show_alert =True );return
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 )
user_to_unban_data =await get_user (user_id_to_unban );user_display_name =(user_to_unban_data ['first_name']or (f"@{user_to_unban_data['username']}"if user_to_unban_data .get ('username')else f"ID {user_id_to_unban}"))if user_to_unban_data else f"ID {user_id_to_unban}"
if not user_to_unban_data or not user_to_unban_data ['is_banned']:
await callback .answer (_ ("admin_user_not_banned",user_id_or_username =user_display_name ),show_alert =True )
if callback .message :await show_user_card_handler (callback ,i18n_data ,settings ,panel_service ,state ,force_user_id =user_id_to_unban ,force_page =banned_list_page )
return
await set_user_ban_status_db (user_id_to_unban ,is_banned =False )
panel_user_uuid =user_to_unban_data ['panel_user_uuid']if user_to_unban_data and 'panel_user_uuid'in user_to_unban_data .keys ()and user_to_unban_data ['panel_user_uuid']else None
if panel_user_uuid :
if not await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =True ):logging .warning (f"Panel status update fail for unban {user_id_to_unban}")
await callback .answer (_ ("admin_user_unbanned_from_card",user_display =user_display_name ,user_id =user_id_to_unban ),show_alert =False )
if callback .message :await show_user_card_handler (callback ,i18n_data ,settings ,panel_service ,state ,force_user_id =user_id_to_unban ,force_page =banned_list_page )
@router .callback_query (F .data .startswith ("admin_ban_do:"))
async def do_ban_user_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,panel_service :PanelApiService ,state :FSMContext ):
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id_to_ban =int (user_id_str );banned_list_page =int (page_str )
except :await callback .answer ("Invalid data",show_alert =True );return
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 )
user_to_ban_data =await get_user (user_id_to_ban );user_display_name =(user_to_ban_data ['first_name']or (f"@{user_to_ban_data['username']}"if user_to_ban_data .get ('username')else f"ID {user_id_to_ban}"))if user_to_ban_data else f"ID {user_id_to_ban}"
if not user_to_ban_data :await callback .answer (_ ("admin_user_not_found_in_bot_db",user_id =user_id_to_ban ),show_alert =True );return
if user_to_ban_data ['is_banned']:await callback .answer (_ ("admin_user_already_banned",user_id_or_username =user_display_name ),show_alert =True );return
if user_id_to_ban ==callback .from_user .id or user_id_to_ban in settings .ADMIN_IDS :await callback .answer (_ ("admin_cannot_ban_self_or_admin"),show_alert =True );return
await set_user_ban_status_db (user_id_to_ban ,is_banned =True )
panel_user_uuid =user_to_ban_data ['panel_user_uuid']if user_to_ban_data and 'panel_user_uuid'in user_to_ban_data .keys ()and user_to_ban_data ['panel_user_uuid']else None
if panel_user_uuid :
if not await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =False ):logging .warning (f"Panel status update fail for ban {user_id_to_ban}")
await callback .answer (_ ("admin_user_banned_from_card",user_display =user_display_name ,user_id =user_id_to_ban ),show_alert =False )
if callback .message :await show_user_card_handler (callback ,i18n_data ,settings ,panel_service ,state ,force_user_id =user_id_to_ban ,force_page =banned_list_page )
@router .callback_query (F .data =="admin_action:main",AdminStates .waiting_for_user_id_to_ban )
@router .callback_query (F .data =="admin_action:main",AdminStates .waiting_for_user_id_to_unban )
async def cancel_user_management_input_state (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ,bot :Bot ):
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 callback .message :
try :await callback .message .edit_text (_ ("admin_action_cancelled_default"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings ))
except :await callback .message .answer (_ ("admin_action_cancelled_default"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings ))
await callback .answer (_ ("admin_action_cancelled_default"));await state .clear ()
+21
View File
@@ -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 )
+166
View File
@@ -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")
+127
View File
@@ -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 )
+81
View File
@@ -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 )
+165
View File
@@ -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 )
+207
View File
@@ -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 )
+113
View File
@@ -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 )