Initial commit
This commit is contained in:
@@ -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 )
|
||||
|
||||
@@ -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 ()
|
||||
@@ -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 )
|
||||
@@ -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 )
|
||||
@@ -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 ()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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.")
|
||||
@@ -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")
|
||||
@@ -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 ()
|
||||
Reference in New Issue
Block a user