Code Refactoring

This commit is contained in:
machka
2025-05-14 08:43:23 +00:00
parent 7f94d794d6
commit d406baf0c1
33 changed files with 6015 additions and 3571 deletions
+4 -1
View File
@@ -2,11 +2,14 @@ from typing import List ,Union
from aiogram.filters import Filter from aiogram.filters import Filter
from aiogram.types import Message, CallbackQuery, User from aiogram.types import Message, CallbackQuery, User
class AdminFilter(Filter): class AdminFilter(Filter):
def __init__(self, admin_ids: List[int]): def __init__(self, admin_ids: List[int]):
self.admin_ids = admin_ids self.admin_ids = admin_ids
async def __call__ (self ,event :Union [Message ,CallbackQuery ],event_from_user :User )->bool : async def __call__(self, event: Union[Message, CallbackQuery],
event_from_user: User) -> bool:
if not event_from_user: if not event_from_user:
return False return False
if not self.admin_ids: if not self.admin_ids:
-4
View File
@@ -1,6 +1,5 @@
from aiogram import Router from aiogram import Router
from .common import router as common_admin_router from .common import router as common_admin_router
from .promo_codes import router as promo_codes_admin_router from .promo_codes import router as promo_codes_admin_router
from .user_management import router as user_management_admin_router from .user_management import router as user_management_admin_router
@@ -9,10 +8,8 @@ from .statistics import router as statistics_admin_router
from .sync_admin import router as sync_admin_router from .sync_admin import router as sync_admin_router
from .logs_admin import router as logs_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(common_admin_router)
admin_router_aggregate.include_router(promo_codes_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(user_management_admin_router)
@@ -20,4 +17,3 @@ admin_router_aggregate .include_router (broadcast_admin_router )
admin_router_aggregate.include_router(statistics_admin_router) admin_router_aggregate.include_router(statistics_admin_router)
admin_router_aggregate.include_router(sync_admin_router) admin_router_aggregate.include_router(sync_admin_router)
admin_router_aggregate.include_router(logs_admin_router) admin_router_aggregate.include_router(logs_admin_router)
+93 -61
View File
@@ -14,13 +14,12 @@ 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 , async def broadcast_message_prompt_handler(callback: types.CallbackQuery,
state :FSMContext , state: FSMContext, i18n_data: dict,
i18n_data :dict , settings: Settings):
settings :Settings current_lang = i18n_data.get("current_language",
): getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in broadcast_message_prompt_handler") logging.error("i18n missing in broadcast_message_prompt_handler")
@@ -32,22 +31,27 @@ settings :Settings
if callback.message: if callback.message:
try: try:
await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) await callback.message.edit_text(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
except Exception as e: except Exception as e:
logging .warning (f"Could not edit message for broadcast prompt: {e}") logging.warning(
await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) 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 callback.answer()
await state.set_state(AdminStates.waiting_for_broadcast_message) await state.set_state(AdminStates.waiting_for_broadcast_message)
@router.message(AdminStates.waiting_for_broadcast_message, F.text) @router.message(AdminStates.waiting_for_broadcast_message, F.text)
async def process_broadcast_message_handler ( async def process_broadcast_message_handler(message: types.Message,
message :types .Message , state: FSMContext, i18n_data: dict,
state :FSMContext , settings: Settings):
i18n_data :dict , current_lang = i18n_data.get("current_language",
settings :Settings getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
):
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in process_broadcast_message_handler") logging.error("i18n missing in process_broadcast_message_handler")
@@ -59,54 +63,65 @@ settings :Settings
broadcast_message_text = message.html_text broadcast_message_text = message.html_text
await state.update_data(broadcast_message=broadcast_message_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 preview_snippet = broadcast_message_text[:200] + "..." if len(
confirmation_prompt =_ ("admin_broadcast_confirm_prompt",message_preview =preview_snippet ) broadcast_message_text) > 200 else broadcast_message_text
confirmation_prompt = _("admin_broadcast_confirm_prompt",
message_preview=preview_snippet)
await message .answer ( await message.answer(confirmation_prompt,
confirmation_prompt , reply_markup=get_broadcast_confirmation_keyboard(
reply_markup =get_broadcast_confirmation_keyboard (current_lang ,i18n ), current_lang, i18n),
parse_mode ="HTML" parse_mode="HTML")
)
await state.set_state(AdminStates.confirming_broadcast) await state.set_state(AdminStates.confirming_broadcast)
@router .callback_query (F .data =="admin_action:main",AdminStates .waiting_for_broadcast_message ) @router.callback_query(F.data == "admin_action:main",
async def cancel_broadcast_at_prompt_stage (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ): 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 from .common import admin_panel_actions_callback_handler
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) current_lang = i18n_data.get("current_language",
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return 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) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
if callback.message: if callback.message:
try :await callback .message .edit_text (_ ("admin_broadcast_cancelled"),reply_markup =None ) try:
except Exception :await callback .message .answer (_ ("admin_broadcast_cancelled")) 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 callback.answer(_("admin_broadcast_cancelled"))
await state.clear() await state.clear()
callback.data = "admin_action:main" callback.data = "admin_action:main"
from bot.keyboards.inline.admin_keyboards import get_admin_panel_keyboard from bot.keyboards.inline.admin_keyboards import get_admin_panel_keyboard
if callback.message: if callback.message:
await callback .message .answer (_ ("admin_panel_title"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang )) await callback.message.answer(_("admin_panel_title"),
reply_markup=get_admin_panel_keyboard(
i18n, current_lang))
@router .callback_query (F .data .startswith ("broadcast_final_action:"),AdminStates .confirming_broadcast ) @router.callback_query(F.data.startswith("broadcast_final_action:"),
async def confirm_broadcast_callback_handler ( AdminStates.confirming_broadcast)
callback :types .CallbackQuery , async def confirm_broadcast_callback_handler(callback: types.CallbackQuery,
state: FSMContext, state: FSMContext,
i18n_data :dict , i18n_data: dict, bot: Bot,
bot :Bot , settings: Settings):
settings :Settings current_lang = i18n_data.get("current_language",
): getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return 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) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
@@ -116,26 +131,36 @@ settings :Settings
if not callback.message: if not callback.message:
await callback.answer("Error: message context lost.", show_alert=True) await callback.answer("Error: message context lost.", show_alert=True)
await state .clear ();return await state.clear()
return
if action == "send": if action == "send":
if not broadcast_message: if not broadcast_message:
await callback .message .edit_text (_ ("admin_broadcast_error_no_message")) await callback.message.edit_text(
await state .clear ();await callback .answer (show_alert =True );return _("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.message.edit_text(_("admin_broadcast_sending_started"),
reply_markup=None)
await callback.answer() await callback.answer()
users_to_broadcast = await get_all_users_for_broadcast() users_to_broadcast = await get_all_users_for_broadcast()
sent_count =0 ;failed_count =0 sent_count = 0
logging .info (f"Starting broadcast: '{broadcast_message[:50]}...' to {len(users_to_broadcast)} users.") failed_count = 0
logging.info(
f"Starting broadcast: '{broadcast_message[:50]}...' to {len(users_to_broadcast)} users."
)
admin_user = callback.from_user admin_user = callback.from_user
for user_row in users_to_broadcast: for user_row in users_to_broadcast:
user_id = user_row['user_id'] user_id = user_row['user_id']
try: try:
await bot .send_message (user_id ,broadcast_message ,parse_mode ="HTML") await bot.send_message(user_id,
broadcast_message,
parse_mode="HTML")
sent_count += 1 sent_count += 1
await log_user_action( await log_user_action(
@@ -145,28 +170,35 @@ settings :Settings
event_type="admin_broadcast_sent", event_type="admin_broadcast_sent",
content=f"To user {user_id}: {broadcast_message[:70]}...", content=f"To user {user_id}: {broadcast_message[:70]}...",
is_admin_event=True, is_admin_event=True,
target_user_id =user_id target_user_id=user_id)
)
except Exception as e: except Exception as e:
failed_count += 1 failed_count += 1
logging .warning (f"Failed to send broadcast to user {user_id}: {type(e).__name__} - {e}") logging.warning(
f"Failed to send broadcast to user {user_id}: {type(e).__name__} - {e}"
)
await log_user_action( await log_user_action(
user_id=admin_user.id, user_id=admin_user.id,
telegram_username=admin_user.username, telegram_username=admin_user.username,
telegram_first_name=admin_user.first_name, telegram_first_name=admin_user.first_name,
event_type="admin_broadcast_failed", event_type="admin_broadcast_failed",
content =f"For user {user_id}: {type(e).__name__} - {str(e)[:70]}...", content=
f"For user {user_id}: {type(e).__name__} - {str(e)[:70]}...",
is_admin_event=True, is_admin_event=True,
target_user_id =user_id target_user_id=user_id)
)
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
result_message =_ ("admin_broadcast_finished_stats",sent_count =sent_count ,failed_count =failed_count ) result_message = _("admin_broadcast_finished_stats",
await callback .message .answer (result_message ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) 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": elif action == "cancel":
await callback .message .edit_text (_ ("admin_broadcast_cancelled"),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) await callback.message.edit_text(
_("admin_broadcast_cancelled"),
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
await callback.answer() await callback.answer()
await state.clear() await state.clear()
+48 -30
View File
@@ -9,7 +9,6 @@ from bot .keyboards .inline .admin_keyboards import get_admin_panel_keyboard
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from . import broadcast as admin_broadcast_handlers from . import broadcast as admin_broadcast_handlers
from . import promo_codes as admin_promo_handlers from . import promo_codes as admin_promo_handlers
from . import user_management as admin_user_mgmnt_handlers from . import user_management as admin_user_mgmnt_handlers
@@ -19,9 +18,12 @@ from .import logs_admin as admin_logs_handlers
router = Router(name="admin_common_router") router = Router(name="admin_common_router")
@router.message(Command("admin")) @router.message(Command("admin"))
async def admin_panel_command_handler (message :types .Message ,settings :Settings ,i18n_data :dict ): async def admin_panel_command_handler(message: types.Message,
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in admin_panel_command_handler") logging.error("i18n missing in admin_panel_command_handler")
@@ -29,24 +31,22 @@ async def admin_panel_command_handler (message :types .Message ,settings :Settin
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
await message .answer ( await message.answer(_(key="admin_panel_title"),
_ (key ="admin_panel_title"), reply_markup=get_admin_panel_keyboard(
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings ) i18n, current_lang, settings))
)
@router.callback_query(F.data.startswith("admin_action:")) @router.callback_query(F.data.startswith("admin_action:"))
async def admin_panel_actions_callback_handler ( async def admin_panel_actions_callback_handler(callback: types.CallbackQuery,
callback :types .CallbackQuery ,
state: FSMContext, state: FSMContext,
settings: Settings, settings: Settings,
i18n_data :dict , i18n_data: dict, bot: Bot,
bot :Bot , panel_service: PanelApiService):
panel_service :PanelApiService
):
action_parts = callback.data.split(":") action_parts = callback.data.split(":")
action = action_parts[1] action = action_parts[1]
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) current_lang = i18n_data.get("current_language",
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in admin_panel_actions_callback_handler") logging.error("i18n missing in admin_panel_actions_callback_handler")
@@ -55,42 +55,60 @@ panel_service :PanelApiService
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
if not callback.message: if not callback.message:
logging .error (f"CallbackQuery {callback.id} from {callback.from_user.id} has no message for action {action}") logging.error(
await callback .answer ("Error processing action: message context lost.",show_alert =True ) 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 return
if action == "stats": if action == "stats":
await admin_stats_handlers .show_statistics_handler (callback ,i18n_data ,settings ) await admin_stats_handlers.show_statistics_handler(
callback, i18n_data, settings)
elif action == "broadcast": elif action == "broadcast":
await admin_broadcast_handlers .broadcast_message_prompt_handler (callback ,state ,i18n_data ,settings ) await admin_broadcast_handlers.broadcast_message_prompt_handler(
callback, state, i18n_data, settings)
elif action == "create_promo": elif action == "create_promo":
await admin_promo_handlers .create_promo_prompt_handler (callback ,state ,i18n_data ,settings ) await admin_promo_handlers.create_promo_prompt_handler(
callback, state, i18n_data, settings)
elif action == "view_promos": elif action == "view_promos":
await admin_promo_handlers .view_promo_codes_handler (callback ,i18n_data ,settings ) await admin_promo_handlers.view_promo_codes_handler(
callback, i18n_data, settings)
elif action == "ban_user_prompt": elif action == "ban_user_prompt":
await admin_user_mgmnt_handlers .ban_user_prompt_handler (callback ,state ,i18n_data ,settings ) await admin_user_mgmnt_handlers.ban_user_prompt_handler(
callback, state, i18n_data, settings)
elif action == "unban_user_prompt": elif action == "unban_user_prompt":
await admin_user_mgmnt_handlers .unban_user_prompt_handler (callback ,state ,i18n_data ,settings ) await admin_user_mgmnt_handlers.unban_user_prompt_handler(
callback, state, i18n_data, settings)
elif action == "view_banned": elif action == "view_banned":
await admin_user_mgmnt_handlers .view_banned_users_handler (callback ,i18n_data ,settings ,state ) await admin_user_mgmnt_handlers.view_banned_users_handler(
callback, i18n_data, settings, state)
elif action == "view_logs_menu": elif action == "view_logs_menu":
await admin_logs_handlers .display_logs_menu (callback ,i18n_data ,settings ) await admin_logs_handlers.display_logs_menu(callback, i18n_data,
settings)
elif action == "sync_panel": 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 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")) await callback.answer(_("admin_sync_initiated_from_panel"))
elif action == "main": elif action == "main":
try: try:
await callback.message.edit_text( await callback.message.edit_text(
_(key="admin_panel_title"), _(key="admin_panel_title"),
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings ) reply_markup=get_admin_panel_keyboard(i18n, current_lang,
) settings))
except Exception: except Exception:
await callback.message.answer( await callback.message.answer(
_(key="admin_panel_title"), _(key="admin_panel_title"),
reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings ) reply_markup=get_admin_panel_keyboard(i18n, current_lang,
) settings))
await callback.answer() await callback.answer()
else: else:
logging .warning (f"Unknown admin_action received: {action} from callback {callback.data}") logging.warning(
f"Unknown admin_action received: {action} from callback {callback.data}"
)
await callback.answer(_("admin_unknown_action"), show_alert=True) await callback.answer(_("admin_unknown_action"), show_alert=True)
+170 -82
View File
@@ -7,24 +7,23 @@ from typing import Optional ,List ,Dict ,Any
import aiosqlite import aiosqlite
from config.settings import Settings from config.settings import Settings
from db .database import ( from db.database import (get_all_message_logs_paginated,
get_all_message_logs_paginated ,count_all_message_logs , count_all_message_logs,
get_user_message_logs_paginated ,count_user_message_logs , get_user_message_logs_paginated,
get_user ,get_user_by_telegram_username count_user_message_logs, get_user,
) get_user_by_telegram_username)
from bot.states.admin_states import AdminStates from bot.states.admin_states import AdminStates
from bot.keyboards.inline.admin_keyboards import ( from bot.keyboards.inline.admin_keyboards import (
get_logs_menu_keyboard , get_logs_menu_keyboard, get_logs_pagination_keyboard,
get_logs_pagination_keyboard , get_back_to_admin_panel_keyboard, get_admin_panel_keyboard)
get_back_to_admin_panel_keyboard ,
get_admin_panel_keyboard
)
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
router = Router(name="admin_logs_router") router = Router(name="admin_logs_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 display_logs_menu (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ):
async def display_logs_menu(callback: types.CallbackQuery, i18n_data: dict,
settings: Settings):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
@@ -40,20 +39,18 @@ async def display_logs_menu (callback :types .CallbackQuery ,i18n_data :dict ,se
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: try:
await callback .message .edit_text ( await callback.message.edit_text(text=_(key="admin_logs_menu_title"),
text =_ (key ="admin_logs_menu_title"), reply_markup=get_logs_menu_keyboard(
reply_markup =get_logs_menu_keyboard (i18n ,current_lang ) i18n, current_lang))
)
except Exception as e: except Exception as e:
logging.warning(f"Failed to edit message for logs menu: {e}") logging.warning(f"Failed to edit message for logs menu: {e}")
await callback .message .answer ( await callback.message.answer(text=_(key="admin_logs_menu_title"),
text =_ (key ="admin_logs_menu_title"), reply_markup=get_logs_menu_keyboard(
reply_markup =get_logs_menu_keyboard (i18n ,current_lang ) i18n, current_lang))
)
await callback.answer() await callback.answer()
async def _display_formatted_logs (
target_message :types .Message , async def _display_formatted_logs(target_message: types.Message,
logs: List[aiosqlite.Row], logs: List[aiosqlite.Row],
total_logs: int, total_logs: int,
current_page: int, current_page: int,
@@ -62,62 +59,105 @@ title_key :str ,
base_pagination_callback_data: str, base_pagination_callback_data: str,
i18n: JsonI18n, i18n: JsonI18n,
current_lang: str, current_lang: str,
title_kwargs :Optional [Dict [str ,Any ]]=None title_kwargs: Optional[Dict[str,
): Any]] = None):
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
page_size = settings.LOGS_PAGE_SIZE page_size = settings.LOGS_PAGE_SIZE
actual_title_kwargs = title_kwargs or {} actual_title_kwargs = title_kwargs or {}
if not logs and total_logs == 0: 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") text = _(
reply_markup =get_logs_pagination_keyboard (current_page ,1 ,base_pagination_callback_data ,i18n ,current_lang ,back_to_logs_menu =True ) 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: else:
total_pages = math.ceil(total_logs / page_size) if page_size > 0 else 1 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" text = _(title_key,
current_page=current_page + 1,
total_pages=max(1, total_pages),
**actual_title_kwargs) + "\n"
log_entries_text = [] log_entries_text = []
for log_entry in logs: for log_entry in logs:
user_display_parts = [] 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_first_name = log_entry[
telegram_username =log_entry ['telegram_username']if 'telegram_username'in log_entry .keys ()and log_entry ['telegram_username']else None 'telegram_first_name'] if 'telegram_first_name' in log_entry.keys(
user_id_from_log =log_entry ['user_id']if 'user_id'in log_entry .keys ()and log_entry ['user_id']else None ) 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_first_name:
if telegram_username :user_display_parts .append (f"(@{telegram_username})") user_display_parts.append(telegram_first_name)
if telegram_username:
user_display_parts.append(f"(@{telegram_username})")
user_display = " ".join(user_display_parts).strip() user_display = " ".join(user_display_parts).strip()
if not user_display: if not user_display:
user_display =_ ("system_or_unknown_user")if not user_id_from_log else f"ID: {user_id_from_log}" 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" user_id_display = str(
content_raw =log_entry ['content']if 'content'in log_entry .keys ()and log_entry ['content']else "" user_id_from_log) if user_id_from_log is not None else "N/A"
content_preview =(content_raw [:100 ]+"...")if len (content_raw )>100 else (content_raw or "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( log_entries_text.append(
_("admin_log_entry_format", _("admin_log_entry_format",
timestamp_str =log_entry ['timestamp_str']if 'timestamp_str'in log_entry .keys ()else 'N/A', timestamp_str=log_entry['timestamp_str']
if 'timestamp_str' in log_entry.keys() else 'N/A',
user_display=user_display, user_display=user_display,
user_id=user_id_display, user_id=user_id_display,
event_type =log_entry ['event_type']if 'event_type'in log_entry .keys ()else 'N/A', event_type=log_entry['event_type']
content_preview =content_preview if 'event_type' in log_entry.keys() else 'N/A',
).replace ("\n","\n ") content_preview=content_preview).replace("\n", "\n "))
)
text += "\n\n".join(log_entries_text) 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 ) reply_markup = get_logs_pagination_keyboard(
current_page,
total_pages,
base_pagination_callback_data,
i18n,
current_lang,
back_to_logs_menu=True)
try: try:
await target_message .edit_text (text ,reply_markup =reply_markup ,parse_mode ="HTML",disable_web_page_preview =True ) await target_message.edit_text(text,
reply_markup=reply_markup,
parse_mode="HTML",
disable_web_page_preview=True)
except Exception as e: except Exception as e:
logging .warning (f"Failed to edit message for logs display: {e}. Content length: {len(text)}") logging.warning(
f"Failed to edit message for logs display: {e}. Content length: {len(text)}"
)
chunk_size = 4000 chunk_size = 4000
for i in range(0, len(text), chunk_size): for i in range(0, len(text), chunk_size):
chunk =text [i :i +chunk_size ];is_last_chunk =(i +chunk_size )>=len (text ) chunk = text[i:i + chunk_size]
await target_message .answer (chunk ,reply_markup =reply_markup if is_last_chunk else None ,parse_mode ="HTML",disable_web_page_preview =True ) 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")) @router.callback_query(F.data.startswith("admin_logs:view_all"))
async def view_all_logs_handler (callback :types .CallbackQuery ,settings :Settings ,i18n_data :dict ): async def view_all_logs_handler(callback: types.CallbackQuery,
settings: Settings, i18n_data: dict):
page = 0 page = 0
parts = callback.data.split(":") parts = callback.data.split(":")
@@ -133,49 +173,70 @@ async def view_all_logs_handler (callback :types .CallbackQuery ,settings :Setti
await callback.answer("Error processing request.", show_alert=True) await callback.answer("Error processing request.", show_alert=True)
return 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( await _display_formatted_logs(
target_message =callback .message ,logs =logs ,total_logs =total_logs , target_message=callback.message,
current_page =page ,settings =settings , logs=logs,
title_key ="admin_all_logs_title",base_pagination_callback_data ="admin_logs:view_all", total_logs=total_logs,
i18n =i18n ,current_lang =current_lang 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() await callback.answer()
@router.callback_query(F.data == "admin_logs:prompt_user") @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 ): 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
if not i18n or not callback .message :await callback .answer ("Error");return if not i18n or not callback.message:
await callback.answer("Error")
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
await callback.message.edit_text( await callback.message.edit_text(
text=_("admin_prompt_for_user_id_or_username_logs"), text=_("admin_prompt_for_user_id_or_username_logs"),
reply_markup =get_logs_menu_keyboard (i18n ,current_lang ) reply_markup=get_logs_menu_keyboard(i18n, current_lang))
)
await state.set_state(AdminStates.waiting_for_user_id_for_logs) await state.set_state(AdminStates.waiting_for_user_id_for_logs)
await callback.answer() await callback.answer()
@router.message(AdminStates.waiting_for_user_id_for_logs, F.text) @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 ): 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() current_state_fsm = await state.get_state()
logging .info (f"Processing user input for logs in state {current_state_fsm}: '{message.text}'") logging.info(
f"Processing user input for logs in state {current_state_fsm}: '{message.text}'"
)
await state.clear() await state.clear()
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
if not i18n :await message .reply ("Language error.");return if not i18n:
await message.reply("Language error.")
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
input_text = message.text.strip() input_text = message.text.strip()
user_data_for_logs: Optional[aiosqlite.Row] = None user_data_for_logs: Optional[aiosqlite.Row] = None
if input_text.isdigit(): if input_text.isdigit():
try :user_data_for_logs =await get_user (int (input_text )) try:
except ValueError :pass user_data_for_logs = await get_user(int(input_text))
except ValueError:
pass
elif input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]): elif input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]):
user_data_for_logs =await get_user_by_telegram_username (input_text [1 :]) user_data_for_logs = await get_user_by_telegram_username(input_text[1:]
)
elif USERNAME_REGEX.match(input_text): elif USERNAME_REGEX.match(input_text):
user_data_for_logs = await get_user_by_telegram_username(input_text) user_data_for_logs = await get_user_by_telegram_username(input_text)
@@ -184,50 +245,77 @@ async def process_user_id_for_logs_handler (message :types .Message ,state :FSMC
return return
target_user_id = user_data_for_logs['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}") 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( await _display_formatted_logs(
target_message =message ,logs =logs ,total_logs =total_logs , target_message=message,
current_page =0 ,settings =settings , logs=logs,
total_logs=total_logs,
current_page=0,
settings=settings,
title_key="admin_user_logs_title", title_key="admin_user_logs_title",
base_pagination_callback_data=f"admin_logs:view_user:{target_user_id}", base_pagination_callback_data=f"admin_logs:view_user:{target_user_id}",
i18n =i18n ,current_lang =current_lang , i18n=i18n,
title_kwargs ={"user_display":user_display } current_lang=current_lang,
) title_kwargs={"user_display": user_display})
@router.callback_query(F.data.startswith("admin_logs:view_user:")) @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 ): async def view_user_logs_paginated_handler(callback: types.CallbackQuery,
settings: Settings,
i18n_data: dict):
try: try:
parts = callback.data.split(":") parts = callback.data.split(":")
target_user_id = int(parts[2]) target_user_id = int(parts[2])
page = int(parts[3]) page = int(parts[3])
except (IndexError, ValueError): except (IndexError, ValueError):
await callback .answer ("Invalid log request.",show_alert =True );return await callback.answer("Invalid log request.", show_alert=True)
return
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
if not i18n or not callback .message :await callback .answer ("Error");return if not i18n or not callback.message:
await callback.answer("Error")
return
user_data_for_logs = await get_user(target_user_id) user_data_for_logs = await get_user(target_user_id)
if not user_data_for_logs: if not user_data_for_logs:
await callback .message .edit_text ("User not found for logs.");await callback .answer ();return await callback.message.edit_text("User not found for logs.")
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 callback.answer()
return
user_display = user_data_for_logs['first_name'] or (
f"@{user_data_for_logs['username']}"
if user_data_for_logs.get('username') else f"ID {target_user_id}")
logs ,total_logs =await get_user_message_logs_paginated (target_user_id ,settings .LOGS_PAGE_SIZE ,page *settings .LOGS_PAGE_SIZE ),await count_user_message_logs (target_user_id ) logs, total_logs = await get_user_message_logs_paginated(
target_user_id, settings.LOGS_PAGE_SIZE, page *
settings.LOGS_PAGE_SIZE), await count_user_message_logs(target_user_id)
await _display_formatted_logs( await _display_formatted_logs(
target_message =callback .message ,logs =logs ,total_logs =total_logs , target_message=callback.message,
current_page =page ,settings =settings , logs=logs,
total_logs=total_logs,
current_page=page,
settings=settings,
title_key="admin_user_logs_title", title_key="admin_user_logs_title",
base_pagination_callback_data=f"admin_logs:view_user:{target_user_id}", base_pagination_callback_data=f"admin_logs:view_user:{target_user_id}",
i18n =i18n ,current_lang =current_lang , i18n=i18n,
title_kwargs ={"user_display":user_display } current_lang=current_lang,
) title_kwargs={"user_display": user_display})
await callback.answer() 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 ): @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 state.clear()
await display_logs_menu(callback, i18n_data, settings) await display_logs_menu(callback, i18n_data, settings)
+82 -52
View File
@@ -13,13 +13,12 @@ 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 , async def create_promo_prompt_handler(callback: types.CallbackQuery,
state :FSMContext , state: FSMContext, i18n_data: dict,
i18n_data :dict , settings: Settings):
settings :Settings current_lang = i18n_data.get("current_language",
): getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in create_promo_prompt_handler") logging.error("i18n missing in create_promo_prompt_handler")
@@ -28,25 +27,32 @@ settings :Settings
_ = 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: if callback.message:
try: try:
await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) await callback.message.edit_text(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
except Exception as e: except Exception as e:
logging.warning(f"Could not edit message for promo prompt: {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.message.answer(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
await callback.answer() await callback.answer()
await state.set_state(AdminStates.waiting_for_promo_details) await state.set_state(AdminStates.waiting_for_promo_details)
@router.message(AdminStates.waiting_for_promo_details, F.text) @router.message(AdminStates.waiting_for_promo_details, F.text)
async def process_promo_code_details_handler ( async def process_promo_code_details_handler(message: types.Message,
message :types .Message ,
state: FSMContext, state: FSMContext,
i18n_data: dict, i18n_data: dict,
settings :Settings settings: Settings):
): current_lang = i18n_data.get("current_language",
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in process_promo_code_details_handler") logging.error("i18n missing in process_promo_code_details_handler")
@@ -63,39 +69,54 @@ settings :Settings
try: try:
code = parts[0].upper() code = parts[0].upper()
if not (3 <= len(code) <= 30 and code.isalnum()): if not (3 <= len(code) <= 30 and code.isalnum()):
raise ValueError ("Promo code must be 3-30 alphanumeric characters.") raise ValueError(
bonus_days =int (parts [1 ]);max_activations =int (parts [2 ]) "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_date: Optional[datetime] = None
valid_until_str_display = _("admin_promo_valid_indefinitely") valid_until_str_display = _("admin_promo_valid_indefinitely")
if len(parts) == 4: if len(parts) == 4:
valid_days_from_now = int(parts[3]) valid_days_from_now = int(parts[3])
if valid_days_from_now <=0 :raise ValueError ("Validity days (if provided) must be positive.") 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_date = datetime.now(
valid_until_str_display =_ ("admin_promo_valid_until_display",date =valid_until_date .strftime ('%Y-%m-%d')) timezone.utc) + timedelta(days=valid_days_from_now)
if bonus_days <=0 or max_activations <=0 :raise ValueError ("Bonus days and max activations must be positive.") 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: except ValueError as e:
await message.answer(_("admin_promo_invalid_values", error=str(e))) await message.answer(_("admin_promo_invalid_values", error=str(e)))
return 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: 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 ) success_text = _("admin_promo_created_success",
await message .answer (success_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) 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: else:
await message.answer(_("admin_promo_creation_failed")) await message.answer(_("admin_promo_creation_failed"))
await state.clear() await state.clear()
async def view_promo_codes_handler (
callback :types .CallbackQuery , async def view_promo_codes_handler(callback: types.CallbackQuery,
i18n_data :dict , i18n_data: dict, settings: Settings):
settings :Settings current_lang = i18n_data.get("current_language",
): getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in view_promo_codes_handler") logging.error("i18n missing in view_promo_codes_handler")
@@ -106,11 +127,15 @@ settings :Settings
promos = await get_promo_codes_db(is_active_only=True, limit=20) promos = await get_promo_codes_db(is_active_only=True, limit=20)
if not callback.message: if not callback.message:
await callback .answer ("Error: message context lost.",show_alert =True );return await callback.answer("Error: message context lost.", show_alert=True)
return
if not promos: 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.message.edit_text(
await callback .answer ();return _("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"] response_text_parts = [f"<b>{_('admin_active_promos_list_header')}</b>\n"]
for promo in promos: for promo in promos:
@@ -118,40 +143,45 @@ settings :Settings
if promo['valid_until']: if promo['valid_until']:
try: try:
valid_until_dt: Optional[datetime] = None valid_until_dt: Optional[datetime] = None
if 'T' in promo['valid_until']: if 'T' in promo['valid_until']:
valid_until_dt =datetime .fromisoformat (promo ['valid_until'].replace ("Z","+00:00")) valid_until_dt = datetime.fromisoformat(
promo['valid_until'].replace("Z", "+00:00"))
else: else:
valid_until_dt =datetime .strptime (promo ['valid_until'],'%Y-%m-%d %H:%M:%S') 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: if valid_until_dt and valid_until_dt.tzinfo is None:
valid_until_dt =valid_until_dt .replace (tzinfo =timezone .utc ) valid_until_dt = valid_until_dt.replace(
tzinfo=timezone.utc)
valid_until_display_text = valid_until_dt.strftime('%Y-%m-%d') valid_until_display_text = valid_until_dt.strftime('%Y-%m-%d')
except ValueError as e: except ValueError as e:
logging .warning (f"Could not parse valid_until date string '{promo['valid_until']}' for promo code {promo['code']}: {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'] valid_until_display_text = promo['valid_until']
response_text_parts.append( response_text_parts.append(
_ ("admin_promo_list_item",code =promo ['code'],bonus =promo ['bonus_days'], _("admin_promo_list_item",
current =promo ['current_activations'],max =promo ['max_activations'], code=promo['code'],
valid_until =valid_until_display_text ) 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) final_text = "\n".join(response_text_parts)
try: try:
await callback .message .edit_text (final_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ),parse_mode ="HTML") 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: except Exception as e:
logging.warning(f"Failed to edit message for promo list: {e}") logging.warning(f"Failed to edit message for promo list: {e}")
if callback.message: 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.message.answer(
final_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n),
parse_mode="HTML")
await callback.answer() await callback.answer()
+51 -33
View File
@@ -11,8 +11,10 @@ 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 ): async def show_statistics_handler(callback: types.CallbackQuery,
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in show_statistics_handler") logging.error("i18n missing in show_statistics_handler")
@@ -25,57 +27,68 @@ async def show_statistics_handler (callback :types .CallbackQuery ,i18n_data :di
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() user_stats = await get_user_count_stats()
stats_text_parts.append( stats_text_parts.append(
_("admin_stats_users", _("admin_stats_users",
total_users=user_stats.get("total_users", 0), total_users=user_stats.get("total_users", 0),
banned_users=user_stats.get("banned_users", 0), banned_users=user_stats.get("banned_users", 0),
active_subs =user_stats .get ("users_with_active_subscriptions",0 ) active_subs=user_stats.get("users_with_active_subscriptions", 0)))
)
)
last_payments = await get_payment_logs(limit=5) last_payments = await get_payment_logs(limit=5)
if last_payments: if last_payments:
stats_text_parts .append (f"\n<b>{_('admin_stats_recent_payments_header')}</b>") stats_text_parts.append(
f"\n<b>{_('admin_stats_recent_payments_header')}</b>")
for payment in last_payments: for payment in last_payments:
status_emoji =""if payment ['status']=='succeeded'else (""if payment ['status']=='pending'else "") status_emoji = "" if payment['status'] == 'succeeded' else (
user_info =f"User {payment['user_id']}"+(f" (@{payment['username']})"if payment ['username']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'] payment_date_str = payment['created_at']
if isinstance (payment_date_str ,str )and len (payment_date_str )>10 : if isinstance(payment_date_str,
str) and len(payment_date_str) > 10:
payment_date_str = payment_date_str[:10] payment_date_str = payment_date_str[:10]
stats_text_parts.append( stats_text_parts.append(
_("admin_stats_payment_item", _("admin_stats_payment_item",
status_emoji =status_emoji ,amount =payment ['amount'],currency =payment ['currency'], status_emoji=status_emoji,
user_info =user_info ,p_status =payment ['status'],p_date =payment_date_str amount=payment['amount'],
) currency=payment['currency'],
) user_info=user_info,
p_status=payment['status'],
p_date=payment_date_str))
else: else:
stats_text_parts.append(f"\n{_('admin_stats_no_payments_found')}") stats_text_parts.append(f"\n{_('admin_stats_no_payments_found')}")
sync_status = await get_last_sync_status() sync_status = await get_last_sync_status()
if sync_status: if sync_status:
stats_text_parts .append (f"\n<b>{_('admin_stats_last_sync_header')}</b>") stats_text_parts.append(
f"\n<b>{_('admin_stats_last_sync_header')}</b>")
sync_time_val = sync_status['last_sync_time'] sync_time_val = sync_status['last_sync_time']
sync_time_str = "N/A" sync_time_str = "N/A"
if isinstance(sync_time_val, datetime): if isinstance(sync_time_val, datetime):
sync_time_str = sync_time_val.strftime('%Y-%m-%d %H:%M:%S UTC') sync_time_str = sync_time_val.strftime('%Y-%m-%d %H:%M:%S UTC')
elif isinstance(sync_time_val, str): elif isinstance(sync_time_val, str):
sync_time_str =sync_time_val [:19 ]if len (sync_time_val )>19 else sync_time_val sync_time_str = sync_time_val[:19] if len(
sync_time_val) > 19 else sync_time_val
details_val = sync_status['details'] details_val = sync_status['details']
details_str =(details_val [:100 ]+"...")if details_val and len (details_val )>100 else (details_val or "N/A") details_str = (details_val[:100] +
"...") if details_val and len(details_val) > 100 else (
details_val or "N/A")
stats_text_parts.append(
stats_text_parts .append (f" {_('admin_stats_sync_time')}: {sync_time_str}") f" {_('admin_stats_sync_time')}: {sync_time_str}")
stats_text_parts .append (f" {_('admin_stats_sync_status')}: {sync_status['status']}") stats_text_parts.append(
stats_text_parts .append (f" {_('admin_stats_sync_users_processed')}: {sync_status['users_processed_from_panel']}") f" {_('admin_stats_sync_status')}: {sync_status['status']}")
stats_text_parts .append (f" {_('admin_stats_sync_subs_synced')}: {sync_status['subscriptions_synced']}") stats_text_parts.append(
stats_text_parts .append (f" {_('admin_stats_sync_details_label')}: {details_str}") 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: else:
stats_text_parts.append(f"\n{_('admin_sync_status_never_run')}") stats_text_parts.append(f"\n{_('admin_sync_status_never_run')}")
@@ -85,17 +98,22 @@ async def show_statistics_handler (callback :types .CallbackQuery ,i18n_data :di
try: try:
await callback.message.edit_text( await callback.message.edit_text(
final_text, final_text,
reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n ), reply_markup=get_back_to_admin_panel_keyboard(
parse_mode ="HTML" current_lang, i18n),
) parse_mode="HTML")
except Exception as e: except Exception as e:
logging .error (f"Error editing message for statistics: {e}",exc_info =True ) 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 )]: for chunk in [
final_text[i:i + 4000]
for i in range(0, len(final_text), 4000)
]:
await callback.message.answer( await callback.message.answer(
chunk, chunk,
reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )if chunk ==final_text [-len (chunk ):]else None , reply_markup=get_back_to_admin_panel_keyboard(
parse_mode ="HTML" current_lang, i18n)
) if chunk == final_text[-len(chunk):] else None,
parse_mode="HTML")
else: else:
logging.error("Cannot send statistics, callback.message is None.") logging.error("Cannot send statistics, callback.message is None.")
+68 -46
View File
@@ -12,17 +12,18 @@ from bot .keyboards .inline .admin_keyboards import get_back_to_admin_panel_keyb
router = Router(name="admin_sync_router") router = Router(name="admin_sync_router")
@router.message(Command("sync")) @router.message(Command("sync"))
async def sync_command_handler ( async def sync_command_handler(message: types.Message, bot: Bot,
message :types .Message , settings: Settings, i18n_data: dict,
bot :Bot , panel_service: PanelApiService):
settings :Settings , current_lang = i18n_data.get("current_language",
i18n_data :dict , getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
panel_service :PanelApiService
):
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :logging .error ("i18n missing");await message .answer ("Language error.");return if not i18n:
logging.error("i18n missing")
await message.answer("Language error.")
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
await message.answer(_("sync_started")) await message.answer(_("sync_started"))
@@ -45,82 +46,103 @@ panel_service :PanelApiService
if not panel_users_data: if not panel_users_data:
status_msg = "No users found in the panel to sync." status_msg = "No users found in the panel to sync."
await update_sync_status("success", status_msg, 0, 0) await update_sync_status("success", status_msg, 0, 0)
await message .answer (_ ("sync_completed",status ="Success",details =status_msg )) await message.answer(
_("sync_completed", status="Success", details=status_msg))
return return
total_panel_users = len(panel_users_data) total_panel_users = len(panel_users_data)
logging.info(f"Starting sync for {total_panel_users} panel users.") logging.info(f"Starting sync for {total_panel_users} panel users.")
for panel_user_dict in panel_users_data: for panel_user_dict in panel_users_data:
users_processed_count += 1 users_processed_count += 1
telegram_id_from_panel = panel_user_dict.get('telegramId') telegram_id_from_panel = panel_user_dict.get('telegramId')
panel_uuid = panel_user_dict.get('uuid') panel_uuid = panel_user_dict.get('uuid')
if not telegram_id_from_panel: 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.") logging.info(
f"Panel user {panel_uuid} (username: {panel_user_dict.get('username')}) has no 'telegramId'. Skipping TG ID based sync."
)
continue continue
if not panel_uuid: if not panel_uuid:
logging .warning (f"Panel user (TG ID: {telegram_id_from_panel}) missing 'uuid'. Skipping.") logging.warning(
sync_errors .append (f"Panel user data for TG ID {telegram_id_from_panel} missing UUID.") 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 continue
if await sync_panel_user_data(panel_user_dict): if await sync_panel_user_data(panel_user_dict):
users_synced_successfully += 1 users_synced_successfully += 1
else: else:
sync_errors .append (f"Sync issue for panel user: {panel_uuid} (TG ID: {telegram_id_from_panel})") sync_errors.append(
f"Sync issue for panel user: {panel_uuid} (TG ID: {telegram_id_from_panel})"
)
if users_processed_count % 20 == 0: if users_processed_count % 20 == 0:
logging .info (f"Sync progress: {users_processed_count}/{total_panel_users} users processed.") 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}." status_msg = f"Panel users checked: {total_panel_users}. Users/Subscriptions synced via TG ID: {users_synced_successfully}."
if sync_errors: if sync_errors:
status_msg += f" Errors encountered: {len(sync_errors)}. See logs for details." status_msg += f" Errors encountered: {len(sync_errors)}. See logs for details."
error_preview = "\n".join(sync_errors[:3]) error_preview = "\n".join(sync_errors[:3])
await update_sync_status ("partial_success",status_msg +" "+error_preview ,total_panel_users ,users_synced_successfully ) await update_sync_status("partial_success",
await message .answer (_ ("sync_completed",status ="Partial Success",details =status_msg )) status_msg + " " + error_preview,
total_panel_users,
users_synced_successfully)
await message.answer(
_("sync_completed",
status="Partial Success",
details=status_msg))
else: else:
await update_sync_status ("success",status_msg ,total_panel_users ,users_synced_successfully ) await update_sync_status("success", status_msg, total_panel_users,
await message .answer (_ ("sync_completed",status ="Success",details =status_msg )) users_synced_successfully)
await message.answer(
_("sync_completed", status="Success", details=status_msg))
except Exception as e: except Exception as e:
logging.error(f"Error during /sync command: {e}", exc_info=True) logging.error(f"Error during /sync command: {e}", exc_info=True)
error_detail = f"An unexpected error occurred during sync: {str(e)}" 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 update_sync_status("failed", error_detail, users_processed_count,
users_synced_successfully)
await message.answer(_("sync_failed", details=error_detail)) await message.answer(_("sync_failed", details=error_detail))
@router .message (Command ("syncstatus"))
async def sync_status_command_handler (message :types .Message ,i18n_data :dict ,settings :Settings ):
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") @router.message(Command("syncstatus"))
if not i18n :await message .answer ("Language error.");return async def sync_status_command_handler(message: types.Message, i18n_data: dict,
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );status_record =await get_last_sync_status ();response_text ="" 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: 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 ) last_time_val = status_record['last_sync_time']
if isinstance (last_time_str ,str )and len (last_time_str )>19 :last_time_str =last_time_str [:19 ] last_time_str = last_time_val.strftime(
details_val =status_record ['details'];details_str =(details_val [:200 ]+"...")if details_val and len (details_val )>200 else (details_val or "N/A") '%Y-%m-%d %H:%M:%S UTC') if isinstance(
response_text =(f"<b>{_('admin_stats_last_sync_header')}</b>\n" 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_time')}: {last_time_str}\n"
f" {_('admin_stats_sync_status')}: {status_record['status']}\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_users_processed')}: {status_record['users_processed_from_panel']}\n"
f" {_('admin_stats_sync_subs_synced')}: {status_record['subscriptions_synced']}\n" f" {_('admin_stats_sync_subs_synced')}: {status_record['subscriptions_synced']}\n"
f" {_('admin_stats_sync_details_label')}: {details_str}") f" {_('admin_stats_sync_details_label')}: {details_str}")
else :response_text =_ ("admin_sync_status_never_run") else:
response_text = _("admin_sync_status_never_run")
await message.answer(response_text, parse_mode="HTML") await message.answer(response_text, parse_mode="HTML")
+491 -131
View File
@@ -8,16 +8,16 @@ import aiosqlite
from datetime import datetime from datetime import datetime
from config.settings import Settings from config.settings import Settings
from db .database import ( from db.database import (get_user, set_user_ban_status_db,
get_user ,set_user_ban_status_db ,get_user_by_telegram_username , get_user_by_telegram_username,
get_banned_users_list_paginated ,get_user_active_subscription_end_date get_banned_users_list_paginated,
) get_user_active_subscription_end_date)
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from bot.states.admin_states import AdminStates from bot.states.admin_states import AdminStates
from bot.keyboards.inline.admin_keyboards import ( from bot.keyboards.inline.admin_keyboards import (
get_back_to_admin_panel_keyboard, get_user_card_keyboard, get_back_to_admin_panel_keyboard, get_user_card_keyboard,
get_banned_users_keyboard ,get_confirmation_keyboard ,get_admin_panel_keyboard get_banned_users_keyboard, get_confirmation_keyboard,
) get_admin_panel_keyboard)
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
router = Router(name="admin_user_management_router") router = Router(name="admin_user_management_router")
@@ -25,196 +25,556 @@ 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") current_lang = i18n_data.get("current_language",
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );prompt_text =_ ("admin_ban_user_prompt") 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: if callback.message:
try :await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) try:
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.message.edit_text(
await callback .answer ();await state .set_state (AdminStates .waiting_for_user_id_to_ban ) 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) @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 ): 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") current_lang = i18n_data.get("current_language",
if not i18n :logging .error ("i18n missing");await message .reply ("Language error.");return getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
_ =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 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(): 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 ) try:
except ValueError :await message .answer (_ ("admin_invalid_user_id_format"));return user_id_to_ban_val = int(input_text)
elif input_text .startswith ("@")and USERNAME_REGEX .match (input_text [1 :]):user_to_ban_data =await get_user_by_telegram_username (input_text [1 :]) user_to_ban_data = await get_user(user_id_to_ban_val)
elif USERNAME_REGEX .match (input_text ):user_to_ban_data =await get_user_by_telegram_username (input_text ) except ValueError:
else :await message .answer (_ ("admin_invalid_user_id_format")+" "+_ ("admin_invalid_username_format"));return await message.answer(_("admin_invalid_user_id_format"))
if not user_to_ban_data :await message .answer (_ ("admin_user_not_found_in_bot_db",user_id =input_text ));await state .clear ();return 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 ) elif input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]):
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 user_to_ban_data = await get_user_by_telegram_username(input_text[1:])
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 elif USERNAME_REGEX.match(input_text):
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 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) 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) reply_markup_val = get_back_to_admin_panel_keyboard(current_lang, i18n)
if panel_user_uuid: if panel_user_uuid:
panel_ban_success =await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =False ) panel_ban_success = await panel_service.update_user_status_on_panel(
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 ) panel_user_uuid, enable=False)
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 ) if panel_ban_success:
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 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() await state.clear()
async def unban_user_prompt_handler (callback :types .CallbackQuery ,state :FSMContext ,i18n_data :dict ,settings :Settings ): 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") current_lang = i18n_data.get("current_language",
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
_ =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs );prompt_text =_ ("admin_unban_user_prompt") 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: if callback.message:
try :await callback .message .edit_text (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) try:
except :await callback .message .answer (prompt_text ,reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) await callback.message.edit_text(
await callback .answer ();await state .set_state (AdminStates .waiting_for_user_id_to_unban ) 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) @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 ): 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") current_lang = i18n_data.get("current_language",
if not i18n :logging .error ("i18n missing");await message .reply ("Language error.");return getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
_ =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 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(): 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 ) try:
except ValueError :await message .answer (_ ("admin_invalid_user_id_format"));return user_id_to_unban_val = int(input_text)
elif input_text .startswith ("@")and USERNAME_REGEX .match (input_text [1 :]):user_to_unban_data =await get_user_by_telegram_username (input_text [1 :]) user_to_unban_data = await get_user(user_id_to_unban_val)
elif USERNAME_REGEX .match (input_text ):user_to_unban_data =await get_user_by_telegram_username (input_text ) except ValueError:
else :await message .answer (_ ("admin_invalid_user_id_format")+" "+_ ("admin_invalid_username_format"));return await message.answer(_("admin_invalid_user_id_format"))
if not user_to_unban_data :await message .answer (_ ("admin_user_not_found_in_bot_db",user_id =input_text ));await state .clear ();return 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 ) elif input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]):
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 user_to_unban_data = await get_user_by_telegram_username(input_text[1:]
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 )
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) 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) reply_markup_val = get_back_to_admin_panel_keyboard(current_lang, i18n)
if panel_user_uuid: if panel_user_uuid:
panel_unban_success =await panel_service .update_user_status_on_panel (panel_user_uuid ,enable =True ) panel_unban_success = await panel_service.update_user_status_on_panel(
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 ) panel_user_uuid, enable=True)
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 ) if panel_unban_success:
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 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() await state.clear()
async def view_banned_users_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ,state :FSMContext ): async def view_banned_users_handler(callback: types.CallbackQuery,
await state .clear ();current_page =0 ; i18n_data: dict, settings: Settings,
state: FSMContext):
await state.clear()
current_page = 0
if ":" in callback.data and callback.data.count(":") == 2: if ":" in callback.data and callback.data.count(":") == 2:
try :current_page =int (callback .data .split (":")[-1 ]) try:
except ValueError :current_page =0 current_page = int(callback.data.split(":")[-1])
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") except ValueError:
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = 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)
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.")
if not callback .message :await callback .answer ("Error.");return return
if total_banned == 0: if total_banned == 0:
await callback .message .edit_text (_ ("admin_no_banned_users"),reply_markup =get_back_to_admin_panel_keyboard (current_lang ,i18n )) await callback.message.edit_text(
_("admin_no_banned_users"),
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
else: else:
total_pages =math .ceil (total_banned /settings .LOGS_PAGE_SIZE )if settings .LOGS_PAGE_SIZE >0 else 1 total_pages = math.ceil(
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 )) 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() 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 @router.callback_query(F.data.startswith("admin_user_card:"))
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 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: 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 try:
except (IndexError ,ValueError ):await callback .answer ("Invalid user card data.",show_alert =True );return parts = callback.data.split(":")
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") user_id_to_show = int(parts[1])
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
user_data = await get_user(user_id_to_show) user_data = await get_user(user_id_to_show)
if not callback .message :await callback .answer ("Error.");return if not callback.message:
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 await callback.answer("Error.")
user_display_name =user_data ['first_name']or (f"@{user_data['username']}"if user_data .get ('username')else f"ID: {user_id_to_show}") return
sub_end_date_str =await get_user_active_subscription_end_date (user_id_to_show )or _ ("user_card_sub_na") if not user_data:
reg_date_from_db =user_data ['registration_date_str']if 'registration_date_str'in user_data .keys ()else "N/A" await callback.message.edit_text(
reg_date_display =reg_date_from_db [:10 ]if reg_date_from_db and reg_date_from_db !="N/A"else "N/A" _("admin_user_not_found_in_bot_db", user_id=user_id_to_show),
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 ) reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
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") 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() 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 ) @router.callback_query(F.data.startswith("admin_unban_confirm:"))
except :await callback .answer ("Invalid data",show_alert =True );return async def confirm_unban_handler(callback: types.CallbackQuery, i18n_data: dict,
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") settings: Settings):
if not i18n :await callback .answer ("Language error.",show_alert =True );return
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) _ = 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}" user_data = await get_user(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") 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() await callback.answer()
@router.callback_query(F.data.startswith("admin_ban_confirm:")) @router.callback_query(F.data.startswith("admin_ban_confirm:"))
async def confirm_ban_handler (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ): async def confirm_ban_handler(callback: types.CallbackQuery, i18n_data: dict,
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id =int (user_id_str );banned_list_page =int (page_str ) settings: Settings):
except :await callback .answer ("Invalid data",show_alert =True );return try:
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") _, user_id_str, page_str = callback.data.split(":")
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = 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}" 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: 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.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() await callback.answer()
@router.callback_query(F.data.startswith("admin_unban_do:")) @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 ): 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 ) try:
except :await callback .answer ("Invalid data",show_alert =True );return _, user_id_str, page_str = callback.data.split(":")
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") user_id_to_unban = int(user_id_str)
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = 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}" 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']: 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 ) await callback.answer(_("admin_user_not_banned",
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 ) 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 return
await set_user_ban_status_db(user_id_to_unban, is_banned=False) 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 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 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}") if not await panel_service.update_user_status_on_panel(panel_user_uuid,
await callback .answer (_ ("admin_user_unbanned_from_card",user_display =user_display_name ,user_id =user_id_to_unban ),show_alert =False ) enable=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 ) 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:")) @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 ): async def do_ban_user_handler(callback: types.CallbackQuery, i18n_data: dict,
try :_ ,user_id_str ,page_str =callback .data .split (":");user_id_to_ban =int (user_id_str );banned_list_page =int (page_str ) settings: Settings,
except :await callback .answer ("Invalid data",show_alert =True );return panel_service: PanelApiService,
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'));i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") state: FSMContext):
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = 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}" user_to_ban_data = await get_user(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 user_display_name = (
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 user_to_ban_data['first_name'] or
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 (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) 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 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 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}") if not await panel_service.update_user_status_on_panel(panel_user_uuid,
await callback .answer (_ ("admin_user_banned_from_card",user_display =user_display_name ,user_id =user_id_to_ban ),show_alert =False ) enable=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 ) 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",
@router .callback_query (F .data =="admin_action:main",AdminStates .waiting_for_user_id_to_unban ) AdminStates.waiting_for_user_id_to_ban)
async def cancel_user_management_input_state (callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ,bot :Bot ): @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") current_lang = i18n_data.get("current_language",
if not i18n :await callback .answer ("Language error.",show_alert =True );return 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) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
if callback.message: if callback.message:
try :await callback .message .edit_text (_ ("admin_action_cancelled_default"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings )) try:
except :await callback .message .answer (_ ("admin_action_cancelled_default"),reply_markup =get_admin_panel_keyboard (i18n ,current_lang ,settings )) await callback.message.edit_text(
await callback .answer (_ ("admin_action_cancelled_default"));await state .clear () _("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()
-6
View File
@@ -1,21 +1,15 @@
from aiogram import Router from aiogram import Router
from . import start from . import start
from . import subscription from . import subscription
from . import referral from . import referral
from . import promo_user from . import promo_user
from . import trial_handler from . import trial_handler
user_router_aggregate = Router(name="user_router_aggregate") user_router_aggregate = Router(name="user_router_aggregate")
user_router_aggregate.include_router(promo_user.router) user_router_aggregate.include_router(promo_user.router)
user_router_aggregate.include_router(trial_handler.router) user_router_aggregate.include_router(trial_handler.router)
user_router_aggregate.include_router(start.router) user_router_aggregate.include_router(start.router)
user_router_aggregate.include_router(subscription.router) user_router_aggregate.include_router(subscription.router)
user_router_aggregate.include_router(referral.router) user_router_aggregate.include_router(referral.router)
+213 -68
View File
@@ -25,12 +25,13 @@ YOOKASSA_EVENT_PAYMENT_CANCELED ='payment.canceled'
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture' YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture'
YOOKASSA_EVENT_REFUND_SUCCEEDED = 'refund.succeeded' YOOKASSA_EVENT_REFUND_SUCCEEDED = 'refund.succeeded'
async def process_successful_payment (
bot :Bot ,payment_info_from_webhook :dict ,i18n :JsonI18n , async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
settings :Settings ,panel_service :PanelApiService ,yk_service :YooKassaService , i18n: JsonI18n, settings: Settings,
panel_service: PanelApiService,
yk_service: YooKassaService,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
referral_service :ReferralService referral_service: ReferralService):
):
metadata = payment_info_from_webhook.get("metadata", {}) metadata = payment_info_from_webhook.get("metadata", {})
user_id_str = metadata.get("user_id") user_id_str = metadata.get("user_id")
subscription_months_str = metadata.get("subscription_months") subscription_months_str = metadata.get("subscription_months")
@@ -38,16 +39,23 @@ referral_service :ReferralService
payment_db_id_str = metadata.get("payment_db_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: 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}") logging.error(
f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}"
)
return return
try: try:
user_id =int (user_id_str );subscription_months =int (subscription_months_str ) user_id = int(user_id_str)
subscription_months = int(subscription_months_str)
payment_db_id = int(payment_db_id_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 promo_code_id = int(
amount_data =payment_info_from_webhook .get ("amount",{}); 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)) payment_value = float(amount_data.get("value", 0.0))
except (TypeError, ValueError) as e: except (TypeError, ValueError) as e:
logging .error (f"Invalid metadata format for payment processing: {metadata} - {e}") logging.error(
f"Invalid metadata format for payment processing: {metadata} - {e}"
)
return return
final_end_date_for_user: Optional[datetime] = None final_end_date_for_user: Optional[datetime] = None
@@ -57,110 +65,247 @@ referral_service :ReferralService
async with get_db_connection_manager() as db: async with get_db_connection_manager() as db:
await _setup_db_connection(db) await _setup_db_connection(db)
try: 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"))) 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'): if new_sub_details and new_sub_details.get('end_date'):
base_subscription_end_date = new_sub_details['end_date'] base_subscription_end_date = new_sub_details['end_date']
final_end_date_for_user = base_subscription_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"): if referral_bonus_info and referral_bonus_info.get(
final_end_date_for_user =referral_bonus_info ["referee_new_end_date"] "referee_new_end_date"):
applied_referee_bonus_days =referral_bonus_info .get ("referee_bonus_applied_days") 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 ) user_lang = await subscription_service.get_user_language(
_ =lambda key ,**kwargs :i18n .gettext (user_lang ,key ,**kwargs ) user_id)
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs
)
success_message = "" success_message = ""
if applied_referee_bonus_days and final_end_date_for_user: if applied_referee_bonus_days and final_end_date_for_user:
referee_user_data = await get_user(user_id) referee_user_data = await get_user(user_id)
inviter_name_for_msg = _("friend_placeholder") inviter_name_for_msg = _("friend_placeholder")
if referee_user_data and referee_user_data ['referred_by_id']is not None : if referee_user_data and referee_user_data[
inviter_user_data_for_msg =await get_user (referee_user_data ['referred_by_id']) 'referred_by_id'] is not None:
if inviter_user_data_for_msg and inviter_user_data_for_msg ['first_name']: inviter_user_data_for_msg = await get_user(
inviter_name_for_msg =inviter_user_data_for_msg ['first_name'] 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", success_message = _(
"payment_successful_with_referral_bonus",
months=subscription_months, months=subscription_months,
base_end_date =base_subscription_end_date .strftime ('%Y-%m-%d')if base_subscription_end_date else "N/A", 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, bonus_days=applied_referee_bonus_days,
final_end_date =final_end_date_for_user .strftime ('%Y-%m-%d'), final_end_date=final_end_date_for_user.strftime(
inviter_name =inviter_name_for_msg '%Y-%m-%d'),
) inviter_name=inviter_name_for_msg)
elif final_end_date_for_user: elif final_end_date_for_user:
success_message =_ ("payment_successful", success_message = _(
"payment_successful",
months=subscription_months, months=subscription_months,
end_date =final_end_date_for_user .strftime ('%Y-%m-%d') end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
)
else: else:
logging .error (f"Critical error: final_end_date_for_user is None for user {user_id}") logging.error(
f"Critical error: final_end_date_for_user is None for user {user_id}"
)
success_message = _("payment_successful_error_details") success_message = _("payment_successful_error_details")
try :await bot .send_message (user_id ,success_message ) try:
except Exception as e :logging .error (f"Failed to send final payment success message to user {user_id}: {e}") 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: else:
logging .error (f"Failed to activate subscription for user {user_id} after payment {payment_info_from_webhook.get('id')}") logging.error(
f"Failed to activate subscription for user {user_id} after payment {payment_info_from_webhook.get('id')}"
)
await db.rollback() await db.rollback()
except Exception as e: except Exception as e:
logging .error (f"Error during process_successful_payment transaction for user {user_id}: {e}",exc_info =True ) logging.error(
f"Error during process_successful_payment transaction for user {user_id}: {e}",
exc_info=True)
await db.rollback() await db.rollback()
try: try:
user_lang_for_error =await subscription_service .get_user_language (user_id ) user_lang_for_error = await subscription_service.get_user_language(
_err =lambda key ,**kwargs :i18n .gettext (user_lang_for_error ,key ,**kwargs ) user_id)
await bot .send_message (user_id ,_err ("error_processing_your_payment")) _err = lambda key, **kwargs: i18n.gettext(
except Exception as notify_err :logging .error (f"Failed to send error notification to user {user_id}: {notify_err}") 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 ): async def process_cancelled_payment(bot: Bot, payment_info_from_webhook: dict,
i18n: JsonI18n, settings: Settings):
metadata = payment_info_from_webhook.get("metadata", {}) metadata = payment_info_from_webhook.get("metadata", {})
user_id_str =metadata .get ("user_id");payment_db_id_str =metadata .get ("payment_db_id") user_id_str = metadata.get("user_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 payment_db_id_str = metadata.get("payment_db_id")
try :user_id =int (user_id_str );payment_db_id =int (payment_db_id_str ) if not user_id_str or not payment_db_id_str:
except ValueError :logging .error (f"Invalid metadata in cancelled payment: {metadata}");return 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: async with get_db_connection_manager() as db:
await _setup_db_connection(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.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() await db.commit()
user_lang = getattr(settings, 'DEFAULT_LANGUAGE', 'en') user_lang = getattr(settings, 'DEFAULT_LANGUAGE', 'en')
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
try :await bot .send_message (user_id ,_ ("payment_failed")) try:
except Exception as e :logging .error (f"Failed to send payment cancellation message to user {user_id}: {e}") 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): async def yookassa_webhook_route(request: web.Request):
logging .info (f"YooKassa Webhook Route: Available keys in request.app: {list(request.app.keys())}") logging.info(
f"YooKassa Webhook Route: Available keys in request.app: {list(request.app.keys())}"
)
try: try:
bot :Bot =request .app ['bot'];i18n_instance :JsonI18n =request .app ['i18n'];settings :Settings =request .app ['settings'] bot: Bot = request.app['bot']
yk_service :YooKassaService =request .app ['yookassa_service'];panel_service :PanelApiService =request .app ['panel_service'] i18n_instance: JsonI18n = request.app['i18n']
subscription_service :SubscriptionService =request .app ['subscription_service'] 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'] 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") 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: try:
event_json =await request .json ();notification_object =WebhookNotification (event_json ) event_json = await request.json()
notification_object = WebhookNotification(event_json)
payment_data_from_notification = notification_object.object 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}'") logging.info(
if not payment_data_from_notification or not hasattr (payment_data_from_notification ,'metadata')or payment_data_from_notification .metadata is None : f"YooKassa Webhook Parsed: Event='{notification_object.event}', PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'"
logging .error (f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata.");return web .Response (status =200 ,text ="ok_error_no_metadata") )
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 = {} payment_dict_for_processing = {}
if hasattr(payment_data_from_notification, 'model_dump'): if hasattr(payment_data_from_notification, 'model_dump'):
payment_dict_for_processing =payment_data_from_notification .model_dump (exclude_none =True ) payment_dict_for_processing = payment_data_from_notification.model_dump(
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 )} exclude_none=True)
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"} if 'amount' in payment_dict_for_processing and isinstance(
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 } payment_dict_for_processing['amount'], Amount):
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") 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: async with payment_processing_lock:
if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED: if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED:
if payment_dict_for_processing .get ("paid")and payment_dict_for_processing .get ("status")=="succeeded": if payment_dict_for_processing.get(
await process_successful_payment (bot ,payment_dict_for_processing ,i18n_instance ,settings ,panel_service ,yk_service ,subscription_service ,referral_service ) "paid") and payment_dict_for_processing.get(
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')}'") "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: elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
await process_cancelled_payment (bot ,payment_dict_for_processing ,i18n_instance ,settings ) await process_cancelled_payment(bot,
payment_dict_for_processing,
i18n_instance, settings)
return web.Response(status=200, text="ok") 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 json.JSONDecodeError:
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") logging.error("YooKassa Webhook: Invalid JSON.")
except Exception as e :logging .error (f"YooKassa Webhook processing error: {e}",exc_info =True );return web .Response (status =200 ,text ="ok_internal_error") 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")
+82 -49
View File
@@ -18,110 +18,143 @@ router =Router (name ="user_promo_router")
SUSPICIOUS_SQL_KEYWORDS_REGEX = re.compile( 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", 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 re.IGNORECASE)
)
SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)") SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)")
async def prompt_promo_code_input (
callback :types .CallbackQuery , async def prompt_promo_code_input(callback: types.CallbackQuery,
state :FSMContext , state: FSMContext, i18n_data: dict,
i18n_data :dict , settings: Settings):
settings :Settings current_lang = i18n_data.get("current_language",
): getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :await callback .answer ("Language error.",show_alert =True );return if not i18n:
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: if not callback.message:
logging .error ("CallbackQuery has no message in prompt_promo_code_input") logging.error(
await callback .answer (_ ("error_occurred_processing_request"),show_alert =True ) "CallbackQuery has no message in prompt_promo_code_input")
await callback.answer(_("error_occurred_processing_request"),
show_alert=True)
return return
try: try:
await callback.message.edit_text( await callback.message.edit_text(
text=_(key="promo_code_prompt"), text=_(key="promo_code_prompt"),
reply_markup =get_back_to_main_menu_markup (current_lang ,i18n ) reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
)
except Exception as e: except Exception as e:
logging.warning(f"Failed to edit message for promo prompt: {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.message.answer(
text=_(key="promo_code_prompt"),
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
await callback.answer() await callback.answer()
await state.set_state(UserPromoStates.waiting_for_promo_code) 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()}") 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) @router.message(UserPromoStates.waiting_for_promo_code, F.text)
async def process_promo_code_input ( async def process_promo_code_input(message: types.Message, state: FSMContext,
message :types .Message , settings: Settings, i18n_data: dict,
state :FSMContext ,
settings :Settings ,
i18n_data :dict ,
promo_code_service: PromoCodeService, promo_code_service: PromoCodeService,
bot :Bot bot: Bot):
): logging.info(
logging .info (f"Processing promo code input from user {message.from_user.id} in state {await state.get_state()}: '{message.text}'") 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')) current_lang = i18n_data.get("current_language",
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not promo_code_service: if not i18n or not promo_code_service:
logging.error("Deps missing in process_promo_code_input") logging.error("Deps missing in process_promo_code_input")
await message.reply("Service error. Please try again.") await message.reply("Service error. Please try again.")
await state .clear ();return await state.clear()
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
code_input =message .text .strip ();user =message .from_user code_input = message.text.strip()
user = message.from_user
is_suspicious = False is_suspicious = False
if SUSPICIOUS_SQL_KEYWORDS_REGEX .search (code_input )or SUSPICIOUS_CHARS_REGEX .search (code_input )or len (code_input )>100 : if SUSPICIOUS_SQL_KEYWORDS_REGEX.search(
code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input) or len(
code_input) > 100:
is_suspicious = True is_suspicious = True
logging .warning (f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'") logging.warning(
f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'"
)
response_to_user_text = "" response_to_user_text = ""
if is_suspicious: if is_suspicious:
admin_notify_key = "admin_suspicious_promo_attempt_notification_no_username" if not user.username else "admin_suspicious_promo_attempt_notification" 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_lang = settings.DEFAULT_LANGUAGE
_admin = lambda k, **kw: i18n.gettext(admin_lang, k, **kw) _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 )) admin_notification_text = _admin(admin_notify_key,
try :await bot .send_message (settings .ADMIN_ID ,admin_notification_text ,parse_mode ="HTML") user_id=user.id,
except Exception as e_admin_notify :logging .error (f"Failed to send suspicious promo notification to admin: {e_admin_notify}") user_username=user.username or "N/A",
response_to_user_text =_ ("promo_code_not_found",code =code_input .upper ()) 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: else:
success ,response_text_from_service =await promo_code_service .apply_promo_code (user .id ,code_input ,current_lang ) 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 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 message.answer(response_to_user_text,
reply_markup=get_back_to_main_menu_markup(
current_lang, i18n))
await state.clear() await state.clear()
logging .info (f"Promo code '{code_input}' processing finished for user {message.from_user.id}. State cleared.") 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",
@router .callback_query (F .data =="main_action:back_to_main",UserPromoStates .waiting_for_promo_code ) UserPromoStates.waiting_for_promo_code)
async def cancel_promo_input_via_button( async def cancel_promo_input_via_button(
callback :types .CallbackQuery , callback: types.CallbackQuery, state: FSMContext, settings: Settings,
state :FSMContext , i18n_data: dict, subscription_service: SubscriptionService):
settings :Settings , current_lang = i18n_data.get("current_language",
i18n_data :dict , getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
subscription_service :SubscriptionService
):
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in cancel_promo_input_via_button") logging.error("i18n missing in cancel_promo_input_via_button")
await callback.answer("Language error", show_alert=True) await callback.answer("Language error", show_alert=True)
return return
logging .info (f"User {callback.from_user.id} cancelled promo code input via button from state {await state.get_state()}. Clearing 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() await state.clear()
logging .info (f"State after clear for user {callback.from_user.id}: {await state.get_state()}") logging.info(
f"State after clear for user {callback.from_user.id}: {await state.get_state()}"
)
if callback.message: if callback.message:
show_trial_button_on_back = False show_trial_button_on_back = False
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ): if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
callback.from_user.id):
show_trial_button_on_back = True 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 ) await send_main_menu(callback,
settings,
i18n_data,
show_trial_button_flag=show_trial_button_on_back,
is_edit=True)
else: else:
await callback.answer("Promo code input cancelled.", show_alert=False) await callback.answer("Promo code input cancelled.", show_alert=False)
+47 -28
View File
@@ -11,25 +11,30 @@ 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 , async def referral_command_handler(event: types.Message | types.CallbackQuery,
settings :Settings , settings: Settings, i18n_data: dict,
i18n_data :dict ,
referral_service: ReferralService, referral_service: ReferralService,
bot :Bot bot: Bot):
): current_lang = i18n_data.get("current_language",
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
target_message =event .message if isinstance (event ,types .CallbackQuery )else event target_message = event.message if isinstance(
event, types.CallbackQuery) else event
if not target_message: if not target_message:
logging .error ("Target message is None in referral_command_handler from callback.") logging.error(
if isinstance (event ,types .CallbackQuery ):await event .answer ("Error displaying referral info.") "Target message is None in referral_command_handler from callback."
)
if isinstance(event, types.CallbackQuery):
await event.answer("Error displaying referral info.")
return return
if not i18n or not referral_service: if not i18n or not referral_service:
logging.error("Deps missing in referral_command_handler") 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 ) 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 isinstance(event, types.CallbackQuery): await event.answer()
return return
@@ -38,10 +43,12 @@ bot :Bot
bot_info = await bot.get_me() bot_info = await bot.get_me()
bot_username = bot_info.username bot_username = bot_info.username
inviter_user_id = event.from_user.id inviter_user_id = event.from_user.id
referral_link =referral_service .generate_referral_link (bot_username ,inviter_user_id ) referral_link = referral_service.generate_referral_link(
bot_username, inviter_user_id)
bonus_info_parts = [] bonus_info_parts = []
if hasattr (settings ,'subscription_options')and settings .subscription_options : if hasattr(settings,
'subscription_options') and settings.subscription_options:
for months_period in sorted(settings.subscription_options.keys()): for months_period in sorted(settings.subscription_options.keys()):
inv_bonus = settings.referral_bonus_inviter.get(months_period) inv_bonus = settings.referral_bonus_inviter.get(months_period)
ref_bonus = settings.referral_bonus_referee.get(months_period) ref_bonus = settings.referral_bonus_referee.get(months_period)
@@ -49,33 +56,45 @@ bot :Bot
bonus_info_parts.append( bonus_info_parts.append(
_("referral_bonus_per_period", _("referral_bonus_per_period",
months=months_period, months=months_period,
inviter_bonus_days =inv_bonus if inv_bonus is not None else _ ("no_bonus_days"), inviter_bonus_days=inv_bonus
referee_bonus_days =ref_bonus if ref_bonus is not None else _ ("no_bonus_days") 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") bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
text =_ ("referral_program_info_new",referral_link =referral_link ,bonus_details =bonus_details_str ) "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) reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
if isinstance(event, types.Message): if isinstance(event, types.Message):
await event .answer (text ,reply_markup =reply_markup_val ,disable_web_page_preview =True ) await event.answer(text,
reply_markup=reply_markup_val,
disable_web_page_preview=True)
elif isinstance(event, types.CallbackQuery): elif isinstance(event, types.CallbackQuery):
try: try:
await event .message .edit_text (text ,reply_markup =reply_markup_val ,disable_web_page_preview =True ) await event.message.edit_text(text,
reply_markup=reply_markup_val,
disable_web_page_preview=True)
except Exception as e: except Exception as e:
logging.warning(f"Failed to edit message for referral info: {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.message.answer(text,
reply_markup=reply_markup_val,
disable_web_page_preview=True)
await event.answer() await event.answer()
@router.callback_query(F.data == "copy_referral_link_ack") @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 ): async def copy_referral_link_ack_callback_handler(
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :return await callback .answer ("Language service error.",show_alert =True ) if not i18n:
return await callback.answer("Language service error.",
show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
await callback .answer (text =_ (key ="referral_link_for_copying_reminder"),show_alert =False ) await callback.answer(text=_(key="referral_link_for_copying_reminder"),
show_alert=False)
+191 -71
View File
@@ -17,32 +17,56 @@ 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')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
answered_callback_internally = False answered_callback_internally = False
if not i18n: if not i18n:
logging.error("i18n_instance missing in send_main_menu") 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 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." error_text_fallback = "Error: Language service unavailable."
if target_mc_for_error: if target_mc_for_error:
try :await target_mc_for_error .answer (error_text_fallback ) try:
except Exception as e_ans :logging .error (f"Failed to send error message in send_main_menu: {e_ans}") await target_mc_for_error.answer(error_text_fallback)
if isinstance (message_or_callback ,types .CallbackQuery ):await message_or_callback .answer ();answered_callback_internally =True 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 return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
user_full_name = message_or_callback.from_user.full_name user_full_name = message_or_callback.from_user.full_name
text = _(key="main_menu_greeting", user_name=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 ) reply_markup: Optional[
InlineKeyboardMarkup] = get_main_menu_inline_keyboard(
current_lang, i18n, settings, show_trial_button_flag)
target_message: Optional[types.Message] = None target_message: Optional[types.Message] = None
if isinstance (message_or_callback ,types .Message ):target_message =message_or_callback if isinstance(message_or_callback, types.Message):
elif isinstance (message_or_callback ,types .CallbackQuery ):target_message =message_or_callback .message target_message = message_or_callback
elif isinstance(message_or_callback, types.CallbackQuery):
target_message = message_or_callback.message
if not target_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}.") logging.error(
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally :await message_or_callback .answer ("Error displaying menu.");answered_callback_internally =True 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 return
try: try:
@@ -51,115 +75,211 @@ async def send_main_menu (message_or_callback :types .Message |types .CallbackQu
else: else:
await target_message.answer(text, reply_markup=reply_markup) await target_message.answer(text, reply_markup=reply_markup)
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally : if isinstance(
message_or_callback,
types.CallbackQuery) and not answered_callback_internally:
await message_or_callback.answer() await message_or_callback.answer()
answered_callback_internally = True answered_callback_internally = True
except Exception as e_send_edit: 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}.") logging.warning(
f"Failed to send/edit main menu (user: {message_or_callback.from_user.id}): {e_send_edit}."
)
if is_edit: if is_edit:
try :await target_message .answer (text ,reply_markup =reply_markup ) try:
except Exception as e_send_new :logging .error (f"Also failed to send new main menu message: {e_send_new}") await target_message.answer(text, reply_markup=reply_markup)
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally : 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() await message_or_callback.answer()
answered_callback_internally = True answered_callback_internally = True
if isinstance(message_or_callback,
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback_internally : 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.") logging.warning(
f"Callback {message_or_callback.id} was not answered in send_main_menu main logic paths."
)
await message_or_callback.answer() await message_or_callback.answer()
@router.message(CommandStart()) @router.message(CommandStart())
async def start_command_handler (message :types .Message ,state :FSMContext ,settings :Settings ,i18n_data :dict ,subscription_service :SubscriptionService ,bot :Bot ): 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 ) await state.clear()
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :logging .error ("i18n_instance not found");await message .answer ("Language service error.");return 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) _ = 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 () 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": if len(args) > 1 and args[0] == "/start":
try: try:
referral_param = args[1] referral_param = args[1]
if referral_param .startswith ("ref_")and referral_param .split ("_")[1 ].isdigit (): if referral_param.startswith("ref_") and referral_param.split(
"_")[1].isdigit():
potential_referrer_id = int(referral_param.split("_")[1]) potential_referrer_id = int(referral_param.split("_")[1])
if potential_referrer_id !=user_id :referred_by_user_id =potential_referrer_id if potential_referrer_id != user_id:
except (ValueError ,IndexError )as e :logging .warning (f"Could not parse referral: '{args[1]}' - {e}") referred_by_user_id = potential_referrer_id
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 ) except (ValueError, IndexError) as e:
if not db_op_success :await message .answer (_ ("error_occurred_processing_request"));return logging.warning(f"Could not parse referral: '{args[1]}' - {e}")
if referred_by_user_id :logging .info (f"User {user_id} started with referral from {referred_by_user_id}.") db_op_success, was_new_bot_user = await add_user_if_not_exists(
await message .answer (_ (key ="welcome",user_name =message .from_user .full_name )) 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 show_trial_button_in_menu = False
if settings.TRIAL_ENABLED: if settings.TRIAL_ENABLED:
if not await subscription_service.has_had_any_subscription(user_id): if not await subscription_service.has_had_any_subscription(user_id):
show_trial_button_in_menu = True show_trial_button_in_menu = True
logging.info(f"User {user_id} is eligible for a trial button.") 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:
else :logging .info (f"Trial period is disabled in settings. No trial button.") logging.info(
await send_main_menu (message ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_in_menu ) 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.message(Command("language"))
@router.callback_query(F.data == "main_action:language") @router.callback_query(F.data == "main_action:language")
async def language_command_handler (event :types .Message |types .CallbackQuery ,i18n_data :dict ,settings :Settings ): 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") current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
target_message_for_reply :Optional [types .Message ]=None ;is_callback =isinstance (event ,types .CallbackQuery );answered_callback =False i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if is_callback :await event .answer ();answered_callback =True ;target_message_for_reply =event .message target_message_for_reply: Optional[types.Message] = None
else :target_message_for_reply =event is_callback = isinstance(event, types.CallbackQuery)
if not i18n : answered_callback = False
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: if is_callback:
try :await target_message_for_reply .edit_text (text_to_send ,reply_markup =reply_markup_to_send ) await event.answer()
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 ) answered_callback = True
else :await target_message_for_reply .answer (text_to_send ,reply_markup =reply_markup_to_send ) target_message_for_reply = event.message
else:
target_message_for_reply = event
if not i18n:
logging.error("i18n instance is missing in language_command_handler.")
error_message_text = "Language service error."
if target_message_for_reply:
await target_message_for_reply.answer(error_message_text)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
text_to_send = _(key="choose_language")
reply_markup_to_send = get_language_selection_keyboard(i18n, current_lang)
if not target_message_for_reply:
logging.warning("language_command_handler: No target message.")
return
if is_callback:
try:
await target_message_for_reply.edit_text(
text_to_send, reply_markup=reply_markup_to_send)
except Exception as e:
logging.info(
f"Could not edit for lang selection: {e}. Sending new.")
await target_message_for_reply.answer(
text_to_send, reply_markup=reply_markup_to_send)
else:
await target_message_for_reply.answer(
text_to_send, reply_markup=reply_markup_to_send)
@router.callback_query(F.data.startswith("set_lang_")) @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 ): 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: if not i18n or not callback.message:
await callback .answer ("Language service error.",show_alert =True );return await callback.answer("Language service error.", show_alert=True)
return
lang_code =callback .data .split ("_")[2 ];user_id =callback .from_user .id lang_code = callback.data.split("_")[2]
await update_user_language_code (user_id ,lang_code );i18n_data ["current_language"]=lang_code 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) _ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
await callback.answer(_(key="language_set_alert")) await callback.answer(_(key="language_set_alert"))
show_trial_button_after_lang_change = False show_trial_button_after_lang_change = False
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (user_id ): if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
user_id):
show_trial_button_after_lang_change = True show_trial_button_after_lang_change = True
await send_main_menu(
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =show_trial_button_after_lang_change ,is_edit =True ) 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:")) @router.callback_query(F.data.startswith("main_action:"))
async def main_action_callback_handler( 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):
callback :types .CallbackQuery ,state :FSMContext ,settings :Settings ,i18n_data :dict ,bot :Bot , action = callback.data.split(":")[1]
subscription_service :SubscriptionService ,referral_service :ReferralService , from . import subscription as user_subscription_handlers
panel_service :PanelApiService ,promo_code_service :PromoCodeService 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
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 action == "subscribe":
if not callback .message :logging .error (f"Callback {callback.id} no message for {action}");await callback .answer ("Error.");return await user_subscription_handlers.display_subscription_options(
callback, i18n_data, settings)
if action =="subscribe":await user_subscription_handlers .display_subscription_options (callback ,i18n_data ,settings ) elif action == "my_subscription":
elif action =="my_subscription":await user_subscription_handlers .my_subscription_command_handler (callback ,i18n_data ,settings ,panel_service ,subscription_service ) await user_subscription_handlers.my_subscription_command_handler(
elif action =="referral":await user_referral_handlers .referral_command_handler (callback ,settings ,i18n_data ,referral_service ,bot ) callback, i18n_data, settings, panel_service, subscription_service)
elif action =="apply_promo":await user_promo_handlers .prompt_promo_code_input (callback ,state ,i18n_data ,settings ) elif action == "referral":
elif action =="request_trial":await user_trial_handlers .request_trial_confirmation_handler (callback ,settings ,i18n_data ,subscription_service ) await user_referral_handlers.referral_command_handler(
elif action =="language":await language_command_handler (callback ,i18n_data ,settings ) 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": elif action == "back_to_main":
show_trial_button_on_back = False show_trial_button_on_back = False
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ): if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
callback.from_user.id):
show_trial_button_on_back = True 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 ) await send_main_menu(callback,
else :await callback .answer ("Unknown action.",show_alert =True ) settings,
i18n_data,
show_trial_button_flag=show_trial_button_on_back,
is_edit=True)
else:
await callback.answer("Unknown action.", show_alert=True)
+204 -84
View File
@@ -11,11 +11,8 @@ from aiogram .types import InlineKeyboardMarkup
from config.settings import Settings from config.settings import Settings
from db.database import add_payment_record, get_db_connection_manager, _setup_db_connection from db.database import add_payment_record, get_db_connection_manager, _setup_db_connection
from bot.keyboards.inline.user_keyboards import ( from bot.keyboards.inline.user_keyboards import (
get_subscription_options_keyboard , get_subscription_options_keyboard, get_confirm_subscription_keyboard,
get_confirm_subscription_keyboard , get_payment_url_keyboard, get_back_to_main_menu_markup)
get_payment_url_keyboard ,
get_back_to_main_menu_markup
)
from bot.services.payment_service import YooKassaService from bot.services.payment_service import YooKassaService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
@@ -23,24 +20,37 @@ 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')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging.error("i18n missing in display_subscription_options") 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 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 target_msg: await target_msg.answer("Language service error.")
if isinstance (message_or_callback ,types .CallbackQuery ):await message_or_callback .answer () if isinstance(message_or_callback, types.CallbackQuery):
await message_or_callback.answer()
return return
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs ) get_translation = lambda key, **kwargs: i18n.gettext(
current_lang, key, **kwargs)
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
text =get_translation ("select_subscription_period")if settings .subscription_options else get_translation ("no_subscription_options_available") text = get_translation(
reply_markup =get_subscription_options_keyboard (settings .subscription_options ,currency_symbol_val ,current_lang ,i18n )if settings .subscription_options else None "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 target_message = message_or_callback.message if isinstance(
message_or_callback, types.CallbackQuery) else message_or_callback
answered_callback = False answered_callback = False
if isinstance(message_or_callback, types.CallbackQuery): if isinstance(message_or_callback, types.CallbackQuery):
@@ -59,149 +69,259 @@ async def display_subscription_options (message_or_callback :types .Message |typ
elif isinstance(message_or_callback, types.Message): elif isinstance(message_or_callback, types.Message):
await message_or_callback.answer(text, reply_markup=reply_markup) await message_or_callback.answer(text, reply_markup=reply_markup)
if isinstance(message_or_callback,
if isinstance (message_or_callback ,types .CallbackQuery )and not answered_callback : types.CallbackQuery) and not answered_callback:
await message_or_callback.answer() await message_or_callback.answer()
@router.callback_query(F.data.startswith("subscribe_period:")) @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 ): async def select_subscription_period_callback_handler(
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) 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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: if not i18n:
logging .error ("i18n missing in select_subscription_period_callback_handler") logging.error(
await callback .answer ("Service error. Please try again.",show_alert =True ) "i18n missing in select_subscription_period_callback_handler")
await callback.answer("Service error. Please try again.",
show_alert=True)
return 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 ]) try:
months = int(callback.data.split(":")[-1])
except ValueError: except ValueError:
logging.error(f"Invalid sub period: {callback.data}") logging.error(f"Invalid sub period: {callback.data}")
await callback .answer (get_translation ("error_try_again"),show_alert =True );return await callback.answer(get_translation("error_try_again"),
show_alert=True)
return
price = settings.subscription_options.get(months) price = settings.subscription_options.get(months)
if price is None: if price is None:
logging.error(f"Price not found for {months} months subscription.") logging.error(f"Price not found for {months} months subscription.")
await callback .answer (get_translation ("error_try_again"),show_alert =True );return await callback.answer(get_translation("error_try_again"),
show_alert=True)
return
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
confirmation_text =get_translation ("confirm_subscription_prompt",months =months ,price =price ,currency_symbol =currency_symbol_val ) confirmation_text = get_translation("confirm_subscription_prompt",
reply_markup =get_confirm_subscription_keyboard (months ,price ,currency_symbol_val ,current_lang ,i18n ) 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: if callback.message:
try :await callback .message .edit_text (confirmation_text ,reply_markup =reply_markup ) try:
except Exception as e :logging .warning (f"Edit failed: {e}");await callback .message .answer (confirmation_text ,reply_markup =reply_markup ) 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() await callback.answer()
@router.callback_query(F.data.startswith("confirm_sub:")) @router.callback_query(F.data.startswith("confirm_sub:"))
async def confirm_subscription_callback_handler( async def confirm_subscription_callback_handler(
callback :types .CallbackQuery ,state :FSMContext , callback: types.CallbackQuery, state: FSMContext, settings: Settings,
settings :Settings ,i18n_data :dict ,yookassa_service :YooKassaService i18n_data: dict, yookassa_service: YooKassaService):
):
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) current_lang = i18n_data.get("current_language",
if not i18n :logging .error ("i18n missing");await callback .answer ("Language error.",show_alert =True );return getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs ) 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: if not yookassa_service or not yookassa_service.configured:
logging.error("YooKassa service missing or not 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.message.edit_text(
await callback .answer (get_translation ("payment_service_unavailable"),show_alert =True );return get_translation("payment_service_unavailable")
try :_ ,data_payload =callback .data .split (":",1 );months_str ,price_str =data_payload .split (":");months =int (months_str );price =float (price_str ) ) if callback.message else None
except ValueError :logging .error (f"Invalid confirm data: {callback.data}");await callback .answer (get_translation ("error_try_again"),show_alert =True );return 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 user_id = callback.from_user.id
description =get_translation ("payment_description_subscription",months =months ) description = get_translation("payment_description_subscription",
months=months)
currency = settings.DEFAULT_CURRENCY_SYMBOL currency = settings.DEFAULT_CURRENCY_SYMBOL
payment_metadata ={"user_id":str (user_id ),"subscription_months":str (months ),"description":description } payment_metadata = {
payment_db_id =await add_payment_record (user_id ,None ,None ,price ,currency ,"pending_creation",description ,months ,None ) "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 not payment_db_id:
if callback .message :await callback .message .edit_text (get_translation ("error_creating_payment_record")) if callback.message:
await callback .answer (show_alert =True );return 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_metadata["payment_db_id"] = str(payment_db_id)
payment_response =await yookassa_service .create_payment (price ,currency ,description ,payment_metadata ) payment_response = await yookassa_service.create_payment(
price, currency, description, payment_metadata)
if callback.message: if callback.message:
if payment_response and payment_response.get("confirmation_url"): 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 () async with get_db_connection_manager() as db:
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 ) 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: 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 () async with get_db_connection_manager() as db:
await callback .message .edit_text (get_translation ("error_payment_gateway")) 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() await callback.answer()
@router.callback_query(F.data == "main_action:subscribe") @router.callback_query(F.data == "main_action:subscribe")
async def reshow_subscription_options_callback (callback :types .CallbackQuery ,i18n_data :dict ,settings :Settings ): async def reshow_subscription_options_callback(callback: types.CallbackQuery,
i18n_data: dict,
settings: Settings):
await display_subscription_options(callback, i18n_data, settings) await display_subscription_options(callback, i18n_data, settings)
async def my_subscription_command_handler( async def my_subscription_command_handler(
message_event :types .Message |types .CallbackQuery , message_event: types.Message | types.CallbackQuery, i18n_data: dict,
i18n_data :dict , settings: Settings, panel_service: PanelApiService,
settings :Settings , subscription_service: SubscriptionService):
panel_service :PanelApiService , target_message = message_event.message if isinstance(
subscription_service :SubscriptionService message_event, types.CallbackQuery) else message_event
):
target_message =message_event .message if isinstance (message_event ,types .CallbackQuery )else message_event
user = message_event.from_user user = message_event.from_user
if isinstance (message_event ,types .CallbackQuery ):await message_event .answer () if isinstance(message_event, types.CallbackQuery):
await message_event.answer()
current_lang =i18n_data .get ("current_language",getattr (settings ,'DEFAULT_LANGUAGE','en')) current_lang = i18n_data.get("current_language",
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n :logging .error ("i18n missing");await target_message .answer ("Lang error");return if not i18n:
get_translation =lambda key ,**kwargs :i18n .gettext (current_lang ,key ,**kwargs ) logging.error("i18n missing")
if not panel_service or not subscription_service :logging .error ("Services missing");await target_message .answer (get_translation ("error_service_unavailable"));return 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) active_sub = await subscription_service.get_active_subscription(user.id)
sub_info_text = "" sub_info_text = ""
if active_sub: if active_sub:
end_date_obj = active_sub.get('end_date') end_date_obj = active_sub.get('end_date')
if isinstance(end_date_obj, str): if isinstance(end_date_obj, str):
try :end_date_obj =datetime .fromisoformat (end_date_obj .replace ("Z","+00:00")) try:
except ValueError :logging .warning (f"Could not parse date string '{end_date_obj}'.");end_date_obj =datetime .now (timezone .utc ) end_date_obj = datetime.fromisoformat(
end_date_obj.replace("Z", "+00:00"))
if not isinstance (end_date_obj ,datetime ):end_date_obj =datetime .now (timezone .utc ) except ValueError:
if end_date_obj .tzinfo is None :end_date_obj =end_date_obj .replace (tzinfo =timezone .utc ) logging.warning(
f"Could not parse date string '{end_date_obj}'.")
end_date_obj = datetime.now(timezone.utc)
if not isinstance(end_date_obj, datetime):
end_date_obj = datetime.now(timezone.utc)
if end_date_obj.tzinfo is None:
end_date_obj = end_date_obj.replace(tzinfo=timezone.utc)
today_date_utc = datetime.now(timezone.utc).date() today_date_utc = datetime.now(timezone.utc).date()
end_date_only = end_date_obj.date() end_date_only = end_date_obj.date()
days_left = (end_date_only - today_date_utc).days 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') actual_config_link = get_translation("config_link_not_available")
panel_user_uuid = active_sub.get('panel_user_uuid')
if panel_user_uuid: if panel_user_uuid:
panel_user_data =await panel_service .get_user_by_uuid (panel_user_uuid ) panel_user_data = await panel_service.get_user_by_uuid(
panel_user_uuid)
if panel_user_data: if panel_user_data:
if panel_user_data .get ('subscriptionUrl'):actual_config_link =panel_user_data ['subscriptionUrl'] if panel_user_data.get('subscriptionUrl'):
actual_config_link = panel_user_data['subscriptionUrl']
elif panel_user_data.get('shortUuid'): elif panel_user_data.get('shortUuid'):
link =await panel_service .get_subscription_link (panel_user_data ['shortUuid']) link = await panel_service.get_subscription_link(
panel_user_data['shortUuid'])
if link: actual_config_link = link if link: actual_config_link = link
traffic_limit_gb =get_translation ("traffic_unlimited");traffic_used_gb =get_translation ("traffic_na") traffic_limit_gb = get_translation("traffic_unlimited")
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" traffic_used_gb = get_translation("traffic_na")
if active_sub .get ('traffic_used_bytes')is not None :traffic_used_gb =f"{active_sub['traffic_used_bytes'] / (1024**3):.2f} GB" 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 ) 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: else:
sub_info_text = get_translation("subscription_not_active") sub_info_text = get_translation("subscription_not_active")
reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n) reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
if isinstance (message_event ,types .CallbackQuery )and message_event .message : if isinstance(message_event,
try :await message_event .message .edit_text (sub_info_text ,reply_markup =reply_markup_val ,parse_mode ="HTML",disable_web_page_preview =True ) types.CallbackQuery) and message_event.message:
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 ) try:
else :await target_message .answer (sub_info_text ,reply_markup =reply_markup_val ,parse_mode ="HTML",disable_web_page_preview =True ) 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")) @router.message(Command("connect"))
async def connect_command_handler ( async def connect_command_handler(message: types.Message, i18n_data: dict,
message :types .Message ,
i18n_data :dict ,
settings: Settings, settings: Settings,
panel_service: PanelApiService, panel_service: PanelApiService,
subscription_service :SubscriptionService subscription_service: SubscriptionService):
):
"""Handles the /connect command, showing subscription info.""" """Handles the /connect command, showing subscription info."""
logging.info(f"User {message.from_user.id} used /connect command.") logging.info(f"User {message.from_user.id} used /connect command.")
await my_subscription_command_handler (message ,i18n_data ,settings ,panel_service ,subscription_service ) await my_subscription_command_handler(message, i18n_data, settings,
panel_service, subscription_service)
+91 -40
View File
@@ -14,49 +14,86 @@ 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 ): async def request_trial_confirmation_handler(
user_id =callback .from_user .id ;current_lang =i18n_data .get ("current_language",settings .DEFAULT_LANGUAGE );i18n :Optional [JsonI18n ]=i18n_data .get ("i18n_instance") callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
if not i18n or not callback .message :await callback .answer ("Error.",show_alert =True );return 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) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
show_trial_btn_if_fail =False ; 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 settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
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 user_id):
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 show_trial_btn_if_fail = True
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") if not settings.TRIAL_ENABLED:
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 () 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") @router.callback_query(F.data == "trial_action:confirm_activate")
async def confirm_activate_trial_handler( async def confirm_activate_trial_handler(
callback :types .CallbackQuery , callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
settings :Settings ,
i18n_data :dict ,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
panel_service :PanelApiService , panel_service: PanelApiService, bot: Bot):
bot :Bot
):
user_id = callback.from_user.id user_id = callback.from_user.id
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback .message :await callback .answer ("Error.",show_alert =True );return if not i18n or not callback.message:
await callback.answer("Error.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
show_trial_button_after_action = False show_trial_button_after_action = False
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (user_id ): if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
user_id):
show_trial_button_after_action = True show_trial_button_after_action = True
if not settings.TRIAL_ENABLED: if not settings.TRIAL_ENABLED:
await callback.answer(_("trial_feature_disabled"), show_alert=True) 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 ) await send_main_menu(callback,
settings,
i18n_data,
show_trial_button_flag=False,
is_edit=True)
return return
if await subscription_service.has_had_any_subscription(user_id): if await subscription_service.has_had_any_subscription(user_id):
await callback .answer (_ ("trial_already_had_subscription_or_trial"),show_alert =True ) await callback.answer(_("trial_already_had_subscription_or_trial"),
await send_main_menu (callback ,settings ,i18n_data ,show_trial_button_flag =False ,is_edit =True ) show_alert=True)
await send_main_menu(callback,
settings,
i18n_data,
show_trial_button_flag=False,
is_edit=True)
return return
activation_result =await subscription_service .activate_trial_subscription (user_id ) activation_result = await subscription_service.activate_trial_subscription(
user_id)
final_message_text_in_chat = "" final_message_text_in_chat = ""
if activation_result and activation_result.get("activated"): if activation_result and activation_result.get("activated"):
@@ -65,49 +102,63 @@ bot :Bot
end_date = activation_result.get("end_date") end_date = activation_result.get("end_date")
config_link_for_trial = _("config_link_not_available") config_link_for_trial = _("config_link_not_available")
if activation_result.get("subscription_url"): if activation_result.get("subscription_url"):
config_link_for_trial = activation_result["subscription_url"] config_link_for_trial = activation_result["subscription_url"]
elif activation_result.get("panel_short_uuid"): elif activation_result.get("panel_short_uuid"):
link =await panel_service .get_subscription_link (activation_result ["panel_short_uuid"]) link = await panel_service.get_subscription_link(
activation_result["panel_short_uuid"])
if link: config_link_for_trial = link if link: config_link_for_trial = link
traffic_gb_val =activation_result .get ("traffic_gb",settings .TRIAL_TRAFFIC_LIMIT_GB ) traffic_gb_val = activation_result.get("traffic_gb",
traffic_display =f"{traffic_gb_val} GB"if traffic_gb_val and traffic_gb_val >0 else _ ("traffic_unlimited") 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 = _( final_message_text_in_chat = _(
"trial_activated_details_message", "trial_activated_details_message",
days=activation_result.get("days", settings.TRIAL_DURATION_DAYS), 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", end_date=end_date.strftime('%Y-%m-%d') if isinstance(
end_date, datetime) else "N/A",
config_link=config_link_for_trial, config_link=config_link_for_trial,
traffic_gb =traffic_display traffic_gb=traffic_display)
)
show_trial_button_after_action = False show_trial_button_after_action = False
else: else:
message_key =activation_result .get ("message_key","trial_activation_failed")if activation_result else "trial_activation_failed" message_key = activation_result.get(
"message_key", "trial_activation_failed"
) if activation_result else "trial_activation_failed"
final_message_text_in_chat = _(message_key) final_message_text_in_chat = _(message_key)
await callback.answer(final_message_text_in_chat, show_alert=True) await callback.answer(final_message_text_in_chat, show_alert=True)
if callback.message: if callback.message:
try: try:
await callback .message .edit_text (final_message_text_in_chat , await callback.message.edit_text(
final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_button_after_action ), reply_markup=get_main_menu_inline_keyboard(
current_lang, i18n, settings,
show_trial_button_after_action),
disable_web_page_preview=True) disable_web_page_preview=True)
except Exception as e_edit: except Exception as e_edit:
logging.warning(f"Could not edit trial result message: {e_edit}") logging.warning(f"Could not edit trial result message: {e_edit}")
await callback .message .answer (final_message_text_in_chat , await callback.message.answer(
final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup =get_main_menu_inline_keyboard (current_lang ,i18n ,settings ,show_trial_button_after_action ), reply_markup=get_main_menu_inline_keyboard(
current_lang, i18n, settings,
show_trial_button_after_action),
disable_web_page_preview=True) disable_web_page_preview=True)
@router.callback_query(F.data == "main_action:cancel_trial") @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 ): async def cancel_trial_activation(callback: types.CallbackQuery,
settings: Settings, i18n_data: dict,
subscription_service: SubscriptionService):
show_trial_button_on_back = False show_trial_button_on_back = False
if settings .TRIAL_ENABLED and not await subscription_service .has_had_any_subscription (callback .from_user .id ): if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
callback.from_user.id):
show_trial_button_on_back = True 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 ) await send_main_menu(callback,
settings,
i18n_data,
show_trial_button_flag=show_trial_button_on_back,
is_edit=True)
+133 -43
View File
@@ -5,78 +5,168 @@ import math
from config.settings import Settings from config.settings import Settings
def get_admin_panel_keyboard (i18n_instance ,lang :str ,settings :Settings )->InlineKeyboardMarkup :
def get_admin_panel_keyboard(i18n_instance, lang: str,
settings: Settings) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =_ (key ="admin_stats_button"),callback_data ="admin_action:stats") builder.button(text=_(key="admin_stats_button"),
builder .button (text =_ (key ="admin_broadcast_button"),callback_data ="admin_action:broadcast") callback_data="admin_action:stats")
builder .button (text =_ (key ="admin_create_promo_button"),callback_data ="admin_action:create_promo") builder.button(text=_(key="admin_broadcast_button"),
builder .button (text =_ (key ="admin_view_promos_button"),callback_data ="admin_action:view_promos") callback_data="admin_action:broadcast")
builder .button (text =_ (key ="admin_ban_user_button"),callback_data ="admin_action:ban_user_prompt") builder.button(text=_(key="admin_create_promo_button"),
builder .button (text =_ (key ="admin_unban_user_button"),callback_data ="admin_action:unban_user_prompt") callback_data="admin_action:create_promo")
builder .button (text =_ (key ="admin_view_banned_users_button"),callback_data ="admin_action:view_banned:0") builder.button(text=_(key="admin_view_promos_button"),
builder .button (text =_ (key ="admin_view_logs_menu_button"),callback_data ="admin_action:view_logs_menu") callback_data="admin_action:view_promos")
builder .button (text =_ (key ="admin_sync_panel_button"),callback_data ="admin_action:sync_panel") 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) builder.adjust(2, 2, 2, 2, 1)
return builder.as_markup() return builder.as_markup()
def get_logs_menu_keyboard(i18n_instance, lang: str) -> InlineKeyboardMarkup: def get_logs_menu_keyboard(i18n_instance, lang: str) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =_ (key ="admin_view_all_logs_button"),callback_data ="admin_logs:view_all:0") builder.button(text=_(key="admin_view_all_logs_button"),
builder .button (text =_ (key ="admin_view_user_logs_prompt_button"),callback_data ="admin_logs:prompt_user") callback_data="admin_logs:view_all:0")
builder .row (InlineKeyboardButton (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main")) 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() 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 :
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) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
row_buttons = [] row_buttons = []
if current_page > 0: if current_page > 0:
row_buttons .append (InlineKeyboardButton (text ="⬅️ "+_ ("prev_page_button",default ="Prev"),callback_data =f"{base_callback_data}:{current_page - 1}")) 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: 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}")) 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) if row_buttons: builder.row(*row_buttons)
if back_to_logs_menu: if back_to_logs_menu:
builder .row (InlineKeyboardButton (text =_ (key ="admin_logs_menu_title"),callback_data ="admin_action:view_logs_menu")) builder.row(
InlineKeyboardButton(text=_(key="admin_logs_menu_title"),
callback_data="admin_action:view_logs_menu"))
else: else:
builder .row (InlineKeyboardButton (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main")) builder.row(
InlineKeyboardButton(text=_(key="back_to_admin_panel_button"),
callback_data="admin_action:main"))
return builder.as_markup() 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 () 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 page_size = settings.LOGS_PAGE_SIZE
total_pages =math .ceil (total_users_banned /page_size )if page_size >0 else 1 total_pages = math.ceil(total_users_banned /
page_size) if page_size > 0 else 1
if total_users_banned == 0: total_pages = 1 if total_users_banned == 0: total_pages = 1
for user_row in users: for user_row in users:
user_display =user_row ['first_name']or f"ID: {user_row['user_id']}"; user_display = user_row['first_name'] or f"ID: {user_row['user_id']}"
if user_row['username']: user_display += f" (@{user_row['username']})" 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 else:
builder .button (text =user_display ,callback_data =f"admin_user_card:{user_row['user_id']}:{current_page}") 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 = [] 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 > 0:
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}")) 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) 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 () 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 : def get_user_card_keyboard(user_id: int,
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder () is_banned: bool,
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 () 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_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 : def get_confirmation_keyboard(yes_callback_data: str, no_callback_data: str,
_ =lambda key ,**kwargs :i18n_instance .gettext (lang ,key ,**kwargs );builder =InlineKeyboardBuilder () i18n_instance,
builder .button (text =_ (key ="back_to_admin_panel_button"),callback_data ="admin_action:main");return builder .as_markup () 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()
+84 -44
View File
@@ -9,108 +9,148 @@ def get_main_menu_inline_keyboard (
lang: str, lang: str,
i18n_instance, i18n_instance,
settings: Settings, settings: Settings,
show_trial_button :bool =False show_trial_button: bool = False) -> InlineKeyboardMarkup:
)->InlineKeyboardMarkup :
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if show_trial_button and settings.TRIAL_ENABLED: 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_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_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(
builder .row (InlineKeyboardButton (text =_ (key ="menu_my_subscription_inline"),callback_data ="main_action:my_subscription")) text=_(key="menu_referral_inline"),
callback_data="main_action:referral")
promo_button = InlineKeyboardButton(
referral_button =InlineKeyboardButton (text =_ (key ="menu_referral_inline"),callback_data ="main_action:referral") text=_(key="menu_apply_promo_button"),
promo_button =InlineKeyboardButton (text =_ (key ="menu_apply_promo_button"),callback_data ="main_action:apply_promo") callback_data="main_action:apply_promo")
builder.row(referral_button, promo_button) builder.row(referral_button, promo_button)
language_button = InlineKeyboardButton(
language_button =InlineKeyboardButton (text =_ (key ="menu_language_settings_inline"),callback_data ="main_action:language") text=_(key="menu_language_settings_inline"),
callback_data="main_action:language")
status_button_list = [] status_button_list = []
if settings.SERVER_STATUS_URL: if settings.SERVER_STATUS_URL:
status_button_list .append (InlineKeyboardButton (text =_ (key ="menu_server_status_button"),url =settings .SERVER_STATUS_URL )) status_button_list.append(
InlineKeyboardButton(text=_(key="menu_server_status_button"),
url=settings.SERVER_STATUS_URL))
if status_button_list: if status_button_list:
builder.row(language_button, *status_button_list) builder.row(language_button, *status_button_list)
else: else:
builder.row(language_button) builder.row(language_button)
if settings.SUPPORT_LINK: if settings.SUPPORT_LINK:
builder .row (InlineKeyboardButton (text =_ (key ="menu_support_button"),url =settings .SUPPORT_LINK )) builder.row(
InlineKeyboardButton(text=_(key="menu_support_button"),
url=settings.SUPPORT_LINK))
return builder.as_markup() return builder.as_markup()
def get_language_selection_keyboard(i18n_instance,
def get_language_selection_keyboard (i18n_instance ,current_lang :str )->InlineKeyboardMarkup : current_lang: str) -> InlineKeyboardMarkup:
_ =lambda key ,**kwargs :i18n_instance .gettext (current_lang ,key ,**kwargs ) _ = lambda key, **kwargs: i18n_instance.gettext(current_lang, key, **kwargs
)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =f"🇬🇧 English {'' if current_lang == 'en' else ''}",callback_data ="set_lang_en") builder.button(text=f"🇬🇧 English {'' if current_lang == 'en' else ''}",
builder .button (text =f"🇷🇺 Русский {'' if current_lang == 'ru' else ''}",callback_data ="set_lang_ru") callback_data="set_lang_en")
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main") 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) builder.adjust(1)
return builder.as_markup() return builder.as_markup()
def get_trial_confirmation_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
def get_trial_confirmation_keyboard(lang: str,
i18n_instance) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =_ (key ="trial_confirm_activate_button"),callback_data ="trial_action:confirm_activate") builder.button(text=_(key="trial_confirm_activate_button"),
builder .button (text =_ (key ="cancel_button"),callback_data ="main_action:back_to_main") callback_data="trial_action:confirm_activate")
builder.button(text=_(key="cancel_button"),
callback_data="main_action:back_to_main")
builder.adjust(1) builder.adjust(1)
return builder.as_markup() return builder.as_markup()
def get_subscription_options_keyboard (subscription_options :Dict [int ,Optional [int ]],currency_symbol_val :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
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) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if subscription_options: if subscription_options:
for months, price in subscription_options.items(): for months, price in subscription_options.items():
if price is not None: if price is not None:
button_text =_ ("subscribe_for_months_button",months =months ,price =price ,currency_symbol =currency_symbol_val ) button_text = _("subscribe_for_months_button",
builder .button (text =button_text ,callback_data =f"subscribe_period:{months}") months=months,
price=price,
currency_symbol=currency_symbol_val)
builder.button(text=button_text,
callback_data=f"subscribe_period:{months}")
builder.adjust(1) builder.adjust(1)
builder .row (InlineKeyboardButton (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main")) builder.row(
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
callback_data="main_action:back_to_main"))
return builder.as_markup() return builder.as_markup()
def get_confirm_subscription_keyboard (months :int ,price :float ,currency_symbol_val :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
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) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
confirm_text =_ (key ="confirm_payment_button",price =price ,currency_symbol =currency_symbol_val ) confirm_text = _(key="confirm_payment_button",
builder .button (text =confirm_text ,callback_data =f"confirm_sub:{months}:{price}") price=price,
builder .button (text =_ (key ="cancel_button"),callback_data ="main_action:subscribe") 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) builder.adjust(1)
return builder.as_markup() return builder.as_markup()
def get_payment_url_keyboard (payment_url :str ,lang :str ,i18n_instance )->InlineKeyboardMarkup :
def get_payment_url_keyboard(payment_url: str, lang: str,
i18n_instance) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.button(text=_(key="pay_button"), url=payment_url) 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.button(text=_(key="back_to_main_menu_button"),
callback_data="main_action:back_to_main")
builder.adjust(1) builder.adjust(1)
return builder.as_markup() return builder.as_markup()
def get_referral_link_keyboard (lang :str ,i18n_instance )->InlineKeyboardMarkup :
def get_referral_link_keyboard(lang: str,
i18n_instance) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main") 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_back_to_main_menu_markup (lang :str ,i18n_instance )->InlineKeyboardMarkup :
def get_back_to_main_menu_markup(lang: str,
i18n_instance) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder .button (text =_ (key ="back_to_main_menu_button"),callback_data ="main_action:back_to_main") 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_user_banned_keyboard (support_link :Optional [str ],lang :str ,i18n_instance )->Optional [InlineKeyboardMarkup ]:
def get_user_banned_keyboard(support_link: Optional[str], lang: str,
i18n_instance) -> Optional[InlineKeyboardMarkup]:
if not support_link: if not support_link:
return None return None
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+189 -66
View File
@@ -10,7 +10,6 @@ from aiogram .fsm .storage .memory import MemoryStorage
from aiohttp import web from aiohttp import web
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from config.settings import Settings, get_settings from config.settings import Settings, get_settings
from .middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n from .middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n
from .middlewares.ban_check_middleware import BanCheckMiddleware from .middlewares.ban_check_middleware import BanCheckMiddleware
@@ -34,7 +33,8 @@ from .services .promo_code_service import PromoCodeService
async def register_all_routers(dp: Dispatcher, settings: Settings): async def register_all_routers(dp: Dispatcher, settings: Settings):
dp.include_router(user_router_aggregate) dp.include_router(user_router_aggregate)
admin_filtered_router_wrapper =Router (name ="admin_filtered_router_wrapper") admin_filtered_router_wrapper = Router(
name="admin_filtered_router_wrapper")
admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS) admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS)
admin_filtered_router_wrapper.message.filter(admin_filter_instance) admin_filtered_router_wrapper.message.filter(admin_filter_instance)
admin_filtered_router_wrapper.callback_query.filter(admin_filter_instance) admin_filtered_router_wrapper.callback_query.filter(admin_filter_instance)
@@ -42,43 +42,94 @@ async def register_all_routers (dp :Dispatcher ,settings :Settings ):
dp.include_router(admin_filtered_router_wrapper) dp.include_router(admin_filtered_router_wrapper)
logging.info("All application routers registered.") 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"] bot: Bot = dispatcher["bot_instance"]
settings: Settings = dispatcher["settings"]
i18n_instance: JsonI18n = dispatcher["i18n_instance"]
logging.info("STARTUP: on_startup_configured executing...") logging.info("STARTUP: on_startup_configured executing...")
scheduler = AsyncIOScheduler(timezone="UTC") 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: 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)}") await schedule_subscription_notifications(bot, settings, i18n_instance,
if full_telegram_webhook_url !="ERROR_URL": scheduler)
set_success =await bot .set_webhook (url =full_telegram_webhook_url ,drop_pending_updates =True ,allowed_updates =dispatcher .resolve_used_update_types ()) scheduler.start()
if set_success :logging .info (f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True).") dispatcher["scheduler"] = scheduler
else :logging .error (f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False).") logging.info("STARTUP: APScheduler started.")
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)}") except Exception as e:
if not new_webhook_info .url :logging .error ("STARTUP: CRITICAL - Webhook URL EMPTY after set attempt.") logging.error(f"STARTUP: Failed to start APScheduler: {e}",
else :logging .error ("STARTUP: Skipped setting webhook due to URL config error.") exc_info=True)
except Exception as e_setwebhook :logging .error (f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",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: else:
logging .info ("STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set. Attempting to delete webhook.");await bot .delete_webhook (drop_pending_updates =True ) 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.") 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") logging.warning("SHUTDOWN: on_shutdown_configured executing...")
scheduler: AsyncIOScheduler = dispatcher.get("scheduler")
if scheduler and scheduler.running: if scheduler and scheduler.running:
try :scheduler .shutdown (wait =False );logging .info ("SHUTDOWN: APScheduler shut down.") try:
except Exception as e :logging .error (f"SHUTDOWN: Error APScheduler: {e}",exc_info =True ) 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") 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 () if panel_service and panel_service._session and not panel_service._session.closed:
await panel_service.close_session()
bot: Bot = dispatcher.get("bot_instance") bot: Bot = dispatcher.get("bot_instance")
if bot and bot .session and not bot .session .closed :await bot .session .close () if bot and bot.session and not bot.session.closed:
await bot.session.close()
logging.info("SHUTDOWN: Bot on_shutdown_configured completed.") logging.info("SHUTDOWN: Bot on_shutdown_configured completed.")
@@ -95,77 +146,149 @@ async def run_bot (settings_param :Settings ):
actual_bot_username = bot_info.username actual_bot_username = bot_info.username
logging.info(f"Bot username: @{actual_bot_username}") logging.info(f"Bot username: @{actual_bot_username}")
except Exception as e: except Exception as e:
logging .error (f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}") logging.error(
f"Failed to get bot info: {e}. Using fallback username: {actual_bot_username}"
)
default_lang = settings_param.DEFAULT_LANGUAGE default_lang = settings_param.DEFAULT_LANGUAGE
i18n_instance = get_i18n_instance(path="locales", default=default_lang) i18n_instance = get_i18n_instance(path="locales", default=default_lang)
yookassa_service = YooKassaService( yookassa_service = YooKassaService(
shop_id=settings_param.YOOKASSA_SHOP_ID, shop_id=settings_param.YOOKASSA_SHOP_ID,
secret_key=settings_param.YOOKASSA_SECRET_KEY, secret_key=settings_param.YOOKASSA_SECRET_KEY,
configured_return_url=settings_param.YOOKASSA_RETURN_URL, configured_return_url=settings_param.YOOKASSA_RETURN_URL,
bot_username_for_default=actual_bot_username, bot_username_for_default=actual_bot_username,
settings_obj =settings_param settings_obj=settings_param)
)
panel_service = PanelApiService(settings_param) panel_service = PanelApiService(settings_param)
subscription_service =SubscriptionService (get_db_connection_manager ,settings_param ,panel_service ) subscription_service = SubscriptionService(get_db_connection_manager,
referral_service =ReferralService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance ) settings_param, panel_service)
promo_code_service =PromoCodeService (get_db_connection_manager ,settings_param ,subscription_service ,bot ,i18n_instance ) 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_instance"] = i18n_instance
dp ["i18n_data"]={"i18n_instance":i18n_instance ,"current_language":default_lang } dp["i18n_data"] = {
"i18n_instance": i18n_instance,
"current_language": default_lang
}
dp["yookassa_service"] = yookassa_service dp["yookassa_service"] = yookassa_service
dp["panel_service"] = panel_service dp["panel_service"] = panel_service
dp["subscription_service"] = subscription_service dp["subscription_service"] = subscription_service
dp["referral_service"] = referral_service dp["referral_service"] = referral_service
dp["promo_code_service"] = promo_code_service dp["promo_code_service"] = promo_code_service
dp.update.outer_middleware(
I18nMiddleware(i18n=i18n_instance, settings=settings_param))
dp .update .outer_middleware (I18nMiddleware (i18n =i18n_instance ,settings =settings_param )) dp.update.outer_middleware(
dp .update .outer_middleware (BanCheckMiddleware (settings =settings_param ,i18n_instance =i18n_instance )) BanCheckMiddleware(settings=settings_param,
i18n_instance=i18n_instance))
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param)) dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param))
dp.startup.register(on_startup_configured) dp.startup.register(on_startup_configured)
dp.shutdown.register(on_shutdown_configured) dp.shutdown.register(on_shutdown_configured)
await register_all_routers(dp, settings_param) await register_all_routers(dp, settings_param)
tg_webhook_base = getattr(settings_param, 'TELEGRAM_WEBHOOK_BASE_URL',
tg_webhook_base =getattr (settings_param ,'TELEGRAM_WEBHOOK_BASE_URL',None );yk_webhook_base =getattr (settings_param ,'YOOKASSA_WEBHOOK_BASE_URL',None ) 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}'") yk_webhook_base = getattr(settings_param, 'YOOKASSA_WEBHOOK_BASE_URL',
should_run_aiohttp =bool (yk_webhook_base and settings_param .yookassa_webhook_path )or bool (tg_webhook_base ) None)
telegram_uses_webhook =bool (tg_webhook_base );telegram_should_poll =not telegram_uses_webhook logging.info(f"--- Determining Run Mode ---")
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 ---") logging.info(f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}'")
web_app_runner =None ;main_tasks_to_await =[] 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: if should_run_aiohttp:
app =web .Application ();app ['bot']=bot ;app ['dp']=dp ;app ['settings']=settings_param ;app ['i18n']=i18n_instance app = web.Application()
app ['yookassa_service']=yookassa_service ;app ['panel_service']=panel_service ;app ['subscription_service']=subscription_service app['bot'] = bot
app ['referral_service']=referral_service ;app ['promo_code_service']=promo_code_service app['dp'] = dp
setup_application (app ,dp ,bot =bot );logging .info ("AIOHTTP app context populated and dispatcher lifecycle linked.") app['settings'] = settings_param
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}.") app['i18n'] = i18n_instance
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}") app['yookassa_service'] = yookassa_service
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 ) app['panel_service'] = panel_service
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 ) app['subscription_service'] = subscription_service
main_tasks_to_await .append (asyncio .create_task (web_server_task_wrapper (),name ="AIOHTTPServerWrapperTask")) app['referral_service'] = referral_service
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")) app['promo_code_service'] = promo_code_service
if not main_tasks_to_await :logging .error ("Bot not configured for any mode.");await dp .emit_shutdown ();return setup_application(app, dp, bot=bot)
logging .info (f"Starting bot with main tasks: {[task.get_name() for task in main_tasks_to_await]}") 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: try:
if main_tasks_to_await: await asyncio.gather(*main_tasks_to_await) 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__}") except (KeyboardInterrupt, SystemExit, asyncio.CancelledError) as e:
logging.info(f"Main loop interrupted: {type(e).__name__}")
finally: finally:
logging .info ("Initiating final shutdown sequence..."); logging.info("Initiating final shutdown sequence...")
for task in main_tasks_to_await: for task in main_tasks_to_await:
if task and not task .done ():task .cancel (); if task and not task.done(): task.cancel()
try :await task try:
except asyncio .CancelledError :logging .info (f"Task '{task.get_name()}' cancelled.") await task
except Exception as e_cancel :logging .error (f"Error cancelling task '{task.get_name()}': {e_cancel}",exc_info =True ) except asyncio.CancelledError:
if web_app_runner :await web_app_runner .cleanup ();logging .info ("AIOHTTP AppRunner cleaned up.") logging.info(f"Task '{task.get_name()}' cancelled.")
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 ())): except Exception as e_cancel:
logging .info ("Explicitly calling dp.emit_shutdown() in run_bot finally.") 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() await dp.emit_shutdown()
if bot.session and not bot.session.closed: await bot.session.close() if bot.session and not bot.session.closed: await bot.session.close()
logging.info("Bot run_bot function finished.") logging.info("Bot run_bot function finished.")
+22 -26
View File
@@ -8,17 +8,16 @@ from aiogram .types import Update ,Message ,CallbackQuery ,User
from db.database import log_user_action from db.database import log_user_action
from config.settings import Settings from config.settings import Settings
class ActionLoggerMiddleware(BaseMiddleware): class ActionLoggerMiddleware(BaseMiddleware):
def __init__(self, settings: Settings): def __init__(self, settings: Settings):
super().__init__() super().__init__()
self.settings = settings self.settings = settings
async def __call__ ( async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
self , Awaitable[Any]], event: Update,
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]], data: Dict[str, Any]) -> Any:
event :Update ,
data :Dict [str ,Any ]
)->Any :
event_user: Optional[User] = data.get("event_from_user") event_user: Optional[User] = data.get("event_from_user")
bot: Optional[Bot] = data.get("bot") bot: Optional[Bot] = data.get("bot")
@@ -40,11 +39,11 @@ class ActionLoggerMiddleware (BaseMiddleware ):
raw_update_snippet = None raw_update_snippet = None
try: try:
raw_update_snippet =event .model_dump_json (exclude_none =True ,indent =None )[:1000 ] raw_update_snippet = event.model_dump_json(exclude_none=True,
indent=None)[:1000]
except Exception: except Exception:
raw_update_snippet = str(event)[:1000] raw_update_snippet = str(event)[:1000]
if event.message: if event.message:
msg = event.message msg = event.message
if msg.text: if msg.text:
@@ -61,34 +60,31 @@ class ActionLoggerMiddleware (BaseMiddleware ):
event_type = "callback_query" event_type = "callback_query"
content = cb.data content = cb.data
if user_id and event_type and content: if user_id and event_type and content:
try: try:
await log_user_action ( await log_user_action(user_id=user_id,
user_id =user_id ,
telegram_username=telegram_username, telegram_username=telegram_username,
telegram_first_name=telegram_first_name, telegram_first_name=telegram_first_name,
event_type=event_type, event_type=event_type,
content=content[:1000], content=content[:1000],
raw_update_preview=raw_update_snippet, raw_update_preview=raw_update_snippet,
is_admin_event =is_admin_event_flag is_admin_event=is_admin_event_flag)
)
except Exception as e_log: except Exception as e_log:
logging .error (f"ActionLoggerMiddleware: Failed to log event for user {user_id}: {e_log}",exc_info =True ) logging.error(
f"ActionLoggerMiddleware: Failed to log event for user {user_id}: {e_log}",
exc_info=True)
elif user_id and event_type: elif user_id and event_type:
try: 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 ) 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: except Exception as e_log:
logging .error (f"ActionLoggerMiddleware: Failed to log event (no content) for user {user_id}: {e_log}",exc_info =True ) 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)
+132 -52
View File
@@ -11,33 +11,35 @@ from db .database import get_user
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.keyboards.inline.user_keyboards import get_user_banned_keyboard from bot.keyboards.inline.user_keyboards import get_user_banned_keyboard
class BanCheckMiddleware(BaseMiddleware): class BanCheckMiddleware(BaseMiddleware):
def __init__(self, settings: Settings, i18n_instance: JsonI18n): def __init__(self, settings: Settings, i18n_instance: JsonI18n):
super().__init__() super().__init__()
self.settings = settings self.settings = settings
self.i18n_main_instance = i18n_instance self.i18n_main_instance = i18n_instance
async def __call__ ( async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
self , Awaitable[Any]], event: Update,
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]], data: Dict[str, Any]) -> Any:
event :Update ,
data :Dict [str ,Any ]
)->Any :
event_user: Optional[User] = data.get("event_from_user") event_user: Optional[User] = data.get("event_from_user")
if not event_user: return await handler(event, data) if not event_user: return await handler(event, data)
if event_user.id in self.settings.ADMIN_IDS: if event_user.id in self.settings.ADMIN_IDS:
return await handler(event, data) return await handler(event, data)
try: try:
db_user_data = await get_user(user_id=event_user.id) db_user_data = await get_user(user_id=event_user.id)
except Exception as e_db: except Exception as e_db:
logging .error (f"BanCheckMiddleware: DB error fetching user {event_user.id}: {e_db}",exc_info =True ) logging.error(
f"BanCheckMiddleware: DB error fetching user {event_user.id}: {e_db}",
exc_info=True)
return await handler(event, data) return await handler(event, data)
if db_user_data and db_user_data['is_banned'] == 1: 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.") 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." ban_message_text = "You are blocked. Please contact support."
keyboard: Optional[InlineKeyboardMarkup] = None keyboard: Optional[InlineKeyboardMarkup] = None
@@ -45,88 +47,166 @@ class BanCheckMiddleware (BaseMiddleware ):
i18n_to_use: Optional[JsonI18n] = None i18n_to_use: Optional[JsonI18n] = None
actual_event_object: Optional[Union[Message, CallbackQuery]] = None actual_event_object: Optional[Union[Message, CallbackQuery]] = None
if event.message: if event.message:
actual_event_object = event.message actual_event_object = event.message
elif event.callback_query: elif event.callback_query:
actual_event_object = event.callback_query actual_event_object = event.callback_query
if not actual_event_object: 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}") 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: try:
bot_instance: Bot = data["bot"] bot_instance: Bot = data["bot"]
await bot_instance .send_message (event_user .id ,ban_message_text ) await bot_instance.send_message(event_user.id,
ban_message_text)
except Exception as e_direct_send: except Exception as e_direct_send:
logging .error (f"BanCheck: Failed to send direct ban message to {event_user.id}: {e_direct_send}") logging.error(
f"BanCheck: Failed to send direct ban message to {event_user.id}: {e_direct_send}"
)
return return
try: try:
logging .debug ("BanCheck: [A] Inside main try block for banned user notification.") logging.debug(
"BanCheck: [A] Inside main try block for banned user notification."
)
i18n_data_from_event = data.get("i18n_data", {}) i18n_data_from_event = data.get("i18n_data", {})
current_lang =i18n_data_from_event .get ("current_language",self .settings .DEFAULT_LANGUAGE ) current_lang = i18n_data_from_event.get(
"current_language", self.settings.DEFAULT_LANGUAGE)
i18n_to_use = i18n_data_from_event.get("i18n_instance") 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}") logging.debug(
f"BanCheck: [B] i18n_instance from event_data: {type(i18n_to_use)}. Current lang: {current_lang}"
)
if not i18n_to_use: if not i18n_to_use:
i18n_to_use = self.i18n_main_instance 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)}") 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: if i18n_to_use:
_ =lambda k ,**kw :i18n_to_use .gettext (current_lang ,k ,**kw ) _ = lambda k, **kw: i18n_to_use.gettext(
logging .debug ("BanCheck: [D] Attempting to get 'user_is_banned' text.") current_lang, k, **kw)
logging.debug(
"BanCheck: [D] Attempting to get 'user_is_banned' text."
)
ban_message_text = _("user_is_banned") ban_message_text = _("user_is_banned")
logging .debug (f"BanCheck: [E] Ban message text: '{ban_message_text}'") logging.debug(
f"BanCheck: [E] Ban message text: '{ban_message_text}'"
)
if self.settings.SUPPORT_LINK: if self.settings.SUPPORT_LINK:
logging .debug ("BanCheck: [F] Support link found. Attempting to get user_banned_keyboard.") logging.debug(
keyboard =get_user_banned_keyboard (self .settings .SUPPORT_LINK ,current_lang ,i18n_to_use ) "BanCheck: [F] Support link found. Attempting to get user_banned_keyboard."
logging .debug (f"BanCheck: [G] Keyboard created: {keyboard is not None}") )
else :logging .debug ("BanCheck: [F_alt] No support link configured.") 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: else:
logging .error (f"BanCheck: [CRITICAL] No i18n instance for user {event_user.id}. Using hardcoded text.") 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: if self.settings.SUPPORT_LINK:
kb_temp =InlineKeyboardBuilder ();kb_temp .button (text ="Support",url =self .settings .SUPPORT_LINK );keyboard =kb_temp .as_markup () kb_temp = InlineKeyboardBuilder()
logging .debug ("BanCheck: [G_alt] Fallback keyboard created due to no i18n.") kb_temp.button(text="Support",
url=self.settings.SUPPORT_LINK)
logging .debug (f"BanCheck: [H] Final pre-send check. Message: '{ban_message_text}', Keyboard: {keyboard is not None}") 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}"
)
if isinstance(actual_event_object, Message): if isinstance(actual_event_object, Message):
logging .debug (f"BanCheck: [I_Msg] Attempting actual_event_object.answer for Message to user {event_user.id}") logging.debug(
await actual_event_object .answer (ban_message_text ,reply_markup =keyboard ) f"BanCheck: [I_Msg] Attempting actual_event_object.answer for Message to user {event_user.id}"
logging .info (f"BanCheck: [J_Msg] Ban notification 'actual_event_object.answer' attempted for user {event_user.id} (Message).") )
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): 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}") logging.debug(
await actual_event_object .answer (ban_message_text ,show_alert =True ) f"BanCheck: [I_CB] Attempting actual_event_object.answer (alert) for CallbackQuery to user {event_user.id}"
logging .info (f"BanCheck: [J_CB] Ban alert 'actual_event_object.answer' attempted for user {event_user.id} (CallbackQuery).") )
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 target_message_obj = actual_event_object.message
if target_message_obj: if target_message_obj:
target_chat_id = target_message_obj.chat.id target_chat_id = target_message_obj.chat.id
try: try:
logging .debug (f"BanCheck: [K_CB_Edit] Attempting target_message_obj.edit_text for user {event_user.id}") logging.debug(
await target_message_obj .edit_text (ban_message_text ,reply_markup =keyboard ) f"BanCheck: [K_CB_Edit] Attempting target_message_obj.edit_text for user {event_user.id}"
logging .info (f"BanCheck: [L_CB_Edit] Ban msg 'target_message_obj.edit_text' attempted for user {event_user.id} (Callback).") )
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: 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.") logging.warning(
await actual_event_object .bot .send_message (target_chat_id ,ban_message_text ,reply_markup =keyboard ) f"BanCheck: [M_CB_EditFail] Failed to edit message for banned user {event_user.id}: {type(e_edit).__name__} - {e_edit}. Sending new message."
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).") )
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: else:
logging .warning (f"BanCheck: [K_CB_NoMsg] CallbackQuery from {event_user.id} has no .message attribute. Sending new message directly.") logging.warning(
await actual_event_object .bot .send_message (actual_event_object .from_user .id ,ban_message_text ,reply_markup =keyboard ) f"BanCheck: [K_CB_NoMsg] CallbackQuery from {event_user.id} has no .message attribute. Sending new message directly."
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).") )
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: else:
logging .error (f"BanCheck: [UNHANDLED_EVENT_TYPE_INTERNAL] actual_event_object type {type(actual_event_object)} was not Message or CallbackQuery.") 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 TelegramForbiddenError as 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 ) logging.warning(
except TelegramAPIError as e_api :logging .error (f"BanCheck: TelegramAPIError sending ban msg to {event_user.id}: {e_api}",exc_info =True ) f"BanCheck: TelegramForbiddenError sending ban msg to {event_user.id}: {e_forbidden}"
except AiogramError as e_aio :logging .error (f"BanCheck: AiogramError sending ban msg to {event_user.id}: {e_aio}",exc_info =True ) )
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: 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.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
return await handler(event, data) return await handler(event, data)
+76 -33
View File
@@ -7,30 +7,37 @@ from typing import Any ,Awaitable ,Callable ,Dict ,Optional
from aiogram import BaseMiddleware 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 db.database import get_user, get_db_connection_manager, _setup_db_connection
from config.settings import Settings from config.settings import Settings
class JsonI18n: class JsonI18n:
def __init__(self, path: str, default: str = "en", domain: str = "bot"): def __init__(self, path: str, default: str = "en", domain: str = "bot"):
self.domain = domain self.domain = domain
self.path = path self.path = path
self.default_lang = default self.default_lang = default
self.locales_data: Dict[str, Dict[str, str]] = {} self.locales_data: Dict[str, Dict[str, str]] = {}
self._load_locales() self._load_locales()
logging .info (f"JsonI18n initialized. Loaded languages: {list(self.locales_data.keys())}. Default: {self.default_lang}") logging.info(
f"JsonI18n initialized. Loaded languages: {list(self.locales_data.keys())}. Default: {self.default_lang}"
)
def _load_locales(self): def _load_locales(self):
if not os .path .isdir (self .path ):logging .error (f"Locales path not found: {self.path}");return if not os.path.isdir(self.path):
logging.error(f"Locales path not found: {self.path}")
return
for item in os.listdir(self.path): for item in os.listdir(self.path):
if item.endswith(".json"): if item.endswith(".json"):
lang_code =item .split (".")[0 ];file_path =os .path .join (self .path ,item ) lang_code = item.split(".")[0]
file_path = os.path.join(self.path, item)
try: try:
with open (file_path ,"r",encoding ="utf-8")as f :self .locales_data [lang_code ]=json .load (f ) with open(file_path, "r", encoding="utf-8") as f:
except Exception as e :logging .error (f"Error loading locale {lang_code} from {file_path}: {e}") 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: def gettext(self, lang_code: Optional[str], key: str, **kwargs) -> str:
effective_lang_code = lang_code effective_lang_code = lang_code
@@ -38,76 +45,112 @@ class JsonI18n :
effective_lang_code = self.default_lang effective_lang_code = self.default_lang
lang_data = self.locales_data.get(effective_lang_code) lang_data = self.locales_data.get(effective_lang_code)
if lang_data is None: 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 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) text = lang_data.get(key)
if text is None: if text is None:
if effective_lang_code != self.default_lang: if effective_lang_code != self.default_lang:
default_lang_data =self .locales_data .get (self .default_lang ,{});text =default_lang_data .get (key ) 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 if text is None: return key.format(**kwargs) if kwargs else key
try :return text .format (**kwargs )if kwargs else text try:
except KeyError as e :logging .warning (f"Missing format key {e} for key '{key}' (lang: {effective_lang_code}). Text: '{text}'");return text return text.format(**kwargs) if kwargs else text
except Exception as e :logging .error (f"Error formatting key '{key}' (lang: {effective_lang_code}): {e}. Text: '{text}'");return 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 _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 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 return _i18n_instance
class I18nMiddleware(BaseMiddleware): class I18nMiddleware(BaseMiddleware):
def __init__(self, i18n: JsonI18n, settings: Settings): def __init__(self, i18n: JsonI18n, settings: Settings):
super().__init__() super().__init__()
self.i18n = i18n self.i18n = i18n
self.settings = settings self.settings = settings
async def __call__ ( async def __call__(self, handler: Callable[[Update, Dict[str, Any]],
self , Awaitable[Any]], event: Update,
handler :Callable [[Update ,Dict [str ,Any ]],Awaitable [Any ]], data: Dict[str, Any]) -> Any:
event :Update ,
data :Dict [str ,Any ]
)->Any :
event_user: Optional[User] = data.get("event_from_user") event_user: Optional[User] = data.get("event_from_user")
current_language = self.i18n.default_lang current_language = self.i18n.default_lang
if event_user: if event_user:
logging .debug (f"I18nMiddleware: Processing for user {event_user.id}") logging.debug(
f"I18nMiddleware: Processing for user {event_user.id}")
try: try:
user_db_data = await get_user(event_user.id) 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 : 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'] current_language = user_db_data['language_code']
logging .debug (f"I18nMiddleware: User {event_user.id} language loaded from DB: {current_language}") logging.debug(
f"I18nMiddleware: User {event_user.id} language loaded from DB: {current_language}"
)
else: else:
if event_user.language_code: if event_user.language_code:
lang_prefix = event_user.language_code.split('-')[0] lang_prefix = event_user.language_code.split('-')[0]
if lang_prefix in self.i18n.locales_data: if lang_prefix in self.i18n.locales_data:
current_language = lang_prefix current_language = lang_prefix
logging .debug (f"I18nMiddleware: User {event_user.id} language set from Telegram client (prefix): {current_language}") 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: elif event_user.language_code in self.i18n.locales_data:
current_language = event_user.language_code current_language = event_user.language_code
logging .debug (f"I18nMiddleware: User {event_user.id} language set from Telegram client (full): {current_language}") logging.debug(
f"I18nMiddleware: User {event_user.id} language set from Telegram client (full): {current_language}"
)
else: else:
logging .debug (f"I18nMiddleware: User {event_user.id} Telegram client language '{event_user.language_code}' not supported. Using default: {current_language}") logging.debug(
f"I18nMiddleware: User {event_user.id} Telegram client language '{event_user.language_code}' not supported. Using default: {current_language}"
)
else: else:
logging .debug (f"I18nMiddleware: User {event_user.id} has no language_code from Telegram. Using default: {current_language}") logging.debug(
f"I18nMiddleware: User {event_user.id} has no language_code from Telegram. Using default: {current_language}"
)
except Exception as e_db_lang: 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.") 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: if event_user.language_code:
lang_prefix = event_user.language_code.split('-')[0] lang_prefix = event_user.language_code.split('-')[0]
if lang_prefix in self .i18n .locales_data :current_language =lang_prefix if lang_prefix in self.i18n.locales_data:
elif event_user .language_code in self .i18n .locales_data :current_language =event_user .language_code current_language = lang_prefix
elif event_user.language_code in self.i18n.locales_data:
current_language = event_user.language_code
data["i18n_data"] = { data["i18n_data"] = {
"i18n_instance": self.i18n, "i18n_instance": self.i18n,
"current_language": current_language "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)
+42 -25
View File
@@ -12,30 +12,40 @@ from bot .middlewares .i18n import JsonI18n
from .panel_api_service import PanelApiService 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. Sends expiration warnings to users whose subscriptions are ending soon.
This function is called by the scheduler. 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: if not expiring_subs:
logging.info("No subscriptions found ending soon for notification.") logging.info("No subscriptions found ending soon for notification.")
return return
logging .info (f"Found {len(expiring_subs)} subscriptions ending soon for notification.") logging.info(
f"Found {len(expiring_subs)} subscriptions ending soon for notification."
)
for sub_info in expiring_subs: for sub_info in expiring_subs:
user_id = sub_info['user_id'] user_id = sub_info['user_id']
if not user_id: if not user_id:
logging .warning (f"Skipping notification for subscription without user_id: {sub_info}") logging.warning(
f"Skipping notification for subscription without user_id: {sub_info}"
)
continue continue
user_lang =sub_info .get ('language_code')if sub_info .get ('language_code')else getattr (settings ,'DEFAULT_LANGUAGE','en') 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') first_name = sub_info.get('first_name', 'User')
end_date_str = sub_info['end_date_str'] end_date_str = sub_info['end_date_str']
@@ -46,52 +56,59 @@ async def send_expiration_warnings (bot :Bot ,settings :Settings ,i18n :JsonI18n
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) _ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
message_text =_ ( message_text = _("subscription_ending_soon_notification",
"subscription_ending_soon_notification",
user_name=first_name, user_name=first_name,
end_date=end_date_str, end_date=end_date_str,
days_left =days_left_display days_left=days_left_display)
)
try: try:
await bot.send_message(user_id, message_text) await bot.send_message(user_id, message_text)
await sub_service .update_last_notification_sent (user_id ,end_date_str ) await sub_service.update_last_notification_sent(
logging .info (f"Sent expiration warning to user {user_id} for subscription ending {end_date_str}") 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: except Exception as e:
logging .error (f"Failed to send expiration warning to user {user_id}: {e}") logging.error(
f"Failed to send expiration warning to user {user_id}: {e}")
await asyncio.sleep(0.1) 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.""" """Schedules the daily job to send expiration warnings."""
async def job_wrapper(): async def job_wrapper():
panel_service = PanelApiService(settings) panel_service = PanelApiService(settings)
try: try:
await send_expiration_warnings(bot, settings, i18n, panel_service) await send_expiration_warnings(bot, settings, i18n, panel_service)
except Exception as e: except Exception as e:
logging .error (f"Error in scheduled job 'send_expiration_warnings': {e}",exc_info =True ) logging.error(
f"Error in scheduled job 'send_expiration_warnings': {e}",
exc_info=True)
finally: finally:
await panel_service.close_session() await panel_service.close_session()
try: try:
notification_hour = int(settings.SUBSCRIPTION_NOTIFICATION_HOUR_UTC) notification_hour = int(settings.SUBSCRIPTION_NOTIFICATION_HOUR_UTC)
notification_minute =int (settings .SUBSCRIPTION_NOTIFICATION_MINUTE_UTC ) notification_minute = int(
settings.SUBSCRIPTION_NOTIFICATION_MINUTE_UTC)
except (ValueError, TypeError): except (ValueError, TypeError):
logging .warning ("SUBSCRIPTION_NOTIFICATION_HOUR_UTC or MINUTE_UTC is invalid. Defaulting to 9:00 UTC.") logging.warning(
"SUBSCRIPTION_NOTIFICATION_HOUR_UTC or MINUTE_UTC is invalid. Defaulting to 9:00 UTC."
)
notification_hour = 9 notification_hour = 9
notification_minute = 0 notification_minute = 0
scheduler .add_job ( scheduler.add_job(job_wrapper,
job_wrapper ,
'cron', 'cron',
hour=notification_hour, hour=notification_hour,
minute=notification_minute, minute=notification_minute,
name="daily_subscription_expiration_warnings", name="daily_subscription_expiration_warnings",
misfire_grace_time =60 *15 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.")
+119 -43
View File
@@ -5,7 +5,9 @@ from datetime import datetime ,timedelta ,timezone
from config.settings import Settings from config.settings import Settings
class PanelApiService: class PanelApiService:
def __init__(self, settings: Settings): def __init__(self, settings: Settings):
self.settings = settings self.settings = settings
self.base_url = settings.PANEL_API_URL self.base_url = settings.PANEL_API_URL
@@ -34,53 +36,90 @@ class PanelApiService :
if self.api_key: if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}" headers["Authorization"] = f"Bearer {self.api_key}"
return headers return headers
async def _request (self ,method :str ,endpoint :str ,**kwargs )->Optional [Dict [str ,Any ]]: async def _request(self, method: str, endpoint: str,
**kwargs) -> Optional[Dict[str, Any]]:
if not self.base_url: if not self.base_url:
logging.error("Panel API URL not configured.") logging.error("Panel API URL not configured.")
return {"error":True ,"status_code":0 ,"message":"Panel API URL not configured."} return {
"error": True,
"status_code": 0,
"message": "Panel API URL not configured."
}
session = await self._get_session() session = await self._get_session()
headers = await self._prepare_headers() headers = await self._prepare_headers()
if "Authorization" not in headers and self.api_key: if "Authorization" not in headers and self.api_key:
logging .warning (f"Authorization header missing for panel endpoint {endpoint} despite API key being set.") logging.warning(
f"Authorization header missing for panel endpoint {endpoint} despite API key being set."
)
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
json_payload_for_log =kwargs .get ('json')if method in ["POST","PATCH","PUT"]else None json_payload_for_log = kwargs.get('json') if method in [
"POST", "PATCH", "PUT"
] else None
log_prefix = f"Panel API {method} {url}" log_prefix = f"Panel API {method} {url}"
if json_payload_for_log :log_prefix +=f" Payload: {json_payload_for_log}" if json_payload_for_log:
log_prefix += f" Payload: {json_payload_for_log}"
try: try:
async with session .request (method ,url ,headers =headers ,**kwargs )as response : async with session.request(method, url, headers=headers,
**kwargs) as response:
if 200 <= response.status < 300: if 200 <= response.status < 300:
try: try:
data = await response.json() data = await response.json()
logging .debug (f"{log_prefix} - Success ({response.status})") logging.debug(
f"{log_prefix} - Success ({response.status})")
return data return data
except aiohttp.ContentTypeError: except aiohttp.ContentTypeError:
logging .debug (f"{log_prefix} - Success ({response.status}) with non-JSON response.") logging.debug(
return {"status":"success","code":response .status ,"data_text":await response .text ()} f"{log_prefix} - Success ({response.status}) with non-JSON response."
)
return {
"status": "success",
"code": response.status,
"data_text": await response.text()
}
else: else:
try: try:
error_json = await response.json() error_json = await response.json()
logging .error (f"{log_prefix} - Failed ({response.status}): {error_json}") logging.error(
return {"error":True ,"status_code":response .status ,"response":error_json , f"{log_prefix} - Failed ({response.status}): {error_json}"
"message":error_json .get ("message"),"errorCode":error_json .get ("errorCode")} )
return {
"error": True,
"status_code": response.status,
"response": error_json,
"message": error_json.get("message"),
"errorCode": error_json.get("errorCode")
}
except aiohttp.ContentTypeError: except aiohttp.ContentTypeError:
error_text = await response.text() error_text = await response.text()
logging .error (f"{log_prefix} - Failed ({response.status}): {error_text}") logging.error(
return {"error":True ,"status_code":response .status ,"message":error_text } f"{log_prefix} - Failed ({response.status}): {error_text}"
)
return {
"error": True,
"status_code": response.status,
"message": error_text
}
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logging.error(f"Panel API client request error to {url}: {e}") logging.error(f"Panel API client request error to {url}: {e}")
return {"error": True, "status_code": -1, "message": str(e)} return {"error": True, "status_code": -1, "message": str(e)}
except Exception as e: except Exception as e:
logging .error (f"Unexpected Panel API request error to {url}: {e}",exc_info =True ) logging.error(f"Unexpected Panel API request error to {url}: {e}",
return {"error":True ,"status_code":-2 ,"message":f"Unexpected error: {str(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.""" """Fetches users from panel by username."""
if not username: if not username:
logging.warning("get_users_by_filter called without username.") logging.warning("get_users_by_filter called without username.")
@@ -91,9 +130,13 @@ class PanelApiService :
if response_data and not response_data.get("error"): if response_data and not response_data.get("error"):
users_list = response_data.get("response", {}).get("users", []) users_list = response_data.get("response", {}).get("users", [])
logging .info (f"Found {len(users_list)} panel users matching filter: {params}") logging.info(
f"Found {len(users_list)} panel users matching filter: {params}"
)
return users_list 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 return None
async def create_panel_user( async def create_panel_user(
@@ -108,14 +151,24 @@ class PanelApiService :
activate_all_inbounds_default_flag: bool = True activate_all_inbounds_default_flag: bool = True
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if not (6 <=len (username )<=34 and username .replace ('_','').replace ('-','').isalnum ()): 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, _, -)." msg = f"Username '{username}' for panel does not meet requirements (6-34 chars, alphanumeric, _, -)."
logging.error(msg) logging.error(msg)
return {"error":True ,"status_code":400 ,"message":msg ,"response":{"message":msg ,"errorCode":"VALIDATION_ERROR"}} return {
"error": True,
"status_code": 400,
"message": msg,
"response": {
"message": msg,
"errorCode": "VALIDATION_ERROR"
}
}
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
expire_at_dt = now + timedelta(days=default_expire_days) expire_at_dt = now + timedelta(days=default_expire_days)
expire_at_iso =expire_at_dt .isoformat (timespec ='milliseconds').replace ('+00:00','Z') expire_at_iso = expire_at_dt.isoformat(
timespec='milliseconds').replace('+00:00', 'Z')
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"username": username, "username": username,
@@ -126,8 +179,6 @@ class PanelApiService :
if specific_inbound_uuids: if specific_inbound_uuids:
payload["activeUserInbounds"] = specific_inbound_uuids payload["activeUserInbounds"] = specific_inbound_uuids
payload["activateAllInbounds"] = False payload["activateAllInbounds"] = False
else: else:
payload["activateAllInbounds"] = activate_all_inbounds_default_flag payload["activateAllInbounds"] = activate_all_inbounds_default_flag
@@ -137,47 +188,72 @@ class PanelApiService :
return await self._request("POST", "/users", json=payload) return await self._request("POST", "/users", json=payload)
async def update_user_details_on_panel (self ,user_uuid :str ,update_payload :Dict [str ,Any ])->Optional [Dict [str ,Any ]]: 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 if 'uuid' not in update_payload: update_payload['uuid'] = user_uuid
update_payload.pop('activateAllInbounds', None) update_payload.pop('activateAllInbounds', None)
full_response =await self ._request ("PATCH","/users",json =update_payload ) full_response = await self._request("PATCH",
if full_response and not full_response .get ("error")and full_response .get ("response"): "/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.") logging.info(f"User {user_uuid} details updated on panel.")
return full_response.get("response") return full_response.get("response")
logging .error (f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Resp: {full_response}") logging.error(
f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Resp: {full_response}"
)
return None return None
async def get_all_panel_users (self ,page_size :int =100 )->Optional [List [Dict [str ,Any ]]]: async def get_all_panel_users(self,
all_users =[];start_offset =0 page_size: int = 100
) -> Optional[List[Dict[str, Any]]]:
all_users = []
start_offset = 0
while True: while True:
params = {"size": page_size, "start": start_offset} params = {"size": page_size, "start": start_offset}
response_data = await self._request("GET", "/users", params=params) 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 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", []) users_batch = response_data.get("response", {}).get("users", [])
if not users_batch: break if not users_batch: break
all_users.extend(users_batch) all_users.extend(users_batch)
if len(users_batch) < page_size: break if len(users_batch) < page_size: break
start_offset += page_size start_offset += page_size
logging .info (f"Fetched {len(all_users)} users from panel API.");return all_users 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 ]]: async def get_user_by_uuid(self,
user_uuid: str) -> Optional[Dict[str, Any]]:
full_response = await self._request("GET", f"/users/{user_uuid}") full_response = await self._request("GET", f"/users/{user_uuid}")
if full_response and not full_response .get ("error")and full_response .get ("response"): if full_response and not full_response.get(
"error") and full_response.get("response"):
return full_response.get("response") return full_response.get("response")
return None return None
async def update_user_status_on_panel (self ,user_uuid :str ,enable :bool )->bool : 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'}" endpoint = f"/users/{user_uuid}/actions/{'enable' if enable else 'disable'}"
response_data = await self._request("POST", endpoint) 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"): if response_data and not response_data.get("error") and (
logging .info (f"User {user_uuid} status on panel -> {'enabled' if enable else 'disabled'}.");return True response_data.get("response")
logging .error (f"Failed to update user {user_uuid} status on panel. Resp: {response_data}");return False 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 ]: 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 if not self.settings.PANEL_API_URL: return None
return f"{self.settings.PANEL_API_URL.rstrip('/')}/sub/{short_uuid_or_sub_uuid}" return f"{self.settings.PANEL_API_URL.rstrip('/')}/sub/{short_uuid_or_sub_uuid}"
+107 -63
View File
@@ -3,29 +3,25 @@ import logging
from yookassa import Configuration, Payment from yookassa import Configuration, Payment
from yookassa.domain.request.payment_request_builder import PaymentRequestBuilder from yookassa.domain.request.payment_request_builder import PaymentRequestBuilder
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from config.settings import Settings from config.settings import Settings
class YooKassaService: class YooKassaService:
def __init__ (
self , def __init__(self,
shop_id: Optional[str], shop_id: Optional[str],
secret_key: Optional[str], secret_key: Optional[str],
configured_return_url: Optional[str], configured_return_url: Optional[str],
bot_username_for_default: Optional[str] = None, bot_username_for_default: Optional[str] = None,
settings_obj :Optional [Settings ]=None settings_obj: Optional[Settings] = None):
):
self.settings = settings_obj self.settings = settings_obj
if not shop_id or not secret_key: if not shop_id or not secret_key:
logging .warning ("YooKassa SHOP_ID or SECRET_KEY not configured. Payment functionality will be disabled.") logging.warning(
"YooKassa SHOP_ID or SECRET_KEY not configured. Payment functionality will be disabled."
)
self.configured = False self.configured = False
else: else:
Configuration.account_id = shop_id Configuration.account_id = shop_id
@@ -37,108 +33,156 @@ class YooKassaService :
self.return_url = configured_return_url self.return_url = configured_return_url
elif bot_username_for_default: elif bot_username_for_default:
self.return_url = f"https://t.me/{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}") logging.info(
f"YOOKASSA_RETURN_URL not set, using dynamic default: {self.return_url}"
)
else: else:
self.return_url = "https://example.com/payment_error_no_return_url" 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.warning(
logging .info (f"YooKassa Service effective return_url: {self.return_url}") 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(
async def create_payment (self ,amount :float ,currency :str ,description :str ,metadata :Dict [str ,Any ])->Optional [Dict [str ,Any ]]: self, amount: float, currency: str, description: str,
metadata: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if not self.configured: if not self.configured:
logging.error("YooKassa is not configured. Cannot create payment.") logging.error("YooKassa is not configured. Cannot create payment.")
return None return None
if not self.settings: if not self.settings:
logging .error ("YooKassaService: Settings object not available for receipt creation.") logging.error(
return {"error":True ,"internal_message":"Service settings not initialized."} "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: 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.") logging.error(
return {"error":True ,"internal_message":"YooKassa receipt email not configured by admin."} "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: try:
builder = PaymentRequestBuilder() builder = PaymentRequestBuilder()
builder .set_amount ({"value":str (round (amount ,2 )),"currency":currency .upper ()}) builder.set_amount({
"value": str(round(amount, 2)),
"currency": currency.upper()
})
builder.set_capture(True) builder.set_capture(True)
builder .set_confirmation ({"type":"redirect","return_url":self .return_url }) builder.set_confirmation({
"type": "redirect",
"return_url": self.return_url
})
builder.set_description(description) builder.set_description(description)
builder.set_metadata(metadata) builder.set_metadata(metadata)
receipt_items: List[Dict[str, Any]] = [{
receipt_items :List [Dict [str ,Any ]]=[ "description":
{ description,
"description":description , "quantity":
"quantity":"1.00", "1.00",
"amount": { "amount": {
"value": str(round(amount, 2)), "value": str(round(amount, 2)),
"currency": currency.upper() "currency": currency.upper()
}, },
"vat_code":str (self .settings .YOOKASSA_VAT_CODE ), "vat_code":
"payment_mode":self .settings .YOOKASSA_PAYMENT_MODE , str(self.settings.YOOKASSA_VAT_CODE),
"payment_subject":self .settings .YOOKASSA_PAYMENT_SUBJECT "payment_mode":
} self.settings.YOOKASSA_PAYMENT_MODE,
] "payment_subject":
self.settings.YOOKASSA_PAYMENT_SUBJECT
}]
receipt_customer: Dict[str, str] = {} receipt_customer: Dict[str, str] = {}
if self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL: if self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL:
receipt_customer ["email"]=self .settings .YOOKASSA_DEFAULT_RECEIPT_EMAIL receipt_customer[
"email"] = self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
if not receipt_customer: if not receipt_customer:
logging .error ("YooKassa: No customer contact (email/phone) for receipt.") logging.error(
return {"error":True ,"internal_message":"Receipt customer contact missing."} "YooKassa: No customer contact (email/phone) for receipt.")
return {
"error": True,
"internal_message": "Receipt customer contact missing."
}
receipt_payload: Dict[str, Any] = { receipt_payload: Dict[str, Any] = {
"customer": receipt_customer, "customer": receipt_customer,
"items": receipt_items "items": receipt_items
} }
builder.set_receipt(receipt_payload) builder.set_receipt(receipt_payload)
idempotence_key = str(uuid.uuid4()) idempotence_key = str(uuid.uuid4())
payment_request = builder.build() 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) res = Payment.create(payment_request, idempotence_key)
logging .info (f"YooKassa Payment.create response: ID={res.id}, Status={res.status}") logging.info(
f"YooKassa Payment.create response: ID={res.id}, Status={res.status}"
)
return { return {
"id":res .id , "id":
"confirmation_url":res .confirmation .confirmation_url if res .confirmation else None , res.id,
"status":res .status , "confirmation_url":
"metadata":res .metadata , res.confirmation.confirmation_url
"amount_value":float (res .amount .value ), if res.confirmation else None,
"amount_currency":res .amount .currency , "status":
"idempotence_key":idempotence_key , res.status,
"paid":res .paid , "metadata":
"refundable":res .refundable , res.metadata,
"created_at":res .created_at .isoformat ()if hasattr (res .created_at ,'isoformat')else str (res .created_at ) "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: except Exception as e:
logging .error (f"YooKassa payment creation failed: {e}",exc_info =True ) logging.error(f"YooKassa payment creation failed: {e}",
exc_info=True)
return None return None
async def get_payment_info (self ,payment_id :str )->Optional [Dict [str ,Any ]]: async def get_payment_info(self,
payment_id: str) -> Optional[Dict[str, Any]]:
if not self.configured: if not self.configured:
logging .error ("YooKassa is not configured. Cannot get payment info.") logging.error(
"YooKassa is not configured. Cannot get payment info.")
return None return None
try: try:
payment_info = Payment.find_one(payment_id) payment_info = Payment.find_one(payment_id)
if payment_info: if payment_info:
return {"id":payment_info .id ,"status":payment_info .status ,"paid":payment_info .paid , return {
"amount_value":float (payment_info .amount .value ),"amount_currency":payment_info .amount .currency , "id": payment_info.id,
"metadata":payment_info .metadata ,"description":payment_info .description ,} "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 return None
except Exception as e: except Exception as e:
logging .error (f"YooKassa get payment info for {payment_id} failed: {e}") logging.error(
f"YooKassa get payment info for {payment_id} failed: {e}")
return None return None
+28 -28
View File
@@ -9,22 +9,20 @@ from db .database import get_promo_code_by_code ,increment_promo_activation ,get
from .subscription_service import SubscriptionService from .subscription_service import SubscriptionService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
class PromoCodeService: class PromoCodeService:
def __init__ (
self , def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
db_conn_provider :Callable [[],Any ], subscription_service: SubscriptionService, bot: Bot,
settings :Settings , i18n: JsonI18n):
subscription_service :SubscriptionService ,
bot :Bot ,
i18n :JsonI18n
):
self.db_conn_provider = db_conn_provider self.db_conn_provider = db_conn_provider
self.settings = settings self.settings = settings
self.subscription_service = subscription_service self.subscription_service = subscription_service
self.bot = bot self.bot = bot
self.i18n = i18n self.i18n = i18n
async def apply_promo_code (self ,user_id :int ,code_input :str ,user_lang :str )->Tuple [bool ,str ]: 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. Applies a promo code for a user.
Returns: (success_status: bool, message_text_for_user: str) Returns: (success_status: bool, message_text_for_user: str)
@@ -35,47 +33,47 @@ class PromoCodeService :
async with self.db_conn_provider() as db: async with self.db_conn_provider() as db:
await _setup_db_connection(db) await _setup_db_connection(db)
try: try:
promo_data =await get_promo_code_by_code (code_input_upper ,db_conn =db ) promo_data = await get_promo_code_by_code(code_input_upper,
db_conn=db)
if not promo_data: if not promo_data:
return False ,_ ("promo_code_not_found",code =code_input_upper ) return False, _("promo_code_not_found",
code=code_input_upper)
if promo_data ['current_activations']>=promo_data ['max_activations']: if promo_data['current_activations'] >= promo_data[
return False ,_ ("promo_code_max_activations_reached",code =code_input_upper ) 'max_activations']:
return False, _("promo_code_max_activations_reached",
code=code_input_upper)
active_sub = await self.subscription_service.get_active_subscription(
user_id)
active_sub =await self .subscription_service .get_active_subscription (user_id )
if not active_sub: if not active_sub:
return False, _("promo_code_no_active_subscription") return False, _("promo_code_no_active_subscription")
bonus_days = promo_data['bonus_days'] bonus_days = promo_data['bonus_days']
new_end_date = await self.subscription_service.extend_active_subscription_days( new_end_date = await self.subscription_service.extend_active_subscription_days(
user_id=user_id, user_id=user_id,
bonus_days=bonus_days, bonus_days=bonus_days,
db_conn=db, db_conn=db,
reason =f"promo code {code_input_upper}" reason=f"promo code {code_input_upper}")
)
if new_end_date: if new_end_date:
activation_success = await increment_promo_activation(
activation_success =await increment_promo_activation (promo_data ['promo_code_id'],user_id ,db_conn =db ,payment_id =None ) promo_data['promo_code_id'],
user_id,
db_conn=db,
payment_id=None)
if activation_success: if activation_success:
await db.commit() await db.commit()
return True ,_ ("promo_code_applied_success", return True, _(
"promo_code_applied_success",
code=code_input_upper, code=code_input_upper,
bonus_days=bonus_days, bonus_days=bonus_days,
new_end_date=new_end_date.strftime('%Y-%m-%d')) new_end_date=new_end_date.strftime('%Y-%m-%d'))
else: else:
await db.rollback() await db.rollback()
return False, _("promo_code_invalid_or_expired") return False, _("promo_code_invalid_or_expired")
else: else:
@@ -83,6 +81,8 @@ class PromoCodeService :
return False, _("error_applying_promo_bonus") return False, _("error_applying_promo_bonus")
except Exception as e: except Exception as e:
logging .error (f"Error applying promo code {code_input_upper} for user {user_id}: {e}",exc_info =True ) logging.error(
f"Error applying promo code {code_input_upper} for user {user_id}: {e}",
exc_info=True)
await db.rollback() await db.rollback()
return False, _("error_try_again") return False, _("error_try_again")
+67 -34
View File
@@ -13,84 +13,117 @@ if TYPE_CHECKING :
class ReferralService: class ReferralService:
def __init__ (self ,db_conn_provider :Callable [[],Any ],settings :Settings ,subscription_service_instance :'SubscriptionService',bot :Bot ,i18n :JsonI18n ):
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.db_conn_provider = db_conn_provider
self.settings = settings self.settings = settings
self.subscription_service = subscription_service_instance self.subscription_service = subscription_service_instance
self.bot = bot self.bot = bot
self.i18n = i18n self.i18n = i18n
async def process_new_user_referral(self, referee_user_id: int,
inviter_user_id: Optional[int]):
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: if inviter_user_id and referee_user_id != inviter_user_id:
logging.info(
logging .info (f"Referral link used: User {referee_user_id} was invited by {inviter_user_id}.") f"Referral link used: User {referee_user_id} was invited by {inviter_user_id}."
)
pass pass
async def apply_referral_bonuses_for_payment( async def apply_referral_bonuses_for_payment(
self, referee_user_id: int, purchased_subscription_months: int, self, referee_user_id: int, purchased_subscription_months: int,
db_conn :aiosqlite .Connection db_conn: aiosqlite.Connection) -> Dict[str, Any]:
)->Dict [str ,Any ]:
"""Applies bonuses. Notifies inviter. Returns referee bonus details.""" """Applies bonuses. Notifies inviter. Returns referee bonus details."""
referee_final_end_date: Optional[datetime] = None referee_final_end_date: Optional[datetime] = None
referee_bonus_applied_days: Optional[int] = None referee_bonus_applied_days: Optional[int] = None
try: try:
referee_user_row = await get_user(referee_user_id, db_conn=db_conn) 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 : if not referee_user_row or referee_user_row[
logging .debug (f"User {referee_user_id} not referred or inviter ID missing. No referral bonuses.") 'referred_by_id'] is None:
return {"referee_bonus_applied_days":None ,"referee_new_end_date":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_id = referee_user_row['referred_by_id']
inviter_user_row = await get_user(inviter_user_id, db_conn=db_conn) 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}" referee_name = referee_user_row[
'first_name'] or f"User {referee_user_id}"
default_lang_for_placeholder =getattr (self .settings ,'DEFAULT_LANGUAGE','en') default_lang_for_placeholder = getattr(self.settings,
inviter_name =inviter_user_row ['first_name']if inviter_user_row else self .i18n .gettext (default_lang_for_placeholder ,"friend_placeholder") '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 ) inviter_bonus = self.settings.referral_bonus_inviter.get(
referee_bonus =self .settings .referral_bonus_referee .get (purchased_subscription_months ) 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: if inviter_bonus and inviter_bonus > 0 and inviter_user_row:
new_end_date_inviter = await self.subscription_service.extend_subscription_for_referral( 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 user_id=inviter_user_id,
) bonus_days=inviter_bonus,
db_conn=db_conn)
if new_end_date_inviter: if new_end_date_inviter:
logging .info (f"Bonus applied for inviter {inviter_user_id}.") logging.info(
f"Bonus applied for inviter {inviter_user_id}.")
try: try:
inviter_lang =inviter_user_row .get ('language_code',default_lang_for_placeholder ) inviter_lang = inviter_user_row.get(
_i =lambda k ,**kw :self .i18n .gettext (inviter_lang ,k ,**kw ) 'language_code', default_lang_for_placeholder)
_i = lambda k, **kw: self.i18n.gettext(
inviter_lang, k, **kw)
await self.bot.send_message( await self.bot.send_message(
inviter_user_id, inviter_user_id,
_i("referral_bonus_inviter_notification_extended", _i("referral_bonus_inviter_notification_extended",
days=inviter_bonus, days=inviter_bonus,
referee_name=referee_name, referee_name=referee_name,
new_end_date =new_end_date_inviter .strftime ('%Y-%m-%d') new_end_date=new_end_date_inviter.strftime(
) '%Y-%m-%d')))
)
except Exception as e: except Exception as e:
logging .error (f"Failed to send bonus notification to inviter {inviter_user_id}: {e}") logging.error(
else :logging .warning (f"Failed to apply bonus subscription extension for inviter {inviter_user_id}.") 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: if referee_bonus and referee_bonus > 0:
new_end_date_referee = await self.subscription_service.extend_subscription_for_referral( 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 user_id=referee_user_id,
) bonus_days=referee_bonus,
db_conn=db_conn,
is_referee_bonus=True)
if new_end_date_referee: if new_end_date_referee:
logging .info (f"Bonus applied for referee {referee_user_id}.") logging.info(
f"Bonus applied for referee {referee_user_id}.")
referee_final_end_date = new_end_date_referee referee_final_end_date = new_end_date_referee
referee_bonus_applied_days = referee_bonus referee_bonus_applied_days = referee_bonus
else :logging .warning (f"Failed to apply bonus subscription extension for referee {referee_user_id}.") 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 } return {
"referee_bonus_applied_days": referee_bonus_applied_days,
"referee_new_end_date": referee_final_end_date
}
except Exception as e: 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 ) logging.error(
f"Error in apply_referral_bonuses_for_payment (db_conn: {db_conn is not None}): {e}",
exc_info=True)
raise raise
def generate_referral_link (self ,bot_username :str ,inviter_user_id :int )->str : 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}" return f"https://t.me/{bot_username}?start=ref_{inviter_user_id}"
+480 -146
View File
@@ -7,17 +7,22 @@ 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 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 from .panel_api_service import PanelApiService
if False: if False:
from .referral_service import ReferralService from .referral_service import ReferralService
class SubscriptionService: class SubscriptionService:
def __init__ (self ,db_conn_provider :Callable [[],Any ],settings :Settings ,panel_service :PanelApiService ):
def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
panel_service: PanelApiService):
self.db_conn_provider = db_conn_provider self.db_conn_provider = db_conn_provider
self.settings = settings self.settings = settings
self.panel_service = panel_service self.panel_service = panel_service
async def _get_db (self ,existing_conn :Optional [aiosqlite .Connection ]=None )->tuple [aiosqlite .Connection ,bool ]: 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).""" """Helper to get a DB connection. Returns (connection, should_manage_flag)."""
if existing_conn: if existing_conn:
return existing_conn, False return existing_conn, False
@@ -31,7 +36,12 @@ class SubscriptionService :
raise 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.""" """Helper to release/close a DB connection if this service instance opened it."""
if should_manage: if should_manage:
await db.__aexit__(exc_type, exc_val, exc_tb) await db.__aexit__(exc_type, exc_val, exc_tb)
@@ -42,202 +52,457 @@ class SubscriptionService :
try: try:
user_record = await get_user(user_id, db_conn=db) user_record = await get_user(user_id, db_conn=db)
default_lang = self.settings.DEFAULT_LANGUAGE 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 return user_record[
'language_code'] if user_record and 'language_code' in user_record.keys(
) and user_record['language_code'] else default_lang
finally: finally:
await self._release_db(db, should_manage) 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.""" """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) db_to_use, should_manage_this_conn = await self._get_db(db_conn)
try: try:
user_record = await get_user(user_id, db_conn=db_to_use) 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 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: finally:
await self._release_db(db_to_use, should_manage_this_conn) 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.""" """Checks if the user has any record in the subscriptions table."""
db_to_use, should_manage_this_conn = await self._get_db(db_conn) db_to_use, should_manage_this_conn = await self._get_db(db_conn)
try: try:
return await db_has_had_any_subscription (user_id ,db_conn =db_to_use ) return await db_has_had_any_subscription(user_id,
db_conn=db_to_use)
finally: finally:
await self._release_db(db_to_use, should_manage_this_conn) 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. Activates a trial subscription for an eligible user.
Manages its own database connection and transaction. Manages its own database connection and transaction.
Returns dict with trial details or specific error dict on failure/ineligibility. 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: 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}.") logging.info(
return {"eligible":False ,"activated":False ,"message_key":"trial_feature_disabled"} 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: async with self.db_conn_provider() as db:
await _setup_db_connection(db) await _setup_db_connection(db)
try: try:
if await db_has_had_any_subscription(user_id, db_conn=db): if await db_has_had_any_subscription(user_id, db_conn=db):
logging .info (f"User {user_id} has prior subscriptions. Trial not applicable.") logging.info(
return {"eligible":False ,"activated":False ,"message_key":"trial_not_eligible_already_subscribed"} 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_user_uuid = await self.get_panel_user_uuid(user_id,
panel_subscription_uuid_for_link =None ;panel_short_uuid_for_link =None db_conn=db)
panel_user_interacted_now =False ;specific_inbounds =self .settings .parsed_default_panel_user_inbound_uuids 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 panel_actual_subscription_url: Optional[str] = None
if not panel_user_uuid: if not panel_user_uuid:
logging .info (f"No panel_user_uuid for TG user_id {user_id} for trial. Creating panel user.") 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}" panel_username_to_create = f"tg_{user_id}"
creation_response = await self.panel_service.create_panel_user( creation_response = await self.panel_service.create_panel_user(
username =panel_username_to_create ,telegram_id =user_id , username=panel_username_to_create,
telegram_id=user_id,
default_expire_days=self.settings.TRIAL_DURATION_DAYS, default_expire_days=self.settings.TRIAL_DURATION_DAYS,
default_traffic_limit_bytes =self .settings .trial_traffic_limit_bytes , default_traffic_limit_bytes=self.settings.
default_traffic_limit_strategy =self .settings .PANEL_USER_DEFAULT_TRAFFIC_STRATEGY , trial_traffic_limit_bytes,
default_traffic_limit_strategy=self.settings.
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
specific_inbound_uuids=specific_inbounds, specific_inbound_uuids=specific_inbounds,
activate_all_inbounds_default_flag =False if specific_inbounds else True activate_all_inbounds_default_flag=False
) if specific_inbounds else True)
panel_user_obj_from_api = None panel_user_obj_from_api = None
if creation_response and not creation_response .get ("error"):panel_user_obj_from_api =creation_response .get ("response") if creation_response and not creation_response.get(
elif creation_response and creation_response .get ("errorCode")=="A019": "error"):
existing_users_list =await self .panel_service .get_users_by_filter (username =panel_username_to_create ) panel_user_obj_from_api = creation_response.get(
if existing_users_list and len (existing_users_list )==1 :panel_user_obj_from_api =existing_users_list [0 ] "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]
if panel_user_obj_from_api and panel_user_obj_from_api .get ('uuid'): if panel_user_obj_from_api and panel_user_obj_from_api.get(
'uuid'):
panel_user_uuid = panel_user_obj_from_api['uuid'] panel_user_uuid = panel_user_obj_from_api['uuid']
panel_subscription_uuid_for_link =panel_user_obj_from_api .get ('subscriptionUuid') panel_subscription_uuid_for_link = panel_user_obj_from_api.get(
panel_short_uuid_for_link =panel_user_obj_from_api .get ('shortUuid') 'subscriptionUuid')
panel_actual_subscription_url =panel_user_obj_from_api .get ('subscriptionUrl') panel_short_uuid_for_link = panel_user_obj_from_api.get(
await db .execute ("UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",(panel_user_uuid ,user_id )) '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 panel_user_interacted_now = True
if panel_user_obj_from_api .get ('telegramId')!=user_id : if panel_user_obj_from_api.get(
await self .panel_service .update_user_details_on_panel (panel_user_uuid ,{"telegramId":user_id }) 'telegramId') != user_id:
await self.panel_service.update_user_details_on_panel(
panel_user_uuid, {"telegramId": user_id})
else: 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'}") 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() await db.rollback()
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"} return {
"eligible": True,
"activated": False,
"message_key": "trial_activation_failed"
}
else: else:
panel_user_data =await self .panel_service .get_user_by_uuid (panel_user_uuid ) panel_user_data = await self.panel_service.get_user_by_uuid(
panel_user_uuid)
if panel_user_data: if panel_user_data:
panel_subscription_uuid_for_link =panel_user_data .get ('subscriptionUuid') panel_subscription_uuid_for_link = panel_user_data.get(
panel_short_uuid_for_link =panel_user_data .get ('shortUuid') 'subscriptionUuid')
panel_actual_subscription_url =panel_user_data .get ('subscriptionUrl') 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: if not panel_subscription_uuid_for_link and panel_short_uuid_for_link:
panel_subscription_uuid_for_link = panel_short_uuid_for_link panel_subscription_uuid_for_link = panel_short_uuid_for_link
if not panel_subscription_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}).") logging.error(
f"Critical: panel_subscription_uuid for link is None for trial (panel_uuid {panel_user_uuid})."
)
await db.rollback() await db.rollback()
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"} return {
"eligible": True,
"activated": False,
"message_key": "trial_activation_failed"
}
start_date = datetime.now(timezone.utc) start_date = datetime.now(timezone.utc)
end_date =start_date +timedelta (days =self .settings .TRIAL_DURATION_DAYS ) end_date = start_date + timedelta(
await db .execute ("UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",(panel_user_uuid ,)) 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; """ 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 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 ) params = (user_id, panel_user_uuid,
panel_subscription_uuid_for_link,
start_date.isoformat(), end_date.isoformat(),
trial_traffic_val)
upsert_cursor = await db.execute(upsert_sql, params) upsert_cursor = await db.execute(upsert_sql, params)
trial_subscription_id = upsert_cursor.lastrowid trial_subscription_id = upsert_cursor.lastrowid
if not trial_subscription_id or trial_subscription_id == 0: 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_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() id_fetch_row = await id_fetch_cursor.fetchone()
if id_fetch_cursor: await id_fetch_cursor.close() if id_fetch_cursor: await id_fetch_cursor.close()
if id_fetch_row :trial_subscription_id =id_fetch_row ['subscription_id'] if id_fetch_row:
logging .info (f"Local trial subscription (ID: {trial_subscription_id}) for user {user_id} prepared. Ends: {end_date.isoformat()}.") 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()}."
)
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 ,} panel_update_payload: Dict[str, Any] = {
if specific_inbounds :panel_update_payload ["activeUserInbounds"]=specific_inbounds "uuid":
elif panel_user_interacted_now :panel_update_payload ["activateAllInbounds"]=True 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
updated_panel_user =await self .panel_service .update_user_details_on_panel (panel_user_uuid ,panel_update_payload ) updated_panel_user = await self.panel_service.update_user_details_on_panel(
if not updated_panel_user :logging .warning (f"Panel user details update FAILED for trial user {panel_user_uuid}.") 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: else:
logging .info (f"Panel user {panel_user_uuid} details updated for trial. Panel ExpireAt: {updated_panel_user.get('expireAt')}") logging.info(
if updated_panel_user .get ('subscriptionUrl'):panel_actual_subscription_url =updated_panel_user .get ('subscriptionUrl') f"Panel user {panel_user_uuid} details updated for trial. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
if updated_panel_user .get ('shortUuid'):panel_short_uuid_for_link =updated_panel_user .get ('shortUuid') )
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')
await db.commit() 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 } 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: except Exception as e:
logging .error (f"Error activating trial for user {user_id}: {e}",exc_info =True ) logging.error(
f"Error activating trial for user {user_id}: {e}",
exc_info=True)
await db.rollback() await db.rollback()
return {"eligible":True ,"activated":False ,"message_key":"trial_activation_failed"} return {
"eligible": True,
"activated": False,
"message_key": "trial_activation_failed"
}
async def activate_subscription( async def activate_subscription(
self ,user_id :int ,months :int ,payment_amount :float , self,
payment_id_internal :int ,db_conn :aiosqlite .Connection , user_id: int,
promo_code_id :Optional [int ]=None months: int,
)->Optional [Dict [str ,Any ]]: payment_amount: float,
payment_id_internal: int,
db_conn: aiosqlite.Connection,
promo_code_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
db = db_conn db = db_conn
try: try:
panel_user_uuid =await self .get_panel_user_uuid (user_id ,db_conn =db ) panel_user_uuid = await self.get_panel_user_uuid(user_id,
panel_subscription_uuid_for_link =None ;panel_short_uuid_for_link =None ;panel_user_interacted_now =False 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 panel_actual_subscription_url: Optional[str] = None
specific_inbounds_from_settings = self.settings.parsed_default_panel_user_inbound_uuids specific_inbounds_from_settings = self.settings.parsed_default_panel_user_inbound_uuids
if not panel_user_uuid: if not panel_user_uuid:
panel_username_to_create = f"tg_{user_id}" 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 ) 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 panel_user_object_from_api = None
if creation_response and not creation_response .get ("error"):panel_user_object_from_api =creation_response .get ("response") if creation_response and not creation_response.get("error"):
elif creation_response and creation_response .get ("errorCode")=="A019": panel_user_object_from_api = creation_response.get(
existing_users_list =await self .panel_service .get_users_by_filter (username =panel_username_to_create ) "response")
if existing_users_list and len (existing_users_list )==1 :panel_user_object_from_api =existing_users_list [0 ] elif creation_response and creation_response.get(
if panel_user_object_from_api and panel_user_object_from_api .get ('uuid'): "errorCode") == "A019":
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') existing_users_list = await self.panel_service.get_users_by_filter(
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 () username=panel_username_to_create)
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 if existing_users_list and len(existing_users_list) == 1:
await db .execute ("UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",(panel_user_uuid ,user_id ));panel_user_interacted_now =True panel_user_object_from_api = existing_users_list[0]
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 }) if panel_user_object_from_api and panel_user_object_from_api.get(
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 '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: else:
panel_user_data =await self .panel_service .get_user_by_uuid (panel_user_uuid ) logging.error(
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') f"Failed to create/link panel user for TG_ID {user_id}. Resp: {creation_response if 'creation_response' in locals() else 'N/A'}"
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 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
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 () cursor = await db.execute(
current_sub_end_date_str =current_sub_row ['end_date']if current_sub_row else None ;start_date =datetime .now (timezone .utc ) "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: if current_sub_end_date_str:
try: try:
parsed_current_end_date =datetime .fromisoformat (current_sub_end_date_str .replace ("Z","+00:00")) parsed_current_end_date = datetime.fromisoformat(
if parsed_current_end_date .tzinfo is None :parsed_current_end_date =parsed_current_end_date .replace (tzinfo =timezone .utc ) current_sub_end_date_str.replace("Z", "+00:00"))
if parsed_current_end_date >start_date :start_date =parsed_current_end_date if parsed_current_end_date.tzinfo is None:
except ValueError :logging .warning (f"Bad current_sub_end_date string: {current_sub_end_date_str} for panel_user {panel_user_uuid}.") 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) final_end_date = start_date + timedelta(days=months * 30)
if promo_code_id: 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 () promo_cursor = await db.execute(
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 ,)) "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 .execute ("UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",(panel_user_uuid ,)) 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; """ 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 ) params = (user_id, panel_user_uuid,
upsert_cursor =await db .execute (upsert_sql ,params );subscription_id_to_return =upsert_cursor .lastrowid 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: 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 () id_cursor = await db.execute(
if id_row :subscription_id_to_return =id_row ['subscription_id'] "SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",
logging .info (f"Local subscription UPSERTED (ID: {subscription_id_to_return}) for user {user_id}. Ends: {final_end_date.isoformat()}.") (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 } panel_update_payload: Dict[str, Any] = {
if specific_inbounds_from_settings :panel_update_payload ["activeUserInbounds"]=specific_inbounds_from_settings "uuid":
elif panel_user_interacted_now :panel_update_payload ["activateAllInbounds"]=True panel_user_uuid,
if "activateAllInbounds"in panel_update_payload and not specific_inbounds_from_settings and not panel_user_interacted_now :del panel_update_payload ["activateAllInbounds"] "expireAt":
logging .info (f"Attempting to update panel user {panel_user_uuid} for paid sub: {panel_update_payload}") final_end_date.isoformat(timespec='milliseconds').replace(
updated_panel_user =await self .panel_service .update_user_details_on_panel (panel_user_uuid ,panel_update_payload ) '+00:00', 'Z'),
if not updated_panel_user :logging .warning (f"Panel user details update FAILED for {panel_user_uuid}.") "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: else:
logging .info (f"Panel user {panel_user_uuid} details updated. Panel ExpireAt: {updated_panel_user.get('expireAt')}") logging.info(
if updated_panel_user .get ('subscriptionUrl'):panel_actual_subscription_url =updated_panel_user .get ('subscriptionUrl') f"Panel user {panel_user_uuid} details updated. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
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 } if updated_panel_user.get('subscriptionUrl'):
except Exception as e :logging .error (f"Error in activate_subscription (paid) for user {user_id}: {e}",exc_info =True );return None 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 ]: 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 db = db_conn
try: try:
user_cursor =await db .execute ("SELECT panel_user_uuid FROM users WHERE user_id = ?",(user_id ,)) user_cursor = await db.execute(
"SELECT panel_user_uuid FROM users WHERE user_id = ?",
(user_id, ))
user_panel_data = await user_cursor.fetchone() user_panel_data = await user_cursor.fetchone()
if user_cursor: await user_cursor.close() 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 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 " 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, ) params_select_active_sub: tuple = (user_id, )
@@ -245,92 +510,161 @@ class SubscriptionService :
sql_select_active_sub += "AND panel_user_uuid = ? " sql_select_active_sub += "AND panel_user_uuid = ? "
params_select_active_sub += (panel_user_uuid_for_update, ) params_select_active_sub += (panel_user_uuid_for_update, )
else: 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).") 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" sql_select_active_sub += "ORDER BY end_date DESC LIMIT 1"
cursor =await db .execute (sql_select_active_sub ,params_select_active_sub ) cursor = await db.execute(sql_select_active_sub,
params_select_active_sub)
active_sub_row = await cursor.fetchone() active_sub_row = await cursor.fetchone()
if cursor: await cursor.close() if cursor: await cursor.close()
if not active_sub_row or not active_sub_row['end_date']: 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.") 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 return None
current_end_date_str = active_sub_row['end_date'] current_end_date_str = active_sub_row['end_date']
try: 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 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): if not isinstance(current_end_date, datetime):
raise ValueError ("current_end_date is not a datetime object after parsing") raise ValueError(
"current_end_date is not a datetime object after parsing"
)
except ValueError as ve: except ValueError as ve:
logging .error (f"Error parsing current_end_date '{current_end_date_str}' for user {user_id}: {ve}") logging.error(
f"Error parsing current_end_date '{current_end_date_str}' for user {user_id}: {ve}"
)
return None return None
if current_end_date.tzinfo is None: if current_end_date.tzinfo is None:
current_end_date =current_end_date .replace (tzinfo =timezone .utc ) current_end_date = current_end_date.replace(
tzinfo=timezone.utc)
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
start_point_for_bonus = current_end_date if current_end_date > now_utc else now_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) 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'])) await db.execute(
logging .info (f"Subscription for user {user_id} extended by {bonus_days} days ({reason}). New end date: {new_end_date.isoformat()}") "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: 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')} panel_update_payload = {
if not await self .panel_service .update_user_details_on_panel (panel_user_uuid_for_update ,panel_update_payload ): "uuid":
logging .warning (f"Failed to update panel expiry for {panel_user_uuid_for_update} after {reason} bonus.") 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 return new_end_date
except Exception as e: 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 ) logging.error(
f"Error extending subscription with {reason} bonus for user {user_id} (using provided db_conn): {e}",
exc_info=True)
return None 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 ]: 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" 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 ) 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 ]]: async def get_active_subscription(
self, user_id: int) -> Optional[Dict[str, Any]]:
db, should_manage = await self._get_db() db, should_manage = await self._get_db()
try: try:
now_iso_utc = datetime.now(timezone.utc).isoformat() 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 )) cursor = await db.execute(
sub_row =await cursor .fetchone ();await cursor .close () """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: if sub_row:
sub_dict = dict(sub_row) sub_dict = dict(sub_row)
for date_key in ['start_date', 'end_date']: for date_key in ['start_date', 'end_date']:
if sub_dict .get (date_key )and isinstance (sub_dict [date_key ],str ): if sub_dict.get(date_key) and isinstance(
try :sub_dict [date_key ]=datetime .fromisoformat (sub_dict [date_key ].replace ("Z","+00:00")) sub_dict[date_key], str):
except ValueError :logging .warning (f"Could not parse date string {sub_dict[date_key]} for key {date_key} in get_active_subscription") 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 sub_dict
return None 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 ]]: async def get_subscriptions_ending_soon(
self, days_threshold: int) -> List[Dict[str, Any]]:
db, should_manage = await self._get_db() db, should_manage = await self._get_db()
try: try:
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
threshold_date = now_utc + timedelta(days=days_threshold) threshold_date = now_utc + timedelta(days=days_threshold)
today_date_str = now_utc.strftime('%Y-%m-%d') 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 """ 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 () cursor = await db.execute(
query, (now_utc.isoformat(), threshold_date.isoformat(),
today_date_str))
rows = await cursor.fetchall()
await cursor.close()
processed_rows = [] processed_rows = []
for row_data in rows: for row_data in rows:
row_dict =dict (row_data );end_date_obj_for_calc =None row_dict = dict(row_data)
end_date_obj_for_calc = None
if isinstance(row_dict['end_date_raw'], str): if isinstance(row_dict['end_date_raw'], str):
try: try:
end_date_obj_for_calc =datetime .fromisoformat (row_dict ['end_date_raw'].replace ("Z","+00:00")) end_date_obj_for_calc = datetime.fromisoformat(
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['end_date_raw'].replace("Z", "+00:00"))
row_dict ['days_left']=(end_date_obj_for_calc -now_utc ).total_seconds ()/(24 *3600 ) if end_date_obj_for_calc.tzinfo is None:
except ValueError :row_dict ['days_left']=None end_date_obj_for_calc = end_date_obj_for_calc.replace(
else :row_dict ['days_left']=None 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) processed_rows.append(row_dict)
return processed_rows 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 ): async def update_last_notification_sent(self, user_id: int,
subscription_end_date_iso: str):
db, should_manage = await self._get_db() db, should_manage = await self._get_db()
try: try:
today_iso_date_str =datetime .now (timezone .utc ).strftime ('%Y-%m-%d') today_iso_date_str = datetime.now(
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 )) 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() 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 () except Exception as e:
finally :await self ._release_db (db ,should_manage ) 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
View File
@@ -1,5 +1,6 @@
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
class AdminStates(StatesGroup): class AdminStates(StatesGroup):
waiting_for_broadcast_message = State() waiting_for_broadcast_message = State()
+1
View File
@@ -1,4 +1,5 @@
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
class UserPromoStates(StatesGroup): class UserPromoStates(StatesGroup):
waiting_for_promo_code = State() waiting_for_promo_code = State()
+87 -35
View File
@@ -3,43 +3,58 @@ from pydantic_settings import BaseSettings ,SettingsConfigDict
from pydantic import Field, field_validator, ValidationError from pydantic import Field, field_validator, ValidationError
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
class Settings(BaseSettings): class Settings(BaseSettings):
BOT_TOKEN: str BOT_TOKEN: str
ADMIN_IDS_STR :str =Field (default ="",alias ="ADMIN_IDS",description ="Comma-separated list of admin Telegram User IDs") ADMIN_IDS_STR: str = Field(
default="",
alias="ADMIN_IDS",
description="Comma-separated list of admin Telegram User IDs")
DB_NAME: str = Field(default="bot_database.sqlite3") DB_NAME: str = Field(default="bot_database.sqlite3")
DEFAULT_LANGUAGE: str = Field(default="ru") DEFAULT_LANGUAGE: str = Field(default="ru")
DEFAULT_CURRENCY_SYMBOL: str = Field(default="RUB") DEFAULT_CURRENCY_SYMBOL: str = Field(default="RUB")
SUPPORT_LINK :Optional [str ]=Field (default =None ,description ="Link to support contact (e.g., t.me/your_support_contact)") SUPPORT_LINK: Optional[str] = Field(
SERVER_STATUS_URL :Optional [str ]=Field (default =None ,description ="Link to server status page (e.g., Uptime Kuma)") default=None,
description="Link to support contact (e.g., t.me/your_support_contact)"
)
SERVER_STATUS_URL: Optional[str] = Field(
default=None,
description="Link to server status page (e.g., Uptime Kuma)")
YOOKASSA_SHOP_ID: Optional[str] = None YOOKASSA_SHOP_ID: Optional[str] = None
YOOKASSA_SECRET_KEY: Optional[str] = None YOOKASSA_SECRET_KEY: Optional[str] = None
YOOKASSA_WEBHOOK_BASE_URL: Optional[str] = None YOOKASSA_WEBHOOK_BASE_URL: Optional[str] = None
YOOKASSA_RETURN_URL: Optional[str] = None YOOKASSA_RETURN_URL: Optional[str] = None
YOOKASSA_DEFAULT_RECEIPT_EMAIL :Optional [str ]=Field (default =None ,description ="Default email for YooKassa receipts (REQUIRED for 54-FZ)") YOOKASSA_DEFAULT_RECEIPT_EMAIL: Optional[str] = Field(
YOOKASSA_VAT_CODE :int =Field (default =6 ,description ="YooKassa VAT code (e.g., 1=20%, 2=10%, 4=0%, 6=No VAT - check YooKassa docs!)") default=None,
YOOKASSA_PAYMENT_MODE :str =Field (default ="full_prepayment",description ="YooKassa payment mode (e.g., full_prepayment, full_payment)") description="Default email for YooKassa receipts (REQUIRED for 54-FZ)")
YOOKASSA_PAYMENT_SUBJECT :str =Field (default ="service",description ="YooKassa payment subject (e.g., service, commodity)") YOOKASSA_VAT_CODE: int = Field(
default=6,
description=
"YooKassa VAT code (e.g., 1=20%, 2=10%, 4=0%, 6=No VAT - check YooKassa docs!)"
)
YOOKASSA_PAYMENT_MODE: str = Field(
default="full_prepayment",
description=
"YooKassa payment mode (e.g., full_prepayment, full_payment)")
YOOKASSA_PAYMENT_SUBJECT: str = Field(
default="service",
description="YooKassa payment subject (e.g., service, commodity)")
TELEGRAM_WEBHOOK_BASE_URL: Optional[str] = None TELEGRAM_WEBHOOK_BASE_URL: Optional[str] = None
PRICE_1_MONTH: Optional[int] = None PRICE_1_MONTH: Optional[int] = None
PRICE_3_MONTHS: Optional[int] = None PRICE_3_MONTHS: Optional[int] = None
PRICE_6_MONTHS: Optional[int] = None PRICE_6_MONTHS: Optional[int] = None
PRICE_12_MONTHS: Optional[int] = None PRICE_12_MONTHS: Optional[int] = None
SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS: int = Field(default=7) SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS: int = Field(default=7)
SUBSCRIPTION_NOTIFICATION_HOUR_UTC: int = Field(default=9) SUBSCRIPTION_NOTIFICATION_HOUR_UTC: int = Field(default=9)
SUBSCRIPTION_NOTIFICATION_MINUTE_UTC: int = Field(default=0) SUBSCRIPTION_NOTIFICATION_MINUTE_UTC: int = Field(default=0)
REFERRAL_BONUS_DAYS_1_MONTH: Optional[int] = 3 REFERRAL_BONUS_DAYS_1_MONTH: Optional[int] = 3
REFERRAL_BONUS_DAYS_3_MONTHS: Optional[int] = 7 REFERRAL_BONUS_DAYS_3_MONTHS: Optional[int] = 7
REFERRAL_BONUS_DAYS_6_MONTHS: Optional[int] = 15 REFERRAL_BONUS_DAYS_6_MONTHS: Optional[int] = 15
@@ -49,7 +64,6 @@ class Settings (BaseSettings ):
REFEREE_BONUS_DAYS_6_MONTHS: Optional[int] = 7 REFEREE_BONUS_DAYS_6_MONTHS: Optional[int] = 7
REFEREE_BONUS_DAYS_12_MONTHS: Optional[int] = 15 REFEREE_BONUS_DAYS_12_MONTHS: Optional[int] = 15
PANEL_API_URL: Optional[str] = None PANEL_API_URL: Optional[str] = None
PANEL_API_KEY: Optional[str] = None PANEL_API_KEY: Optional[str] = None
PANEL_USER_DEFAULT_EXPIRE_DAYS: int = Field(default=1) PANEL_USER_DEFAULT_EXPIRE_DAYS: int = Field(default=1)
@@ -57,7 +71,6 @@ class Settings (BaseSettings ):
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY: str = Field(default="NO_RESET") PANEL_USER_DEFAULT_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
PANEL_USER_DEFAULT_INBOUND_UUIDS: Optional[str] = Field(default=None) PANEL_USER_DEFAULT_INBOUND_UUIDS: Optional[str] = Field(default=None)
TRIAL_ENABLED: bool = Field(default=True) TRIAL_ENABLED: bool = Field(default=True)
TRIAL_DURATION_DAYS: int = Field(default=3) TRIAL_DURATION_DAYS: int = Field(default=3)
TRIAL_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=5.0) TRIAL_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=5.0)
@@ -73,12 +86,20 @@ class Settings (BaseSettings ):
if self._admin_ids_list is None: if self._admin_ids_list is None:
if self.ADMIN_IDS_STR: if self.ADMIN_IDS_STR:
try: try:
self ._admin_ids_list =[int (admin_id .strip ())for admin_id in self .ADMIN_IDS_STR .split (',')if admin_id .strip ().isdigit ()] self._admin_ids_list = [
int(admin_id.strip())
for admin_id in self.ADMIN_IDS_STR.split(',')
if admin_id.strip().isdigit()
]
if not self._admin_ids_list and self.ADMIN_IDS_STR: if not self._admin_ids_list and self.ADMIN_IDS_STR:
logging .error (f"ADMIN_IDS_STR ('{self.ADMIN_IDS_STR}') contains non-integer values or is malformed. No admin IDs loaded from string.") logging.error(
f"ADMIN_IDS_STR ('{self.ADMIN_IDS_STR}') contains non-integer values or is malformed. No admin IDs loaded from string."
)
self._admin_ids_list = [] self._admin_ids_list = []
except ValueError: except ValueError:
logging .error (f"Invalid ADMIN_IDS_STR format: '{self.ADMIN_IDS_STR}'. Expected comma-separated integers.") logging.error(
f"Invalid ADMIN_IDS_STR format: '{self.ADMIN_IDS_STR}'. Expected comma-separated integers."
)
self._admin_ids_list = [] self._admin_ids_list = []
else: else:
self._admin_ids_list = [] self._admin_ids_list = []
@@ -93,54 +114,85 @@ class Settings (BaseSettings ):
@property @property
def trial_traffic_limit_bytes(self) -> int: def trial_traffic_limit_bytes(self) -> int:
if self .TRIAL_TRAFFIC_LIMIT_GB is None or self .TRIAL_TRAFFIC_LIMIT_GB <=0 :return 0 if self.TRIAL_TRAFFIC_LIMIT_GB is None or self.TRIAL_TRAFFIC_LIMIT_GB <= 0:
return 0
return int(self.TRIAL_TRAFFIC_LIMIT_GB * (1024**3)) return int(self.TRIAL_TRAFFIC_LIMIT_GB * (1024**3))
@property @property
def parsed_default_panel_user_inbound_uuids(self) -> Optional[List[str]]: def parsed_default_panel_user_inbound_uuids(self) -> Optional[List[str]]:
if self .PANEL_USER_DEFAULT_INBOUND_UUIDS :return [uuid .strip ()for uuid in self .PANEL_USER_DEFAULT_INBOUND_UUIDS .split (',')if uuid .strip ()] if self.PANEL_USER_DEFAULT_INBOUND_UUIDS:
return [
uuid.strip()
for uuid in self.PANEL_USER_DEFAULT_INBOUND_UUIDS.split(',')
if uuid.strip()
]
return None return None
@property @property
def yookassa_webhook_path (self )->str :return "/webhook/yookassa" def yookassa_webhook_path(self) -> str:
return "/webhook/yookassa"
@property @property
def yookassa_full_webhook_url(self) -> Optional[str]: def yookassa_full_webhook_url(self) -> Optional[str]:
if self .YOOKASSA_WEBHOOK_BASE_URL :return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}" if self.YOOKASSA_WEBHOOK_BASE_URL:
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}"
return None return None
@property @property
def subscription_options(self) -> Dict[int, int]: def subscription_options(self) -> Dict[int, int]:
options :Dict [int ,int ]={}; options: Dict[int, int] = {}
if self.PRICE_1_MONTH is not None: options[1] = self.PRICE_1_MONTH if self.PRICE_1_MONTH is not None: options[1] = self.PRICE_1_MONTH
if self.PRICE_3_MONTHS is not None: options[3] = self.PRICE_3_MONTHS if self.PRICE_3_MONTHS is not None: options[3] = self.PRICE_3_MONTHS
if self.PRICE_6_MONTHS is not None: options[6] = self.PRICE_6_MONTHS if self.PRICE_6_MONTHS is not None: options[6] = self.PRICE_6_MONTHS
if self.PRICE_12_MONTHS is not None: options[12] = self.PRICE_12_MONTHS if self.PRICE_12_MONTHS is not None: options[12] = self.PRICE_12_MONTHS
return options return options
@property @property
def referral_bonus_inviter(self) -> Dict[int, int]: def referral_bonus_inviter(self) -> Dict[int, int]:
bonuses :Dict [int ,int ]={}; bonuses: Dict[int, int] = {}
if self .REFERRAL_BONUS_DAYS_1_MONTH is not None :bonuses [1 ]=self .REFERRAL_BONUS_DAYS_1_MONTH if self.REFERRAL_BONUS_DAYS_1_MONTH is not None:
if self .REFERRAL_BONUS_DAYS_3_MONTHS is not None :bonuses [3 ]=self .REFERRAL_BONUS_DAYS_3_MONTHS bonuses[1] = self.REFERRAL_BONUS_DAYS_1_MONTH
if self .REFERRAL_BONUS_DAYS_6_MONTHS is not None :bonuses [6 ]=self .REFERRAL_BONUS_DAYS_6_MONTHS if self.REFERRAL_BONUS_DAYS_3_MONTHS is not None:
if self .REFERRAL_BONUS_DAYS_12_MONTHS is not None :bonuses [12 ]=self .REFERRAL_BONUS_DAYS_12_MONTHS bonuses[3] = self.REFERRAL_BONUS_DAYS_3_MONTHS
if self.REFERRAL_BONUS_DAYS_6_MONTHS is not None:
bonuses[6] = self.REFERRAL_BONUS_DAYS_6_MONTHS
if self.REFERRAL_BONUS_DAYS_12_MONTHS is not None:
bonuses[12] = self.REFERRAL_BONUS_DAYS_12_MONTHS
return bonuses return bonuses
@property @property
def referral_bonus_referee(self) -> Dict[int, int]: def referral_bonus_referee(self) -> Dict[int, int]:
bonuses :Dict [int ,int ]={}; bonuses: Dict[int, int] = {}
if self .REFEREE_BONUS_DAYS_1_MONTH is not None :bonuses [1 ]=self .REFEREE_BONUS_DAYS_1_MONTH if self.REFEREE_BONUS_DAYS_1_MONTH is not None:
if self .REFEREE_BONUS_DAYS_3_MONTHS is not None :bonuses [3 ]=self .REFEREE_BONUS_DAYS_3_MONTHS bonuses[1] = self.REFEREE_BONUS_DAYS_1_MONTH
if self .REFEREE_BONUS_DAYS_6_MONTHS is not None :bonuses [6 ]=self .REFEREE_BONUS_DAYS_6_MONTHS if self.REFEREE_BONUS_DAYS_3_MONTHS is not None:
if self .REFEREE_BONUS_DAYS_12_MONTHS is not None :bonuses [12 ]=self .REFEREE_BONUS_DAYS_12_MONTHS bonuses[3] = self.REFEREE_BONUS_DAYS_3_MONTHS
if self.REFEREE_BONUS_DAYS_6_MONTHS is not None:
bonuses[6] = self.REFEREE_BONUS_DAYS_6_MONTHS
if self.REFEREE_BONUS_DAYS_12_MONTHS is not None:
bonuses[12] = self.REFEREE_BONUS_DAYS_12_MONTHS
return bonuses return bonuses
model_config =SettingsConfigDict (env_file ='.env',env_file_encoding ='utf-8',extra ='ignore',populate_by_name =True ) model_config = SettingsConfigDict(env_file='.env',
env_file_encoding='utf-8',
extra='ignore',
populate_by_name=True)
_settings_instance: Optional[Settings] = None _settings_instance: Optional[Settings] = None
def get_settings() -> Settings: def get_settings() -> Settings:
global _settings_instance global _settings_instance
if _settings_instance is None: if _settings_instance is None:
try: try:
_settings_instance = Settings() _settings_instance = Settings()
if not _settings_instance.ADMIN_IDS: if not _settings_instance.ADMIN_IDS:
logging .warning ("CRITICAL: ADMIN_IDS not set or contains no valid integer IDs in .env. Admin functionality will be restricted.") logging.warning(
"CRITICAL: ADMIN_IDS not set or contains no valid integer IDs in .env. Admin functionality will be restricted."
)
except ValidationError as e: except ValidationError as e:
logging .critical (f"Pydantic validation error while loading settings: {e}") logging.critical(
f"Pydantic validation error while loading settings: {e}")
raise SystemExit(f"CRITICAL SETTINGS ERROR: {e}") raise SystemExit(f"CRITICAL SETTINGS ERROR: {e}")
return _settings_instance return _settings_instance
+688 -214
View File
File diff suppressed because it is too large Load Diff
+3 -12
View File
@@ -6,33 +6,24 @@ from aiogram import Bot ,Dispatcher
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from dotenv import load_dotenv from dotenv import load_dotenv
from bot.main_bot import run_bot from bot.main_bot import run_bot
from config.settings import get_settings, Settings from config.settings import get_settings, Settings
from db.database import init_db from db.database import init_db
async def main(): async def main():
load_dotenv() load_dotenv()
settings = get_settings() settings = get_settings()
await init_db() await init_db()
await run_bot(settings) await run_bot(settings)
if __name__ == "__main__": if __name__ == "__main__":
logging .basicConfig (level =logging .INFO ,stream =sys .stdout , logging.basicConfig(
level=logging.INFO,
stream=sys.stdout,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
try: try:
asyncio.run(main()) asyncio.run(main())