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