Code Refactoring
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -9,15 +8,12 @@ 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 =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 )
|
||||
|
||||
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)
|
||||
|
||||
+167
-135
@@ -1,172 +1,204 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .filters import Command
|
||||
from aiogram .fsm .context import FSMContext
|
||||
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 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
|
||||
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")
|
||||
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 )
|
||||
|
||||
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")
|
||||
_ = 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 )
|
||||
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.")
|
||||
@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 )
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
broadcast_message_text =message .html_text
|
||||
await state .update_data (broadcast_message =broadcast_message_text )
|
||||
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 )
|
||||
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 )
|
||||
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 ):
|
||||
@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 )
|
||||
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 ()
|
||||
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))
|
||||
|
||||
|
||||
callback .data ="admin_action:main"
|
||||
@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
|
||||
|
||||
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 ))
|
||||
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."
|
||||
)
|
||||
|
||||
@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
|
||||
admin_user = callback.from_user
|
||||
|
||||
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs )
|
||||
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
|
||||
|
||||
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
|
||||
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}"
|
||||
)
|
||||
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 )
|
||||
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 ))
|
||||
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 ()
|
||||
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 ()
|
||||
await state.clear()
|
||||
|
||||
+100
-82
@@ -1,96 +1,114 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .filters import Command
|
||||
from aiogram .fsm .context import FSMContext
|
||||
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 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")
|
||||
|
||||
|
||||
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.")
|
||||
@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 )
|
||||
)
|
||||
_ = 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 )
|
||||
@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 )
|
||||
_ = 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 )
|
||||
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":
|
||||
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 )
|
||||
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)
|
||||
|
||||
+286
-198
@@ -1,233 +1,321 @@
|
||||
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
|
||||
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
|
||||
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}$")
|
||||
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 )
|
||||
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 )
|
||||
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 )
|
||||
_ = 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 )
|
||||
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)}"
|
||||
)
|
||||
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 ()
|
||||
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)
|
||||
|
||||
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 {}
|
||||
@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 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"
|
||||
if len(parts) == 3:
|
||||
try:
|
||||
page = int(parts[2])
|
||||
except ValueError:
|
||||
page = 0
|
||||
|
||||
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 )
|
||||
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 ()
|
||||
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 _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 callback .answer ()
|
||||
await state.clear()
|
||||
|
||||
@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 )
|
||||
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)
|
||||
|
||||
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 ()
|
||||
input_text = message.text.strip()
|
||||
user_data_for_logs: Optional[aiosqlite.Row] = None
|
||||
|
||||
@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 ()
|
||||
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)
|
||||
|
||||
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 ))
|
||||
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}")
|
||||
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 )
|
||||
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 }
|
||||
)
|
||||
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
|
||||
@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
|
||||
|
||||
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}")
|
||||
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
|
||||
|
||||
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 )
|
||||
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}")
|
||||
|
||||
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 ()
|
||||
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)
|
||||
|
||||
@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 )
|
||||
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)
|
||||
|
||||
+155
-125
@@ -1,157 +1,187 @@
|
||||
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 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
|
||||
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")
|
||||
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 )
|
||||
|
||||
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 )
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
prompt_text =_ ("admin_promo_create_prompt",example_format ="MYPROMO20 7 100 30")
|
||||
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 )
|
||||
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.")
|
||||
|
||||
@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 )
|
||||
_ = 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"))
|
||||
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")
|
||||
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.")
|
||||
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 )))
|
||||
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
|
||||
admin_id = message.from_user.id
|
||||
|
||||
promo_id =await create_promo_code_db (code ,bonus_days ,max_activations ,admin_id ,valid_until_date )
|
||||
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 ()
|
||||
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 )
|
||||
|
||||
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 )
|
||||
_ = 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 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
|
||||
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 :
|
||||
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))
|
||||
|
||||
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 ()
|
||||
|
||||
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,101 +1,119 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types
|
||||
from typing import Optional ,Dict
|
||||
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
|
||||
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")
|
||||
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 )
|
||||
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 )
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
await callback .answer ()
|
||||
await callback.answer()
|
||||
|
||||
stats_text_parts =[f"<b>{_('admin_stats_header')}</b>"]
|
||||
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)))
|
||||
|
||||
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')}")
|
||||
|
||||
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 ]
|
||||
sync_status = await get_last_sync_status()
|
||||
if sync_status:
|
||||
stats_text_parts.append(
|
||||
f"\n<b>{_('admin_stats_last_sync_header')}</b>")
|
||||
|
||||
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_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")
|
||||
|
||||
sync_status =await get_last_sync_status ()
|
||||
if sync_status :
|
||||
stats_text_parts .append (f"\n<b>{_('admin_stats_last_sync_header')}</b>")
|
||||
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')}")
|
||||
|
||||
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
|
||||
final_text = "\n".join(stats_text_parts)
|
||||
|
||||
details_val =sync_status ['details']
|
||||
details_str =(details_val [:100 ]+"...")if details_val and len (details_val )>100 else (details_val or "N/A")
|
||||
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)
|
||||
|
||||
|
||||
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.")
|
||||
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.")
|
||||
|
||||
@@ -1,126 +1,148 @@
|
||||
import logging
|
||||
from aiogram import Router ,types ,Bot
|
||||
from aiogram .filters import Command
|
||||
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 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
|
||||
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 = 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.")
|
||||
@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)
|
||||
|
||||
users_processed_count =0
|
||||
users_synced_successfully =0
|
||||
sync_errors =[]
|
||||
await message.answer(_("sync_started"))
|
||||
logging.info("Admin triggered panel sync.")
|
||||
|
||||
try :
|
||||
panel_users_data =await panel_service .get_all_panel_users ()
|
||||
users_processed_count = 0
|
||||
users_synced_successfully = 0
|
||||
sync_errors = []
|
||||
|
||||
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 ))
|
||||
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 ))
|
||||
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.")
|
||||
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.")
|
||||
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 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."
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
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 ):
|
||||
@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")
|
||||
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")
|
||||
|
||||
@@ -1,220 +1,580 @@
|
||||
import logging
|
||||
import re
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .fsm .context import FSMContext
|
||||
from typing import Optional ,Tuple ,List ,Any
|
||||
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
|
||||
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")
|
||||
router = Router(name="admin_user_management_router")
|
||||
|
||||
USERNAME_REGEX =re .compile (r"^[a-zA-Z0-9_]{5,32}$")
|
||||
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 ):
|
||||
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 )
|
||||
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
|
||||
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 )
|
||||
_ = 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 .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 ):
|
||||
@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 :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 ()
|
||||
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()
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
from aiogram import Router
|
||||
|
||||
from . import start
|
||||
from . import subscription
|
||||
from . import referral
|
||||
from . import promo_user
|
||||
from . import trial_handler
|
||||
|
||||
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 )
|
||||
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)
|
||||
|
||||
+281
-136
@@ -2,165 +2,310 @@ import logging
|
||||
import json
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from datetime import datetime ,timezone
|
||||
from typing import Optional ,Dict ,Any
|
||||
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 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
|
||||
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 ()
|
||||
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'
|
||||
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}")
|
||||
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}")
|
||||
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
|
||||
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")))
|
||||
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 )
|
||||
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
|
||||
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 )
|
||||
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")
|
||||
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 ()
|
||||
await db.commit()
|
||||
|
||||
user_lang =await subscription_service .get_user_language (user_id )
|
||||
_ =lambda key ,**kwargs :i18n .gettext (user_lang ,key ,**kwargs )
|
||||
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")
|
||||
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']
|
||||
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
|
||||
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}"
|
||||
)
|
||||
elif final_end_date_for_user :
|
||||
success_message =_ ("payment_successful",
|
||||
months =subscription_months ,
|
||||
end_date =final_end_date_for_user .strftime ('%Y-%m-%d')
|
||||
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"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}")
|
||||
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 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")
|
||||
|
||||
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")
|
||||
|
||||
+139
-106
@@ -1,127 +1,160 @@
|
||||
import logging
|
||||
import re
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .fsm .context import FSMContext
|
||||
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 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")
|
||||
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|;|\*\/|\/\*)")
|
||||
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 )
|
||||
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 )
|
||||
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}'"
|
||||
)
|
||||
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()}")
|
||||
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 .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 )
|
||||
@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()}")
|
||||
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
|
||||
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 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 )
|
||||
await callback.answer("Promo code input cancelled.", show_alert=False)
|
||||
|
||||
@@ -1,81 +1,100 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram .filters import Command
|
||||
from typing import Optional ,Dict
|
||||
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
|
||||
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")
|
||||
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.")
|
||||
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 ()
|
||||
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 )
|
||||
_ = 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 =[]
|
||||
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 )
|
||||
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()
|
||||
|
||||
|
||||
|
||||
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 )
|
||||
@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)
|
||||
|
||||
+260
-140
@@ -1,165 +1,285 @@
|
||||
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 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
|
||||
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")
|
||||
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
|
||||
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
|
||||
_ = 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
|
||||
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 )
|
||||
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:
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
|
||||
|
||||
@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 )
|
||||
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 );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 )
|
||||
_ = 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 .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
|
||||
@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):
|
||||
|
||||
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 )
|
||||
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)
|
||||
|
||||
|
||||
await callback .answer (_ (key ="language_set_alert"))
|
||||
@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
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@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 )
|
||||
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)
|
||||
|
||||
+292
-172
@@ -1,207 +1,327 @@
|
||||
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 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
|
||||
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")
|
||||
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 ()
|
||||
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
|
||||
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
|
||||
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
|
||||
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 ):
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
|
||||
await message_or_callback .answer ()
|
||||
answered_callback =True
|
||||
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 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()
|
||||
|
||||
|
||||
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 )
|
||||
@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 )
|
||||
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
|
||||
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
|
||||
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 )
|
||||
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 ()
|
||||
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
|
||||
@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 )
|
||||
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 ()
|
||||
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 )
|
||||
@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()
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
@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 )
|
||||
logging.info(f"User {message.from_user.id} used /connect command.")
|
||||
await my_subscription_command_handler(message, i18n_data, settings,
|
||||
panel_service, subscription_service)
|
||||
|
||||
@@ -1,113 +1,164 @@
|
||||
import logging
|
||||
from aiogram import Router ,F ,types ,Bot
|
||||
from aiogram import Router, F, types, Bot
|
||||
|
||||
from typing import Optional ,Dict ,Any
|
||||
from datetime import datetime ,timedelta ,timezone
|
||||
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 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")
|
||||
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 )
|
||||
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
|
||||
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 )
|
||||
_ = 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 =""
|
||||
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 )
|
||||
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")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
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 )
|
||||
@router.callback_query(F.data == "main_action:cancel_trial")
|
||||
async def cancel_trial_activation(callback: types.CallbackQuery,
|
||||
settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService):
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
is_edit=True)
|
||||
|
||||
Reference in New Issue
Block a user