Code Refactoring
This commit is contained in:
+13
-10
@@ -1,14 +1,17 @@
|
||||
from typing import List ,Union
|
||||
from aiogram .filters import Filter
|
||||
from aiogram .types import Message ,CallbackQuery ,User
|
||||
from typing import List, Union
|
||||
from aiogram.filters import Filter
|
||||
from aiogram.types import Message, CallbackQuery, User
|
||||
|
||||
class AdminFilter (Filter ):
|
||||
def __init__ (self ,admin_ids :List [int ]):
|
||||
self .admin_ids =admin_ids
|
||||
|
||||
async def __call__ (self ,event :Union [Message ,CallbackQuery ],event_from_user :User )->bool :
|
||||
if not event_from_user :
|
||||
class AdminFilter(Filter):
|
||||
|
||||
def __init__(self, admin_ids: List[int]):
|
||||
self.admin_ids = admin_ids
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery],
|
||||
event_from_user: User) -> bool:
|
||||
if not event_from_user:
|
||||
return False
|
||||
if not self .admin_ids :
|
||||
if not self.admin_ids:
|
||||
return False
|
||||
return event_from_user .id in self .admin_ids
|
||||
return event_from_user.id in self.admin_ids
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,82 +1,172 @@
|
||||
from aiogram .utils .keyboard import InlineKeyboardBuilder ,InlineKeyboardButton
|
||||
from aiogram .types import InlineKeyboardMarkup ,WebAppInfo
|
||||
from typing import Optional ,List ,Any
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||||
from aiogram.types import InlineKeyboardMarkup, WebAppInfo
|
||||
from typing import Optional, List, Any
|
||||
import math
|
||||
|
||||
from config .settings import Settings
|
||||
from config.settings import Settings
|
||||
|
||||
def get_admin_panel_keyboard (i18n_instance ,lang :str ,settings :Settings )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="admin_stats_button"),callback_data ="admin_action:stats")
|
||||
builder .button (text =_ (key ="admin_broadcast_button"),callback_data ="admin_action:broadcast")
|
||||
builder .button (text =_ (key ="admin_create_promo_button"),callback_data ="admin_action:create_promo")
|
||||
builder .button (text =_ (key ="admin_view_promos_button"),callback_data ="admin_action:view_promos")
|
||||
builder .button (text =_ (key ="admin_ban_user_button"),callback_data ="admin_action:ban_user_prompt")
|
||||
builder .button (text =_ (key ="admin_unban_user_button"),callback_data ="admin_action:unban_user_prompt")
|
||||
builder .button (text =_ (key ="admin_view_banned_users_button"),callback_data ="admin_action:view_banned:0")
|
||||
builder .button (text =_ (key ="admin_view_logs_menu_button"),callback_data ="admin_action:view_logs_menu")
|
||||
builder .button (text =_ (key ="admin_sync_panel_button"),callback_data ="admin_action:sync_panel")
|
||||
builder .adjust (2 ,2 ,2 ,2 ,1 )
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_logs_menu_keyboard (i18n_instance ,lang :str )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="admin_view_all_logs_button"),callback_data ="admin_logs:view_all:0")
|
||||
builder .button (text =_ (key ="admin_view_user_logs_prompt_button"),callback_data ="admin_logs:prompt_user")
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main"))
|
||||
return builder .as_markup ()
|
||||
def get_admin_panel_keyboard(i18n_instance, lang: str,
|
||||
settings: Settings) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="admin_stats_button"),
|
||||
callback_data="admin_action:stats")
|
||||
builder.button(text=_(key="admin_broadcast_button"),
|
||||
callback_data="admin_action:broadcast")
|
||||
builder.button(text=_(key="admin_create_promo_button"),
|
||||
callback_data="admin_action:create_promo")
|
||||
builder.button(text=_(key="admin_view_promos_button"),
|
||||
callback_data="admin_action:view_promos")
|
||||
builder.button(text=_(key="admin_ban_user_button"),
|
||||
callback_data="admin_action:ban_user_prompt")
|
||||
builder.button(text=_(key="admin_unban_user_button"),
|
||||
callback_data="admin_action:unban_user_prompt")
|
||||
builder.button(text=_(key="admin_view_banned_users_button"),
|
||||
callback_data="admin_action:view_banned:0")
|
||||
builder.button(text=_(key="admin_view_logs_menu_button"),
|
||||
callback_data="admin_action:view_logs_menu")
|
||||
builder.button(text=_(key="admin_sync_panel_button"),
|
||||
callback_data="admin_action:sync_panel")
|
||||
builder.adjust(2, 2, 2, 2, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
def get_logs_pagination_keyboard (current_page :int ,total_pages :int ,base_callback_data :str ,i18n_instance ,lang :str ,back_to_logs_menu :bool =False )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
row_buttons =[]
|
||||
if current_page >0 :
|
||||
row_buttons .append (InlineKeyboardButton (text ="⬅️ "+_ ("prev_page_button",default ="Prev"),callback_data =f"{base_callback_data}:{current_page - 1}"))
|
||||
if current_page <total_pages -1 :
|
||||
row_buttons .append (InlineKeyboardButton (text =_ ("next_page_button",default ="Next")+" ➡️",callback_data =f"{base_callback_data}:{current_page + 1}"))
|
||||
|
||||
if row_buttons :builder .row (*row_buttons )
|
||||
def get_logs_menu_keyboard(i18n_instance, lang: str) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="admin_view_all_logs_button"),
|
||||
callback_data="admin_logs:view_all:0")
|
||||
builder.button(text=_(key="admin_view_user_logs_prompt_button"),
|
||||
callback_data="admin_logs:prompt_user")
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_admin_panel_button"),
|
||||
callback_data="admin_action:main"))
|
||||
return builder.as_markup()
|
||||
|
||||
if back_to_logs_menu :
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="admin_logs_menu_title"),callback_data ="admin_action:view_logs_menu"))
|
||||
else :
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main"))
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_banned_users_keyboard (users :List [Any ],current_page :int ,total_users_banned :int ,i18n_instance ,lang :str ,settings :Settings )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder ()
|
||||
page_size =settings .LOGS_PAGE_SIZE
|
||||
total_pages =math .ceil (total_users_banned /page_size )if page_size >0 else 1
|
||||
if total_users_banned ==0 :total_pages =1
|
||||
for user_row in users :
|
||||
user_display =user_row ['first_name']or f"ID: {user_row['user_id']}";
|
||||
if user_row ['username']:user_display +=f" (@{user_row['username']})"
|
||||
else :user_display =f"ID: {user_row['user_id']}"if not user_row ['first_name']else user_display
|
||||
builder .button (text =user_display ,callback_data =f"admin_user_card:{user_row['user_id']}:{current_page}")
|
||||
pagination_row =[]
|
||||
if current_page >0 :pagination_row .append (InlineKeyboardButton (text ="⬅️ "+_ ("prev_page_button",default ="Prev"),callback_data =f"admin_action:view_banned:{current_page - 1}"))
|
||||
if (current_page +1 )*page_size <total_users_banned :pagination_row .append (InlineKeyboardButton (text =_ ("next_page_button",default ="Next")+" ➡️",callback_data =f"admin_action:view_banned:{current_page + 1}"))
|
||||
if pagination_row :builder .row (*pagination_row )
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main"));builder .adjust (1 );return builder .as_markup ()
|
||||
def get_logs_pagination_keyboard(
|
||||
current_page: int,
|
||||
total_pages: int,
|
||||
base_callback_data: str,
|
||||
i18n_instance,
|
||||
lang: str,
|
||||
back_to_logs_menu: bool = False) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
row_buttons = []
|
||||
if current_page > 0:
|
||||
row_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ " + _("prev_page_button", default="Prev"),
|
||||
callback_data=f"{base_callback_data}:{current_page - 1}"))
|
||||
if current_page < total_pages - 1:
|
||||
row_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text=_("next_page_button", default="Next") + " ➡️",
|
||||
callback_data=f"{base_callback_data}:{current_page + 1}"))
|
||||
|
||||
def get_user_card_keyboard (user_id :int ,is_banned :bool ,i18n_instance ,lang :str ,banned_list_page :int =0 )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder ()
|
||||
if is_banned :builder .button (text =_ (key ="user_card_unban_button"),callback_data =f"admin_unban_confirm:{user_id}:{banned_list_page}")
|
||||
else :builder .button (text =_ (key ="user_card_ban_button"),callback_data =f"admin_ban_confirm:{user_id}:{banned_list_page}")
|
||||
builder .button (text =_ (key ="user_card_back_to_banned_list_button"),callback_data =f"admin_action:view_banned:{banned_list_page}")
|
||||
builder .button (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main");builder .adjust (1 );return builder .as_markup ()
|
||||
if row_buttons: builder.row(*row_buttons)
|
||||
|
||||
def get_confirmation_keyboard (yes_callback_data :str ,no_callback_data :str ,i18n_instance ,lang :str )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="yes_button"),callback_data =yes_callback_data );builder .button (text =_ (key ="no_button"),callback_data =no_callback_data );return builder .as_markup ()
|
||||
if back_to_logs_menu:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="admin_logs_menu_title"),
|
||||
callback_data="admin_action:view_logs_menu"))
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_admin_panel_button"),
|
||||
callback_data="admin_action:main"))
|
||||
return builder.as_markup()
|
||||
|
||||
def get_broadcast_confirmation_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="confirm_broadcast_send_button"),callback_data ="broadcast_final_action:send")
|
||||
builder .button (text =_ (key ="cancel_broadcast_button"),callback_data ="broadcast_final_action:cancel");builder .adjust (2 );return builder .as_markup ()
|
||||
|
||||
def get_back_to_admin_panel_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main");return builder .as_markup ()
|
||||
def get_banned_users_keyboard(users: List[Any], current_page: int,
|
||||
total_users_banned: int, i18n_instance,
|
||||
lang: str,
|
||||
settings: Settings) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
page_size = settings.LOGS_PAGE_SIZE
|
||||
total_pages = math.ceil(total_users_banned /
|
||||
page_size) if page_size > 0 else 1
|
||||
if total_users_banned == 0: total_pages = 1
|
||||
for user_row in users:
|
||||
user_display = user_row['first_name'] or f"ID: {user_row['user_id']}"
|
||||
if user_row['username']: user_display += f" (@{user_row['username']})"
|
||||
else:
|
||||
user_display = f"ID: {user_row['user_id']}" if not user_row[
|
||||
'first_name'] else user_display
|
||||
builder.button(text=user_display,
|
||||
callback_data=
|
||||
f"admin_user_card:{user_row['user_id']}:{current_page}")
|
||||
pagination_row = []
|
||||
if current_page > 0:
|
||||
pagination_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ " + _("prev_page_button", default="Prev"),
|
||||
callback_data=f"admin_action:view_banned:{current_page - 1}"))
|
||||
if (current_page + 1) * page_size < total_users_banned:
|
||||
pagination_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=_("next_page_button", default="Next") + " ➡️",
|
||||
callback_data=f"admin_action:view_banned:{current_page + 1}"))
|
||||
if pagination_row: builder.row(*pagination_row)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_admin_panel_button"),
|
||||
callback_data="admin_action:main"))
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_user_card_keyboard(user_id: int,
|
||||
is_banned: bool,
|
||||
i18n_instance,
|
||||
lang: str,
|
||||
banned_list_page: int = 0) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
if is_banned:
|
||||
builder.button(
|
||||
text=_(key="user_card_unban_button"),
|
||||
callback_data=f"admin_unban_confirm:{user_id}:{banned_list_page}")
|
||||
else:
|
||||
builder.button(
|
||||
text=_(key="user_card_ban_button"),
|
||||
callback_data=f"admin_ban_confirm:{user_id}:{banned_list_page}")
|
||||
builder.button(
|
||||
text=_(key="user_card_back_to_banned_list_button"),
|
||||
callback_data=f"admin_action:view_banned:{banned_list_page}")
|
||||
builder.button(text=_(key="back_to_admin_panel_button"),
|
||||
callback_data="admin_action:main")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_confirmation_keyboard(yes_callback_data: str, no_callback_data: str,
|
||||
i18n_instance,
|
||||
lang: str) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="yes_button"), callback_data=yes_callback_data)
|
||||
builder.button(text=_(key="no_button"), callback_data=no_callback_data)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_broadcast_confirmation_keyboard(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="confirm_broadcast_send_button"),
|
||||
callback_data="broadcast_final_action:send")
|
||||
builder.button(text=_(key="cancel_broadcast_button"),
|
||||
callback_data="broadcast_final_action:cancel")
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_back_to_admin_panel_keyboard(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="back_to_admin_panel_button"),
|
||||
callback_data="admin_action:main")
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -1,119 +1,159 @@
|
||||
from aiogram .utils .keyboard import InlineKeyboardBuilder ,InlineKeyboardButton
|
||||
from aiogram .types import InlineKeyboardMarkup
|
||||
from typing import Dict ,Optional ,List
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
from config .settings import Settings
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
def get_main_menu_inline_keyboard (
|
||||
lang :str ,
|
||||
i18n_instance ,
|
||||
settings :Settings ,
|
||||
show_trial_button :bool =False
|
||||
)->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
def get_main_menu_inline_keyboard(
|
||||
lang: str,
|
||||
i18n_instance,
|
||||
settings: Settings,
|
||||
show_trial_button: bool = False) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if show_trial_button and settings.TRIAL_ENABLED:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_activate_trial_button"),
|
||||
callback_data="main_action:request_trial"))
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_subscribe_inline"),
|
||||
callback_data="main_action:subscribe"))
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_my_subscription_inline"),
|
||||
callback_data="main_action:my_subscription"))
|
||||
|
||||
referral_button = InlineKeyboardButton(
|
||||
text=_(key="menu_referral_inline"),
|
||||
callback_data="main_action:referral")
|
||||
promo_button = InlineKeyboardButton(
|
||||
text=_(key="menu_apply_promo_button"),
|
||||
callback_data="main_action:apply_promo")
|
||||
builder.row(referral_button, promo_button)
|
||||
|
||||
language_button = InlineKeyboardButton(
|
||||
text=_(key="menu_language_settings_inline"),
|
||||
callback_data="main_action:language")
|
||||
status_button_list = []
|
||||
if settings.SERVER_STATUS_URL:
|
||||
status_button_list.append(
|
||||
InlineKeyboardButton(text=_(key="menu_server_status_button"),
|
||||
url=settings.SERVER_STATUS_URL))
|
||||
|
||||
if status_button_list:
|
||||
builder.row(language_button, *status_button_list)
|
||||
else:
|
||||
builder.row(language_button)
|
||||
|
||||
if settings.SUPPORT_LINK:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_support_button"),
|
||||
url=settings.SUPPORT_LINK))
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
if show_trial_button and settings .TRIAL_ENABLED :
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="menu_activate_trial_button"),callback_data ="main_action:request_trial"))
|
||||
def get_language_selection_keyboard(i18n_instance,
|
||||
current_lang: str) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(current_lang, key, **kwargs
|
||||
)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=f"🇬🇧 English {'✅' if current_lang == 'en' else ''}",
|
||||
callback_data="set_lang_en")
|
||||
builder.button(text=f"🇷🇺 Русский {'✅' if current_lang == 'ru' else ''}",
|
||||
callback_data="set_lang_ru")
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="menu_subscribe_inline"),callback_data ="main_action:subscribe"))
|
||||
def get_trial_confirmation_keyboard(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="trial_confirm_activate_button"),
|
||||
callback_data="trial_action:confirm_activate")
|
||||
builder.button(text=_(key="cancel_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="menu_my_subscription_inline"),callback_data ="main_action:my_subscription"))
|
||||
def get_subscription_options_keyboard(subscription_options: Dict[
|
||||
int, Optional[int]], currency_symbol_val: str, lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
if subscription_options:
|
||||
for months, price in subscription_options.items():
|
||||
if price is not None:
|
||||
button_text = _("subscribe_for_months_button",
|
||||
months=months,
|
||||
price=price,
|
||||
currency_symbol=currency_symbol_val)
|
||||
builder.button(text=button_text,
|
||||
callback_data=f"subscribe_period:{months}")
|
||||
builder.adjust(1)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main"))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
referral_button =InlineKeyboardButton (text =_ (key ="menu_referral_inline"),callback_data ="main_action:referral")
|
||||
promo_button =InlineKeyboardButton (text =_ (key ="menu_apply_promo_button"),callback_data ="main_action:apply_promo")
|
||||
builder .row (referral_button ,promo_button )
|
||||
def get_confirm_subscription_keyboard(months: int, price: float,
|
||||
currency_symbol_val: str, lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
confirm_text = _(key="confirm_payment_button",
|
||||
price=price,
|
||||
currency_symbol=currency_symbol_val)
|
||||
builder.button(text=confirm_text,
|
||||
callback_data=f"confirm_sub:{months}:{price}")
|
||||
builder.button(text=_(key="cancel_button"),
|
||||
callback_data="main_action:subscribe")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
language_button =InlineKeyboardButton (text =_ (key ="menu_language_settings_inline"),callback_data ="main_action:language")
|
||||
status_button_list =[]
|
||||
if settings .SERVER_STATUS_URL :
|
||||
status_button_list .append (InlineKeyboardButton (text =_ (key ="menu_server_status_button"),url =settings .SERVER_STATUS_URL ))
|
||||
|
||||
if status_button_list :
|
||||
builder .row (language_button ,*status_button_list )
|
||||
else :
|
||||
builder .row (language_button )
|
||||
def get_payment_url_keyboard(payment_url: str, lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="pay_button"), url=payment_url)
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
if settings .SUPPORT_LINK :
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="menu_support_button"),url =settings .SUPPORT_LINK ))
|
||||
def get_referral_link_keyboard(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_back_to_main_menu_markup(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
|
||||
return builder .as_markup ()
|
||||
|
||||
|
||||
|
||||
def get_language_selection_keyboard (i18n_instance ,current_lang :str )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (current_lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =f"🇬🇧 English {'✅' if current_lang == 'en' else ''}",callback_data ="set_lang_en")
|
||||
builder .button (text =f"🇷🇺 Русский {'✅' if current_lang == 'ru' else ''}",callback_data ="set_lang_ru")
|
||||
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main")
|
||||
builder .adjust (1 )
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_trial_confirmation_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="trial_confirm_activate_button"),callback_data ="trial_action:confirm_activate")
|
||||
builder .button (text =_ (key ="cancel_button"),callback_data ="main_action:back_to_main")
|
||||
builder .adjust (1 )
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_subscription_options_keyboard (subscription_options :Dict [int ,Optional [int ]],currency_symbol_val :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
if subscription_options :
|
||||
for months ,price in subscription_options .items ():
|
||||
if price is not None :
|
||||
button_text =_ ("subscribe_for_months_button",months =months ,price =price ,currency_symbol =currency_symbol_val )
|
||||
builder .button (text =button_text ,callback_data =f"subscribe_period:{months}")
|
||||
builder .adjust (1 )
|
||||
builder .row (InlineKeyboardButton (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main"))
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_confirm_subscription_keyboard (months :int ,price :float ,currency_symbol_val :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
confirm_text =_ (key ="confirm_payment_button",price =price ,currency_symbol =currency_symbol_val )
|
||||
builder .button (text =confirm_text ,callback_data =f"confirm_sub:{months}:{price}")
|
||||
builder .button (text =_ (key ="cancel_button"),callback_data ="main_action:subscribe")
|
||||
builder .adjust (1 )
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_payment_url_keyboard (payment_url :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="pay_button"),url =payment_url )
|
||||
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main")
|
||||
builder .adjust (1 )
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_referral_link_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main")
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_back_to_main_menu_markup (lang :str ,i18n_instance )->InlineKeyboardMarkup :
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main")
|
||||
return builder .as_markup ()
|
||||
|
||||
def get_user_banned_keyboard (support_link :Optional [str ],lang :str ,i18n_instance )->Optional [InlineKeyboardMarkup ]:
|
||||
if not support_link :
|
||||
def get_user_banned_keyboard(support_link: Optional[str], lang: str,
|
||||
i18n_instance) -> Optional[InlineKeyboardMarkup]:
|
||||
if not support_link:
|
||||
return None
|
||||
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs )
|
||||
builder =InlineKeyboardBuilder ()
|
||||
builder .button (text =_ (key ="contact_support_button"),url =support_link )
|
||||
return builder .as_markup ()
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="contact_support_button"), url=support_link)
|
||||
return builder.as_markup()
|
||||
|
||||
+274
-151
@@ -1,171 +1,294 @@
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from aiogram import Bot ,Dispatcher ,Router ,F
|
||||
from aiogram .enums import ParseMode
|
||||
from aiogram .filters import CommandStart ,Command
|
||||
from aiogram .client .default import DefaultBotProperties
|
||||
from aiogram .webhook .aiohttp_server import SimpleRequestHandler ,setup_application
|
||||
from aiogram .fsm .storage .memory import MemoryStorage
|
||||
from aiogram import Bot, Dispatcher, Router, F
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiohttp import web
|
||||
from apscheduler .schedulers .asyncio import AsyncIOScheduler
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from config.settings import Settings, get_settings
|
||||
from .middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n
|
||||
from .middlewares.ban_check_middleware import BanCheckMiddleware
|
||||
from .middlewares.action_logger_middleware import ActionLoggerMiddleware
|
||||
|
||||
from .handlers.user import user_router_aggregate
|
||||
from .handlers.user import payment as user_payment_webhook_module
|
||||
|
||||
from .handlers.admin import admin_router_aggregate
|
||||
from .filters.admin_filter import AdminFilter
|
||||
|
||||
from db.database import get_db_connection_manager
|
||||
|
||||
from .services.notification_service import schedule_subscription_notifications
|
||||
from .services.payment_service import YooKassaService
|
||||
from .services.panel_api_service import PanelApiService
|
||||
from .services.subscription_service import SubscriptionService
|
||||
from .services.referral_service import ReferralService
|
||||
from .services.promo_code_service import PromoCodeService
|
||||
|
||||
|
||||
from config .settings import Settings ,get_settings
|
||||
from .middlewares .i18n import I18nMiddleware ,get_i18n_instance ,JsonI18n
|
||||
from .middlewares .ban_check_middleware import BanCheckMiddleware
|
||||
from .middlewares .action_logger_middleware import ActionLoggerMiddleware
|
||||
|
||||
from .handlers .user import user_router_aggregate
|
||||
from .handlers .user import payment as user_payment_webhook_module
|
||||
|
||||
from .handlers .admin import admin_router_aggregate
|
||||
from .filters .admin_filter import AdminFilter
|
||||
|
||||
from db .database import get_db_connection_manager
|
||||
|
||||
from .services .notification_service import schedule_subscription_notifications
|
||||
from .services .payment_service import YooKassaService
|
||||
from .services .panel_api_service import PanelApiService
|
||||
from .services .subscription_service import SubscriptionService
|
||||
from .services .referral_service import ReferralService
|
||||
from .services .promo_code_service import PromoCodeService
|
||||
async def register_all_routers(dp: Dispatcher, settings: Settings):
|
||||
dp.include_router(user_router_aggregate)
|
||||
admin_filtered_router_wrapper = Router(
|
||||
name="admin_filtered_router_wrapper")
|
||||
admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS)
|
||||
admin_filtered_router_wrapper.message.filter(admin_filter_instance)
|
||||
admin_filtered_router_wrapper.callback_query.filter(admin_filter_instance)
|
||||
admin_filtered_router_wrapper.include_router(admin_router_aggregate)
|
||||
dp.include_router(admin_filtered_router_wrapper)
|
||||
logging.info("All application routers registered.")
|
||||
|
||||
|
||||
async def register_all_routers (dp :Dispatcher ,settings :Settings ):
|
||||
dp .include_router (user_router_aggregate )
|
||||
admin_filtered_router_wrapper =Router (name ="admin_filtered_router_wrapper")
|
||||
admin_filter_instance =AdminFilter (admin_ids =settings .ADMIN_IDS )
|
||||
admin_filtered_router_wrapper .message .filter (admin_filter_instance )
|
||||
admin_filtered_router_wrapper .callback_query .filter (admin_filter_instance )
|
||||
admin_filtered_router_wrapper .include_router (admin_router_aggregate )
|
||||
dp .include_router (admin_filtered_router_wrapper )
|
||||
logging .info ("All application routers registered.")
|
||||
async def on_startup_configured(dispatcher: Dispatcher):
|
||||
|
||||
async def on_startup_configured (dispatcher :Dispatcher ):
|
||||
|
||||
bot :Bot =dispatcher ["bot_instance"];settings :Settings =dispatcher ["settings"];i18n_instance :JsonI18n =dispatcher ["i18n_instance"]
|
||||
logging .info ("STARTUP: on_startup_configured executing...")
|
||||
scheduler =AsyncIOScheduler (timezone ="UTC")
|
||||
try :await schedule_subscription_notifications (bot ,settings ,i18n_instance ,scheduler );scheduler .start ();dispatcher ["scheduler"]=scheduler ;logging .info ("STARTUP: APScheduler started.")
|
||||
except Exception as e :logging .error (f"STARTUP: Failed to start APScheduler: {e}",exc_info =True )
|
||||
telegram_webhook_url_to_set =getattr (settings ,'TELEGRAM_WEBHOOK_BASE_URL',None )
|
||||
if telegram_webhook_url_to_set :
|
||||
if settings .BOT_TOKEN in telegram_webhook_url_to_set :logging .error (f"CRITICAL: Bot token in TELEGRAM_WEBHOOK_BASE_URL ('{telegram_webhook_url_to_set}').");full_telegram_webhook_url ="ERROR_URL"
|
||||
else :full_telegram_webhook_url =f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
|
||||
logging .info (f"STARTUP: Attempting to set Telegram webhook to: {full_telegram_webhook_url}")
|
||||
try :
|
||||
current_webhook_info_before =await bot .get_webhook_info ();logging .info (f"STARTUP: Current webhook info BEFORE: {current_webhook_info_before.model_dump_json(exclude_none=True, indent=2)}")
|
||||
if full_telegram_webhook_url !="ERROR_URL":
|
||||
set_success =await bot .set_webhook (url =full_telegram_webhook_url ,drop_pending_updates =True ,allowed_updates =dispatcher .resolve_used_update_types ())
|
||||
if set_success :logging .info (f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True).")
|
||||
else :logging .error (f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False).")
|
||||
new_webhook_info =await bot .get_webhook_info ();logging .info (f"STARTUP: Webhook info AFTER: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}")
|
||||
if not new_webhook_info .url :logging .error ("STARTUP: CRITICAL - Webhook URL EMPTY after set attempt.")
|
||||
else :logging .error ("STARTUP: Skipped setting webhook due to URL config error.")
|
||||
except Exception as e_setwebhook :logging .error (f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",exc_info =True )
|
||||
else :
|
||||
logging .info ("STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set. Attempting to delete webhook.");await bot .delete_webhook (drop_pending_updates =True )
|
||||
logging .info ("STARTUP: Bot on_startup_configured completed.")
|
||||
bot: Bot = dispatcher["bot_instance"]
|
||||
settings: Settings = dispatcher["settings"]
|
||||
i18n_instance: JsonI18n = dispatcher["i18n_instance"]
|
||||
logging.info("STARTUP: on_startup_configured executing...")
|
||||
scheduler = AsyncIOScheduler(timezone="UTC")
|
||||
try:
|
||||
await schedule_subscription_notifications(bot, settings, i18n_instance,
|
||||
scheduler)
|
||||
scheduler.start()
|
||||
dispatcher["scheduler"] = scheduler
|
||||
logging.info("STARTUP: APScheduler started.")
|
||||
except Exception as e:
|
||||
logging.error(f"STARTUP: Failed to start APScheduler: {e}",
|
||||
exc_info=True)
|
||||
telegram_webhook_url_to_set = getattr(settings,
|
||||
'TELEGRAM_WEBHOOK_BASE_URL', None)
|
||||
if telegram_webhook_url_to_set:
|
||||
if settings.BOT_TOKEN in telegram_webhook_url_to_set:
|
||||
logging.error(
|
||||
f"CRITICAL: Bot token in TELEGRAM_WEBHOOK_BASE_URL ('{telegram_webhook_url_to_set}')."
|
||||
)
|
||||
full_telegram_webhook_url = "ERROR_URL"
|
||||
else:
|
||||
full_telegram_webhook_url = f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
|
||||
logging.info(
|
||||
f"STARTUP: Attempting to set Telegram webhook to: {full_telegram_webhook_url}"
|
||||
)
|
||||
try:
|
||||
current_webhook_info_before = await bot.get_webhook_info()
|
||||
logging.info(
|
||||
f"STARTUP: Current webhook info BEFORE: {current_webhook_info_before.model_dump_json(exclude_none=True, indent=2)}"
|
||||
)
|
||||
if full_telegram_webhook_url != "ERROR_URL":
|
||||
set_success = await bot.set_webhook(
|
||||
url=full_telegram_webhook_url,
|
||||
drop_pending_updates=True,
|
||||
allowed_updates=dispatcher.resolve_used_update_types())
|
||||
if set_success:
|
||||
logging.info(
|
||||
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True)."
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False)."
|
||||
)
|
||||
new_webhook_info = await bot.get_webhook_info()
|
||||
logging.info(
|
||||
f"STARTUP: Webhook info AFTER: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
|
||||
)
|
||||
if not new_webhook_info.url:
|
||||
logging.error(
|
||||
"STARTUP: CRITICAL - Webhook URL EMPTY after set attempt."
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"STARTUP: Skipped setting webhook due to URL config error."
|
||||
)
|
||||
except Exception as e_setwebhook:
|
||||
logging.error(
|
||||
f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",
|
||||
exc_info=True)
|
||||
else:
|
||||
logging.info(
|
||||
"STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set. Attempting to delete webhook."
|
||||
)
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
logging.info("STARTUP: Bot on_startup_configured completed.")
|
||||
|
||||
|
||||
async def on_shutdown_configured (dispatcher :Dispatcher ):
|
||||
async def on_shutdown_configured(dispatcher: Dispatcher):
|
||||
|
||||
logging .warning ("SHUTDOWN: on_shutdown_configured executing...");scheduler :AsyncIOScheduler =dispatcher .get ("scheduler")
|
||||
if scheduler and scheduler .running :
|
||||
try :scheduler .shutdown (wait =False );logging .info ("SHUTDOWN: APScheduler shut down.")
|
||||
except Exception as e :logging .error (f"SHUTDOWN: Error APScheduler: {e}",exc_info =True )
|
||||
panel_service :PanelApiService =dispatcher .get ("panel_service")
|
||||
if panel_service and panel_service ._session and not panel_service ._session .closed :await panel_service .close_session ()
|
||||
bot :Bot =dispatcher .get ("bot_instance")
|
||||
if bot and bot .session and not bot .session .closed :await bot .session .close ()
|
||||
logging .info ("SHUTDOWN: Bot on_shutdown_configured completed.")
|
||||
logging.warning("SHUTDOWN: on_shutdown_configured executing...")
|
||||
scheduler: AsyncIOScheduler = dispatcher.get("scheduler")
|
||||
if scheduler and scheduler.running:
|
||||
try:
|
||||
scheduler.shutdown(wait=False)
|
||||
logging.info("SHUTDOWN: APScheduler shut down.")
|
||||
except Exception as e:
|
||||
logging.error(f"SHUTDOWN: Error APScheduler: {e}", exc_info=True)
|
||||
panel_service: PanelApiService = dispatcher.get("panel_service")
|
||||
if panel_service and panel_service._session and not panel_service._session.closed:
|
||||
await panel_service.close_session()
|
||||
bot: Bot = dispatcher.get("bot_instance")
|
||||
if bot and bot.session and not bot.session.closed:
|
||||
await bot.session.close()
|
||||
logging.info("SHUTDOWN: Bot on_shutdown_configured completed.")
|
||||
|
||||
|
||||
async def run_bot (settings_param :Settings ):
|
||||
storage =MemoryStorage ()
|
||||
default_props =DefaultBotProperties (parse_mode =ParseMode .HTML )
|
||||
bot =Bot (token =settings_param .BOT_TOKEN ,default =default_props )
|
||||
async def run_bot(settings_param: Settings):
|
||||
storage = MemoryStorage()
|
||||
default_props = DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
bot = Bot(token=settings_param.BOT_TOKEN, default=default_props)
|
||||
|
||||
dp =Dispatcher (storage =storage ,settings =settings_param ,bot_instance =bot )
|
||||
dp = Dispatcher(storage=storage, settings=settings_param, bot_instance=bot)
|
||||
|
||||
actual_bot_username ="your_bot_username"
|
||||
try :
|
||||
bot_info =await bot .get_me ()
|
||||
actual_bot_username =bot_info .username
|
||||
logging .info (f"Bot username: @{actual_bot_username}")
|
||||
except Exception as e :
|
||||
logging .error (f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}")
|
||||
actual_bot_username = "your_bot_username"
|
||||
try:
|
||||
bot_info = await bot.get_me()
|
||||
actual_bot_username = bot_info.username
|
||||
logging.info(f"Bot username: @{actual_bot_username}")
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}"
|
||||
)
|
||||
|
||||
default_lang =settings_param .DEFAULT_LANGUAGE
|
||||
i18n_instance =get_i18n_instance (path ="locales",default =default_lang )
|
||||
default_lang = settings_param.DEFAULT_LANGUAGE
|
||||
i18n_instance = get_i18n_instance(path="locales", default=default_lang)
|
||||
|
||||
yookassa_service = YooKassaService(
|
||||
shop_id=settings_param.YOOKASSA_SHOP_ID,
|
||||
secret_key=settings_param.YOOKASSA_SECRET_KEY,
|
||||
configured_return_url=settings_param.YOOKASSA_RETURN_URL,
|
||||
bot_username_for_default=actual_bot_username,
|
||||
settings_obj=settings_param)
|
||||
|
||||
yookassa_service =YooKassaService (
|
||||
shop_id =settings_param .YOOKASSA_SHOP_ID ,
|
||||
secret_key =settings_param .YOOKASSA_SECRET_KEY ,
|
||||
configured_return_url =settings_param .YOOKASSA_RETURN_URL ,
|
||||
bot_username_for_default =actual_bot_username ,
|
||||
settings_obj =settings_param
|
||||
panel_service = PanelApiService(settings_param)
|
||||
subscription_service = SubscriptionService(get_db_connection_manager,
|
||||
settings_param, panel_service)
|
||||
referral_service = ReferralService(get_db_connection_manager,
|
||||
settings_param, subscription_service,
|
||||
bot, i18n_instance)
|
||||
promo_code_service = PromoCodeService(get_db_connection_manager,
|
||||
settings_param, subscription_service,
|
||||
bot, i18n_instance)
|
||||
|
||||
dp["i18n_instance"] = i18n_instance
|
||||
dp["i18n_data"] = {
|
||||
"i18n_instance": i18n_instance,
|
||||
"current_language": default_lang
|
||||
}
|
||||
dp["yookassa_service"] = yookassa_service
|
||||
dp["panel_service"] = panel_service
|
||||
dp["subscription_service"] = subscription_service
|
||||
dp["referral_service"] = referral_service
|
||||
dp["promo_code_service"] = promo_code_service
|
||||
|
||||
dp.update.outer_middleware(
|
||||
I18nMiddleware(i18n=i18n_instance, settings=settings_param))
|
||||
dp.update.outer_middleware(
|
||||
BanCheckMiddleware(settings=settings_param,
|
||||
i18n_instance=i18n_instance))
|
||||
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param))
|
||||
|
||||
dp.startup.register(on_startup_configured)
|
||||
dp.shutdown.register(on_shutdown_configured)
|
||||
await register_all_routers(dp, settings_param)
|
||||
|
||||
tg_webhook_base = getattr(settings_param, 'TELEGRAM_WEBHOOK_BASE_URL',
|
||||
None)
|
||||
yk_webhook_base = getattr(settings_param, 'YOOKASSA_WEBHOOK_BASE_URL',
|
||||
None)
|
||||
logging.info(f"--- Determining Run Mode ---")
|
||||
logging.info(f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}'")
|
||||
logging.info(f"Configured YOOKASSA_WEBHOOK_BASE_URL: '{yk_webhook_base}'")
|
||||
should_run_aiohttp = bool(
|
||||
yk_webhook_base
|
||||
and settings_param.yookassa_webhook_path) or bool(tg_webhook_base)
|
||||
telegram_uses_webhook = bool(tg_webhook_base)
|
||||
telegram_should_poll = not telegram_uses_webhook
|
||||
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp}")
|
||||
logging.info(f"Decision: Telegram uses webhook: {telegram_uses_webhook}")
|
||||
logging.info(f"Decision: Telegram should poll: {telegram_should_poll}")
|
||||
logging.info(f"--- End Run Mode Decision ---")
|
||||
web_app_runner = None
|
||||
main_tasks_to_await = []
|
||||
if should_run_aiohttp:
|
||||
app = web.Application()
|
||||
app['bot'] = bot
|
||||
app['dp'] = dp
|
||||
app['settings'] = settings_param
|
||||
app['i18n'] = i18n_instance
|
||||
app['yookassa_service'] = yookassa_service
|
||||
app['panel_service'] = panel_service
|
||||
app['subscription_service'] = subscription_service
|
||||
app['referral_service'] = referral_service
|
||||
app['promo_code_service'] = promo_code_service
|
||||
setup_application(app, dp, bot=bot)
|
||||
logging.info(
|
||||
"AIOHTTP app context populated and dispatcher lifecycle linked.")
|
||||
if telegram_uses_webhook:
|
||||
telegram_webhook_path = f"/{settings_param.BOT_TOKEN}"
|
||||
app.router.add_post(telegram_webhook_path,
|
||||
SimpleRequestHandler(dispatcher=dp, bot=bot))
|
||||
logging.info(f"Telegram webhook route: {telegram_webhook_path}.")
|
||||
if yk_webhook_base and settings_param.yookassa_webhook_path:
|
||||
app.router.add_post(
|
||||
settings_param.yookassa_webhook_path,
|
||||
user_payment_webhook_module.yookassa_webhook_route)
|
||||
logging.info(
|
||||
f"YK webhook route: {settings_param.yookassa_webhook_path}")
|
||||
web_app_runner = web.AppRunner(app)
|
||||
await web_app_runner.setup()
|
||||
site = web.TCPSite(web_app_runner,
|
||||
host=settings_param.WEB_SERVER_HOST,
|
||||
port=settings_param.WEB_SERVER_PORT)
|
||||
|
||||
async def web_server_task_wrapper():
|
||||
await site.start()
|
||||
logging.info(
|
||||
f"AIOHTTP server started on {settings_param.WEB_SERVER_HOST}:{settings_param.WEB_SERVER_PORT}."
|
||||
)
|
||||
await asyncio.Event().wait(
|
||||
) if not telegram_should_poll else await asyncio.sleep(31536000)
|
||||
|
||||
main_tasks_to_await.append(
|
||||
asyncio.create_task(web_server_task_wrapper(),
|
||||
name="AIOHTTPServerWrapperTask"))
|
||||
if telegram_should_poll:
|
||||
logging.info("TG polling task created.")
|
||||
main_tasks_to_await.append(
|
||||
asyncio.create_task(dp.start_polling(
|
||||
bot, allowed_updates=dp.resolve_used_update_types()),
|
||||
name="TelegramPollingTask"))
|
||||
if not main_tasks_to_await:
|
||||
logging.error("Bot not configured for any mode.")
|
||||
await dp.emit_shutdown()
|
||||
return
|
||||
logging.info(
|
||||
f"Starting bot with main tasks: {[task.get_name() for task in main_tasks_to_await]}"
|
||||
)
|
||||
|
||||
panel_service =PanelApiService (settings_param )
|
||||
subscription_service =SubscriptionService (get_db_connection_manager ,settings_param ,panel_service )
|
||||
referral_service =ReferralService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance )
|
||||
promo_code_service =PromoCodeService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance )
|
||||
|
||||
|
||||
dp ["i18n_instance"]=i18n_instance
|
||||
dp ["i18n_data"]={"i18n_instance":i18n_instance ,"current_language":default_lang }
|
||||
dp ["yookassa_service"]=yookassa_service
|
||||
dp ["panel_service"]=panel_service
|
||||
dp ["subscription_service"]=subscription_service
|
||||
dp ["referral_service"]=referral_service
|
||||
dp ["promo_code_service"]=promo_code_service
|
||||
|
||||
|
||||
|
||||
dp .update .outer_middleware (I18nMiddleware (i18n =i18n_instance ,settings =settings_param ))
|
||||
dp .update .outer_middleware (BanCheckMiddleware (settings =settings_param ,i18n_instance =i18n_instance ))
|
||||
dp .update .outer_middleware (ActionLoggerMiddleware (settings =settings_param ))
|
||||
|
||||
dp .startup .register (on_startup_configured )
|
||||
dp .shutdown .register (on_shutdown_configured )
|
||||
await register_all_routers (dp ,settings_param )
|
||||
|
||||
|
||||
tg_webhook_base =getattr (settings_param ,'TELEGRAM_WEBHOOK_BASE_URL',None );yk_webhook_base =getattr (settings_param ,'YOOKASSA_WEBHOOK_BASE_URL',None )
|
||||
logging .info (f"--- Determining Run Mode ---");logging .info (f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}'");logging .info (f"Configured YOOKASSA_WEBHOOK_BASE_URL: '{yk_webhook_base}'")
|
||||
should_run_aiohttp =bool (yk_webhook_base and settings_param .yookassa_webhook_path )or bool (tg_webhook_base )
|
||||
telegram_uses_webhook =bool (tg_webhook_base );telegram_should_poll =not telegram_uses_webhook
|
||||
logging .info (f"Decision: Run AIOHTTP server: {should_run_aiohttp}");logging .info (f"Decision: Telegram uses webhook: {telegram_uses_webhook}");logging .info (f"Decision: Telegram should poll: {telegram_should_poll}");logging .info (f"--- End Run Mode Decision ---")
|
||||
web_app_runner =None ;main_tasks_to_await =[]
|
||||
if should_run_aiohttp :
|
||||
app =web .Application ();app ['bot']=bot ;app ['dp']=dp ;app ['settings']=settings_param ;app ['i18n']=i18n_instance
|
||||
app ['yookassa_service']=yookassa_service ;app ['panel_service']=panel_service ;app ['subscription_service']=subscription_service
|
||||
app ['referral_service']=referral_service ;app ['promo_code_service']=promo_code_service
|
||||
setup_application (app ,dp ,bot =bot );logging .info ("AIOHTTP app context populated and dispatcher lifecycle linked.")
|
||||
if telegram_uses_webhook :telegram_webhook_path =f"/{settings_param.BOT_TOKEN}";app .router .add_post (telegram_webhook_path ,SimpleRequestHandler (dispatcher =dp ,bot =bot ));logging .info (f"Telegram webhook route: {telegram_webhook_path}.")
|
||||
if yk_webhook_base and settings_param .yookassa_webhook_path :app .router .add_post (settings_param .yookassa_webhook_path ,user_payment_webhook_module .yookassa_webhook_route );logging .info (f"YK webhook route: {settings_param.yookassa_webhook_path}")
|
||||
web_app_runner =web .AppRunner (app );await web_app_runner .setup ();site =web .TCPSite (web_app_runner ,host =settings_param .WEB_SERVER_HOST ,port =settings_param .WEB_SERVER_PORT )
|
||||
async def web_server_task_wrapper ():await site .start ();logging .info (f"AIOHTTP server started on {settings_param.WEB_SERVER_HOST}:{settings_param.WEB_SERVER_PORT}.");await asyncio .Event ().wait ()if not telegram_should_poll else await asyncio .sleep (31536000 )
|
||||
main_tasks_to_await .append (asyncio .create_task (web_server_task_wrapper (),name ="AIOHTTPServerWrapperTask"))
|
||||
if telegram_should_poll :logging .info ("TG polling task created.");main_tasks_to_await .append (asyncio .create_task (dp .start_polling (bot ,allowed_updates =dp .resolve_used_update_types ()),name ="TelegramPollingTask"))
|
||||
if not main_tasks_to_await :logging .error ("Bot not configured for any mode.");await dp .emit_shutdown ();return
|
||||
logging .info (f"Starting bot with main tasks: {[task.get_name() for task in main_tasks_to_await]}")
|
||||
try :
|
||||
if main_tasks_to_await :await asyncio .gather (*main_tasks_to_await )
|
||||
except (KeyboardInterrupt ,SystemExit ,asyncio .CancelledError )as e :logging .info (f"Main loop interrupted: {type(e).__name__}")
|
||||
finally :
|
||||
logging .info ("Initiating final shutdown sequence...");
|
||||
for task in main_tasks_to_await :
|
||||
if task and not task .done ():task .cancel ();
|
||||
try :await task
|
||||
except asyncio .CancelledError :logging .info (f"Task '{task.get_name()}' cancelled.")
|
||||
except Exception as e_cancel :logging .error (f"Error cancelling task '{task.get_name()}': {e_cancel}",exc_info =True )
|
||||
if web_app_runner :await web_app_runner .cleanup ();logging .info ("AIOHTTP AppRunner cleaned up.")
|
||||
if not (telegram_should_poll and len (main_tasks_to_await )==1 and any (t .get_name ()=="TelegramPollingTask"for t in main_tasks_to_await if t and not t .done ())):
|
||||
logging .info ("Explicitly calling dp.emit_shutdown() in run_bot finally.")
|
||||
await dp .emit_shutdown ()
|
||||
if bot .session and not bot .session .closed :await bot .session .close ()
|
||||
logging .info ("Bot run_bot function finished.")
|
||||
try:
|
||||
if main_tasks_to_await: await asyncio.gather(*main_tasks_to_await)
|
||||
except (KeyboardInterrupt, SystemExit, asyncio.CancelledError) as e:
|
||||
logging.info(f"Main loop interrupted: {type(e).__name__}")
|
||||
finally:
|
||||
logging.info("Initiating final shutdown sequence...")
|
||||
for task in main_tasks_to_await:
|
||||
if task and not task.done(): task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
logging.info(f"Task '{task.get_name()}' cancelled.")
|
||||
except Exception as e_cancel:
|
||||
logging.error(
|
||||
f"Error cancelling task '{task.get_name()}': {e_cancel}",
|
||||
exc_info=True)
|
||||
if web_app_runner:
|
||||
await web_app_runner.cleanup()
|
||||
logging.info("AIOHTTP AppRunner cleaned up.")
|
||||
if not (telegram_should_poll and len(main_tasks_to_await) == 1
|
||||
and any(t.get_name() == "TelegramPollingTask"
|
||||
for t in main_tasks_to_await if t and not t.done())):
|
||||
logging.info(
|
||||
"Explicitly calling dp.emit_shutdown() in run_bot finally.")
|
||||
await dp.emit_shutdown()
|
||||
if bot.session and not bot.session.closed: await bot.session.close()
|
||||
logging.info("Bot run_bot function finished.")
|
||||
|
||||
@@ -1,94 +1,90 @@
|
||||
import logging
|
||||
import json
|
||||
from typing import Callable ,Dict ,Any ,Awaitable ,Union ,Optional
|
||||
from typing import Callable, Dict, Any, Awaitable, Union, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram .types import Update ,Message ,CallbackQuery ,User
|
||||
from aiogram.types import Update, Message, CallbackQuery, User
|
||||
|
||||
from db .database import log_user_action
|
||||
from config .settings import Settings
|
||||
|
||||
class ActionLoggerMiddleware (BaseMiddleware ):
|
||||
def __init__ (self ,settings :Settings ):
|
||||
super ().__init__ ()
|
||||
self .settings =settings
|
||||
|
||||
async def __call__ (
|
||||
self ,
|
||||
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]],
|
||||
event :Update ,
|
||||
data :Dict [str ,Any ]
|
||||
)->Any :
|
||||
|
||||
event_user :Optional [User ]=data .get ("event_from_user")
|
||||
bot :Optional [Bot ]=data .get ("bot")
|
||||
|
||||
user_id :Optional [int ]=None
|
||||
telegram_username :Optional [str ]=None
|
||||
telegram_first_name :Optional [str ]=None
|
||||
event_type :str =event .event_type
|
||||
content :Optional [str ]=None
|
||||
is_admin_event_flag :bool =False
|
||||
|
||||
if event_user :
|
||||
user_id =event_user .id
|
||||
telegram_username =event_user .username
|
||||
telegram_first_name =event_user .first_name
|
||||
if user_id ==self .settings .ADMIN_IDS :
|
||||
is_admin_event_flag =True
|
||||
|
||||
raw_update_snippet =None
|
||||
try :
|
||||
|
||||
raw_update_snippet =event .model_dump_json (exclude_none =True ,indent =None )[:1000 ]
|
||||
except Exception :
|
||||
raw_update_snippet =str (event )[:1000 ]
|
||||
from db.database import log_user_action
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
if event .message :
|
||||
msg =event .message
|
||||
if msg .text :
|
||||
content =msg .text
|
||||
if msg .text .startswith ('/'):
|
||||
event_type ="command"
|
||||
elif msg .caption :
|
||||
content =f"[{msg.content_type}] {msg.caption}"
|
||||
else :
|
||||
content =f"[{msg.content_type}]"
|
||||
class ActionLoggerMiddleware(BaseMiddleware):
|
||||
|
||||
elif event .callback_query :
|
||||
cb =event .callback_query
|
||||
event_type ="callback_query"
|
||||
content =cb .data
|
||||
def __init__(self, settings: Settings):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
|
||||
async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
|
||||
Awaitable[Any]], event: Update,
|
||||
data: Dict[str, Any]) -> Any:
|
||||
|
||||
event_user: Optional[User] = data.get("event_from_user")
|
||||
bot: Optional[Bot] = data.get("bot")
|
||||
|
||||
user_id: Optional[int] = None
|
||||
telegram_username: Optional[str] = None
|
||||
telegram_first_name: Optional[str] = None
|
||||
event_type: str = event.event_type
|
||||
content: Optional[str] = None
|
||||
is_admin_event_flag: bool = False
|
||||
|
||||
if event_user:
|
||||
user_id = event_user.id
|
||||
telegram_username = event_user.username
|
||||
telegram_first_name = event_user.first_name
|
||||
if user_id == self.settings.ADMIN_IDS:
|
||||
is_admin_event_flag = True
|
||||
|
||||
raw_update_snippet = None
|
||||
try:
|
||||
|
||||
raw_update_snippet = event.model_dump_json(exclude_none=True,
|
||||
indent=None)[:1000]
|
||||
except Exception:
|
||||
raw_update_snippet = str(event)[:1000]
|
||||
|
||||
if event.message:
|
||||
msg = event.message
|
||||
if msg.text:
|
||||
content = msg.text
|
||||
if msg.text.startswith('/'):
|
||||
event_type = "command"
|
||||
elif msg.caption:
|
||||
content = f"[{msg.content_type}] {msg.caption}"
|
||||
else:
|
||||
content = f"[{msg.content_type}]"
|
||||
|
||||
elif event.callback_query:
|
||||
cb = event.callback_query
|
||||
event_type = "callback_query"
|
||||
content = cb.data
|
||||
|
||||
if user_id and event_type and content:
|
||||
try:
|
||||
await log_user_action(user_id=user_id,
|
||||
telegram_username=telegram_username,
|
||||
telegram_first_name=telegram_first_name,
|
||||
event_type=event_type,
|
||||
content=content[:1000],
|
||||
raw_update_preview=raw_update_snippet,
|
||||
is_admin_event=is_admin_event_flag)
|
||||
except Exception as e_log:
|
||||
logging.error(
|
||||
f"ActionLoggerMiddleware: Failed to log event for user {user_id}: {e_log}",
|
||||
exc_info=True)
|
||||
elif user_id and event_type:
|
||||
try:
|
||||
await log_user_action(user_id=user_id,
|
||||
telegram_username=telegram_username,
|
||||
telegram_first_name=telegram_first_name,
|
||||
event_type=event_type,
|
||||
content="N/A",
|
||||
raw_update_preview=raw_update_snippet,
|
||||
is_admin_event=is_admin_event_flag)
|
||||
except Exception as e_log:
|
||||
logging.error(
|
||||
f"ActionLoggerMiddleware: Failed to log event (no content) for user {user_id}: {e_log}",
|
||||
exc_info=True)
|
||||
|
||||
if user_id and event_type and content :
|
||||
try :
|
||||
await log_user_action (
|
||||
user_id =user_id ,
|
||||
telegram_username =telegram_username ,
|
||||
telegram_first_name =telegram_first_name ,
|
||||
event_type =event_type ,
|
||||
content =content [:1000 ],
|
||||
raw_update_preview =raw_update_snippet ,
|
||||
is_admin_event =is_admin_event_flag
|
||||
)
|
||||
except Exception as e_log :
|
||||
logging .error (f"ActionLoggerMiddleware: Failed to log event for user {user_id}: {e_log}",exc_info =True )
|
||||
elif user_id and event_type :
|
||||
try :
|
||||
await log_user_action (user_id =user_id ,telegram_username =telegram_username ,telegram_first_name =telegram_first_name ,event_type =event_type ,content ="N/A",raw_update_preview =raw_update_snippet ,is_admin_event =is_admin_event_flag )
|
||||
except Exception as e_log :
|
||||
logging .error (f"ActionLoggerMiddleware: Failed to log event (no content) for user {user_id}: {e_log}",exc_info =True )
|
||||
|
||||
|
||||
|
||||
return await handler (event ,data )
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -1,132 +1,212 @@
|
||||
import logging
|
||||
from typing import Callable ,Dict ,Any ,Awaitable ,Union ,Optional
|
||||
from typing import Callable, Dict, Any, Awaitable, Union, Optional
|
||||
|
||||
from aiogram import BaseMiddleware ,Bot
|
||||
from aiogram .types import Message ,CallbackQuery ,User ,InlineKeyboardMarkup ,Update
|
||||
from aiogram .utils .keyboard import InlineKeyboardBuilder
|
||||
from aiogram .exceptions import TelegramAPIError ,TelegramForbiddenError ,TelegramBadRequest ,AiogramError
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.types import Message, CallbackQuery, User, InlineKeyboardMarkup, Update
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramForbiddenError, TelegramBadRequest, AiogramError
|
||||
|
||||
from config .settings import Settings
|
||||
from db .database import get_user
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from bot .keyboards .inline .user_keyboards import get_user_banned_keyboard
|
||||
|
||||
class BanCheckMiddleware (BaseMiddleware ):
|
||||
def __init__ (self ,settings :Settings ,i18n_instance :JsonI18n ):
|
||||
super ().__init__ ()
|
||||
self .settings =settings
|
||||
self .i18n_main_instance =i18n_instance
|
||||
|
||||
async def __call__ (
|
||||
self ,
|
||||
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]],
|
||||
event :Update ,
|
||||
data :Dict [str ,Any ]
|
||||
)->Any :
|
||||
event_user :Optional [User ]=data .get ("event_from_user")
|
||||
if not event_user :return await handler (event ,data )
|
||||
from config.settings import Settings
|
||||
from db.database import get_user
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.keyboards.inline.user_keyboards import get_user_banned_keyboard
|
||||
|
||||
|
||||
if event_user .id in self .settings .ADMIN_IDS :
|
||||
return await handler (event ,data )
|
||||
class BanCheckMiddleware(BaseMiddleware):
|
||||
|
||||
try :
|
||||
db_user_data =await get_user (user_id =event_user .id )
|
||||
except Exception as e_db :
|
||||
logging .error (f"BanCheckMiddleware: DB error fetching user {event_user.id}: {e_db}",exc_info =True )
|
||||
return await handler (event ,data )
|
||||
def __init__(self, settings: Settings, i18n_instance: JsonI18n):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
self.i18n_main_instance = i18n_instance
|
||||
|
||||
if db_user_data and db_user_data ['is_banned']==1 :
|
||||
logging .info (f"User {event_user.id} ({event_user.username or 'NoUsername'}) is banned. Blocking access and preparing notification.")
|
||||
async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
|
||||
Awaitable[Any]], event: Update,
|
||||
data: Dict[str, Any]) -> Any:
|
||||
event_user: Optional[User] = data.get("event_from_user")
|
||||
if not event_user: return await handler(event, data)
|
||||
|
||||
ban_message_text ="You are blocked. Please contact support."
|
||||
keyboard :Optional [InlineKeyboardMarkup ]=None
|
||||
current_lang =self .settings .DEFAULT_LANGUAGE
|
||||
i18n_to_use :Optional [JsonI18n ]=None
|
||||
actual_event_object :Optional [Union [Message ,CallbackQuery ]]=None
|
||||
if event_user.id in self.settings.ADMIN_IDS:
|
||||
return await handler(event, data)
|
||||
|
||||
try:
|
||||
db_user_data = await get_user(user_id=event_user.id)
|
||||
except Exception as e_db:
|
||||
logging.error(
|
||||
f"BanCheckMiddleware: DB error fetching user {event_user.id}: {e_db}",
|
||||
exc_info=True)
|
||||
return await handler(event, data)
|
||||
|
||||
if event .message :
|
||||
actual_event_object =event .message
|
||||
elif event .callback_query :
|
||||
actual_event_object =event .callback_query
|
||||
if db_user_data and db_user_data['is_banned'] == 1:
|
||||
logging.info(
|
||||
f"User {event_user.id} ({event_user.username or 'NoUsername'}) is banned. Blocking access and preparing notification."
|
||||
)
|
||||
|
||||
ban_message_text = "You are blocked. Please contact support."
|
||||
keyboard: Optional[InlineKeyboardMarkup] = None
|
||||
current_lang = self.settings.DEFAULT_LANGUAGE
|
||||
i18n_to_use: Optional[JsonI18n] = None
|
||||
actual_event_object: Optional[Union[Message, CallbackQuery]] = None
|
||||
|
||||
if not actual_event_object :
|
||||
logging .warning (f"BanCheck: Could not determine specific event type (Message/CallbackQuery) for banned user {event_user.id} from Update object. Update type: {event.type}")
|
||||
if event.message:
|
||||
actual_event_object = event.message
|
||||
elif event.callback_query:
|
||||
actual_event_object = event.callback_query
|
||||
|
||||
try :
|
||||
bot_instance :Bot =data ["bot"]
|
||||
await bot_instance .send_message (event_user .id ,ban_message_text )
|
||||
except Exception as e_direct_send :
|
||||
logging .error (f"BanCheck: Failed to send direct ban message to {event_user.id}: {e_direct_send}")
|
||||
if not actual_event_object:
|
||||
logging.warning(
|
||||
f"BanCheck: Could not determine specific event type (Message/CallbackQuery) for banned user {event_user.id} from Update object. Update type: {event.type}"
|
||||
)
|
||||
|
||||
try:
|
||||
bot_instance: Bot = data["bot"]
|
||||
await bot_instance.send_message(event_user.id,
|
||||
ban_message_text)
|
||||
except Exception as e_direct_send:
|
||||
logging.error(
|
||||
f"BanCheck: Failed to send direct ban message to {event_user.id}: {e_direct_send}"
|
||||
)
|
||||
return
|
||||
|
||||
try :
|
||||
logging .debug ("BanCheck: [A] Inside main try block for banned user notification.")
|
||||
i18n_data_from_event =data .get ("i18n_data",{})
|
||||
current_lang =i18n_data_from_event .get ("current_language",self .settings .DEFAULT_LANGUAGE )
|
||||
i18n_to_use =i18n_data_from_event .get ("i18n_instance")
|
||||
logging .debug (f"BanCheck: [B] i18n_instance from event_data: {type(i18n_to_use)}. Current lang: {current_lang}")
|
||||
try:
|
||||
logging.debug(
|
||||
"BanCheck: [A] Inside main try block for banned user notification."
|
||||
)
|
||||
i18n_data_from_event = data.get("i18n_data", {})
|
||||
current_lang = i18n_data_from_event.get(
|
||||
"current_language", self.settings.DEFAULT_LANGUAGE)
|
||||
i18n_to_use = i18n_data_from_event.get("i18n_instance")
|
||||
logging.debug(
|
||||
f"BanCheck: [B] i18n_instance from event_data: {type(i18n_to_use)}. Current lang: {current_lang}"
|
||||
)
|
||||
|
||||
if not i18n_to_use :
|
||||
i18n_to_use =self .i18n_main_instance
|
||||
logging .warning (f"BanCheck: [B_fallback] Using fallback i18n instance for banned user {event_user.id}. Type: {type(i18n_to_use)}")
|
||||
if not i18n_to_use:
|
||||
i18n_to_use = self.i18n_main_instance
|
||||
logging.warning(
|
||||
f"BanCheck: [B_fallback] Using fallback i18n instance for banned user {event_user.id}. Type: {type(i18n_to_use)}"
|
||||
)
|
||||
|
||||
if i18n_to_use :
|
||||
_ =lambda k ,**kw :i18n_to_use .gettext (current_lang ,k ,**kw )
|
||||
logging .debug ("BanCheck: [D] Attempting to get 'user_is_banned' text.")
|
||||
ban_message_text =_ ("user_is_banned")
|
||||
logging .debug (f"BanCheck: [E] Ban message text: '{ban_message_text}'")
|
||||
if self .settings .SUPPORT_LINK :
|
||||
logging .debug ("BanCheck: [F] Support link found. Attempting to get user_banned_keyboard.")
|
||||
keyboard =get_user_banned_keyboard (self .settings .SUPPORT_LINK ,current_lang ,i18n_to_use )
|
||||
logging .debug (f"BanCheck: [G] Keyboard created: {keyboard is not None}")
|
||||
else :logging .debug ("BanCheck: [F_alt] No support link configured.")
|
||||
else :
|
||||
logging .error (f"BanCheck: [CRITICAL] No i18n instance for user {event_user.id}. Using hardcoded text.")
|
||||
if self .settings .SUPPORT_LINK :
|
||||
kb_temp =InlineKeyboardBuilder ();kb_temp .button (text ="Support",url =self .settings .SUPPORT_LINK );keyboard =kb_temp .as_markup ()
|
||||
logging .debug ("BanCheck: [G_alt] Fallback keyboard created due to no i18n.")
|
||||
if i18n_to_use:
|
||||
_ = lambda k, **kw: i18n_to_use.gettext(
|
||||
current_lang, k, **kw)
|
||||
logging.debug(
|
||||
"BanCheck: [D] Attempting to get 'user_is_banned' text."
|
||||
)
|
||||
ban_message_text = _("user_is_banned")
|
||||
logging.debug(
|
||||
f"BanCheck: [E] Ban message text: '{ban_message_text}'"
|
||||
)
|
||||
if self.settings.SUPPORT_LINK:
|
||||
logging.debug(
|
||||
"BanCheck: [F] Support link found. Attempting to get user_banned_keyboard."
|
||||
)
|
||||
keyboard = get_user_banned_keyboard(
|
||||
self.settings.SUPPORT_LINK, current_lang,
|
||||
i18n_to_use)
|
||||
logging.debug(
|
||||
f"BanCheck: [G] Keyboard created: {keyboard is not None}"
|
||||
)
|
||||
else:
|
||||
logging.debug(
|
||||
"BanCheck: [F_alt] No support link configured.")
|
||||
else:
|
||||
logging.error(
|
||||
f"BanCheck: [CRITICAL] No i18n instance for user {event_user.id}. Using hardcoded text."
|
||||
)
|
||||
if self.settings.SUPPORT_LINK:
|
||||
kb_temp = InlineKeyboardBuilder()
|
||||
kb_temp.button(text="Support",
|
||||
url=self.settings.SUPPORT_LINK)
|
||||
keyboard = kb_temp.as_markup()
|
||||
logging.debug(
|
||||
"BanCheck: [G_alt] Fallback keyboard created due to no i18n."
|
||||
)
|
||||
|
||||
logging .debug (f"BanCheck: [H] Final pre-send check. Message: '{ban_message_text}', Keyboard: {keyboard is not None}")
|
||||
logging.debug(
|
||||
f"BanCheck: [H] Final pre-send check. Message: '{ban_message_text}', Keyboard: {keyboard is not None}"
|
||||
)
|
||||
|
||||
if isinstance(actual_event_object, Message):
|
||||
logging.debug(
|
||||
f"BanCheck: [I_Msg] Attempting actual_event_object.answer for Message to user {event_user.id}"
|
||||
)
|
||||
await actual_event_object.answer(ban_message_text,
|
||||
reply_markup=keyboard)
|
||||
logging.info(
|
||||
f"BanCheck: [J_Msg] Ban notification 'actual_event_object.answer' attempted for user {event_user.id} (Message)."
|
||||
)
|
||||
elif isinstance(actual_event_object, CallbackQuery):
|
||||
logging.debug(
|
||||
f"BanCheck: [I_CB] Attempting actual_event_object.answer (alert) for CallbackQuery to user {event_user.id}"
|
||||
)
|
||||
await actual_event_object.answer(ban_message_text,
|
||||
show_alert=True)
|
||||
logging.info(
|
||||
f"BanCheck: [J_CB] Ban alert 'actual_event_object.answer' attempted for user {event_user.id} (CallbackQuery)."
|
||||
)
|
||||
|
||||
if isinstance (actual_event_object ,Message ):
|
||||
logging .debug (f"BanCheck: [I_Msg] Attempting actual_event_object.answer for Message to user {event_user.id}")
|
||||
await actual_event_object .answer (ban_message_text ,reply_markup =keyboard )
|
||||
logging .info (f"BanCheck: [J_Msg] Ban notification 'actual_event_object.answer' attempted for user {event_user.id} (Message).")
|
||||
elif isinstance (actual_event_object ,CallbackQuery ):
|
||||
logging .debug (f"BanCheck: [I_CB] Attempting actual_event_object.answer (alert) for CallbackQuery to user {event_user.id}")
|
||||
await actual_event_object .answer (ban_message_text ,show_alert =True )
|
||||
logging .info (f"BanCheck: [J_CB] Ban alert 'actual_event_object.answer' attempted for user {event_user.id} (CallbackQuery).")
|
||||
target_message_obj = actual_event_object.message
|
||||
if target_message_obj:
|
||||
target_chat_id = target_message_obj.chat.id
|
||||
try:
|
||||
logging.debug(
|
||||
f"BanCheck: [K_CB_Edit] Attempting target_message_obj.edit_text for user {event_user.id}"
|
||||
)
|
||||
await target_message_obj.edit_text(
|
||||
ban_message_text, reply_markup=keyboard)
|
||||
logging.info(
|
||||
f"BanCheck: [L_CB_Edit] Ban msg 'target_message_obj.edit_text' attempted for user {event_user.id} (Callback)."
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"BanCheck: [M_CB_EditFail] Failed to edit message for banned user {event_user.id}: {type(e_edit).__name__} - {e_edit}. Sending new message."
|
||||
)
|
||||
await actual_event_object.bot.send_message(
|
||||
target_chat_id,
|
||||
ban_message_text,
|
||||
reply_markup=keyboard)
|
||||
logging.info(
|
||||
f"BanCheck: [N_CB_NewMsg] Ban msg 'actual_event_object.bot.send_message' (after edit fail) attempted for user {event_user.id} (Callback)."
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"BanCheck: [K_CB_NoMsg] CallbackQuery from {event_user.id} has no .message attribute. Sending new message directly."
|
||||
)
|
||||
await actual_event_object.bot.send_message(
|
||||
actual_event_object.from_user.id,
|
||||
ban_message_text,
|
||||
reply_markup=keyboard)
|
||||
logging.info(
|
||||
f"BanCheck: [L_CB_NoMsg_NewMsg] Ban msg 'actual_event_object.bot.send_message' (no .message) attempted for user {event_user.id} (Callback)."
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
f"BanCheck: [UNHANDLED_EVENT_TYPE_INTERNAL] actual_event_object type {type(actual_event_object)} was not Message or CallbackQuery."
|
||||
)
|
||||
|
||||
target_message_obj =actual_event_object .message
|
||||
if target_message_obj :
|
||||
target_chat_id =target_message_obj .chat .id
|
||||
try :
|
||||
logging .debug (f"BanCheck: [K_CB_Edit] Attempting target_message_obj.edit_text for user {event_user.id}")
|
||||
await target_message_obj .edit_text (ban_message_text ,reply_markup =keyboard )
|
||||
logging .info (f"BanCheck: [L_CB_Edit] Ban msg 'target_message_obj.edit_text' attempted for user {event_user.id} (Callback).")
|
||||
except Exception as e_edit :
|
||||
logging .warning (f"BanCheck: [M_CB_EditFail] Failed to edit message for banned user {event_user.id}: {type(e_edit).__name__} - {e_edit}. Sending new message.")
|
||||
await actual_event_object .bot .send_message (target_chat_id ,ban_message_text ,reply_markup =keyboard )
|
||||
logging .info (f"BanCheck: [N_CB_NewMsg] Ban msg 'actual_event_object.bot.send_message' (after edit fail) attempted for user {event_user.id} (Callback).")
|
||||
else :
|
||||
logging .warning (f"BanCheck: [K_CB_NoMsg] CallbackQuery from {event_user.id} has no .message attribute. Sending new message directly.")
|
||||
await actual_event_object .bot .send_message (actual_event_object .from_user .id ,ban_message_text ,reply_markup =keyboard )
|
||||
logging .info (f"BanCheck: [L_CB_NoMsg_NewMsg] Ban msg 'actual_event_object.bot.send_message' (no .message) attempted for user {event_user.id} (Callback).")
|
||||
else :
|
||||
logging .error (f"BanCheck: [UNHANDLED_EVENT_TYPE_INTERNAL] actual_event_object type {type(actual_event_object)} was not Message or CallbackQuery.")
|
||||
except TelegramForbiddenError as e_forbidden:
|
||||
logging.warning(
|
||||
f"BanCheck: TelegramForbiddenError sending ban msg to {event_user.id}: {e_forbidden}"
|
||||
)
|
||||
except TelegramBadRequest as e_bad_req:
|
||||
logging.error(
|
||||
f"BanCheck: TelegramBadRequest sending ban msg to {event_user.id}: {e_bad_req}",
|
||||
exc_info=True)
|
||||
except TelegramAPIError as e_api:
|
||||
logging.error(
|
||||
f"BanCheck: TelegramAPIError sending ban msg to {event_user.id}: {e_api}",
|
||||
exc_info=True)
|
||||
except AiogramError as e_aio:
|
||||
logging.error(
|
||||
f"BanCheck: AiogramError sending ban msg to {event_user.id}: {e_aio}",
|
||||
exc_info=True)
|
||||
except Exception as e_general:
|
||||
logging.error(
|
||||
f"BanCheck: Generic failure preparing or sending ban notification to user {event_user.id}: {e_general}",
|
||||
exc_info=True)
|
||||
|
||||
except TelegramForbiddenError as e_forbidden :logging .warning (f"BanCheck: TelegramForbiddenError sending ban msg to {event_user.id}: {e_forbidden}")
|
||||
except TelegramBadRequest as e_bad_req :logging .error (f"BanCheck: TelegramBadRequest sending ban msg to {event_user.id}: {e_bad_req}",exc_info =True )
|
||||
except TelegramAPIError as e_api :logging .error (f"BanCheck: TelegramAPIError sending ban msg to {event_user.id}: {e_api}",exc_info =True )
|
||||
except AiogramError as e_aio :logging .error (f"BanCheck: AiogramError sending ban msg to {event_user.id}: {e_aio}",exc_info =True )
|
||||
except Exception as e_general :
|
||||
logging .error (f"BanCheck: Generic failure preparing or sending ban notification to user {event_user.id}: {e_general}",exc_info =True )
|
||||
|
||||
logging .debug (f"BanCheck: [Z] End of ban processing for user {event_user.id}. Returning to stop further handlers.")
|
||||
logging.debug(
|
||||
f"BanCheck: [Z] End of ban processing for user {event_user.id}. Returning to stop further handlers."
|
||||
)
|
||||
return
|
||||
|
||||
return await handler (event ,data )
|
||||
return await handler(event, data)
|
||||
|
||||
+128
-85
@@ -2,112 +2,155 @@ import logging
|
||||
import json
|
||||
import os
|
||||
import aiosqlite
|
||||
from typing import Any ,Awaitable ,Callable ,Dict ,Optional
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram .types import TelegramObject ,User ,Update
|
||||
from aiogram.types import TelegramObject, User, Update
|
||||
|
||||
from db.database import get_user, get_db_connection_manager, _setup_db_connection
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
from db .database import get_user ,get_db_connection_manager ,_setup_db_connection
|
||||
from config .settings import Settings
|
||||
class JsonI18n:
|
||||
|
||||
def __init__(self, path: str, default: str = "en", domain: str = "bot"):
|
||||
self.domain = domain
|
||||
self.path = path
|
||||
self.default_lang = default
|
||||
self.locales_data: Dict[str, Dict[str, str]] = {}
|
||||
self._load_locales()
|
||||
logging.info(
|
||||
f"JsonI18n initialized. Loaded languages: {list(self.locales_data.keys())}. Default: {self.default_lang}"
|
||||
)
|
||||
|
||||
def _load_locales(self):
|
||||
if not os.path.isdir(self.path):
|
||||
logging.error(f"Locales path not found: {self.path}")
|
||||
return
|
||||
for item in os.listdir(self.path):
|
||||
if item.endswith(".json"):
|
||||
lang_code = item.split(".")[0]
|
||||
file_path = os.path.join(self.path, item)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
self.locales_data[lang_code] = json.load(f)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error loading locale {lang_code} from {file_path}: {e}"
|
||||
)
|
||||
|
||||
def gettext(self, lang_code: Optional[str], key: str, **kwargs) -> str:
|
||||
effective_lang_code = lang_code
|
||||
if not effective_lang_code or effective_lang_code not in self.locales_data:
|
||||
effective_lang_code = self.default_lang
|
||||
lang_data = self.locales_data.get(effective_lang_code)
|
||||
if lang_data is None:
|
||||
logging.warning(
|
||||
f"No language data for '{effective_lang_code}' (default: '{self.default_lang}'). Key '{key}'."
|
||||
)
|
||||
return key.format(**kwargs) if kwargs else key
|
||||
text = lang_data.get(key)
|
||||
if text is None:
|
||||
if effective_lang_code != self.default_lang:
|
||||
default_lang_data = self.locales_data.get(
|
||||
self.default_lang, {})
|
||||
text = default_lang_data.get(key)
|
||||
if text is None: return key.format(**kwargs) if kwargs else key
|
||||
try:
|
||||
return text.format(**kwargs) if kwargs else text
|
||||
except KeyError as e:
|
||||
logging.warning(
|
||||
f"Missing format key {e} for key '{key}' (lang: {effective_lang_code}). Text: '{text}'"
|
||||
)
|
||||
return text
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error formatting key '{key}' (lang: {effective_lang_code}): {e}. Text: '{text}'"
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
_i18n_instance: Optional[JsonI18n] = None
|
||||
|
||||
|
||||
class JsonI18n :
|
||||
def __init__ (self ,path :str ,default :str ="en",domain :str ="bot"):
|
||||
self .domain =domain
|
||||
self .path =path
|
||||
self .default_lang =default
|
||||
self .locales_data :Dict [str ,Dict [str ,str ]]={}
|
||||
self ._load_locales ()
|
||||
logging .info (f"JsonI18n initialized. Loaded languages: {list(self.locales_data.keys())}. Default: {self.default_lang}")
|
||||
|
||||
def _load_locales (self ):
|
||||
if not os .path .isdir (self .path ):logging .error (f"Locales path not found: {self.path}");return
|
||||
for item in os .listdir (self .path ):
|
||||
if item .endswith (".json"):
|
||||
lang_code =item .split (".")[0 ];file_path =os .path .join (self .path ,item )
|
||||
try :
|
||||
with open (file_path ,"r",encoding ="utf-8")as f :self .locales_data [lang_code ]=json .load (f )
|
||||
except Exception as e :logging .error (f"Error loading locale {lang_code} from {file_path}: {e}")
|
||||
|
||||
def gettext (self ,lang_code :Optional [str ],key :str ,**kwargs )->str :
|
||||
effective_lang_code =lang_code
|
||||
if not effective_lang_code or effective_lang_code not in self .locales_data :
|
||||
effective_lang_code =self .default_lang
|
||||
lang_data =self .locales_data .get (effective_lang_code )
|
||||
if lang_data is None :
|
||||
logging .warning (f"No language data for '{effective_lang_code}' (default: '{self.default_lang}'). Key '{key}'.");return key .format (**kwargs )if kwargs else key
|
||||
text =lang_data .get (key )
|
||||
if text is None :
|
||||
if effective_lang_code !=self .default_lang :
|
||||
default_lang_data =self .locales_data .get (self .default_lang ,{});text =default_lang_data .get (key )
|
||||
if text is None :return key .format (**kwargs )if kwargs else key
|
||||
try :return text .format (**kwargs )if kwargs else text
|
||||
except KeyError as e :logging .warning (f"Missing format key {e} for key '{key}' (lang: {effective_lang_code}). Text: '{text}'");return text
|
||||
except Exception as e :logging .error (f"Error formatting key '{key}' (lang: {effective_lang_code}): {e}. Text: '{text}'");return text
|
||||
|
||||
_i18n_instance :Optional [JsonI18n ]=None
|
||||
def get_i18n_instance (path :str ="locales",default :str ="en",domain :str ="bot")->JsonI18n :
|
||||
def get_i18n_instance(path: str = "locales",
|
||||
default: str = "en",
|
||||
domain: str = "bot") -> JsonI18n:
|
||||
global _i18n_instance
|
||||
if _i18n_instance is None :_i18n_instance =JsonI18n (path =path ,default =default ,domain =domain )
|
||||
if _i18n_instance is None:
|
||||
_i18n_instance = JsonI18n(path=path, default=default, domain=domain)
|
||||
return _i18n_instance
|
||||
|
||||
|
||||
class I18nMiddleware (BaseMiddleware ):
|
||||
def __init__ (self ,i18n :JsonI18n ,settings :Settings ):
|
||||
super ().__init__ ()
|
||||
self .i18n =i18n
|
||||
self .settings =settings
|
||||
class I18nMiddleware(BaseMiddleware):
|
||||
|
||||
async def __call__ (
|
||||
self ,
|
||||
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]],
|
||||
event :Update ,
|
||||
data :Dict [str ,Any ]
|
||||
)->Any :
|
||||
event_user :Optional [User ]=data .get ("event_from_user")
|
||||
def __init__(self, i18n: JsonI18n, settings: Settings):
|
||||
super().__init__()
|
||||
self.i18n = i18n
|
||||
self.settings = settings
|
||||
|
||||
current_language =self .i18n .default_lang
|
||||
async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
|
||||
Awaitable[Any]], event: Update,
|
||||
data: Dict[str, Any]) -> Any:
|
||||
event_user: Optional[User] = data.get("event_from_user")
|
||||
|
||||
current_language = self.i18n.default_lang
|
||||
|
||||
if event_user :
|
||||
logging .debug (f"I18nMiddleware: Processing for user {event_user.id}")
|
||||
if event_user:
|
||||
logging.debug(
|
||||
f"I18nMiddleware: Processing for user {event_user.id}")
|
||||
|
||||
try :
|
||||
try:
|
||||
|
||||
user_db_data =await get_user (event_user .id )
|
||||
if user_db_data and user_db_data ['language_code']and user_db_data ['language_code']in self .i18n .locales_data :
|
||||
current_language =user_db_data ['language_code']
|
||||
logging .debug (f"I18nMiddleware: User {event_user.id} language loaded from DB: {current_language}")
|
||||
else :
|
||||
user_db_data = await get_user(event_user.id)
|
||||
if user_db_data and user_db_data[
|
||||
'language_code'] and user_db_data[
|
||||
'language_code'] in self.i18n.locales_data:
|
||||
current_language = user_db_data['language_code']
|
||||
logging.debug(
|
||||
f"I18nMiddleware: User {event_user.id} language loaded from DB: {current_language}"
|
||||
)
|
||||
else:
|
||||
|
||||
if event_user .language_code :
|
||||
lang_prefix =event_user .language_code .split ('-')[0 ]
|
||||
if lang_prefix in self .i18n .locales_data :
|
||||
current_language =lang_prefix
|
||||
logging .debug (f"I18nMiddleware: User {event_user.id} language set from Telegram client (prefix): {current_language}")
|
||||
elif event_user .language_code in self .i18n .locales_data :
|
||||
current_language =event_user .language_code
|
||||
logging .debug (f"I18nMiddleware: User {event_user.id} language set from Telegram client (full): {current_language}")
|
||||
else :
|
||||
if event_user.language_code:
|
||||
lang_prefix = event_user.language_code.split('-')[0]
|
||||
if lang_prefix in self.i18n.locales_data:
|
||||
current_language = lang_prefix
|
||||
logging.debug(
|
||||
f"I18nMiddleware: User {event_user.id} language set from Telegram client (prefix): {current_language}"
|
||||
)
|
||||
elif event_user.language_code in self.i18n.locales_data:
|
||||
current_language = event_user.language_code
|
||||
logging.debug(
|
||||
f"I18nMiddleware: User {event_user.id} language set from Telegram client (full): {current_language}"
|
||||
)
|
||||
else:
|
||||
|
||||
logging .debug (f"I18nMiddleware: User {event_user.id} Telegram client language '{event_user.language_code}' not supported. Using default: {current_language}")
|
||||
else :
|
||||
logging .debug (f"I18nMiddleware: User {event_user.id} has no language_code from Telegram. Using default: {current_language}")
|
||||
except Exception as e_db_lang :
|
||||
logging.debug(
|
||||
f"I18nMiddleware: User {event_user.id} Telegram client language '{event_user.language_code}' not supported. Using default: {current_language}"
|
||||
)
|
||||
else:
|
||||
logging.debug(
|
||||
f"I18nMiddleware: User {event_user.id} has no language_code from Telegram. Using default: {current_language}"
|
||||
)
|
||||
except Exception as e_db_lang:
|
||||
|
||||
logging .error (f"I18nMiddleware: Error fetching user language from DB for user {event_user.id}: {e_db_lang}. Falling back.")
|
||||
if event_user .language_code :
|
||||
lang_prefix =event_user .language_code .split ('-')[0 ]
|
||||
if lang_prefix in self .i18n .locales_data :current_language =lang_prefix
|
||||
elif event_user .language_code in self .i18n .locales_data :current_language =event_user .language_code
|
||||
logging.error(
|
||||
f"I18nMiddleware: Error fetching user language from DB for user {event_user.id}: {e_db_lang}. Falling back."
|
||||
)
|
||||
if event_user.language_code:
|
||||
lang_prefix = event_user.language_code.split('-')[0]
|
||||
if lang_prefix in self.i18n.locales_data:
|
||||
current_language = lang_prefix
|
||||
elif event_user.language_code in self.i18n.locales_data:
|
||||
current_language = event_user.language_code
|
||||
|
||||
data ["i18n_data"]={
|
||||
"i18n_instance":self .i18n ,
|
||||
"current_language":current_language
|
||||
data["i18n_data"] = {
|
||||
"i18n_instance": self.i18n,
|
||||
"current_language": current_language
|
||||
}
|
||||
logging .debug (f"I18nMiddleware: Final current_language for event: {current_language}")
|
||||
logging.debug(
|
||||
f"I18nMiddleware: Final current_language for event: {current_language}"
|
||||
)
|
||||
|
||||
return await handler (event ,data )
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -1,97 +1,114 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from aiogram import Bot
|
||||
from apscheduler .schedulers .asyncio import AsyncIOScheduler
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from datetime import datetime
|
||||
|
||||
from config .settings import Settings
|
||||
from config.settings import Settings
|
||||
from .subscription_service import SubscriptionService
|
||||
|
||||
from db .database import get_db_connection_manager
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from db.database import get_db_connection_manager
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from .panel_api_service import PanelApiService
|
||||
|
||||
|
||||
async def send_expiration_warnings (bot :Bot ,settings :Settings ,i18n :JsonI18n ,panel_service :PanelApiService ):
|
||||
async def send_expiration_warnings(bot: Bot, settings: Settings,
|
||||
i18n: JsonI18n,
|
||||
panel_service: PanelApiService):
|
||||
"""
|
||||
Sends expiration warnings to users whose subscriptions are ending soon.
|
||||
This function is called by the scheduler.
|
||||
"""
|
||||
logging .info (f"Scheduler job: Checking for expiring subscriptions at {datetime.now()}...")
|
||||
logging.info(
|
||||
f"Scheduler job: Checking for expiring subscriptions at {datetime.now()}..."
|
||||
)
|
||||
|
||||
sub_service = SubscriptionService(get_db_connection_manager, settings,
|
||||
panel_service)
|
||||
|
||||
sub_service =SubscriptionService (get_db_connection_manager ,settings ,panel_service )
|
||||
expiring_subs = await sub_service.get_subscriptions_ending_soon(
|
||||
settings.SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS)
|
||||
|
||||
expiring_subs =await sub_service .get_subscriptions_ending_soon (settings .SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS )
|
||||
|
||||
if not expiring_subs :
|
||||
logging .info ("No subscriptions found ending soon for notification.")
|
||||
if not expiring_subs:
|
||||
logging.info("No subscriptions found ending soon for notification.")
|
||||
return
|
||||
|
||||
logging .info (f"Found {len(expiring_subs)} subscriptions ending soon for notification.")
|
||||
for sub_info in expiring_subs :
|
||||
user_id =sub_info ['user_id']
|
||||
if not user_id :
|
||||
logging .warning (f"Skipping notification for subscription without user_id: {sub_info}")
|
||||
logging.info(
|
||||
f"Found {len(expiring_subs)} subscriptions ending soon for notification."
|
||||
)
|
||||
for sub_info in expiring_subs:
|
||||
user_id = sub_info['user_id']
|
||||
if not user_id:
|
||||
logging.warning(
|
||||
f"Skipping notification for subscription without user_id: {sub_info}"
|
||||
)
|
||||
continue
|
||||
|
||||
user_lang =sub_info .get ('language_code')if sub_info .get ('language_code')else getattr (settings ,'DEFAULT_LANGUAGE','en')
|
||||
first_name =sub_info .get ('first_name','User')
|
||||
end_date_str =sub_info ['end_date_str']
|
||||
user_lang = sub_info.get('language_code') if sub_info.get(
|
||||
'language_code') else getattr(settings, 'DEFAULT_LANGUAGE', 'en')
|
||||
first_name = sub_info.get('first_name', 'User')
|
||||
end_date_str = sub_info['end_date_str']
|
||||
|
||||
days_left_float =sub_info .get ('days_left')
|
||||
days_left_display ='N/A'
|
||||
if days_left_float is not None :
|
||||
days_left_float = sub_info.get('days_left')
|
||||
days_left_display = 'N/A'
|
||||
if days_left_float is not None:
|
||||
|
||||
days_left_display =max (0 ,int (round (days_left_float )))
|
||||
days_left_display = max(0, int(round(days_left_float)))
|
||||
|
||||
_ =lambda key ,**kwargs :i18n .gettext (user_lang ,key ,**kwargs )
|
||||
message_text =_ (
|
||||
"subscription_ending_soon_notification",
|
||||
user_name =first_name ,
|
||||
end_date =end_date_str ,
|
||||
days_left =days_left_display
|
||||
)
|
||||
try :
|
||||
await bot .send_message (user_id ,message_text )
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
message_text = _("subscription_ending_soon_notification",
|
||||
user_name=first_name,
|
||||
end_date=end_date_str,
|
||||
days_left=days_left_display)
|
||||
try:
|
||||
await bot.send_message(user_id, message_text)
|
||||
|
||||
await sub_service .update_last_notification_sent (user_id ,end_date_str )
|
||||
logging .info (f"Sent expiration warning to user {user_id} for subscription ending {end_date_str}")
|
||||
except Exception as e :
|
||||
await sub_service.update_last_notification_sent(
|
||||
user_id, end_date_str)
|
||||
logging.info(
|
||||
f"Sent expiration warning to user {user_id} for subscription ending {end_date_str}"
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
logging .error (f"Failed to send expiration warning to user {user_id}: {e}")
|
||||
await asyncio .sleep (0.1 )
|
||||
logging.error(
|
||||
f"Failed to send expiration warning to user {user_id}: {e}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def schedule_subscription_notifications (bot :Bot ,settings :Settings ,i18n :JsonI18n ,scheduler :AsyncIOScheduler ):
|
||||
async def schedule_subscription_notifications(bot: Bot, settings: Settings,
|
||||
i18n: JsonI18n,
|
||||
scheduler: AsyncIOScheduler):
|
||||
"""Schedules the daily job to send expiration warnings."""
|
||||
|
||||
async def job_wrapper():
|
||||
panel_service = PanelApiService(settings)
|
||||
try:
|
||||
|
||||
async def job_wrapper ():
|
||||
panel_service =PanelApiService (settings )
|
||||
try :
|
||||
await send_expiration_warnings(bot, settings, i18n, panel_service)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error in scheduled job 'send_expiration_warnings': {e}",
|
||||
exc_info=True)
|
||||
finally:
|
||||
await panel_service.close_session()
|
||||
|
||||
await send_expiration_warnings (bot ,settings ,i18n ,panel_service )
|
||||
except Exception as e :
|
||||
logging .error (f"Error in scheduled job 'send_expiration_warnings': {e}",exc_info =True )
|
||||
finally :
|
||||
await panel_service .close_session ()
|
||||
try:
|
||||
notification_hour = int(settings.SUBSCRIPTION_NOTIFICATION_HOUR_UTC)
|
||||
notification_minute = int(
|
||||
settings.SUBSCRIPTION_NOTIFICATION_MINUTE_UTC)
|
||||
except (ValueError, TypeError):
|
||||
logging.warning(
|
||||
"SUBSCRIPTION_NOTIFICATION_HOUR_UTC or MINUTE_UTC is invalid. Defaulting to 9:00 UTC."
|
||||
)
|
||||
notification_hour = 9
|
||||
notification_minute = 0
|
||||
|
||||
|
||||
try :
|
||||
notification_hour =int (settings .SUBSCRIPTION_NOTIFICATION_HOUR_UTC )
|
||||
notification_minute =int (settings .SUBSCRIPTION_NOTIFICATION_MINUTE_UTC )
|
||||
except (ValueError ,TypeError ):
|
||||
logging .warning ("SUBSCRIPTION_NOTIFICATION_HOUR_UTC or MINUTE_UTC is invalid. Defaulting to 9:00 UTC.")
|
||||
notification_hour =9
|
||||
notification_minute =0
|
||||
|
||||
scheduler .add_job (
|
||||
job_wrapper ,
|
||||
'cron',
|
||||
hour =notification_hour ,
|
||||
minute =notification_minute ,
|
||||
name ="daily_subscription_expiration_warnings",
|
||||
misfire_grace_time =60 *15
|
||||
scheduler.add_job(job_wrapper,
|
||||
'cron',
|
||||
hour=notification_hour,
|
||||
minute=notification_minute,
|
||||
name="daily_subscription_expiration_warnings",
|
||||
misfire_grace_time=60 * 15)
|
||||
logging.info(
|
||||
f"Subscription expiration warning job scheduled daily at {notification_hour:02d}:{notification_minute:02d} UTC."
|
||||
)
|
||||
logging .info (f"Subscription expiration warning job scheduled daily at {notification_hour:02d}:{notification_minute:02d} UTC.")
|
||||
+219
-143
@@ -1,183 +1,259 @@
|
||||
import aiohttp
|
||||
import logging
|
||||
from typing import Optional ,List ,Dict ,Any
|
||||
from datetime import datetime ,timedelta ,timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from config .settings import Settings
|
||||
from config.settings import Settings
|
||||
|
||||
class PanelApiService :
|
||||
def __init__ (self ,settings :Settings ):
|
||||
self .settings =settings
|
||||
self .base_url =settings .PANEL_API_URL
|
||||
self .api_key =settings .PANEL_API_KEY
|
||||
self ._session :Optional [aiohttp .ClientSession ]=None
|
||||
self .default_client_ip ="127.0.0.1"
|
||||
|
||||
async def _get_session (self )->aiohttp .ClientSession :
|
||||
if self ._session is None or self ._session .closed :
|
||||
self ._session =aiohttp .ClientSession ()
|
||||
return self ._session
|
||||
class PanelApiService:
|
||||
|
||||
async def close_session (self ):
|
||||
if self ._session and not self ._session .closed :
|
||||
await self ._session .close ()
|
||||
self ._session =None
|
||||
logging .info ("Panel API service session closed.")
|
||||
def __init__(self, settings: Settings):
|
||||
self.settings = settings
|
||||
self.base_url = settings.PANEL_API_URL
|
||||
self.api_key = settings.PANEL_API_KEY
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self.default_client_ip = "127.0.0.1"
|
||||
|
||||
async def _prepare_headers (self )->Dict [str ,str ]:
|
||||
headers ={
|
||||
"Content-Type":"application/json",
|
||||
"X-Forwarded-Proto":"https",
|
||||
"X-Forwarded-For":self .default_client_ip ,
|
||||
"X-Real-IP":self .default_client_ip ,
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession()
|
||||
return self._session
|
||||
|
||||
async def close_session(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._session = None
|
||||
logging.info("Panel API service session closed.")
|
||||
|
||||
async def _prepare_headers(self) -> Dict[str, str]:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-For": self.default_client_ip,
|
||||
"X-Real-IP": self.default_client_ip,
|
||||
}
|
||||
if self .api_key :
|
||||
headers ["Authorization"]=f"Bearer {self.api_key}"
|
||||
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
return headers
|
||||
|
||||
async def _request (self ,method :str ,endpoint :str ,**kwargs )->Optional [Dict [str ,Any ]]:
|
||||
if not self .base_url :
|
||||
logging .error ("Panel API URL not configured.")
|
||||
return {"error":True ,"status_code":0 ,"message":"Panel API URL not configured."}
|
||||
async def _request(self, method: str, endpoint: str,
|
||||
**kwargs) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url:
|
||||
logging.error("Panel API URL not configured.")
|
||||
return {
|
||||
"error": True,
|
||||
"status_code": 0,
|
||||
"message": "Panel API URL not configured."
|
||||
}
|
||||
|
||||
session =await self ._get_session ()
|
||||
headers =await self ._prepare_headers ()
|
||||
session = await self._get_session()
|
||||
headers = await self._prepare_headers()
|
||||
|
||||
if "Authorization"not in headers and self .api_key :
|
||||
logging .warning (f"Authorization header missing for panel endpoint {endpoint} despite API key being set.")
|
||||
if "Authorization" not in headers and self.api_key:
|
||||
logging.warning(
|
||||
f"Authorization header missing for panel endpoint {endpoint} despite API key being set."
|
||||
)
|
||||
|
||||
url =f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
||||
json_payload_for_log =kwargs .get ('json')if method in ["POST","PATCH","PUT"]else None
|
||||
log_prefix =f"Panel API {method} {url}"
|
||||
if json_payload_for_log :log_prefix +=f" Payload: {json_payload_for_log}"
|
||||
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
||||
json_payload_for_log = kwargs.get('json') if method in [
|
||||
"POST", "PATCH", "PUT"
|
||||
] else None
|
||||
log_prefix = f"Panel API {method} {url}"
|
||||
if json_payload_for_log:
|
||||
log_prefix += f" Payload: {json_payload_for_log}"
|
||||
|
||||
try :
|
||||
async with session .request (method ,url ,headers =headers ,**kwargs )as response :
|
||||
if 200 <=response .status <300 :
|
||||
try :
|
||||
data =await response .json ()
|
||||
logging .debug (f"{log_prefix} - Success ({response.status})")
|
||||
try:
|
||||
async with session.request(method, url, headers=headers,
|
||||
**kwargs) as response:
|
||||
if 200 <= response.status < 300:
|
||||
try:
|
||||
data = await response.json()
|
||||
logging.debug(
|
||||
f"{log_prefix} - Success ({response.status})")
|
||||
return data
|
||||
except aiohttp .ContentTypeError :
|
||||
logging .debug (f"{log_prefix} - Success ({response.status}) with non-JSON response.")
|
||||
return {"status":"success","code":response .status ,"data_text":await response .text ()}
|
||||
else :
|
||||
try :
|
||||
error_json =await response .json ()
|
||||
logging .error (f"{log_prefix} - Failed ({response.status}): {error_json}")
|
||||
return {"error":True ,"status_code":response .status ,"response":error_json ,
|
||||
"message":error_json .get ("message"),"errorCode":error_json .get ("errorCode")}
|
||||
except aiohttp .ContentTypeError :
|
||||
error_text =await response .text ()
|
||||
logging .error (f"{log_prefix} - Failed ({response.status}): {error_text}")
|
||||
return {"error":True ,"status_code":response .status ,"message":error_text }
|
||||
except aiohttp .ClientError as e :
|
||||
logging .error (f"Panel API client request error to {url}: {e}")
|
||||
return {"error":True ,"status_code":-1 ,"message":str (e )}
|
||||
except Exception as e :
|
||||
logging .error (f"Unexpected Panel API request error to {url}: {e}",exc_info =True )
|
||||
return {"error":True ,"status_code":-2 ,"message":f"Unexpected error: {str(e)}"}
|
||||
except aiohttp.ContentTypeError:
|
||||
logging.debug(
|
||||
f"{log_prefix} - Success ({response.status}) with non-JSON response."
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"code": response.status,
|
||||
"data_text": await response.text()
|
||||
}
|
||||
else:
|
||||
try:
|
||||
error_json = await response.json()
|
||||
logging.error(
|
||||
f"{log_prefix} - Failed ({response.status}): {error_json}"
|
||||
)
|
||||
return {
|
||||
"error": True,
|
||||
"status_code": response.status,
|
||||
"response": error_json,
|
||||
"message": error_json.get("message"),
|
||||
"errorCode": error_json.get("errorCode")
|
||||
}
|
||||
except aiohttp.ContentTypeError:
|
||||
error_text = await response.text()
|
||||
logging.error(
|
||||
f"{log_prefix} - Failed ({response.status}): {error_text}"
|
||||
)
|
||||
return {
|
||||
"error": True,
|
||||
"status_code": response.status,
|
||||
"message": error_text
|
||||
}
|
||||
except aiohttp.ClientError as e:
|
||||
logging.error(f"Panel API client request error to {url}: {e}")
|
||||
return {"error": True, "status_code": -1, "message": str(e)}
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected Panel API request error to {url}: {e}",
|
||||
exc_info=True)
|
||||
return {
|
||||
"error": True,
|
||||
"status_code": -2,
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
}
|
||||
|
||||
async def get_users_by_filter (self ,username :Optional [str ]=None )->Optional [List [Dict [str ,Any ]]]:
|
||||
async def get_users_by_filter(
|
||||
self,
|
||||
username: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Fetches users from panel by username."""
|
||||
if not username :
|
||||
logging .warning ("get_users_by_filter called without username.")
|
||||
if not username:
|
||||
logging.warning("get_users_by_filter called without username.")
|
||||
return None
|
||||
|
||||
params ={"username":username }
|
||||
response_data =await self ._request ("GET","/users",params =params )
|
||||
params = {"username": username}
|
||||
response_data = await self._request("GET", "/users", params=params)
|
||||
|
||||
if response_data and not response_data .get ("error"):
|
||||
users_list =response_data .get ("response",{}).get ("users",[])
|
||||
logging .info (f"Found {len(users_list)} panel users matching filter: {params}")
|
||||
if response_data and not response_data.get("error"):
|
||||
users_list = response_data.get("response", {}).get("users", [])
|
||||
logging.info(
|
||||
f"Found {len(users_list)} panel users matching filter: {params}"
|
||||
)
|
||||
return users_list
|
||||
logging .error (f"Failed to fetch panel users with filter {params}. Response: {response_data}")
|
||||
logging.error(
|
||||
f"Failed to fetch panel users with filter {params}. Response: {response_data}"
|
||||
)
|
||||
return None
|
||||
|
||||
async def create_panel_user (
|
||||
self ,
|
||||
username :str ,
|
||||
telegram_id :Optional [int ]=None ,
|
||||
email :Optional [str ]=None ,
|
||||
default_expire_days :int =1 ,
|
||||
default_traffic_limit_bytes :int =0 ,
|
||||
default_traffic_limit_strategy :str ="NO_RESET",
|
||||
specific_inbound_uuids :Optional [List [str ]]=None ,
|
||||
activate_all_inbounds_default_flag :bool =True
|
||||
)->Optional [Dict [str ,Any ]]:
|
||||
async def create_panel_user(
|
||||
self,
|
||||
username: str,
|
||||
telegram_id: Optional[int] = None,
|
||||
email: Optional[str] = None,
|
||||
default_expire_days: int = 1,
|
||||
default_traffic_limit_bytes: int = 0,
|
||||
default_traffic_limit_strategy: str = "NO_RESET",
|
||||
specific_inbound_uuids: Optional[List[str]] = None,
|
||||
activate_all_inbounds_default_flag: bool = True
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
if not (6 <=len (username )<=34 and username .replace ('_','').replace ('-','').isalnum ()):
|
||||
msg =f"Username '{username}' for panel does not meet requirements (6-34 chars, alphanumeric, _, -)."
|
||||
logging .error (msg )
|
||||
return {"error":True ,"status_code":400 ,"message":msg ,"response":{"message":msg ,"errorCode":"VALIDATION_ERROR"}}
|
||||
if not (6 <= len(username) <= 34
|
||||
and username.replace('_', '').replace('-', '').isalnum()):
|
||||
msg = f"Username '{username}' for panel does not meet requirements (6-34 chars, alphanumeric, _, -)."
|
||||
logging.error(msg)
|
||||
return {
|
||||
"error": True,
|
||||
"status_code": 400,
|
||||
"message": msg,
|
||||
"response": {
|
||||
"message": msg,
|
||||
"errorCode": "VALIDATION_ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
now =datetime .now (timezone .utc )
|
||||
expire_at_dt =now +timedelta (days =default_expire_days )
|
||||
expire_at_iso =expire_at_dt .isoformat (timespec ='milliseconds').replace ('+00:00','Z')
|
||||
now = datetime.now(timezone.utc)
|
||||
expire_at_dt = now + timedelta(days=default_expire_days)
|
||||
expire_at_iso = expire_at_dt.isoformat(
|
||||
timespec='milliseconds').replace('+00:00', 'Z')
|
||||
|
||||
payload :Dict [str ,Any ]={
|
||||
"username":username ,
|
||||
"expireAt":expire_at_iso ,
|
||||
"trafficLimitStrategy":default_traffic_limit_strategy ,
|
||||
"trafficLimitBytes":default_traffic_limit_bytes ,
|
||||
payload: Dict[str, Any] = {
|
||||
"username": username,
|
||||
"expireAt": expire_at_iso,
|
||||
"trafficLimitStrategy": default_traffic_limit_strategy,
|
||||
"trafficLimitBytes": default_traffic_limit_bytes,
|
||||
}
|
||||
if specific_inbound_uuids :
|
||||
payload ["activeUserInbounds"]=specific_inbound_uuids
|
||||
if specific_inbound_uuids:
|
||||
payload["activeUserInbounds"] = specific_inbound_uuids
|
||||
|
||||
payload["activateAllInbounds"] = False
|
||||
else:
|
||||
payload["activateAllInbounds"] = activate_all_inbounds_default_flag
|
||||
|
||||
if telegram_id is not None: payload["telegramId"] = telegram_id
|
||||
if email: payload["email"] = email
|
||||
|
||||
payload ["activateAllInbounds"]=False
|
||||
else :
|
||||
payload ["activateAllInbounds"]=activate_all_inbounds_default_flag
|
||||
return await self._request("POST", "/users", json=payload)
|
||||
|
||||
if telegram_id is not None :payload ["telegramId"]=telegram_id
|
||||
if email :payload ["email"]=email
|
||||
async def update_user_details_on_panel(
|
||||
self, user_uuid: str,
|
||||
update_payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
if 'uuid' not in update_payload: update_payload['uuid'] = user_uuid
|
||||
|
||||
return await self ._request ("POST","/users",json =payload )
|
||||
update_payload.pop('activateAllInbounds', None)
|
||||
|
||||
async def update_user_details_on_panel (self ,user_uuid :str ,update_payload :Dict [str ,Any ])->Optional [Dict [str ,Any ]]:
|
||||
if 'uuid'not in update_payload :update_payload ['uuid']=user_uuid
|
||||
|
||||
|
||||
|
||||
|
||||
update_payload .pop ('activateAllInbounds',None )
|
||||
|
||||
full_response =await self ._request ("PATCH","/users",json =update_payload )
|
||||
if full_response and not full_response .get ("error")and full_response .get ("response"):
|
||||
logging .info (f"User {user_uuid} details updated on panel.")
|
||||
return full_response .get ("response")
|
||||
logging .error (f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Resp: {full_response}")
|
||||
full_response = await self._request("PATCH",
|
||||
"/users",
|
||||
json=update_payload)
|
||||
if full_response and not full_response.get(
|
||||
"error") and full_response.get("response"):
|
||||
logging.info(f"User {user_uuid} details updated on panel.")
|
||||
return full_response.get("response")
|
||||
logging.error(
|
||||
f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Resp: {full_response}"
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_panel_users (self ,page_size :int =100 )->Optional [List [Dict [str ,Any ]]]:
|
||||
all_users =[];start_offset =0
|
||||
while True :
|
||||
params ={"size":page_size ,"start":start_offset }
|
||||
response_data =await self ._request ("GET","/users",params =params )
|
||||
if not response_data or response_data .get ("error"):logging .error (f"Failed to fetch panel users batch: {response_data}");return None
|
||||
users_batch =response_data .get ("response",{}).get ("users",[])
|
||||
if not users_batch :break
|
||||
all_users .extend (users_batch )
|
||||
if len (users_batch )<page_size :break
|
||||
start_offset +=page_size
|
||||
logging .info (f"Fetched {len(all_users)} users from panel API.");return all_users
|
||||
async def get_all_panel_users(self,
|
||||
page_size: int = 100
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
all_users = []
|
||||
start_offset = 0
|
||||
while True:
|
||||
params = {"size": page_size, "start": start_offset}
|
||||
response_data = await self._request("GET", "/users", params=params)
|
||||
if not response_data or response_data.get("error"):
|
||||
logging.error(
|
||||
f"Failed to fetch panel users batch: {response_data}")
|
||||
return None
|
||||
users_batch = response_data.get("response", {}).get("users", [])
|
||||
if not users_batch: break
|
||||
all_users.extend(users_batch)
|
||||
if len(users_batch) < page_size: break
|
||||
start_offset += page_size
|
||||
logging.info(f"Fetched {len(all_users)} users from panel API.")
|
||||
return all_users
|
||||
|
||||
async def get_user_by_uuid (self ,user_uuid :str )->Optional [Dict [str ,Any ]]:
|
||||
full_response =await self ._request ("GET",f"/users/{user_uuid}")
|
||||
if full_response and not full_response .get ("error")and full_response .get ("response"):
|
||||
return full_response .get ("response")
|
||||
async def get_user_by_uuid(self,
|
||||
user_uuid: str) -> Optional[Dict[str, Any]]:
|
||||
full_response = await self._request("GET", f"/users/{user_uuid}")
|
||||
if full_response and not full_response.get(
|
||||
"error") and full_response.get("response"):
|
||||
return full_response.get("response")
|
||||
return None
|
||||
|
||||
async def update_user_status_on_panel (self ,user_uuid :str ,enable :bool )->bool :
|
||||
endpoint =f"/users/{user_uuid}/actions/{'enable' if enable else 'disable'}"
|
||||
response_data =await self ._request ("POST",endpoint )
|
||||
if response_data and not response_data .get ("error")and (response_data .get ("response")or response_data .get ("status")=="success"):
|
||||
logging .info (f"User {user_uuid} status on panel -> {'enabled' if enable else 'disabled'}.");return True
|
||||
logging .error (f"Failed to update user {user_uuid} status on panel. Resp: {response_data}");return False
|
||||
async def update_user_status_on_panel(self, user_uuid: str,
|
||||
enable: bool) -> bool:
|
||||
endpoint = f"/users/{user_uuid}/actions/{'enable' if enable else 'disable'}"
|
||||
response_data = await self._request("POST", endpoint)
|
||||
if response_data and not response_data.get("error") and (
|
||||
response_data.get("response")
|
||||
or response_data.get("status") == "success"):
|
||||
logging.info(
|
||||
f"User {user_uuid} status on panel -> {'enabled' if enable else 'disabled'}."
|
||||
)
|
||||
return True
|
||||
logging.error(
|
||||
f"Failed to update user {user_uuid} status on panel. Resp: {response_data}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_subscription_link (self ,short_uuid_or_sub_uuid :str ,client_type :Optional [str ]=None )->Optional [str ]:
|
||||
if not self .settings .PANEL_API_URL :return None
|
||||
return f"{self.settings.PANEL_API_URL.rstrip('/')}/sub/{short_uuid_or_sub_uuid}"
|
||||
async def get_subscription_link(
|
||||
self,
|
||||
short_uuid_or_sub_uuid: str,
|
||||
client_type: Optional[str] = None) -> Optional[str]:
|
||||
if not self.settings.PANEL_API_URL: return None
|
||||
return f"{self.settings.PANEL_API_URL.rstrip('/')}/sub/{short_uuid_or_sub_uuid}"
|
||||
|
||||
+160
-116
@@ -1,144 +1,188 @@
|
||||
import uuid
|
||||
import logging
|
||||
from yookassa import Configuration ,Payment
|
||||
from yookassa .domain .request .payment_request_builder import PaymentRequestBuilder
|
||||
from yookassa import Configuration, Payment
|
||||
from yookassa.domain.request.payment_request_builder import PaymentRequestBuilder
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
class YooKassaService:
|
||||
|
||||
def __init__(self,
|
||||
shop_id: Optional[str],
|
||||
secret_key: Optional[str],
|
||||
configured_return_url: Optional[str],
|
||||
bot_username_for_default: Optional[str] = None,
|
||||
settings_obj: Optional[Settings] = None):
|
||||
self.settings = settings_obj
|
||||
|
||||
if not shop_id or not secret_key:
|
||||
logging.warning(
|
||||
"YooKassa SHOP_ID or SECRET_KEY not configured. Payment functionality will be disabled."
|
||||
)
|
||||
self.configured = False
|
||||
else:
|
||||
Configuration.account_id = shop_id
|
||||
Configuration.secret_key = secret_key
|
||||
self.configured = True
|
||||
logging.info(f"YooKassa configured for shop_id: {shop_id}")
|
||||
|
||||
if configured_return_url:
|
||||
self.return_url = configured_return_url
|
||||
elif bot_username_for_default:
|
||||
self.return_url = f"https://t.me/{bot_username_for_default}"
|
||||
logging.info(
|
||||
f"YOOKASSA_RETURN_URL not set, using dynamic default: {self.return_url}"
|
||||
)
|
||||
else:
|
||||
self.return_url = "https://example.com/payment_error_no_return_url"
|
||||
logging.warning(
|
||||
f"YOOKASSA_RETURN_URL not set AND bot username not provided. Using placeholder: {self.return_url}"
|
||||
)
|
||||
logging.info(
|
||||
f"YooKassa Service effective return_url: {self.return_url}")
|
||||
|
||||
|
||||
from typing import Optional ,Dict ,Any ,List
|
||||
|
||||
from config .settings import Settings
|
||||
|
||||
class YooKassaService :
|
||||
def __init__ (
|
||||
self ,
|
||||
shop_id :Optional [str ],
|
||||
secret_key :Optional [str ],
|
||||
configured_return_url :Optional [str ],
|
||||
bot_username_for_default :Optional [str ]=None ,
|
||||
settings_obj :Optional [Settings ]=None
|
||||
):
|
||||
self .settings =settings_obj
|
||||
|
||||
if not shop_id or not secret_key :
|
||||
logging .warning ("YooKassa SHOP_ID or SECRET_KEY not configured. Payment functionality will be disabled.")
|
||||
self .configured =False
|
||||
else :
|
||||
Configuration .account_id =shop_id
|
||||
Configuration .secret_key =secret_key
|
||||
self .configured =True
|
||||
logging .info (f"YooKassa configured for shop_id: {shop_id}")
|
||||
|
||||
if configured_return_url :
|
||||
self .return_url =configured_return_url
|
||||
elif bot_username_for_default :
|
||||
self .return_url =f"https://t.me/{bot_username_for_default}"
|
||||
logging .info (f"YOOKASSA_RETURN_URL not set, using dynamic default: {self.return_url}")
|
||||
else :
|
||||
self .return_url ="https://example.com/payment_error_no_return_url"
|
||||
logging .warning (f"YOOKASSA_RETURN_URL not set AND bot username not provided. Using placeholder: {self.return_url}")
|
||||
logging .info (f"YooKassa Service effective return_url: {self.return_url}")
|
||||
|
||||
|
||||
async def create_payment (self ,amount :float ,currency :str ,description :str ,metadata :Dict [str ,Any ])->Optional [Dict [str ,Any ]]:
|
||||
if not self .configured :
|
||||
logging .error ("YooKassa is not configured. Cannot create payment.")
|
||||
async def create_payment(
|
||||
self, amount: float, currency: str, description: str,
|
||||
metadata: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
if not self.configured:
|
||||
logging.error("YooKassa is not configured. Cannot create payment.")
|
||||
return None
|
||||
|
||||
if not self .settings :
|
||||
logging .error ("YooKassaService: Settings object not available for receipt creation.")
|
||||
return {"error":True ,"internal_message":"Service settings not initialized."}
|
||||
|
||||
if not self .settings .YOOKASSA_DEFAULT_RECEIPT_EMAIL :
|
||||
logging .error ("CRITICAL: YOOKASSA_DEFAULT_RECEIPT_EMAIL is not configured. YooKassa payment will fail due to missing receipt customer contact.")
|
||||
return {"error":True ,"internal_message":"YooKassa receipt email not configured by admin."}
|
||||
|
||||
try :
|
||||
builder =PaymentRequestBuilder ()
|
||||
builder .set_amount ({"value":str (round (amount ,2 )),"currency":currency .upper ()})
|
||||
builder .set_capture (True )
|
||||
builder .set_confirmation ({"type":"redirect","return_url":self .return_url })
|
||||
builder .set_description (description )
|
||||
builder .set_metadata (metadata )
|
||||
|
||||
|
||||
receipt_items :List [Dict [str ,Any ]]=[
|
||||
{
|
||||
"description":description ,
|
||||
"quantity":"1.00",
|
||||
"amount":{
|
||||
"value":str (round (amount ,2 )),
|
||||
"currency":currency .upper ()
|
||||
},
|
||||
"vat_code":str (self .settings .YOOKASSA_VAT_CODE ),
|
||||
"payment_mode":self .settings .YOOKASSA_PAYMENT_MODE ,
|
||||
"payment_subject":self .settings .YOOKASSA_PAYMENT_SUBJECT
|
||||
}
|
||||
]
|
||||
|
||||
receipt_customer :Dict [str ,str ]={}
|
||||
if self .settings .YOOKASSA_DEFAULT_RECEIPT_EMAIL :
|
||||
receipt_customer ["email"]=self .settings .YOOKASSA_DEFAULT_RECEIPT_EMAIL
|
||||
|
||||
|
||||
|
||||
|
||||
if not receipt_customer :
|
||||
logging .error ("YooKassa: No customer contact (email/phone) for receipt.")
|
||||
return {"error":True ,"internal_message":"Receipt customer contact missing."}
|
||||
|
||||
receipt_payload :Dict [str ,Any ]={
|
||||
"customer":receipt_customer ,
|
||||
"items":receipt_items
|
||||
if not self.settings:
|
||||
logging.error(
|
||||
"YooKassaService: Settings object not available for receipt creation."
|
||||
)
|
||||
return {
|
||||
"error": True,
|
||||
"internal_message": "Service settings not initialized."
|
||||
}
|
||||
|
||||
if not self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL:
|
||||
logging.error(
|
||||
"CRITICAL: YOOKASSA_DEFAULT_RECEIPT_EMAIL is not configured. YooKassa payment will fail due to missing receipt customer contact."
|
||||
)
|
||||
return {
|
||||
"error":
|
||||
True,
|
||||
"internal_message":
|
||||
"YooKassa receipt email not configured by admin."
|
||||
}
|
||||
|
||||
builder .set_receipt (receipt_payload )
|
||||
try:
|
||||
builder = PaymentRequestBuilder()
|
||||
builder.set_amount({
|
||||
"value": str(round(amount, 2)),
|
||||
"currency": currency.upper()
|
||||
})
|
||||
builder.set_capture(True)
|
||||
builder.set_confirmation({
|
||||
"type": "redirect",
|
||||
"return_url": self.return_url
|
||||
})
|
||||
builder.set_description(description)
|
||||
builder.set_metadata(metadata)
|
||||
|
||||
receipt_items: List[Dict[str, Any]] = [{
|
||||
"description":
|
||||
description,
|
||||
"quantity":
|
||||
"1.00",
|
||||
"amount": {
|
||||
"value": str(round(amount, 2)),
|
||||
"currency": currency.upper()
|
||||
},
|
||||
"vat_code":
|
||||
str(self.settings.YOOKASSA_VAT_CODE),
|
||||
"payment_mode":
|
||||
self.settings.YOOKASSA_PAYMENT_MODE,
|
||||
"payment_subject":
|
||||
self.settings.YOOKASSA_PAYMENT_SUBJECT
|
||||
}]
|
||||
|
||||
receipt_customer: Dict[str, str] = {}
|
||||
if self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL:
|
||||
receipt_customer[
|
||||
"email"] = self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
|
||||
|
||||
if not receipt_customer:
|
||||
logging.error(
|
||||
"YooKassa: No customer contact (email/phone) for receipt.")
|
||||
return {
|
||||
"error": True,
|
||||
"internal_message": "Receipt customer contact missing."
|
||||
}
|
||||
|
||||
receipt_payload: Dict[str, Any] = {
|
||||
"customer": receipt_customer,
|
||||
"items": receipt_items
|
||||
}
|
||||
|
||||
builder.set_receipt(receipt_payload)
|
||||
|
||||
idempotence_key =str (uuid .uuid4 ())
|
||||
payment_request =builder .build ()
|
||||
idempotence_key = str(uuid.uuid4())
|
||||
payment_request = builder.build()
|
||||
|
||||
logging .info (f"Creating YooKassa payment (IDK: {idempotence_key}) with receipt. Email: {self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL}")
|
||||
logging.info(
|
||||
f"Creating YooKassa payment (IDK: {idempotence_key}) with receipt. Email: {self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL}"
|
||||
)
|
||||
|
||||
res =Payment .create (payment_request ,idempotence_key )
|
||||
logging .info (f"YooKassa Payment.create response: ID={res.id}, Status={res.status}")
|
||||
res = Payment.create(payment_request, idempotence_key)
|
||||
logging.info(
|
||||
f"YooKassa Payment.create response: ID={res.id}, Status={res.status}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id":res .id ,
|
||||
"confirmation_url":res .confirmation .confirmation_url if res .confirmation else None ,
|
||||
"status":res .status ,
|
||||
"metadata":res .metadata ,
|
||||
"amount_value":float (res .amount .value ),
|
||||
"amount_currency":res .amount .currency ,
|
||||
"idempotence_key":idempotence_key ,
|
||||
"paid":res .paid ,
|
||||
"refundable":res .refundable ,
|
||||
"created_at":res .created_at .isoformat ()if hasattr (res .created_at ,'isoformat')else str (res .created_at )
|
||||
"id":
|
||||
res.id,
|
||||
"confirmation_url":
|
||||
res.confirmation.confirmation_url
|
||||
if res.confirmation else None,
|
||||
"status":
|
||||
res.status,
|
||||
"metadata":
|
||||
res.metadata,
|
||||
"amount_value":
|
||||
float(res.amount.value),
|
||||
"amount_currency":
|
||||
res.amount.currency,
|
||||
"idempotence_key":
|
||||
idempotence_key,
|
||||
"paid":
|
||||
res.paid,
|
||||
"refundable":
|
||||
res.refundable,
|
||||
"created_at":
|
||||
res.created_at.isoformat() if hasattr(
|
||||
res.created_at, 'isoformat') else str(res.created_at)
|
||||
}
|
||||
except Exception as e :
|
||||
logging .error (f"YooKassa payment creation failed: {e}",exc_info =True )
|
||||
except Exception as e:
|
||||
logging.error(f"YooKassa payment creation failed: {e}",
|
||||
exc_info=True)
|
||||
return None
|
||||
|
||||
async def get_payment_info (self ,payment_id :str )->Optional [Dict [str ,Any ]]:
|
||||
if not self .configured :
|
||||
logging .error ("YooKassa is not configured. Cannot get payment info.")
|
||||
async def get_payment_info(self,
|
||||
payment_id: str) -> Optional[Dict[str, Any]]:
|
||||
if not self.configured:
|
||||
logging.error(
|
||||
"YooKassa is not configured. Cannot get payment info.")
|
||||
return None
|
||||
try :
|
||||
payment_info =Payment .find_one (payment_id )
|
||||
if payment_info :
|
||||
return {"id":payment_info .id ,"status":payment_info .status ,"paid":payment_info .paid ,
|
||||
"amount_value":float (payment_info .amount .value ),"amount_currency":payment_info .amount .currency ,
|
||||
"metadata":payment_info .metadata ,"description":payment_info .description ,}
|
||||
try:
|
||||
payment_info = Payment.find_one(payment_id)
|
||||
if payment_info:
|
||||
return {
|
||||
"id": payment_info.id,
|
||||
"status": payment_info.status,
|
||||
"paid": payment_info.paid,
|
||||
"amount_value": float(payment_info.amount.value),
|
||||
"amount_currency": payment_info.amount.currency,
|
||||
"metadata": payment_info.metadata,
|
||||
"description": payment_info.description,
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"YooKassa get payment info for {payment_id} failed: {e}")
|
||||
return None
|
||||
except Exception as e :
|
||||
logging .error (f"YooKassa get payment info for {payment_id} failed: {e}")
|
||||
return None
|
||||
@@ -1,88 +1,88 @@
|
||||
import logging
|
||||
import aiosqlite
|
||||
from datetime import datetime ,timezone
|
||||
from typing import Optional ,Dict ,Callable ,Any ,Tuple
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Callable, Any, Tuple
|
||||
|
||||
from aiogram import Bot
|
||||
from config .settings import Settings
|
||||
from db .database import get_promo_code_by_code ,increment_promo_activation ,get_db_connection_manager ,_setup_db_connection
|
||||
from config.settings import Settings
|
||||
from db.database import get_promo_code_by_code, increment_promo_activation, get_db_connection_manager, _setup_db_connection
|
||||
from .subscription_service import SubscriptionService
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
class PromoCodeService :
|
||||
def __init__ (
|
||||
self ,
|
||||
db_conn_provider :Callable [[],Any ],
|
||||
settings :Settings ,
|
||||
subscription_service :SubscriptionService ,
|
||||
bot :Bot ,
|
||||
i18n :JsonI18n
|
||||
):
|
||||
self .db_conn_provider =db_conn_provider
|
||||
self .settings =settings
|
||||
self .subscription_service =subscription_service
|
||||
self .bot =bot
|
||||
self .i18n =i18n
|
||||
|
||||
async def apply_promo_code (self ,user_id :int ,code_input :str ,user_lang :str )->Tuple [bool ,str ]:
|
||||
class PromoCodeService:
|
||||
|
||||
def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
|
||||
subscription_service: SubscriptionService, bot: Bot,
|
||||
i18n: JsonI18n):
|
||||
self.db_conn_provider = db_conn_provider
|
||||
self.settings = settings
|
||||
self.subscription_service = subscription_service
|
||||
self.bot = bot
|
||||
self.i18n = i18n
|
||||
|
||||
async def apply_promo_code(self, user_id: int, code_input: str,
|
||||
user_lang: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Applies a promo code for a user.
|
||||
Returns: (success_status: bool, message_text_for_user: str)
|
||||
"""
|
||||
_ =lambda k ,**kw :self .i18n .gettext (user_lang ,k ,**kw )
|
||||
code_input_upper =code_input .strip ().upper ()
|
||||
_ = lambda k, **kw: self.i18n.gettext(user_lang, k, **kw)
|
||||
code_input_upper = code_input.strip().upper()
|
||||
|
||||
async with self .db_conn_provider ()as db :
|
||||
await _setup_db_connection (db )
|
||||
try :
|
||||
promo_data =await get_promo_code_by_code (code_input_upper ,db_conn =db )
|
||||
async with self.db_conn_provider() as db:
|
||||
await _setup_db_connection(db)
|
||||
try:
|
||||
promo_data = await get_promo_code_by_code(code_input_upper,
|
||||
db_conn=db)
|
||||
|
||||
if not promo_data :
|
||||
return False ,_ ("promo_code_not_found",code =code_input_upper )
|
||||
if not promo_data:
|
||||
return False, _("promo_code_not_found",
|
||||
code=code_input_upper)
|
||||
|
||||
if promo_data ['current_activations']>=promo_data ['max_activations']:
|
||||
return False ,_ ("promo_code_max_activations_reached",code =code_input_upper )
|
||||
if promo_data['current_activations'] >= promo_data[
|
||||
'max_activations']:
|
||||
return False, _("promo_code_max_activations_reached",
|
||||
code=code_input_upper)
|
||||
|
||||
active_sub = await self.subscription_service.get_active_subscription(
|
||||
user_id)
|
||||
if not active_sub:
|
||||
return False, _("promo_code_no_active_subscription")
|
||||
|
||||
bonus_days = promo_data['bonus_days']
|
||||
|
||||
new_end_date = await self.subscription_service.extend_active_subscription_days(
|
||||
user_id=user_id,
|
||||
bonus_days=bonus_days,
|
||||
db_conn=db,
|
||||
reason=f"promo code {code_input_upper}")
|
||||
|
||||
if new_end_date:
|
||||
|
||||
activation_success = await increment_promo_activation(
|
||||
promo_data['promo_code_id'],
|
||||
user_id,
|
||||
db_conn=db,
|
||||
payment_id=None)
|
||||
if activation_success:
|
||||
await db.commit()
|
||||
return True, _(
|
||||
"promo_code_applied_success",
|
||||
code=code_input_upper,
|
||||
bonus_days=bonus_days,
|
||||
new_end_date=new_end_date.strftime('%Y-%m-%d'))
|
||||
else:
|
||||
|
||||
active_sub =await self .subscription_service .get_active_subscription (user_id )
|
||||
if not active_sub :
|
||||
return False ,_ ("promo_code_no_active_subscription")
|
||||
await db.rollback()
|
||||
return False, _("promo_code_invalid_or_expired")
|
||||
else:
|
||||
await db.rollback()
|
||||
return False, _("error_applying_promo_bonus")
|
||||
|
||||
bonus_days =promo_data ['bonus_days']
|
||||
|
||||
|
||||
new_end_date =await self .subscription_service .extend_active_subscription_days (
|
||||
user_id =user_id ,
|
||||
bonus_days =bonus_days ,
|
||||
db_conn =db ,
|
||||
reason =f"promo code {code_input_upper}"
|
||||
)
|
||||
|
||||
if new_end_date :
|
||||
|
||||
|
||||
activation_success =await increment_promo_activation (promo_data ['promo_code_id'],user_id ,db_conn =db ,payment_id =None )
|
||||
if activation_success :
|
||||
await db .commit ()
|
||||
return True ,_ ("promo_code_applied_success",
|
||||
code =code_input_upper ,
|
||||
bonus_days =bonus_days ,
|
||||
new_end_date =new_end_date .strftime ('%Y-%m-%d'))
|
||||
else :
|
||||
|
||||
|
||||
|
||||
await db .rollback ()
|
||||
return False ,_ ("promo_code_invalid_or_expired")
|
||||
else :
|
||||
await db .rollback ()
|
||||
return False ,_ ("error_applying_promo_bonus")
|
||||
|
||||
except Exception as e :
|
||||
logging .error (f"Error applying promo code {code_input_upper} for user {user_id}: {e}",exc_info =True )
|
||||
await db .rollback ()
|
||||
return False ,_ ("error_try_again")
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error applying promo code {code_input_upper} for user {user_id}: {e}",
|
||||
exc_info=True)
|
||||
await db.rollback()
|
||||
return False, _("error_try_again")
|
||||
|
||||
@@ -1,96 +1,129 @@
|
||||
import logging
|
||||
import aiosqlite
|
||||
from typing import Optional ,Dict ,Callable ,Any ,TYPE_CHECKING ,Tuple
|
||||
from typing import Optional, Dict, Callable, Any, TYPE_CHECKING, Tuple
|
||||
from aiogram import Bot
|
||||
from datetime import datetime
|
||||
|
||||
from config .settings import Settings
|
||||
from db .database import get_user
|
||||
from bot .middlewares .i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from db.database import get_user
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
if TYPE_CHECKING :
|
||||
if TYPE_CHECKING:
|
||||
from .subscription_service import SubscriptionService
|
||||
|
||||
|
||||
class ReferralService :
|
||||
def __init__ (self ,db_conn_provider :Callable [[],Any ],settings :Settings ,subscription_service_instance :'SubscriptionService',bot :Bot ,i18n :JsonI18n ):
|
||||
self .db_conn_provider =db_conn_provider
|
||||
self .settings =settings
|
||||
self .subscription_service =subscription_service_instance
|
||||
self .bot =bot
|
||||
self .i18n =i18n
|
||||
class ReferralService:
|
||||
|
||||
def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
|
||||
subscription_service_instance: 'SubscriptionService',
|
||||
bot: Bot, i18n: JsonI18n):
|
||||
self.db_conn_provider = db_conn_provider
|
||||
self.settings = settings
|
||||
self.subscription_service = subscription_service_instance
|
||||
self.bot = bot
|
||||
self.i18n = i18n
|
||||
|
||||
async def process_new_user_referral(self, referee_user_id: int,
|
||||
inviter_user_id: Optional[int]):
|
||||
if inviter_user_id and referee_user_id != inviter_user_id:
|
||||
|
||||
|
||||
async def process_new_user_referral (self ,referee_user_id :int ,inviter_user_id :Optional [int ]):
|
||||
if inviter_user_id and referee_user_id !=inviter_user_id :
|
||||
|
||||
|
||||
logging .info (f"Referral link used: User {referee_user_id} was invited by {inviter_user_id}.")
|
||||
logging.info(
|
||||
f"Referral link used: User {referee_user_id} was invited by {inviter_user_id}."
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
async def apply_referral_bonuses_for_payment (
|
||||
self ,referee_user_id :int ,purchased_subscription_months :int ,
|
||||
db_conn :aiosqlite .Connection
|
||||
)->Dict [str ,Any ]:
|
||||
async def apply_referral_bonuses_for_payment(
|
||||
self, referee_user_id: int, purchased_subscription_months: int,
|
||||
db_conn: aiosqlite.Connection) -> Dict[str, Any]:
|
||||
"""Applies bonuses. Notifies inviter. Returns referee bonus details."""
|
||||
referee_final_end_date :Optional [datetime ]=None
|
||||
referee_bonus_applied_days :Optional [int ]=None
|
||||
try :
|
||||
referee_user_row =await get_user (referee_user_id ,db_conn =db_conn )
|
||||
if not referee_user_row or referee_user_row ['referred_by_id']is None :
|
||||
logging .debug (f"User {referee_user_id} not referred or inviter ID missing. No referral bonuses.")
|
||||
return {"referee_bonus_applied_days":None ,"referee_new_end_date":None }
|
||||
|
||||
inviter_user_id =referee_user_row ['referred_by_id']
|
||||
inviter_user_row =await get_user (inviter_user_id ,db_conn =db_conn )
|
||||
|
||||
referee_name =referee_user_row ['first_name']or f"User {referee_user_id}"
|
||||
|
||||
default_lang_for_placeholder =getattr (self .settings ,'DEFAULT_LANGUAGE','en')
|
||||
inviter_name =inviter_user_row ['first_name']if inviter_user_row else self .i18n .gettext (default_lang_for_placeholder ,"friend_placeholder")
|
||||
|
||||
inviter_bonus =self .settings .referral_bonus_inviter .get (purchased_subscription_months )
|
||||
referee_bonus =self .settings .referral_bonus_referee .get (purchased_subscription_months )
|
||||
|
||||
if inviter_bonus and inviter_bonus >0 and inviter_user_row :
|
||||
new_end_date_inviter =await self .subscription_service .extend_subscription_for_referral (
|
||||
user_id =inviter_user_id ,bonus_days =inviter_bonus ,db_conn =db_conn
|
||||
referee_final_end_date: Optional[datetime] = None
|
||||
referee_bonus_applied_days: Optional[int] = None
|
||||
try:
|
||||
referee_user_row = await get_user(referee_user_id, db_conn=db_conn)
|
||||
if not referee_user_row or referee_user_row[
|
||||
'referred_by_id'] is None:
|
||||
logging.debug(
|
||||
f"User {referee_user_id} not referred or inviter ID missing. No referral bonuses."
|
||||
)
|
||||
if new_end_date_inviter :
|
||||
logging .info (f"Bonus applied for inviter {inviter_user_id}.")
|
||||
try :
|
||||
inviter_lang =inviter_user_row .get ('language_code',default_lang_for_placeholder )
|
||||
_i =lambda k ,**kw :self .i18n .gettext (inviter_lang ,k ,**kw )
|
||||
await self .bot .send_message (
|
||||
inviter_user_id ,
|
||||
_i ("referral_bonus_inviter_notification_extended",
|
||||
days =inviter_bonus ,
|
||||
referee_name =referee_name ,
|
||||
new_end_date =new_end_date_inviter .strftime ('%Y-%m-%d')
|
||||
)
|
||||
)
|
||||
except Exception as e :
|
||||
logging .error (f"Failed to send bonus notification to inviter {inviter_user_id}: {e}")
|
||||
else :logging .warning (f"Failed to apply bonus subscription extension for inviter {inviter_user_id}.")
|
||||
return {
|
||||
"referee_bonus_applied_days": None,
|
||||
"referee_new_end_date": None
|
||||
}
|
||||
|
||||
if referee_bonus and referee_bonus >0 :
|
||||
new_end_date_referee =await self .subscription_service .extend_subscription_for_referral (
|
||||
user_id =referee_user_id ,bonus_days =referee_bonus ,db_conn =db_conn ,is_referee_bonus =True
|
||||
)
|
||||
if new_end_date_referee :
|
||||
logging .info (f"Bonus applied for referee {referee_user_id}.")
|
||||
referee_final_end_date =new_end_date_referee
|
||||
referee_bonus_applied_days =referee_bonus
|
||||
else :logging .warning (f"Failed to apply bonus subscription extension for referee {referee_user_id}.")
|
||||
inviter_user_id = referee_user_row['referred_by_id']
|
||||
inviter_user_row = await get_user(inviter_user_id, db_conn=db_conn)
|
||||
|
||||
return {"referee_bonus_applied_days":referee_bonus_applied_days ,"referee_new_end_date":referee_final_end_date }
|
||||
except Exception as e :
|
||||
logging .error (f"Error in apply_referral_bonuses_for_payment (db_conn: {db_conn is not None}): {e}",exc_info =True )
|
||||
referee_name = referee_user_row[
|
||||
'first_name'] or f"User {referee_user_id}"
|
||||
|
||||
default_lang_for_placeholder = getattr(self.settings,
|
||||
'DEFAULT_LANGUAGE', 'en')
|
||||
inviter_name = inviter_user_row[
|
||||
'first_name'] if inviter_user_row else self.i18n.gettext(
|
||||
default_lang_for_placeholder, "friend_placeholder")
|
||||
|
||||
inviter_bonus = self.settings.referral_bonus_inviter.get(
|
||||
purchased_subscription_months)
|
||||
referee_bonus = self.settings.referral_bonus_referee.get(
|
||||
purchased_subscription_months)
|
||||
|
||||
if inviter_bonus and inviter_bonus > 0 and inviter_user_row:
|
||||
new_end_date_inviter = await self.subscription_service.extend_subscription_for_referral(
|
||||
user_id=inviter_user_id,
|
||||
bonus_days=inviter_bonus,
|
||||
db_conn=db_conn)
|
||||
if new_end_date_inviter:
|
||||
logging.info(
|
||||
f"Bonus applied for inviter {inviter_user_id}.")
|
||||
try:
|
||||
inviter_lang = inviter_user_row.get(
|
||||
'language_code', default_lang_for_placeholder)
|
||||
_i = lambda k, **kw: self.i18n.gettext(
|
||||
inviter_lang, k, **kw)
|
||||
await self.bot.send_message(
|
||||
inviter_user_id,
|
||||
_i("referral_bonus_inviter_notification_extended",
|
||||
days=inviter_bonus,
|
||||
referee_name=referee_name,
|
||||
new_end_date=new_end_date_inviter.strftime(
|
||||
'%Y-%m-%d')))
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to send bonus notification to inviter {inviter_user_id}: {e}"
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Failed to apply bonus subscription extension for inviter {inviter_user_id}."
|
||||
)
|
||||
|
||||
if referee_bonus and referee_bonus > 0:
|
||||
new_end_date_referee = await self.subscription_service.extend_subscription_for_referral(
|
||||
user_id=referee_user_id,
|
||||
bonus_days=referee_bonus,
|
||||
db_conn=db_conn,
|
||||
is_referee_bonus=True)
|
||||
if new_end_date_referee:
|
||||
logging.info(
|
||||
f"Bonus applied for referee {referee_user_id}.")
|
||||
referee_final_end_date = new_end_date_referee
|
||||
referee_bonus_applied_days = referee_bonus
|
||||
else:
|
||||
logging.warning(
|
||||
f"Failed to apply bonus subscription extension for referee {referee_user_id}."
|
||||
)
|
||||
|
||||
return {
|
||||
"referee_bonus_applied_days": referee_bonus_applied_days,
|
||||
"referee_new_end_date": referee_final_end_date
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error in apply_referral_bonuses_for_payment (db_conn: {db_conn is not None}): {e}",
|
||||
exc_info=True)
|
||||
|
||||
raise
|
||||
|
||||
def generate_referral_link (self ,bot_username :str ,inviter_user_id :int )->str :
|
||||
return f"https://t.me/{bot_username}?start=ref_{inviter_user_id}"
|
||||
def generate_referral_link(self, bot_username: str,
|
||||
inviter_user_id: int) -> str:
|
||||
return f"https://t.me/{bot_username}?start=ref_{inviter_user_id}"
|
||||
|
||||
@@ -1,336 +1,670 @@
|
||||
import logging
|
||||
import aiosqlite
|
||||
from datetime import datetime ,timedelta ,timezone
|
||||
from typing import Optional ,Dict ,Any ,List ,Callable ,Awaitable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any, List, Callable, Awaitable
|
||||
|
||||
from config .settings import Settings
|
||||
from db .database import get_db_connection_manager ,_setup_db_connection ,get_user ,has_had_any_subscription as db_has_had_any_subscription
|
||||
from config.settings import Settings
|
||||
from db.database import get_db_connection_manager, _setup_db_connection, get_user, has_had_any_subscription as db_has_had_any_subscription
|
||||
from .panel_api_service import PanelApiService
|
||||
|
||||
|
||||
if False :
|
||||
if False:
|
||||
from .referral_service import ReferralService
|
||||
|
||||
class SubscriptionService :
|
||||
def __init__ (self ,db_conn_provider :Callable [[],Any ],settings :Settings ,panel_service :PanelApiService ):
|
||||
self .db_conn_provider =db_conn_provider
|
||||
self .settings =settings
|
||||
self .panel_service =panel_service
|
||||
|
||||
async def _get_db (self ,existing_conn :Optional [aiosqlite .Connection ]=None )->tuple [aiosqlite .Connection ,bool ]:
|
||||
class SubscriptionService:
|
||||
|
||||
def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
|
||||
panel_service: PanelApiService):
|
||||
self.db_conn_provider = db_conn_provider
|
||||
self.settings = settings
|
||||
self.panel_service = panel_service
|
||||
|
||||
async def _get_db(
|
||||
self,
|
||||
existing_conn: Optional[aiosqlite.Connection] = None
|
||||
) -> tuple[aiosqlite.Connection, bool]:
|
||||
"""Helper to get a DB connection. Returns (connection, should_manage_flag)."""
|
||||
if existing_conn :
|
||||
return existing_conn ,False
|
||||
if existing_conn:
|
||||
return existing_conn, False
|
||||
|
||||
conn_manager =self .db_conn_provider ()
|
||||
conn =await conn_manager .__aenter__ ()
|
||||
try :
|
||||
await _setup_db_connection (conn )
|
||||
except Exception as e :
|
||||
await conn_manager .__aexit__ (type (e ),e ,e .__traceback__ )
|
||||
conn_manager = self.db_conn_provider()
|
||||
conn = await conn_manager.__aenter__()
|
||||
try:
|
||||
await _setup_db_connection(conn)
|
||||
except Exception as e:
|
||||
await conn_manager.__aexit__(type(e), e, e.__traceback__)
|
||||
raise
|
||||
return conn ,True
|
||||
return conn, True
|
||||
|
||||
async def _release_db (self ,db :aiosqlite .Connection ,should_manage :bool ,exc_type =None ,exc_val =None ,exc_tb =None ):
|
||||
async def _release_db(self,
|
||||
db: aiosqlite.Connection,
|
||||
should_manage: bool,
|
||||
exc_type=None,
|
||||
exc_val=None,
|
||||
exc_tb=None):
|
||||
"""Helper to release/close a DB connection if this service instance opened it."""
|
||||
if should_manage :
|
||||
await db .__aexit__ (exc_type ,exc_val ,exc_tb )
|
||||
if should_manage:
|
||||
await db.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
async def get_user_language (self ,user_id :int )->str :
|
||||
async def get_user_language(self, user_id: int) -> str:
|
||||
"""Fetches the user's language preference from the local database."""
|
||||
db ,should_manage =await self ._get_db ()
|
||||
try :
|
||||
user_record =await get_user (user_id ,db_conn =db )
|
||||
default_lang =self .settings .DEFAULT_LANGUAGE
|
||||
return user_record ['language_code']if user_record and 'language_code'in user_record .keys ()and user_record ['language_code']else default_lang
|
||||
finally :
|
||||
await self ._release_db (db ,should_manage )
|
||||
db, should_manage = await self._get_db()
|
||||
try:
|
||||
user_record = await get_user(user_id, db_conn=db)
|
||||
default_lang = self.settings.DEFAULT_LANGUAGE
|
||||
return user_record[
|
||||
'language_code'] if user_record and 'language_code' in user_record.keys(
|
||||
) and user_record['language_code'] else default_lang
|
||||
finally:
|
||||
await self._release_db(db, should_manage)
|
||||
|
||||
async def get_panel_user_uuid (self ,user_id :int ,db_conn :Optional [aiosqlite .Connection ]=None )->Optional [str ]:
|
||||
async def get_panel_user_uuid(
|
||||
self,
|
||||
user_id: int,
|
||||
db_conn: Optional[aiosqlite.Connection] = None) -> Optional[str]:
|
||||
"""Fetches panel_user_uuid for a given Telegram user_id from local DB."""
|
||||
db_to_use ,should_manage_this_conn =await self ._get_db (db_conn )
|
||||
try :
|
||||
user_record =await get_user (user_id ,db_conn =db_to_use )
|
||||
return user_record ['panel_user_uuid']if user_record and 'panel_user_uuid'in user_record .keys ()and user_record ['panel_user_uuid']else None
|
||||
finally :
|
||||
await self ._release_db (db_to_use ,should_manage_this_conn )
|
||||
db_to_use, should_manage_this_conn = await self._get_db(db_conn)
|
||||
try:
|
||||
user_record = await get_user(user_id, db_conn=db_to_use)
|
||||
return user_record[
|
||||
'panel_user_uuid'] if user_record and 'panel_user_uuid' in user_record.keys(
|
||||
) and user_record['panel_user_uuid'] else None
|
||||
finally:
|
||||
await self._release_db(db_to_use, should_manage_this_conn)
|
||||
|
||||
async def has_had_any_subscription (self ,user_id :int ,db_conn :Optional [aiosqlite .Connection ]=None )->bool :
|
||||
async def has_had_any_subscription(
|
||||
self,
|
||||
user_id: int,
|
||||
db_conn: Optional[aiosqlite.Connection] = None) -> bool:
|
||||
"""Checks if the user has any record in the subscriptions table."""
|
||||
db_to_use ,should_manage_this_conn =await self ._get_db (db_conn )
|
||||
try :
|
||||
return await db_has_had_any_subscription (user_id ,db_conn =db_to_use )
|
||||
finally :
|
||||
await self ._release_db (db_to_use ,should_manage_this_conn )
|
||||
db_to_use, should_manage_this_conn = await self._get_db(db_conn)
|
||||
try:
|
||||
return await db_has_had_any_subscription(user_id,
|
||||
db_conn=db_to_use)
|
||||
finally:
|
||||
await self._release_db(db_to_use, should_manage_this_conn)
|
||||
|
||||
async def activate_trial_subscription (self ,user_id :int )->Optional [Dict [str ,Any ]]:
|
||||
async def activate_trial_subscription(
|
||||
self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Activates a trial subscription for an eligible user.
|
||||
Manages its own database connection and transaction.
|
||||
Returns dict with trial details or specific error dict on failure/ineligibility.
|
||||
"""
|
||||
if not self .settings .TRIAL_ENABLED or self .settings .TRIAL_DURATION_DAYS <=0 :
|
||||
logging .info (f"Trial subscription feature is disabled or duration invalid for user {user_id}.")
|
||||
return {"eligible":False ,"activated":False ,"message_key":"trial_feature_disabled"}
|
||||
if not self.settings.TRIAL_ENABLED or self.settings.TRIAL_DURATION_DAYS <= 0:
|
||||
logging.info(
|
||||
f"Trial subscription feature is disabled or duration invalid for user {user_id}."
|
||||
)
|
||||
return {
|
||||
"eligible": False,
|
||||
"activated": False,
|
||||
"message_key": "trial_feature_disabled"
|
||||
}
|
||||
|
||||
async with self .db_conn_provider ()as db :
|
||||
await _setup_db_connection (db )
|
||||
try :
|
||||
if await db_has_had_any_subscription (user_id ,db_conn =db ):
|
||||
logging .info (f"User {user_id} has prior subscriptions. Trial not applicable.")
|
||||
return {"eligible":False ,"activated":False ,"message_key":"trial_not_eligible_already_subscribed"}
|
||||
|
||||
panel_user_uuid =await self .get_panel_user_uuid (user_id ,db_conn =db )
|
||||
panel_subscription_uuid_for_link =None ;panel_short_uuid_for_link =None
|
||||
panel_user_interacted_now =False ;specific_inbounds =self .settings .parsed_default_panel_user_inbound_uuids
|
||||
panel_actual_subscription_url :Optional [str ]=None
|
||||
|
||||
if not panel_user_uuid :
|
||||
logging .info (f"No panel_user_uuid for TG user_id {user_id} for trial. Creating panel user.")
|
||||
panel_username_to_create =f"tg_{user_id}"
|
||||
creation_response =await self .panel_service .create_panel_user (
|
||||
username =panel_username_to_create ,telegram_id =user_id ,
|
||||
default_expire_days =self .settings .TRIAL_DURATION_DAYS ,
|
||||
default_traffic_limit_bytes =self .settings .trial_traffic_limit_bytes ,
|
||||
default_traffic_limit_strategy =self .settings .PANEL_USER_DEFAULT_TRAFFIC_STRATEGY ,
|
||||
specific_inbound_uuids =specific_inbounds ,
|
||||
activate_all_inbounds_default_flag =False if specific_inbounds else True
|
||||
async with self.db_conn_provider() as db:
|
||||
await _setup_db_connection(db)
|
||||
try:
|
||||
if await db_has_had_any_subscription(user_id, db_conn=db):
|
||||
logging.info(
|
||||
f"User {user_id} has prior subscriptions. Trial not applicable."
|
||||
)
|
||||
panel_user_obj_from_api =None
|
||||
if creation_response and not creation_response .get ("error"):panel_user_obj_from_api =creation_response .get ("response")
|
||||
elif creation_response and creation_response .get ("errorCode")=="A019":
|
||||
existing_users_list =await self .panel_service .get_users_by_filter (username =panel_username_to_create )
|
||||
if existing_users_list and len (existing_users_list )==1 :panel_user_obj_from_api =existing_users_list [0 ]
|
||||
return {
|
||||
"eligible": False,
|
||||
"activated": False,
|
||||
"message_key": "trial_not_eligible_already_subscribed"
|
||||
}
|
||||
|
||||
if panel_user_obj_from_api and panel_user_obj_from_api .get ('uuid'):
|
||||
panel_user_uuid =panel_user_obj_from_api ['uuid']
|
||||
panel_subscription_uuid_for_link =panel_user_obj_from_api .get ('subscriptionUuid')
|
||||
panel_short_uuid_for_link =panel_user_obj_from_api .get ('shortUuid')
|
||||
panel_actual_subscription_url =panel_user_obj_from_api .get ('subscriptionUrl')
|
||||
await db .execute ("UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",(panel_user_uuid ,user_id ))
|
||||
panel_user_interacted_now =True
|
||||
if panel_user_obj_from_api .get ('telegramId')!=user_id :
|
||||
await self .panel_service .update_user_details_on_panel (panel_user_uuid ,{"telegramId":user_id })
|
||||
else :
|
||||
logging .error (f"Failed to create/link panel user for trial (TG_ID {user_id}). Resp: {creation_response if 'creation_response' in locals() else 'N/A'}")
|
||||
await db .rollback ()
|
||||
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"}
|
||||
else :
|
||||
panel_user_data =await self .panel_service .get_user_by_uuid (panel_user_uuid )
|
||||
if panel_user_data :
|
||||
panel_subscription_uuid_for_link =panel_user_data .get ('subscriptionUuid')
|
||||
panel_short_uuid_for_link =panel_user_data .get ('shortUuid')
|
||||
panel_actual_subscription_url =panel_user_data .get ('subscriptionUrl')
|
||||
panel_user_uuid = await self.get_panel_user_uuid(user_id,
|
||||
db_conn=db)
|
||||
panel_subscription_uuid_for_link = None
|
||||
panel_short_uuid_for_link = None
|
||||
panel_user_interacted_now = False
|
||||
specific_inbounds = self.settings.parsed_default_panel_user_inbound_uuids
|
||||
panel_actual_subscription_url: Optional[str] = None
|
||||
|
||||
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link :
|
||||
panel_subscription_uuid_for_link =panel_short_uuid_for_link
|
||||
if not panel_subscription_uuid_for_link :
|
||||
logging .error (f"Critical: panel_subscription_uuid for link is None for trial (panel_uuid {panel_user_uuid}).")
|
||||
await db .rollback ()
|
||||
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"}
|
||||
if not panel_user_uuid:
|
||||
logging.info(
|
||||
f"No panel_user_uuid for TG user_id {user_id} for trial. Creating panel user."
|
||||
)
|
||||
panel_username_to_create = f"tg_{user_id}"
|
||||
creation_response = await self.panel_service.create_panel_user(
|
||||
username=panel_username_to_create,
|
||||
telegram_id=user_id,
|
||||
default_expire_days=self.settings.TRIAL_DURATION_DAYS,
|
||||
default_traffic_limit_bytes=self.settings.
|
||||
trial_traffic_limit_bytes,
|
||||
default_traffic_limit_strategy=self.settings.
|
||||
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
||||
specific_inbound_uuids=specific_inbounds,
|
||||
activate_all_inbounds_default_flag=False
|
||||
if specific_inbounds else True)
|
||||
panel_user_obj_from_api = None
|
||||
if creation_response and not creation_response.get(
|
||||
"error"):
|
||||
panel_user_obj_from_api = creation_response.get(
|
||||
"response")
|
||||
elif creation_response and creation_response.get(
|
||||
"errorCode") == "A019":
|
||||
existing_users_list = await self.panel_service.get_users_by_filter(
|
||||
username=panel_username_to_create)
|
||||
if existing_users_list and len(
|
||||
existing_users_list) == 1:
|
||||
panel_user_obj_from_api = existing_users_list[0]
|
||||
|
||||
start_date =datetime .now (timezone .utc )
|
||||
end_date =start_date +timedelta (days =self .settings .TRIAL_DURATION_DAYS )
|
||||
await db .execute ("UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",(panel_user_uuid ,))
|
||||
upsert_sql =""" INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel, traffic_limit_bytes) VALUES (?, ?, ?, ?, ?, 0, 1, 'TRIAL', ?) ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = 0, is_active = 1, status_from_panel = 'TRIAL', traffic_limit_bytes = excluded.traffic_limit_bytes, last_notification_sent = NULL; """
|
||||
trial_traffic_val =self .settings .trial_traffic_limit_bytes
|
||||
params =(user_id ,panel_user_uuid ,panel_subscription_uuid_for_link ,start_date .isoformat (),end_date .isoformat (),trial_traffic_val )
|
||||
if panel_user_obj_from_api and panel_user_obj_from_api.get(
|
||||
'uuid'):
|
||||
panel_user_uuid = panel_user_obj_from_api['uuid']
|
||||
panel_subscription_uuid_for_link = panel_user_obj_from_api.get(
|
||||
'subscriptionUuid')
|
||||
panel_short_uuid_for_link = panel_user_obj_from_api.get(
|
||||
'shortUuid')
|
||||
panel_actual_subscription_url = panel_user_obj_from_api.get(
|
||||
'subscriptionUrl')
|
||||
await db.execute(
|
||||
"UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",
|
||||
(panel_user_uuid, user_id))
|
||||
panel_user_interacted_now = True
|
||||
if panel_user_obj_from_api.get(
|
||||
'telegramId') != user_id:
|
||||
await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, {"telegramId": user_id})
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to create/link panel user for trial (TG_ID {user_id}). Resp: {creation_response if 'creation_response' in locals() else 'N/A'}"
|
||||
)
|
||||
await db.rollback()
|
||||
return {
|
||||
"eligible": True,
|
||||
"activated": False,
|
||||
"message_key": "trial_activation_failed"
|
||||
}
|
||||
else:
|
||||
panel_user_data = await self.panel_service.get_user_by_uuid(
|
||||
panel_user_uuid)
|
||||
if panel_user_data:
|
||||
panel_subscription_uuid_for_link = panel_user_data.get(
|
||||
'subscriptionUuid')
|
||||
panel_short_uuid_for_link = panel_user_data.get(
|
||||
'shortUuid')
|
||||
panel_actual_subscription_url = panel_user_data.get(
|
||||
'subscriptionUrl')
|
||||
|
||||
upsert_cursor =await db .execute (upsert_sql ,params )
|
||||
trial_subscription_id =upsert_cursor .lastrowid
|
||||
if not trial_subscription_id or trial_subscription_id ==0 :
|
||||
id_fetch_cursor =await db .execute ("SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",(panel_subscription_uuid_for_link ,))
|
||||
id_fetch_row =await id_fetch_cursor .fetchone ()
|
||||
if id_fetch_cursor :await id_fetch_cursor .close ()
|
||||
if id_fetch_row :trial_subscription_id =id_fetch_row ['subscription_id']
|
||||
logging .info (f"Local trial subscription (ID: {trial_subscription_id}) for user {user_id} prepared. Ends: {end_date.isoformat()}.")
|
||||
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link:
|
||||
panel_subscription_uuid_for_link = panel_short_uuid_for_link
|
||||
if not panel_subscription_uuid_for_link:
|
||||
logging.error(
|
||||
f"Critical: panel_subscription_uuid for link is None for trial (panel_uuid {panel_user_uuid})."
|
||||
)
|
||||
await db.rollback()
|
||||
return {
|
||||
"eligible": True,
|
||||
"activated": False,
|
||||
"message_key": "trial_activation_failed"
|
||||
}
|
||||
|
||||
panel_update_payload :Dict [str ,Any ]={"uuid":panel_user_uuid ,"expireAt":end_date .isoformat (timespec ='milliseconds').replace ('+00:00','Z'),"status":"ACTIVE","trafficLimitBytes":trial_traffic_val ,"trafficLimitStrategy":self .settings .PANEL_USER_DEFAULT_TRAFFIC_STRATEGY ,}
|
||||
if specific_inbounds :panel_update_payload ["activeUserInbounds"]=specific_inbounds
|
||||
elif panel_user_interacted_now :panel_update_payload ["activateAllInbounds"]=True
|
||||
start_date = datetime.now(timezone.utc)
|
||||
end_date = start_date + timedelta(
|
||||
days=self.settings.TRIAL_DURATION_DAYS)
|
||||
await db.execute(
|
||||
"UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",
|
||||
(panel_user_uuid, ))
|
||||
upsert_sql = """ INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel, traffic_limit_bytes) VALUES (?, ?, ?, ?, ?, 0, 1, 'TRIAL', ?) ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = 0, is_active = 1, status_from_panel = 'TRIAL', traffic_limit_bytes = excluded.traffic_limit_bytes, last_notification_sent = NULL; """
|
||||
trial_traffic_val = self.settings.trial_traffic_limit_bytes
|
||||
params = (user_id, panel_user_uuid,
|
||||
panel_subscription_uuid_for_link,
|
||||
start_date.isoformat(), end_date.isoformat(),
|
||||
trial_traffic_val)
|
||||
|
||||
updated_panel_user =await self .panel_service .update_user_details_on_panel (panel_user_uuid ,panel_update_payload )
|
||||
if not updated_panel_user :logging .warning (f"Panel user details update FAILED for trial user {panel_user_uuid}.")
|
||||
else :
|
||||
logging .info (f"Panel user {panel_user_uuid} details updated for trial. Panel ExpireAt: {updated_panel_user.get('expireAt')}")
|
||||
if updated_panel_user .get ('subscriptionUrl'):panel_actual_subscription_url =updated_panel_user .get ('subscriptionUrl')
|
||||
if updated_panel_user .get ('shortUuid'):panel_short_uuid_for_link =updated_panel_user .get ('shortUuid')
|
||||
upsert_cursor = await db.execute(upsert_sql, params)
|
||||
trial_subscription_id = upsert_cursor.lastrowid
|
||||
if not trial_subscription_id or trial_subscription_id == 0:
|
||||
id_fetch_cursor = await db.execute(
|
||||
"SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",
|
||||
(panel_subscription_uuid_for_link, ))
|
||||
id_fetch_row = await id_fetch_cursor.fetchone()
|
||||
if id_fetch_cursor: await id_fetch_cursor.close()
|
||||
if id_fetch_row:
|
||||
trial_subscription_id = id_fetch_row['subscription_id']
|
||||
logging.info(
|
||||
f"Local trial subscription (ID: {trial_subscription_id}) for user {user_id} prepared. Ends: {end_date.isoformat()}."
|
||||
)
|
||||
|
||||
await db .commit ()
|
||||
return {"eligible":True ,"activated":True ,"end_date":end_date ,"days":self .settings .TRIAL_DURATION_DAYS ,"traffic_gb":self .settings .TRIAL_TRAFFIC_LIMIT_GB ,"panel_user_uuid":panel_user_uuid ,"panel_short_uuid":panel_short_uuid_for_link ,"subscription_url":panel_actual_subscription_url }
|
||||
except Exception as e :
|
||||
logging .error (f"Error activating trial for user {user_id}: {e}",exc_info =True )
|
||||
await db .rollback ()
|
||||
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"}
|
||||
panel_update_payload: Dict[str, Any] = {
|
||||
"uuid":
|
||||
panel_user_uuid,
|
||||
"expireAt":
|
||||
end_date.isoformat(timespec='milliseconds').replace(
|
||||
'+00:00', 'Z'),
|
||||
"status":
|
||||
"ACTIVE",
|
||||
"trafficLimitBytes":
|
||||
trial_traffic_val,
|
||||
"trafficLimitStrategy":
|
||||
self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
||||
}
|
||||
if specific_inbounds:
|
||||
panel_update_payload[
|
||||
"activeUserInbounds"] = specific_inbounds
|
||||
elif panel_user_interacted_now:
|
||||
panel_update_payload["activateAllInbounds"] = True
|
||||
|
||||
async def activate_subscription (
|
||||
self ,user_id :int ,months :int ,payment_amount :float ,
|
||||
payment_id_internal :int ,db_conn :aiosqlite .Connection ,
|
||||
promo_code_id :Optional [int ]=None
|
||||
)->Optional [Dict [str ,Any ]]:
|
||||
db =db_conn
|
||||
try :
|
||||
panel_user_uuid =await self .get_panel_user_uuid (user_id ,db_conn =db )
|
||||
panel_subscription_uuid_for_link =None ;panel_short_uuid_for_link =None ;panel_user_interacted_now =False
|
||||
panel_actual_subscription_url :Optional [str ]=None
|
||||
specific_inbounds_from_settings =self .settings .parsed_default_panel_user_inbound_uuids
|
||||
if not panel_user_uuid :
|
||||
panel_username_to_create =f"tg_{user_id}"
|
||||
creation_response =await self .panel_service .create_panel_user (username =panel_username_to_create ,telegram_id =user_id ,default_expire_days =self .settings .PANEL_USER_DEFAULT_EXPIRE_DAYS ,default_traffic_limit_bytes =self .settings .PANEL_USER_DEFAULT_TRAFFIC_BYTES ,default_traffic_limit_strategy =self .settings .PANEL_USER_DEFAULT_TRAFFIC_STRATEGY ,specific_inbound_uuids =specific_inbounds_from_settings ,activate_all_inbounds_default_flag =False if specific_inbounds_from_settings else True )
|
||||
panel_user_object_from_api =None
|
||||
if creation_response and not creation_response .get ("error"):panel_user_object_from_api =creation_response .get ("response")
|
||||
elif creation_response and creation_response .get ("errorCode")=="A019":
|
||||
existing_users_list =await self .panel_service .get_users_by_filter (username =panel_username_to_create )
|
||||
if existing_users_list and len (existing_users_list )==1 :panel_user_object_from_api =existing_users_list [0 ]
|
||||
if panel_user_object_from_api and panel_user_object_from_api .get ('uuid'):
|
||||
panel_user_uuid =panel_user_object_from_api ['uuid'];panel_subscription_uuid_for_link =panel_user_object_from_api .get ('subscriptionUuid');panel_short_uuid_for_link =panel_user_object_from_api .get ('shortUuid');panel_actual_subscription_url =panel_user_object_from_api .get ('subscriptionUrl')
|
||||
cursor_conflict =await db .execute ("SELECT user_id FROM users WHERE panel_user_uuid = ? AND user_id != ?",(panel_user_uuid ,user_id ));conflicting_tg_user =await cursor_conflict .fetchone ();await cursor_conflict .close ()
|
||||
if conflicting_tg_user :logging .error (f"CRITICAL CONFLICT: Panel UUID {panel_user_uuid} already linked to TG user {conflicting_tg_user['user_id']}.");return None
|
||||
await db .execute ("UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",(panel_user_uuid ,user_id ));panel_user_interacted_now =True
|
||||
if panel_user_object_from_api .get ('telegramId')!=user_id :await self .panel_service .update_user_details_on_panel (panel_user_uuid ,{"telegramId":user_id })
|
||||
else :logging .error (f"Failed to create/link panel user for TG_ID {user_id}. Resp: {creation_response if 'creation_response' in locals() else 'N/A'}");return None
|
||||
else :
|
||||
panel_user_data =await self .panel_service .get_user_by_uuid (panel_user_uuid )
|
||||
if panel_user_data :panel_subscription_uuid_for_link =panel_user_data .get ('subscriptionUuid');panel_short_uuid_for_link =panel_user_data .get ('shortUuid');panel_actual_subscription_url =panel_user_data .get ('subscriptionUrl')
|
||||
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link :panel_subscription_uuid_for_link =panel_short_uuid_for_link
|
||||
if not panel_subscription_uuid_for_link :logging .error (f"Critical: panel_subscription_uuid for link is None for panel_user_uuid {panel_user_uuid}.");return None
|
||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, panel_update_payload)
|
||||
if not updated_panel_user:
|
||||
logging.warning(
|
||||
f"Panel user details update FAILED for trial user {panel_user_uuid}."
|
||||
)
|
||||
else:
|
||||
logging.info(
|
||||
f"Panel user {panel_user_uuid} details updated for trial. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
|
||||
)
|
||||
if updated_panel_user.get('subscriptionUrl'):
|
||||
panel_actual_subscription_url = updated_panel_user.get(
|
||||
'subscriptionUrl')
|
||||
if updated_panel_user.get('shortUuid'):
|
||||
panel_short_uuid_for_link = updated_panel_user.get(
|
||||
'shortUuid')
|
||||
|
||||
cursor =await db .execute ("SELECT subscription_id, end_date FROM subscriptions WHERE panel_user_uuid = ? AND is_active = 1 ORDER BY end_date DESC LIMIT 1",(panel_user_uuid ,));current_sub_row =await cursor .fetchone ();await cursor .close ()
|
||||
current_sub_end_date_str =current_sub_row ['end_date']if current_sub_row else None ;start_date =datetime .now (timezone .utc )
|
||||
if current_sub_end_date_str :
|
||||
try :
|
||||
parsed_current_end_date =datetime .fromisoformat (current_sub_end_date_str .replace ("Z","+00:00"))
|
||||
if parsed_current_end_date .tzinfo is None :parsed_current_end_date =parsed_current_end_date .replace (tzinfo =timezone .utc )
|
||||
if parsed_current_end_date >start_date :start_date =parsed_current_end_date
|
||||
except ValueError :logging .warning (f"Bad current_sub_end_date string: {current_sub_end_date_str} for panel_user {panel_user_uuid}.")
|
||||
final_end_date =start_date +timedelta (days =months *30 )
|
||||
if promo_code_id :
|
||||
promo_cursor =await db .execute ("SELECT bonus_days FROM promo_codes WHERE promo_code_id = ?",(promo_code_id ,));promo_row =await promo_cursor .fetchone ();await promo_cursor .close ()
|
||||
if promo_row :final_end_date +=timedelta (days =promo_row ['bonus_days']);await db .execute ("INSERT OR IGNORE INTO promo_code_activations (promo_code_id, user_id, payment_id) VALUES (?, ?, ?)",(promo_code_id ,user_id ,payment_id_internal ));await db .execute ("UPDATE promo_codes SET current_activations = current_activations + 1 WHERE promo_code_id = ?",(promo_code_id ,))
|
||||
await db.commit()
|
||||
return {
|
||||
"eligible": True,
|
||||
"activated": True,
|
||||
"end_date": end_date,
|
||||
"days": self.settings.TRIAL_DURATION_DAYS,
|
||||
"traffic_gb": self.settings.TRIAL_TRAFFIC_LIMIT_GB,
|
||||
"panel_user_uuid": panel_user_uuid,
|
||||
"panel_short_uuid": panel_short_uuid_for_link,
|
||||
"subscription_url": panel_actual_subscription_url
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error activating trial for user {user_id}: {e}",
|
||||
exc_info=True)
|
||||
await db.rollback()
|
||||
return {
|
||||
"eligible": True,
|
||||
"activated": False,
|
||||
"message_key": "trial_activation_failed"
|
||||
}
|
||||
|
||||
await db .execute ("UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",(panel_user_uuid ,))
|
||||
upsert_sql =""" INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel) VALUES (?, ?, ?, ?, ?, ?, 1, 'ACTIVE') ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = excluded.duration_months, is_active = 1, status_from_panel = 'ACTIVE', last_notification_sent = NULL; """
|
||||
params =(user_id ,panel_user_uuid ,panel_subscription_uuid_for_link ,start_date .isoformat (),final_end_date .isoformat (),months )
|
||||
upsert_cursor =await db .execute (upsert_sql ,params );subscription_id_to_return =upsert_cursor .lastrowid
|
||||
if not subscription_id_to_return or subscription_id_to_return ==0 :
|
||||
id_cursor =await db .execute ("SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",(panel_subscription_uuid_for_link ,));id_row =await id_cursor .fetchone ();await id_cursor .close ()
|
||||
if id_row :subscription_id_to_return =id_row ['subscription_id']
|
||||
logging .info (f"Local subscription UPSERTED (ID: {subscription_id_to_return}) for user {user_id}. Ends: {final_end_date.isoformat()}.")
|
||||
|
||||
panel_update_payload :Dict [str ,Any ]={"uuid":panel_user_uuid ,"expireAt":final_end_date .isoformat (timespec ='milliseconds').replace ('+00:00','Z'),"status":"ACTIVE","trafficLimitBytes":self .settings .PANEL_USER_DEFAULT_TRAFFIC_BYTES ,"trafficLimitStrategy":self .settings .PANEL_USER_DEFAULT_TRAFFIC_STRATEGY }
|
||||
if specific_inbounds_from_settings :panel_update_payload ["activeUserInbounds"]=specific_inbounds_from_settings
|
||||
elif panel_user_interacted_now :panel_update_payload ["activateAllInbounds"]=True
|
||||
if "activateAllInbounds"in panel_update_payload and not specific_inbounds_from_settings and not panel_user_interacted_now :del panel_update_payload ["activateAllInbounds"]
|
||||
logging .info (f"Attempting to update panel user {panel_user_uuid} for paid sub: {panel_update_payload}")
|
||||
updated_panel_user =await self .panel_service .update_user_details_on_panel (panel_user_uuid ,panel_update_payload )
|
||||
if not updated_panel_user :logging .warning (f"Panel user details update FAILED for {panel_user_uuid}.")
|
||||
else :
|
||||
logging .info (f"Panel user {panel_user_uuid} details updated. Panel ExpireAt: {updated_panel_user.get('expireAt')}")
|
||||
if updated_panel_user .get ('subscriptionUrl'):panel_actual_subscription_url =updated_panel_user .get ('subscriptionUrl')
|
||||
if updated_panel_user .get ('shortUuid'):panel_short_uuid_for_link =updated_panel_user .get ('shortUuid')
|
||||
return {"subscription_id":subscription_id_to_return ,"end_date":final_end_date ,"is_active":True ,"panel_user_uuid":panel_user_uuid ,"panel_short_uuid":panel_short_uuid_for_link ,"subscription_url":panel_actual_subscription_url }
|
||||
except Exception as e :logging .error (f"Error in activate_subscription (paid) for user {user_id}: {e}",exc_info =True );return None
|
||||
|
||||
async def extend_active_subscription_days (self ,user_id :int ,bonus_days :int ,db_conn :aiosqlite .Connection ,reason :str ="bonus")->Optional [datetime ]:
|
||||
db =db_conn
|
||||
try :
|
||||
user_cursor =await db .execute ("SELECT panel_user_uuid FROM users WHERE user_id = ?",(user_id ,))
|
||||
user_panel_data =await user_cursor .fetchone ()
|
||||
if user_cursor :await user_cursor .close ()
|
||||
panel_user_uuid_for_update =user_panel_data ['panel_user_uuid']if user_panel_data and 'panel_user_uuid'in user_panel_data .keys ()and user_panel_data ['panel_user_uuid']else None
|
||||
|
||||
sql_select_active_sub ="SELECT subscription_id, end_date FROM subscriptions WHERE user_id = ? AND is_active = 1 "
|
||||
params_select_active_sub :tuple =(user_id ,)
|
||||
if panel_user_uuid_for_update :
|
||||
sql_select_active_sub +="AND panel_user_uuid = ? "
|
||||
params_select_active_sub +=(panel_user_uuid_for_update ,)
|
||||
else :
|
||||
logging .warning (f"Extending subscription for user {user_id} without panel_user_uuid. This might be ambiguous if user has multiple panel accounts linked to one TG ID (not typical).")
|
||||
sql_select_active_sub +="ORDER BY end_date DESC LIMIT 1"
|
||||
|
||||
cursor =await db .execute (sql_select_active_sub ,params_select_active_sub )
|
||||
active_sub_row =await cursor .fetchone ()
|
||||
if cursor :await cursor .close ()
|
||||
|
||||
if not active_sub_row or not active_sub_row ['end_date']:
|
||||
logging .info (f"No active subscription found for user {user_id} (panel UUID: {panel_user_uuid_for_update}) to extend with {reason} bonus.")
|
||||
async def activate_subscription(
|
||||
self,
|
||||
user_id: int,
|
||||
months: int,
|
||||
payment_amount: float,
|
||||
payment_id_internal: int,
|
||||
db_conn: aiosqlite.Connection,
|
||||
promo_code_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
|
||||
db = db_conn
|
||||
try:
|
||||
panel_user_uuid = await self.get_panel_user_uuid(user_id,
|
||||
db_conn=db)
|
||||
panel_subscription_uuid_for_link = None
|
||||
panel_short_uuid_for_link = None
|
||||
panel_user_interacted_now = False
|
||||
panel_actual_subscription_url: Optional[str] = None
|
||||
specific_inbounds_from_settings = self.settings.parsed_default_panel_user_inbound_uuids
|
||||
if not panel_user_uuid:
|
||||
panel_username_to_create = f"tg_{user_id}"
|
||||
creation_response = await self.panel_service.create_panel_user(
|
||||
username=panel_username_to_create,
|
||||
telegram_id=user_id,
|
||||
default_expire_days=self.settings.
|
||||
PANEL_USER_DEFAULT_EXPIRE_DAYS,
|
||||
default_traffic_limit_bytes=self.settings.
|
||||
PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
||||
default_traffic_limit_strategy=self.settings.
|
||||
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
||||
specific_inbound_uuids=specific_inbounds_from_settings,
|
||||
activate_all_inbounds_default_flag=False
|
||||
if specific_inbounds_from_settings else True)
|
||||
panel_user_object_from_api = None
|
||||
if creation_response and not creation_response.get("error"):
|
||||
panel_user_object_from_api = creation_response.get(
|
||||
"response")
|
||||
elif creation_response and creation_response.get(
|
||||
"errorCode") == "A019":
|
||||
existing_users_list = await self.panel_service.get_users_by_filter(
|
||||
username=panel_username_to_create)
|
||||
if existing_users_list and len(existing_users_list) == 1:
|
||||
panel_user_object_from_api = existing_users_list[0]
|
||||
if panel_user_object_from_api and panel_user_object_from_api.get(
|
||||
'uuid'):
|
||||
panel_user_uuid = panel_user_object_from_api['uuid']
|
||||
panel_subscription_uuid_for_link = panel_user_object_from_api.get(
|
||||
'subscriptionUuid')
|
||||
panel_short_uuid_for_link = panel_user_object_from_api.get(
|
||||
'shortUuid')
|
||||
panel_actual_subscription_url = panel_user_object_from_api.get(
|
||||
'subscriptionUrl')
|
||||
cursor_conflict = await db.execute(
|
||||
"SELECT user_id FROM users WHERE panel_user_uuid = ? AND user_id != ?",
|
||||
(panel_user_uuid, user_id))
|
||||
conflicting_tg_user = await cursor_conflict.fetchone()
|
||||
await cursor_conflict.close()
|
||||
if conflicting_tg_user:
|
||||
logging.error(
|
||||
f"CRITICAL CONFLICT: Panel UUID {panel_user_uuid} already linked to TG user {conflicting_tg_user['user_id']}."
|
||||
)
|
||||
return None
|
||||
await db.execute(
|
||||
"UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",
|
||||
(panel_user_uuid, user_id))
|
||||
panel_user_interacted_now = True
|
||||
if panel_user_object_from_api.get('telegramId') != user_id:
|
||||
await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, {"telegramId": user_id})
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to create/link panel user for TG_ID {user_id}. Resp: {creation_response if 'creation_response' in locals() else 'N/A'}"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
panel_user_data = await self.panel_service.get_user_by_uuid(
|
||||
panel_user_uuid)
|
||||
if panel_user_data:
|
||||
panel_subscription_uuid_for_link = panel_user_data.get(
|
||||
'subscriptionUuid')
|
||||
panel_short_uuid_for_link = panel_user_data.get(
|
||||
'shortUuid')
|
||||
panel_actual_subscription_url = panel_user_data.get(
|
||||
'subscriptionUrl')
|
||||
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link:
|
||||
panel_subscription_uuid_for_link = panel_short_uuid_for_link
|
||||
if not panel_subscription_uuid_for_link:
|
||||
logging.error(
|
||||
f"Critical: panel_subscription_uuid for link is None for panel_user_uuid {panel_user_uuid}."
|
||||
)
|
||||
return None
|
||||
|
||||
current_end_date_str =active_sub_row ['end_date']
|
||||
try :
|
||||
current_end_date =datetime .fromisoformat (current_end_date_str .replace ("Z","+00:00"))if isinstance (current_end_date_str ,str )else current_end_date_str
|
||||
if not isinstance (current_end_date ,datetime ):
|
||||
raise ValueError ("current_end_date is not a datetime object after parsing")
|
||||
except ValueError as ve :
|
||||
logging .error (f"Error parsing current_end_date '{current_end_date_str}' for user {user_id}: {ve}")
|
||||
return None
|
||||
cursor = await db.execute(
|
||||
"SELECT subscription_id, end_date FROM subscriptions WHERE panel_user_uuid = ? AND is_active = 1 ORDER BY end_date DESC LIMIT 1",
|
||||
(panel_user_uuid, ))
|
||||
current_sub_row = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
current_sub_end_date_str = current_sub_row[
|
||||
'end_date'] if current_sub_row else None
|
||||
start_date = datetime.now(timezone.utc)
|
||||
if current_sub_end_date_str:
|
||||
try:
|
||||
parsed_current_end_date = datetime.fromisoformat(
|
||||
current_sub_end_date_str.replace("Z", "+00:00"))
|
||||
if parsed_current_end_date.tzinfo is None:
|
||||
parsed_current_end_date = parsed_current_end_date.replace(
|
||||
tzinfo=timezone.utc)
|
||||
if parsed_current_end_date > start_date:
|
||||
start_date = parsed_current_end_date
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Bad current_sub_end_date string: {current_sub_end_date_str} for panel_user {panel_user_uuid}."
|
||||
)
|
||||
final_end_date = start_date + timedelta(days=months * 30)
|
||||
if promo_code_id:
|
||||
promo_cursor = await db.execute(
|
||||
"SELECT bonus_days FROM promo_codes WHERE promo_code_id = ?",
|
||||
(promo_code_id, ))
|
||||
promo_row = await promo_cursor.fetchone()
|
||||
await promo_cursor.close()
|
||||
if promo_row:
|
||||
final_end_date += timedelta(days=promo_row['bonus_days'])
|
||||
await db.execute(
|
||||
"INSERT OR IGNORE INTO promo_code_activations (promo_code_id, user_id, payment_id) VALUES (?, ?, ?)",
|
||||
(promo_code_id, user_id, payment_id_internal))
|
||||
await db.execute(
|
||||
"UPDATE promo_codes SET current_activations = current_activations + 1 WHERE promo_code_id = ?",
|
||||
(promo_code_id, ))
|
||||
|
||||
if current_end_date .tzinfo is None :
|
||||
current_end_date =current_end_date .replace (tzinfo =timezone .utc )
|
||||
await db.execute(
|
||||
"UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",
|
||||
(panel_user_uuid, ))
|
||||
upsert_sql = """ INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel) VALUES (?, ?, ?, ?, ?, ?, 1, 'ACTIVE') ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = excluded.duration_months, is_active = 1, status_from_panel = 'ACTIVE', last_notification_sent = NULL; """
|
||||
params = (user_id, panel_user_uuid,
|
||||
panel_subscription_uuid_for_link, start_date.isoformat(),
|
||||
final_end_date.isoformat(), months)
|
||||
upsert_cursor = await db.execute(upsert_sql, params)
|
||||
subscription_id_to_return = upsert_cursor.lastrowid
|
||||
if not subscription_id_to_return or subscription_id_to_return == 0:
|
||||
id_cursor = await db.execute(
|
||||
"SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",
|
||||
(panel_subscription_uuid_for_link, ))
|
||||
id_row = await id_cursor.fetchone()
|
||||
await id_cursor.close()
|
||||
if id_row:
|
||||
subscription_id_to_return = id_row['subscription_id']
|
||||
logging.info(
|
||||
f"Local subscription UPSERTED (ID: {subscription_id_to_return}) for user {user_id}. Ends: {final_end_date.isoformat()}."
|
||||
)
|
||||
|
||||
now_utc =datetime .now (timezone .utc )
|
||||
start_point_for_bonus =current_end_date if current_end_date >now_utc else now_utc
|
||||
new_end_date =start_point_for_bonus +timedelta (days =bonus_days )
|
||||
|
||||
await db .execute ("UPDATE subscriptions SET end_date = ?, last_notification_sent = NULL WHERE subscription_id = ?",(new_end_date .isoformat (),active_sub_row ['subscription_id']))
|
||||
logging .info (f"Subscription for user {user_id} extended by {bonus_days} days ({reason}). New end date: {new_end_date.isoformat()}")
|
||||
|
||||
if panel_user_uuid_for_update :
|
||||
panel_update_payload ={"uuid":panel_user_uuid_for_update ,"expireAt":new_end_date .isoformat (timespec ='milliseconds').replace ('+00:00','Z')}
|
||||
if not await self .panel_service .update_user_details_on_panel (panel_user_uuid_for_update ,panel_update_payload ):
|
||||
logging .warning (f"Failed to update panel expiry for {panel_user_uuid_for_update} after {reason} bonus.")
|
||||
return new_end_date
|
||||
except Exception as e :
|
||||
logging .error (f"Error extending subscription with {reason} bonus for user {user_id} (using provided db_conn): {e}",exc_info =True )
|
||||
panel_update_payload: Dict[str, Any] = {
|
||||
"uuid":
|
||||
panel_user_uuid,
|
||||
"expireAt":
|
||||
final_end_date.isoformat(timespec='milliseconds').replace(
|
||||
'+00:00', 'Z'),
|
||||
"status":
|
||||
"ACTIVE",
|
||||
"trafficLimitBytes":
|
||||
self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
||||
"trafficLimitStrategy":
|
||||
self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY
|
||||
}
|
||||
if specific_inbounds_from_settings:
|
||||
panel_update_payload[
|
||||
"activeUserInbounds"] = specific_inbounds_from_settings
|
||||
elif panel_user_interacted_now:
|
||||
panel_update_payload["activateAllInbounds"] = True
|
||||
if "activateAllInbounds" in panel_update_payload and not specific_inbounds_from_settings and not panel_user_interacted_now:
|
||||
del panel_update_payload["activateAllInbounds"]
|
||||
logging.info(
|
||||
f"Attempting to update panel user {panel_user_uuid} for paid sub: {panel_update_payload}"
|
||||
)
|
||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid, panel_update_payload)
|
||||
if not updated_panel_user:
|
||||
logging.warning(
|
||||
f"Panel user details update FAILED for {panel_user_uuid}.")
|
||||
else:
|
||||
logging.info(
|
||||
f"Panel user {panel_user_uuid} details updated. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
|
||||
)
|
||||
if updated_panel_user.get('subscriptionUrl'):
|
||||
panel_actual_subscription_url = updated_panel_user.get(
|
||||
'subscriptionUrl')
|
||||
if updated_panel_user.get('shortUuid'):
|
||||
panel_short_uuid_for_link = updated_panel_user.get(
|
||||
'shortUuid')
|
||||
return {
|
||||
"subscription_id": subscription_id_to_return,
|
||||
"end_date": final_end_date,
|
||||
"is_active": True,
|
||||
"panel_user_uuid": panel_user_uuid,
|
||||
"panel_short_uuid": panel_short_uuid_for_link,
|
||||
"subscription_url": panel_actual_subscription_url
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error in activate_subscription (paid) for user {user_id}: {e}",
|
||||
exc_info=True)
|
||||
return None
|
||||
|
||||
async def extend_subscription_for_referral (self ,user_id :int ,bonus_days :int ,db_conn :aiosqlite .Connection ,is_referee_bonus :bool =False )->Optional [datetime ]:
|
||||
reason ="referee bonus"if is_referee_bonus else "inviter referral bonus"
|
||||
return await self .extend_active_subscription_days (user_id ,bonus_days ,db_conn ,reason =reason )
|
||||
async def extend_active_subscription_days(
|
||||
self,
|
||||
user_id: int,
|
||||
bonus_days: int,
|
||||
db_conn: aiosqlite.Connection,
|
||||
reason: str = "bonus") -> Optional[datetime]:
|
||||
db = db_conn
|
||||
try:
|
||||
user_cursor = await db.execute(
|
||||
"SELECT panel_user_uuid FROM users WHERE user_id = ?",
|
||||
(user_id, ))
|
||||
user_panel_data = await user_cursor.fetchone()
|
||||
if user_cursor: await user_cursor.close()
|
||||
panel_user_uuid_for_update = user_panel_data[
|
||||
'panel_user_uuid'] if user_panel_data and 'panel_user_uuid' in user_panel_data.keys(
|
||||
) and user_panel_data['panel_user_uuid'] else None
|
||||
|
||||
async def get_active_subscription (self ,user_id :int )->Optional [Dict [str ,Any ]]:
|
||||
db ,should_manage =await self ._get_db ()
|
||||
try :
|
||||
now_iso_utc =datetime .now (timezone .utc ).isoformat ()
|
||||
cursor =await db .execute ("""SELECT s.subscription_id, s.panel_subscription_uuid, s.panel_user_uuid, s.start_date, s.end_date, s.duration_months, s.is_active, s.status_from_panel, s.traffic_limit_bytes, s.traffic_used_bytes, u.username as bot_username FROM subscriptions s LEFT JOIN users u ON s.user_id = u.user_id WHERE s.user_id = ? AND s.is_active = 1 AND s.end_date > ? ORDER BY s.end_date DESC LIMIT 1""",(user_id ,now_iso_utc ))
|
||||
sub_row =await cursor .fetchone ();await cursor .close ()
|
||||
if sub_row :
|
||||
sub_dict =dict (sub_row )
|
||||
for date_key in ['start_date','end_date']:
|
||||
if sub_dict .get (date_key )and isinstance (sub_dict [date_key ],str ):
|
||||
try :sub_dict [date_key ]=datetime .fromisoformat (sub_dict [date_key ].replace ("Z","+00:00"))
|
||||
except ValueError :logging .warning (f"Could not parse date string {sub_dict[date_key]} for key {date_key} in get_active_subscription")
|
||||
sql_select_active_sub = "SELECT subscription_id, end_date FROM subscriptions WHERE user_id = ? AND is_active = 1 "
|
||||
params_select_active_sub: tuple = (user_id, )
|
||||
if panel_user_uuid_for_update:
|
||||
sql_select_active_sub += "AND panel_user_uuid = ? "
|
||||
params_select_active_sub += (panel_user_uuid_for_update, )
|
||||
else:
|
||||
logging.warning(
|
||||
f"Extending subscription for user {user_id} without panel_user_uuid. This might be ambiguous if user has multiple panel accounts linked to one TG ID (not typical)."
|
||||
)
|
||||
sql_select_active_sub += "ORDER BY end_date DESC LIMIT 1"
|
||||
|
||||
cursor = await db.execute(sql_select_active_sub,
|
||||
params_select_active_sub)
|
||||
active_sub_row = await cursor.fetchone()
|
||||
if cursor: await cursor.close()
|
||||
|
||||
if not active_sub_row or not active_sub_row['end_date']:
|
||||
logging.info(
|
||||
f"No active subscription found for user {user_id} (panel UUID: {panel_user_uuid_for_update}) to extend with {reason} bonus."
|
||||
)
|
||||
return None
|
||||
|
||||
current_end_date_str = active_sub_row['end_date']
|
||||
try:
|
||||
current_end_date = datetime.fromisoformat(
|
||||
current_end_date_str.replace("Z", "+00:00")) if isinstance(
|
||||
current_end_date_str, str) else current_end_date_str
|
||||
if not isinstance(current_end_date, datetime):
|
||||
raise ValueError(
|
||||
"current_end_date is not a datetime object after parsing"
|
||||
)
|
||||
except ValueError as ve:
|
||||
logging.error(
|
||||
f"Error parsing current_end_date '{current_end_date_str}' for user {user_id}: {ve}"
|
||||
)
|
||||
return None
|
||||
|
||||
if current_end_date.tzinfo is None:
|
||||
current_end_date = current_end_date.replace(
|
||||
tzinfo=timezone.utc)
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
start_point_for_bonus = current_end_date if current_end_date > now_utc else now_utc
|
||||
new_end_date = start_point_for_bonus + timedelta(days=bonus_days)
|
||||
|
||||
await db.execute(
|
||||
"UPDATE subscriptions SET end_date = ?, last_notification_sent = NULL WHERE subscription_id = ?",
|
||||
(new_end_date.isoformat(), active_sub_row['subscription_id']))
|
||||
logging.info(
|
||||
f"Subscription for user {user_id} extended by {bonus_days} days ({reason}). New end date: {new_end_date.isoformat()}"
|
||||
)
|
||||
|
||||
if panel_user_uuid_for_update:
|
||||
panel_update_payload = {
|
||||
"uuid":
|
||||
panel_user_uuid_for_update,
|
||||
"expireAt":
|
||||
new_end_date.isoformat(timespec='milliseconds').replace(
|
||||
'+00:00', 'Z')
|
||||
}
|
||||
if not await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid_for_update, panel_update_payload):
|
||||
logging.warning(
|
||||
f"Failed to update panel expiry for {panel_user_uuid_for_update} after {reason} bonus."
|
||||
)
|
||||
return new_end_date
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error extending subscription with {reason} bonus for user {user_id} (using provided db_conn): {e}",
|
||||
exc_info=True)
|
||||
return None
|
||||
|
||||
async def extend_subscription_for_referral(
|
||||
self,
|
||||
user_id: int,
|
||||
bonus_days: int,
|
||||
db_conn: aiosqlite.Connection,
|
||||
is_referee_bonus: bool = False) -> Optional[datetime]:
|
||||
reason = "referee bonus" if is_referee_bonus else "inviter referral bonus"
|
||||
return await self.extend_active_subscription_days(user_id,
|
||||
bonus_days,
|
||||
db_conn,
|
||||
reason=reason)
|
||||
|
||||
async def get_active_subscription(
|
||||
self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
db, should_manage = await self._get_db()
|
||||
try:
|
||||
now_iso_utc = datetime.now(timezone.utc).isoformat()
|
||||
cursor = await db.execute(
|
||||
"""SELECT s.subscription_id, s.panel_subscription_uuid, s.panel_user_uuid, s.start_date, s.end_date, s.duration_months, s.is_active, s.status_from_panel, s.traffic_limit_bytes, s.traffic_used_bytes, u.username as bot_username FROM subscriptions s LEFT JOIN users u ON s.user_id = u.user_id WHERE s.user_id = ? AND s.is_active = 1 AND s.end_date > ? ORDER BY s.end_date DESC LIMIT 1""",
|
||||
(user_id, now_iso_utc))
|
||||
sub_row = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
if sub_row:
|
||||
sub_dict = dict(sub_row)
|
||||
for date_key in ['start_date', 'end_date']:
|
||||
if sub_dict.get(date_key) and isinstance(
|
||||
sub_dict[date_key], str):
|
||||
try:
|
||||
sub_dict[date_key] = datetime.fromisoformat(
|
||||
sub_dict[date_key].replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Could not parse date string {sub_dict[date_key]} for key {date_key} in get_active_subscription"
|
||||
)
|
||||
return sub_dict
|
||||
return None
|
||||
finally :await self ._release_db (db ,should_manage )
|
||||
finally:
|
||||
await self._release_db(db, should_manage)
|
||||
|
||||
async def get_subscriptions_ending_soon (self ,days_threshold :int )->List [Dict [str ,Any ]]:
|
||||
db ,should_manage =await self ._get_db ()
|
||||
try :
|
||||
now_utc =datetime .now (timezone .utc )
|
||||
threshold_date =now_utc +timedelta (days =days_threshold )
|
||||
today_date_str =now_utc .strftime ('%Y-%m-%d')
|
||||
query =""" SELECT s.user_id, u.first_name, u.language_code, s.end_date as end_date_raw, strftime('%Y-%m-%d', s.end_date) as end_date_str FROM subscriptions s JOIN users u ON s.user_id = u.user_id WHERE s.is_active = 1 AND s.end_date BETWEEN ? AND ? AND (s.last_notification_sent IS NULL OR s.last_notification_sent < ?) ORDER BY s.end_date ASC """
|
||||
cursor =await db .execute (query ,(now_utc .isoformat (),threshold_date .isoformat (),today_date_str ));rows =await cursor .fetchall ();await cursor .close ()
|
||||
processed_rows =[]
|
||||
for row_data in rows :
|
||||
row_dict =dict (row_data );end_date_obj_for_calc =None
|
||||
if isinstance (row_dict ['end_date_raw'],str ):
|
||||
try :
|
||||
end_date_obj_for_calc =datetime .fromisoformat (row_dict ['end_date_raw'].replace ("Z","+00:00"))
|
||||
if end_date_obj_for_calc .tzinfo is None :end_date_obj_for_calc =end_date_obj_for_calc .replace (tzinfo =timezone .utc )
|
||||
row_dict ['days_left']=(end_date_obj_for_calc -now_utc ).total_seconds ()/(24 *3600 )
|
||||
except ValueError :row_dict ['days_left']=None
|
||||
else :row_dict ['days_left']=None
|
||||
processed_rows .append (row_dict )
|
||||
async def get_subscriptions_ending_soon(
|
||||
self, days_threshold: int) -> List[Dict[str, Any]]:
|
||||
db, should_manage = await self._get_db()
|
||||
try:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
threshold_date = now_utc + timedelta(days=days_threshold)
|
||||
today_date_str = now_utc.strftime('%Y-%m-%d')
|
||||
query = """ SELECT s.user_id, u.first_name, u.language_code, s.end_date as end_date_raw, strftime('%Y-%m-%d', s.end_date) as end_date_str FROM subscriptions s JOIN users u ON s.user_id = u.user_id WHERE s.is_active = 1 AND s.end_date BETWEEN ? AND ? AND (s.last_notification_sent IS NULL OR s.last_notification_sent < ?) ORDER BY s.end_date ASC """
|
||||
cursor = await db.execute(
|
||||
query, (now_utc.isoformat(), threshold_date.isoformat(),
|
||||
today_date_str))
|
||||
rows = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
processed_rows = []
|
||||
for row_data in rows:
|
||||
row_dict = dict(row_data)
|
||||
end_date_obj_for_calc = None
|
||||
if isinstance(row_dict['end_date_raw'], str):
|
||||
try:
|
||||
end_date_obj_for_calc = datetime.fromisoformat(
|
||||
row_dict['end_date_raw'].replace("Z", "+00:00"))
|
||||
if end_date_obj_for_calc.tzinfo is None:
|
||||
end_date_obj_for_calc = end_date_obj_for_calc.replace(
|
||||
tzinfo=timezone.utc)
|
||||
row_dict['days_left'] = (end_date_obj_for_calc -
|
||||
now_utc).total_seconds() / (
|
||||
24 * 3600)
|
||||
except ValueError:
|
||||
row_dict['days_left'] = None
|
||||
else:
|
||||
row_dict['days_left'] = None
|
||||
processed_rows.append(row_dict)
|
||||
return processed_rows
|
||||
finally :await self ._release_db (db ,should_manage )
|
||||
finally:
|
||||
await self._release_db(db, should_manage)
|
||||
|
||||
async def update_last_notification_sent (self ,user_id :int ,subscription_end_date_iso :str ):
|
||||
db ,should_manage =await self ._get_db ()
|
||||
try :
|
||||
today_iso_date_str =datetime .now (timezone .utc ).strftime ('%Y-%m-%d')
|
||||
await db .execute ("UPDATE subscriptions SET last_notification_sent = ? WHERE user_id = ? AND is_active = 1 AND end_date = ?",(today_iso_date_str ,user_id ,subscription_end_date_iso ))
|
||||
await db .commit ()
|
||||
except Exception as e :logging .error (f"Error updating last_notification_sent for {user_id} and end_date {subscription_end_date_iso}: {e}");await db .rollback ()
|
||||
finally :await self ._release_db (db ,should_manage )
|
||||
async def update_last_notification_sent(self, user_id: int,
|
||||
subscription_end_date_iso: str):
|
||||
db, should_manage = await self._get_db()
|
||||
try:
|
||||
today_iso_date_str = datetime.now(
|
||||
timezone.utc).strftime('%Y-%m-%d')
|
||||
await db.execute(
|
||||
"UPDATE subscriptions SET last_notification_sent = ? WHERE user_id = ? AND is_active = 1 AND end_date = ?",
|
||||
(today_iso_date_str, user_id, subscription_end_date_iso))
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error updating last_notification_sent for {user_id} and end_date {subscription_end_date_iso}: {e}"
|
||||
)
|
||||
await db.rollback()
|
||||
finally:
|
||||
await self._release_db(db, should_manage)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from aiogram .fsm .state import State ,StatesGroup
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class AdminStates (StatesGroup ):
|
||||
|
||||
waiting_for_broadcast_message =State ()
|
||||
confirming_broadcast =State ()
|
||||
waiting_for_promo_details =State ()
|
||||
waiting_for_user_id_to_ban =State ()
|
||||
waiting_for_user_id_to_unban =State ()
|
||||
class AdminStates(StatesGroup):
|
||||
|
||||
waiting_for_user_id_for_logs =State ()
|
||||
waiting_for_broadcast_message = State()
|
||||
confirming_broadcast = State()
|
||||
waiting_for_promo_details = State()
|
||||
waiting_for_user_id_to_ban = State()
|
||||
waiting_for_user_id_to_unban = State()
|
||||
|
||||
waiting_for_user_id_for_logs = State()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from aiogram .fsm .state import State ,StatesGroup
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class UserPromoStates (StatesGroup ):
|
||||
waiting_for_promo_code =State ()
|
||||
|
||||
class UserPromoStates(StatesGroup):
|
||||
waiting_for_promo_code = State()
|
||||
|
||||
Reference in New Issue
Block a user