Migrating to Postgres from sqlite3
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .common import router as common_admin_router
|
||||
from .promo_codes import router as promo_codes_admin_router
|
||||
from .user_management import router as user_management_admin_router
|
||||
from .broadcast import router as broadcast_admin_router
|
||||
from .statistics import router as statistics_admin_router
|
||||
from .sync_admin import router as sync_admin_router
|
||||
from .logs_admin import router as logs_admin_router
|
||||
from . import common
|
||||
from . import broadcast
|
||||
from . import promo_codes
|
||||
from . import user_management
|
||||
from . import statistics
|
||||
from . import sync_admin
|
||||
from . import logs_admin
|
||||
|
||||
admin_router_aggregate = Router(name="admin_router_aggregate")
|
||||
admin_router_aggregate = Router(name="admin_features_router")
|
||||
|
||||
admin_router_aggregate.include_router(common_admin_router)
|
||||
admin_router_aggregate.include_router(promo_codes_admin_router)
|
||||
admin_router_aggregate.include_router(user_management_admin_router)
|
||||
admin_router_aggregate.include_router(broadcast_admin_router)
|
||||
admin_router_aggregate.include_router(statistics_admin_router)
|
||||
admin_router_aggregate.include_router(sync_admin_router)
|
||||
admin_router_aggregate.include_router(logs_admin_router)
|
||||
admin_router_aggregate.include_router(common.router)
|
||||
admin_router_aggregate.include_router(broadcast.router)
|
||||
admin_router_aggregate.include_router(promo_codes.router)
|
||||
admin_router_aggregate.include_router(user_management.router)
|
||||
admin_router_aggregate.include_router(statistics.router)
|
||||
admin_router_aggregate.include_router(sync_admin.router)
|
||||
admin_router_aggregate.include_router(logs_admin.router)
|
||||
|
||||
__all__ = ("admin_router_aggregate", )
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
|
||||
from db.database import get_all_users_for_broadcast, log_user_action
|
||||
from db.dal import user_dal, message_log_dal
|
||||
|
||||
from bot.states.admin_states import AdminStates
|
||||
from bot.keyboards.inline.admin_keyboards import get_broadcast_confirmation_keyboard, get_back_to_admin_panel_keyboard
|
||||
from bot.keyboards.inline.admin_keyboards import get_broadcast_confirmation_keyboard, get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="admin_broadcast_router")
|
||||
@@ -17,9 +19,9 @@ 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'))
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in broadcast_message_prompt_handler")
|
||||
@@ -37,7 +39,8 @@ async def broadcast_message_prompt_handler(callback: types.CallbackQuery,
|
||||
current_lang, i18n))
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
f"Could not edit message for broadcast prompt: {e}")
|
||||
f"Could not edit message for broadcast prompt: {e}. Sending new."
|
||||
)
|
||||
await callback.message.answer(
|
||||
prompt_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
@@ -49,9 +52,9 @@ async def broadcast_message_prompt_handler(callback: types.CallbackQuery,
|
||||
@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'))
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in process_broadcast_message_handler")
|
||||
@@ -63,8 +66,9 @@ async def process_broadcast_message_handler(message: types.Message,
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -79,34 +83,27 @@ async def process_broadcast_message_handler(message: types.Message,
|
||||
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'))
|
||||
settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error cancelling.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(_("admin_broadcast_cancelled"),
|
||||
reply_markup=None)
|
||||
except Exception:
|
||||
await callback.message.answer(_("admin_broadcast_cancelled"))
|
||||
await callback.answer(_("admin_broadcast_cancelled"))
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
_("admin_broadcast_cancelled_nav_back"), reply_markup=None)
|
||||
except Exception:
|
||||
await callback.message.answer(_("admin_broadcast_cancelled_nav_back"))
|
||||
|
||||
await callback.answer(_("admin_broadcast_cancelled_alert"))
|
||||
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(_(key="admin_panel_title"),
|
||||
reply_markup=get_admin_panel_keyboard(
|
||||
i18n, current_lang, settings))
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("broadcast_final_action:"),
|
||||
@@ -114,80 +111,87 @@ async def cancel_broadcast_at_prompt_stage(callback: types.CallbackQuery,
|
||||
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'))
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error processing broadcast confirmation.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
action = callback.data.split(":")[1]
|
||||
user_fsm_data = await state.get_data()
|
||||
broadcast_message = user_fsm_data.get("broadcast_message")
|
||||
|
||||
if not callback.message:
|
||||
await callback.answer("Error: message context lost.", show_alert=True)
|
||||
await state.clear()
|
||||
return
|
||||
broadcast_message_to_send = user_fsm_data.get("broadcast_message")
|
||||
|
||||
if action == "send":
|
||||
if not broadcast_message:
|
||||
if not broadcast_message_to_send:
|
||||
await callback.message.edit_text(
|
||||
_("admin_broadcast_error_no_message"))
|
||||
await state.clear()
|
||||
await callback.answer(show_alert=True)
|
||||
await callback.answer(_("admin_broadcast_error_no_message_alert"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(_("admin_broadcast_sending_started"),
|
||||
reply_markup=None)
|
||||
await callback.answer()
|
||||
|
||||
users_to_broadcast = await get_all_users_for_broadcast()
|
||||
user_ids_for_broadcast = await user_dal.get_all_active_user_ids_for_broadcast(
|
||||
session)
|
||||
|
||||
sent_count = 0
|
||||
failed_count = 0
|
||||
logging.info(
|
||||
f"Starting broadcast: '{broadcast_message[:50]}...' to {len(users_to_broadcast)} users."
|
||||
f"Admin {callback.from_user.id} starting broadcast: '{broadcast_message_to_send[:50]}...' to {len(user_ids_for_broadcast)} users."
|
||||
)
|
||||
|
||||
admin_user = callback.from_user
|
||||
|
||||
for user_row in users_to_broadcast:
|
||||
user_id = user_row['user_id']
|
||||
for user_id_to_send in user_ids_for_broadcast:
|
||||
try:
|
||||
await bot.send_message(user_id,
|
||||
broadcast_message,
|
||||
await bot.send_message(user_id_to_send,
|
||||
broadcast_message_to_send,
|
||||
parse_mode="HTML")
|
||||
sent_count += 1
|
||||
|
||||
await log_user_action(
|
||||
user_id=admin_user.id,
|
||||
telegram_username=admin_user.username,
|
||||
telegram_first_name=admin_user.first_name,
|
||||
event_type="admin_broadcast_sent",
|
||||
content=f"To user {user_id}: {broadcast_message[:70]}...",
|
||||
is_admin_event=True,
|
||||
target_user_id=user_id)
|
||||
log_payload_sent = {
|
||||
"user_id": admin_user.id,
|
||||
"telegram_username": admin_user.username,
|
||||
"telegram_first_name": admin_user.first_name,
|
||||
"event_type": "admin_broadcast_sent",
|
||||
"content":
|
||||
f"To user {user_id_to_send}: {broadcast_message_to_send[:70]}...",
|
||||
"is_admin_event": True,
|
||||
"target_user_id": user_id_to_send
|
||||
}
|
||||
await message_log_dal.create_message_log(
|
||||
session, log_payload_sent)
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
logging.warning(
|
||||
f"Failed to send broadcast to user {user_id}: {type(e).__name__} - {e}"
|
||||
f"Failed to send broadcast to user {user_id_to_send}: {type(e).__name__} - {e}"
|
||||
)
|
||||
|
||||
await log_user_action(
|
||||
user_id=admin_user.id,
|
||||
telegram_username=admin_user.username,
|
||||
telegram_first_name=admin_user.first_name,
|
||||
event_type="admin_broadcast_failed",
|
||||
content=
|
||||
f"For user {user_id}: {type(e).__name__} - {str(e)[:70]}...",
|
||||
is_admin_event=True,
|
||||
target_user_id=user_id)
|
||||
log_payload_failed = {
|
||||
"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_to_send}: {type(e).__name__} - {str(e)[:70]}...",
|
||||
"is_admin_event": True,
|
||||
"target_user_id": user_id_to_send
|
||||
}
|
||||
await message_log_dal.create_message_log(
|
||||
session, log_payload_failed)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
try:
|
||||
await session.commit()
|
||||
except Exception as e_commit:
|
||||
await session.rollback()
|
||||
logging.error(f"Error committing broadcast logs: {e_commit}")
|
||||
|
||||
result_message = _("admin_broadcast_finished_stats",
|
||||
sent_count=sent_count,
|
||||
failed_count=failed_count)
|
||||
|
||||
@@ -3,11 +3,13 @@ from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.keyboards.inline.admin_keyboards import get_admin_panel_keyboard
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
|
||||
from . import broadcast as admin_broadcast_handlers
|
||||
from . import promo_codes as admin_promo_handlers
|
||||
@@ -20,10 +22,12 @@ 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", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in admin_panel_command_handler")
|
||||
@@ -37,16 +41,14 @@ async def admin_panel_command_handler(message: types.Message,
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_action:"))
|
||||
async def admin_panel_actions_callback_handler(callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict, bot: Bot,
|
||||
panel_service: PanelApiService):
|
||||
async def admin_panel_actions_callback_handler(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, bot: Bot, panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
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", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in admin_panel_actions_callback_handler")
|
||||
@@ -56,7 +58,7 @@ async def admin_panel_actions_callback_handler(callback: types.CallbackQuery,
|
||||
|
||||
if not callback.message:
|
||||
logging.error(
|
||||
f"CallbackQuery {callback.id} from {callback.from_user.id} has no message for action {action}"
|
||||
f"CallbackQuery {callback.id} from {callback.from_user.id} has no message for admin_action {action}"
|
||||
)
|
||||
await callback.answer("Error processing action: message context lost.",
|
||||
show_alert=True)
|
||||
@@ -64,36 +66,38 @@ async def admin_panel_actions_callback_handler(callback: types.CallbackQuery,
|
||||
|
||||
if action == "stats":
|
||||
await admin_stats_handlers.show_statistics_handler(
|
||||
callback, i18n_data, settings)
|
||||
callback, i18n_data, settings, session)
|
||||
elif action == "broadcast":
|
||||
await admin_broadcast_handlers.broadcast_message_prompt_handler(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "create_promo":
|
||||
await admin_promo_handlers.create_promo_prompt_handler(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "view_promos":
|
||||
await admin_promo_handlers.view_promo_codes_handler(
|
||||
callback, i18n_data, settings)
|
||||
callback, i18n_data, settings, session)
|
||||
elif action == "ban_user_prompt":
|
||||
await admin_user_mgmnt_handlers.ban_user_prompt_handler(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "unban_user_prompt":
|
||||
await admin_user_mgmnt_handlers.unban_user_prompt_handler(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "view_banned":
|
||||
|
||||
await admin_user_mgmnt_handlers.view_banned_users_handler(
|
||||
callback, i18n_data, settings, state)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "view_logs_menu":
|
||||
await admin_logs_handlers.display_logs_menu(callback, i18n_data,
|
||||
settings)
|
||||
settings, session)
|
||||
elif action == "sync_panel":
|
||||
|
||||
await admin_sync_handlers.sync_command_handler(
|
||||
callback.message,
|
||||
message_event=callback,
|
||||
bot=bot,
|
||||
settings=settings,
|
||||
i18n_data=i18n_data,
|
||||
panel_service=panel_service)
|
||||
panel_service=panel_service,
|
||||
session=session)
|
||||
await callback.answer(_("admin_sync_initiated_from_panel"))
|
||||
elif action == "main":
|
||||
try:
|
||||
|
||||
+116
-114
@@ -4,18 +4,18 @@ import re
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, List, Dict, Any
|
||||
import aiosqlite
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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.dal import message_log_dal, user_dal
|
||||
from db.models import MessageLog, User
|
||||
|
||||
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_back_to_admin_panel_keyboard)
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="admin_logs_router")
|
||||
@@ -23,19 +23,13 @@ USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
|
||||
|
||||
|
||||
async def display_logs_menu(callback: types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings):
|
||||
settings: Settings, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
if not i18n:
|
||||
logging.error("i18n_instance missing in display_logs_menu")
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error displaying logs menu.", show_alert=True)
|
||||
return
|
||||
if not callback.message:
|
||||
logging.error("CallbackQuery has no message in display_logs_menu")
|
||||
await callback.answer("Error processing request.", show_alert=True)
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
try:
|
||||
@@ -43,7 +37,8 @@ async def display_logs_menu(callback: types.CallbackQuery, i18n_data: dict,
|
||||
reply_markup=get_logs_menu_keyboard(
|
||||
i18n, current_lang))
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to edit message for logs menu: {e}")
|
||||
logging.warning(
|
||||
f"Failed to edit message for logs menu: {e}. Sending new.")
|
||||
await callback.message.answer(text=_(key="admin_logs_menu_title"),
|
||||
reply_markup=get_logs_menu_keyboard(
|
||||
i18n, current_lang))
|
||||
@@ -51,9 +46,9 @@ async def display_logs_menu(callback: types.CallbackQuery, i18n_data: dict,
|
||||
|
||||
|
||||
async def _display_formatted_logs(target_message: types.Message,
|
||||
logs: List[aiosqlite.Row],
|
||||
logs: List[MessageLog],
|
||||
total_logs: int,
|
||||
current_page: int,
|
||||
current_page_idx: int,
|
||||
settings: Settings,
|
||||
title_key: str,
|
||||
base_pagination_callback_data: str,
|
||||
@@ -63,7 +58,6 @@ async def _display_formatted_logs(target_message: types.Message,
|
||||
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:
|
||||
@@ -71,7 +65,7 @@ async def _display_formatted_logs(target_message: types.Message,
|
||||
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,
|
||||
current_page_idx,
|
||||
1,
|
||||
base_pagination_callback_data,
|
||||
i18n,
|
||||
@@ -80,55 +74,46 @@ async def _display_formatted_logs(target_message: types.Message,
|
||||
else:
|
||||
total_pages = math.ceil(total_logs / page_size) if page_size > 0 else 1
|
||||
text = _(title_key,
|
||||
current_page=current_page + 1,
|
||||
current_page=current_page_idx + 1,
|
||||
total_pages=max(1, total_pages),
|
||||
**actual_title_kwargs) + "\n"
|
||||
|
||||
log_entries_text = []
|
||||
for log_entry in logs:
|
||||
for log_entry_model in logs:
|
||||
user_display_parts = []
|
||||
|
||||
telegram_first_name = log_entry[
|
||||
'telegram_first_name'] if 'telegram_first_name' in log_entry.keys(
|
||||
) and log_entry['telegram_first_name'] else None
|
||||
telegram_username = log_entry[
|
||||
'telegram_username'] if 'telegram_username' in log_entry.keys(
|
||||
) and log_entry['telegram_username'] else None
|
||||
user_id_from_log = log_entry[
|
||||
'user_id'] if 'user_id' in log_entry.keys(
|
||||
) and log_entry['user_id'] else None
|
||||
|
||||
if telegram_first_name:
|
||||
user_display_parts.append(telegram_first_name)
|
||||
if telegram_username:
|
||||
user_display_parts.append(f"(@{telegram_username})")
|
||||
if log_entry_model.telegram_first_name:
|
||||
user_display_parts.append(log_entry_model.telegram_first_name)
|
||||
if log_entry_model.telegram_username:
|
||||
user_display_parts.append(
|
||||
f"(@{log_entry_model.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}"
|
||||
) if not log_entry_model.user_id else f"ID: {log_entry_model.user_id}"
|
||||
|
||||
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 ""
|
||||
log_entry_model.user_id
|
||||
) if log_entry_model.user_id is not None else "N/A"
|
||||
content_raw = log_entry_model.content or ""
|
||||
content_preview = (content_raw[:100] +
|
||||
"...") if len(content_raw) > 100 else (
|
||||
content_raw or "N/A")
|
||||
|
||||
timestamp_str_display = log_entry_model.timestamp.strftime(
|
||||
'%Y-%m-%d %H:%M:%S') if log_entry_model.timestamp else '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=timestamp_str_display,
|
||||
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',
|
||||
event_type=log_entry_model.event_type or 'N/A',
|
||||
content_preview=content_preview).replace("\n", "\n "))
|
||||
text += "\n\n".join(log_entries_text)
|
||||
reply_markup = get_logs_pagination_keyboard(
|
||||
current_page,
|
||||
current_page_idx,
|
||||
total_pages,
|
||||
base_pagination_callback_data,
|
||||
i18n,
|
||||
@@ -142,30 +127,40 @@ async def _display_formatted_logs(target_message: types.Message,
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
f"Failed to edit message for logs display: {e}. Content length: {len(text)}"
|
||||
f"Failed to edit message for logs display (len: {len(text)}): {e}. Sending new message(s)."
|
||||
)
|
||||
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)
|
||||
|
||||
max_chunk_size = 4000
|
||||
for i in range(0, len(text), max_chunk_size):
|
||||
chunk = text[i:i + max_chunk_size]
|
||||
is_last_chunk = (i + max_chunk_size) >= len(text)
|
||||
try:
|
||||
await target_message.answer(
|
||||
chunk,
|
||||
reply_markup=reply_markup if is_last_chunk else None,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_chunk:
|
||||
logging.error(f"Failed to send log chunk: {e_chunk}")
|
||||
|
||||
if i == 0:
|
||||
await target_message.answer(
|
||||
_("error_displaying_logs_too_long"),
|
||||
reply_markup=reply_markup if is_last_chunk else None)
|
||||
break
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_logs:view_all"))
|
||||
async def view_all_logs_handler(callback: types.CallbackQuery,
|
||||
settings: Settings, i18n_data: dict):
|
||||
page = 0
|
||||
settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
page_idx = 0
|
||||
parts = callback.data.split(":")
|
||||
|
||||
if len(parts) == 3:
|
||||
try:
|
||||
page = int(parts[2])
|
||||
page_idx = int(parts[2])
|
||||
except ValueError:
|
||||
page = 0
|
||||
page_idx = 0
|
||||
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
@@ -173,15 +168,15 @@ async def view_all_logs_handler(callback: types.CallbackQuery,
|
||||
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_models = await message_log_dal.get_all_message_logs(
|
||||
session, settings.LOGS_PAGE_SIZE, page_idx * settings.LOGS_PAGE_SIZE)
|
||||
total_logs_count = await message_log_dal.count_all_message_logs(session)
|
||||
|
||||
await _display_formatted_logs(
|
||||
target_message=callback.message,
|
||||
logs=logs,
|
||||
total_logs=total_logs,
|
||||
current_page=page,
|
||||
logs=logs_models,
|
||||
total_logs=total_logs_count,
|
||||
current_page_idx=page_idx,
|
||||
settings=settings,
|
||||
title_key="admin_all_logs_title",
|
||||
base_pagination_callback_data="admin_logs:view_all",
|
||||
@@ -193,11 +188,13 @@ async def view_all_logs_handler(callback: types.CallbackQuery,
|
||||
@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):
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
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")
|
||||
await callback.answer("Error preparing user log prompt.",
|
||||
show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
@@ -211,103 +208,106 @@ async def prompt_user_for_logs_handler(callback: types.CallbackQuery,
|
||||
@router.message(AdminStates.waiting_for_user_id_for_logs, F.text)
|
||||
async def process_user_id_for_logs_handler(message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict):
|
||||
current_state_fsm = await state.get_state()
|
||||
logging.info(
|
||||
f"Processing user input for logs in state {current_state_fsm}: '{message.text}'"
|
||||
)
|
||||
settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
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.")
|
||||
await message.reply("Language service error.")
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
input_text = message.text.strip()
|
||||
user_data_for_logs: Optional[aiosqlite.Row] = None
|
||||
input_text = message.text.strip() if message.text else ""
|
||||
user_model_for_logs: Optional[User] = None
|
||||
|
||||
if input_text.isdigit():
|
||||
try:
|
||||
user_data_for_logs = await get_user(int(input_text))
|
||||
user_model_for_logs = await user_dal.get_user_by_id(
|
||||
session, 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_model_for_logs = await user_dal.get_user_by_username(
|
||||
session, input_text[1:])
|
||||
elif USERNAME_REGEX.match(input_text):
|
||||
user_data_for_logs = await get_user_by_telegram_username(input_text)
|
||||
user_model_for_logs = await user_dal.get_user_by_username(
|
||||
session, input_text)
|
||||
|
||||
if not user_data_for_logs:
|
||||
if not user_model_for_logs:
|
||||
await message.answer(_("admin_log_user_not_found", input=input_text))
|
||||
return
|
||||
|
||||
target_user_id = user_data_for_logs['user_id']
|
||||
user_display = user_data_for_logs['first_name'] or (
|
||||
f"@{user_data_for_logs['username']}"
|
||||
if user_data_for_logs.get('username') else f"ID {target_user_id}")
|
||||
target_user_id = user_model_for_logs.user_id
|
||||
user_display_name = user_model_for_logs.first_name or (
|
||||
f"@{user_model_for_logs.username}"
|
||||
if user_model_for_logs.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_models = await message_log_dal.get_user_message_logs(
|
||||
session, target_user_id, settings.LOGS_PAGE_SIZE, 0)
|
||||
total_user_logs_count = await message_log_dal.count_user_message_logs(
|
||||
session, target_user_id)
|
||||
|
||||
await _display_formatted_logs(
|
||||
target_message=message,
|
||||
logs=logs,
|
||||
total_logs=total_logs,
|
||||
current_page=0,
|
||||
logs=logs_models,
|
||||
total_logs=total_user_logs_count,
|
||||
current_page_idx=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})
|
||||
title_kwargs={"user_display": user_display_name})
|
||||
|
||||
|
||||
@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):
|
||||
settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
try:
|
||||
parts = callback.data.split(":")
|
||||
target_user_id = int(parts[2])
|
||||
page = int(parts[3])
|
||||
page_idx = int(parts[3])
|
||||
except (IndexError, ValueError):
|
||||
await callback.answer("Invalid log request.", show_alert=True)
|
||||
await callback.answer("Invalid log request format.", 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")
|
||||
await callback.answer("Error processing request.", show_alert=True)
|
||||
return
|
||||
|
||||
user_data_for_logs = await get_user(target_user_id)
|
||||
if not user_data_for_logs:
|
||||
user_model_for_logs = await user_dal.get_user_by_id(
|
||||
session, target_user_id)
|
||||
if not user_model_for_logs:
|
||||
await callback.message.edit_text("User not found for logs.")
|
||||
await callback.answer()
|
||||
return
|
||||
user_display = user_data_for_logs['first_name'] or (
|
||||
f"@{user_data_for_logs['username']}"
|
||||
if user_data_for_logs.get('username') else f"ID {target_user_id}")
|
||||
|
||||
logs, total_logs = await get_user_message_logs_paginated(
|
||||
target_user_id, settings.LOGS_PAGE_SIZE, page *
|
||||
settings.LOGS_PAGE_SIZE), await count_user_message_logs(target_user_id)
|
||||
user_display_name = user_model_for_logs.first_name or (
|
||||
f"@{user_model_for_logs.username}"
|
||||
if user_model_for_logs.username else f"ID {target_user_id}")
|
||||
|
||||
logs_models = await message_log_dal.get_user_message_logs(
|
||||
session, target_user_id, settings.LOGS_PAGE_SIZE,
|
||||
page_idx * settings.LOGS_PAGE_SIZE)
|
||||
total_user_logs_count = await message_log_dal.count_user_message_logs(
|
||||
session, target_user_id)
|
||||
|
||||
await _display_formatted_logs(
|
||||
target_message=callback.message,
|
||||
logs=logs,
|
||||
total_logs=total_logs,
|
||||
current_page=page,
|
||||
logs=logs_models,
|
||||
total_logs=total_user_logs_count,
|
||||
current_page_idx=page_idx,
|
||||
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})
|
||||
title_kwargs={"user_display": user_display_name})
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -316,6 +316,8 @@ async def view_user_logs_paginated_handler(callback: types.CallbackQuery,
|
||||
async def cancel_log_user_input_state_to_menu(callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict):
|
||||
i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
await state.clear()
|
||||
await display_logs_menu(callback, i18n_data, settings)
|
||||
|
||||
await display_logs_menu(callback, i18n_data, settings, session)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.database import create_promo_code_db, get_promo_codes_db
|
||||
|
||||
from db.dal import promo_code_dal
|
||||
|
||||
from bot.states.admin_states import AdminStates
|
||||
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
|
||||
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="admin_promo_codes_router")
|
||||
@@ -16,32 +18,29 @@ 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'))
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in create_promo_prompt_handler")
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error preparing promo creation.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
prompt_text = _("admin_promo_create_prompt",
|
||||
example_format="MYPROMO20 7 100 30")
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
prompt_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not edit message for promo prompt: {e}")
|
||||
await callback.message.answer(
|
||||
prompt_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
prompt_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
f"Could not edit message for promo prompt: {e}. Sending new.")
|
||||
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)
|
||||
|
||||
@@ -50,87 +49,112 @@ async def create_promo_prompt_handler(callback: types.CallbackQuery,
|
||||
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,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in process_promo_code_details_handler")
|
||||
await message.reply("Language service error.")
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
if not message.text:
|
||||
await message.answer(_("admin_promo_invalid_format"))
|
||||
return
|
||||
|
||||
parts = message.text.strip().split()
|
||||
if not (3 <= len(parts) <= 4):
|
||||
await message.answer(_("admin_promo_invalid_format"))
|
||||
return
|
||||
|
||||
try:
|
||||
code = parts[0].upper()
|
||||
if not (3 <= len(code) <= 30 and code.isalnum()):
|
||||
raise ValueError(
|
||||
"Promo code must be 3-30 alphanumeric characters.")
|
||||
code_str = parts[0].upper()
|
||||
if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
|
||||
raise ValueError(_("admin_promo_invalid_code_format"))
|
||||
|
||||
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.")
|
||||
|
||||
raise ValueError(_("admin_promo_invalid_validity_days"))
|
||||
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.")
|
||||
raise ValueError(_("admin_promo_invalid_bonus_or_activations"))
|
||||
|
||||
except ValueError as e:
|
||||
await message.answer(_("admin_promo_invalid_values", error=str(e)))
|
||||
return
|
||||
except Exception as e_parse:
|
||||
logging.error(
|
||||
f"Error parsing promo details '{message.text}': {e_parse}")
|
||||
await message.answer(_("admin_promo_invalid_format_general"))
|
||||
return
|
||||
|
||||
admin_id = message.from_user.id
|
||||
admin_id = message.from_user.id if message.from_user else 0
|
||||
|
||||
promo_id = await create_promo_code_db(code, bonus_days, max_activations,
|
||||
admin_id, valid_until_date)
|
||||
promo_data_to_create = {
|
||||
"code": code_str,
|
||||
"bonus_days": bonus_days,
|
||||
"max_activations": max_activations,
|
||||
"created_by_admin_id": admin_id,
|
||||
"valid_until": valid_until_date,
|
||||
"is_active": True,
|
||||
"current_activations": 0
|
||||
}
|
||||
|
||||
if promo_id:
|
||||
success_text = _("admin_promo_created_success",
|
||||
code=code,
|
||||
bonus_days=bonus_days,
|
||||
max_activations=max_activations,
|
||||
valid_until_str=valid_until_str_display)
|
||||
await message.answer(success_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
else:
|
||||
try:
|
||||
created_promo = await promo_code_dal.create_promo_code(
|
||||
session, promo_data_to_create)
|
||||
await session.commit()
|
||||
|
||||
if created_promo:
|
||||
success_text = _("admin_promo_created_success",
|
||||
code=created_promo.code,
|
||||
bonus_days=created_promo.bonus_days,
|
||||
max_activations=created_promo.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_duplicate", code=code_str))
|
||||
|
||||
except Exception as e_db_create:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Failed to create promo code '{code_str}' in DB: {e_db_create}",
|
||||
exc_info=True)
|
||||
await message.answer(_("admin_promo_creation_failed"))
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def view_promo_codes_handler(callback: types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n_data: dict, settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in view_promo_codes_handler")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error displaying promo codes.", show_alert=True)
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
promos = await get_promo_codes_db(is_active_only=True, limit=20)
|
||||
|
||||
if not callback.message:
|
||||
await callback.answer("Error: message context lost.", show_alert=True)
|
||||
return
|
||||
promo_models = await promo_code_dal.get_all_active_promo_codes(session,
|
||||
limit=20,
|
||||
offset=0)
|
||||
|
||||
if not promos:
|
||||
if not promo_models:
|
||||
await callback.message.edit_text(
|
||||
_("admin_no_active_promos"),
|
||||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||||
@@ -138,36 +162,18 @@ async def view_promo_codes_handler(callback: types.CallbackQuery,
|
||||
return
|
||||
|
||||
response_text_parts = [f"<b>{_('admin_active_promos_list_header')}</b>\n"]
|
||||
for promo in promos:
|
||||
for promo in promo_models:
|
||||
valid_until_display_text = _("admin_promo_valid_indefinitely")
|
||||
if promo['valid_until']:
|
||||
try:
|
||||
if promo.valid_until:
|
||||
|
||||
valid_until_dt: Optional[datetime] = None
|
||||
if 'T' in promo['valid_until']:
|
||||
valid_until_dt = datetime.fromisoformat(
|
||||
promo['valid_until'].replace("Z", "+00:00"))
|
||||
else:
|
||||
valid_until_dt = datetime.strptime(promo['valid_until'],
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if valid_until_dt and valid_until_dt.tzinfo is None:
|
||||
valid_until_dt = valid_until_dt.replace(
|
||||
tzinfo=timezone.utc)
|
||||
|
||||
valid_until_display_text = valid_until_dt.strftime('%Y-%m-%d')
|
||||
except ValueError as e:
|
||||
logging.warning(
|
||||
f"Could not parse valid_until date string '{promo['valid_until']}' for promo code {promo['code']}: {e}"
|
||||
)
|
||||
valid_until_display_text = promo['valid_until']
|
||||
valid_until_display_text = promo.valid_until.strftime('%Y-%m-%d')
|
||||
|
||||
response_text_parts.append(
|
||||
_("admin_promo_list_item",
|
||||
code=promo['code'],
|
||||
bonus=promo['bonus_days'],
|
||||
current=promo['current_activations'],
|
||||
max=promo['max_activations'],
|
||||
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)
|
||||
@@ -177,7 +183,8 @@ async def view_promo_codes_handler(callback: types.CallbackQuery,
|
||||
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}")
|
||||
logging.warning(
|
||||
f"Failed to edit message for promo list: {e}. Sending new.")
|
||||
if callback.message:
|
||||
await callback.message.answer(
|
||||
final_text,
|
||||
@@ -185,3 +192,30 @@ async def view_promo_codes_handler(callback: types.CallbackQuery,
|
||||
current_lang, i18n),
|
||||
parse_mode="HTML")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_action:main",
|
||||
AdminStates.waiting_for_promo_details)
|
||||
async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
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 cancelling.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(_("admin_action_cancelled_default"),
|
||||
reply_markup=get_admin_panel_keyboard(
|
||||
i18n, current_lang, settings))
|
||||
except Exception:
|
||||
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_alert"))
|
||||
await state.clear()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, List
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.database import get_user_count_stats, get_payment_logs, get_message_logs_db, get_last_sync_status
|
||||
|
||||
from db.dal import user_dal, payment_dal, panel_sync_dal
|
||||
from db.models import Payment, PanelSyncStatus
|
||||
|
||||
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
@@ -12,67 +16,69 @@ router = Router(name="admin_statistics_router")
|
||||
|
||||
|
||||
async def show_statistics_handler(callback: types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n_data: dict, settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in show_statistics_handler")
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error displaying statistics.", show_alert=True)
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
stats_text_parts = [f"<b>{_('admin_stats_header')}</b>"]
|
||||
|
||||
user_stats = await get_user_count_stats()
|
||||
user_stats_dict = await user_dal.get_user_count_stats_dal(session)
|
||||
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)))
|
||||
total_users=user_stats_dict.get("total_users", 0),
|
||||
banned_users=user_stats_dict.get("banned_users", 0),
|
||||
active_subs=user_stats_dict.get("users_with_active_subscriptions",
|
||||
0)))
|
||||
|
||||
last_payments = await get_payment_logs(limit=5)
|
||||
if last_payments:
|
||||
last_payments_models: List[
|
||||
Payment] = await payment_dal.get_recent_payment_logs_with_user(session,
|
||||
limit=5)
|
||||
if last_payments_models:
|
||||
stats_text_parts.append(
|
||||
f"\n<b>{_('admin_stats_recent_payments_header')}</b>")
|
||||
for payment in last_payments:
|
||||
status_emoji = "✅" if payment['status'] == 'succeeded' else (
|
||||
"⏳" if payment['status'] == 'pending' else "❌")
|
||||
user_info = f"User {payment['user_id']}" + (
|
||||
f" (@{payment['username']})" if payment['username'] else "")
|
||||
payment_date_str = payment['created_at']
|
||||
if isinstance(payment_date_str,
|
||||
str) and len(payment_date_str) > 10:
|
||||
payment_date_str = payment_date_str[:10]
|
||||
for payment in last_payments_models:
|
||||
status_emoji = "✅" if payment.status == 'succeeded' else (
|
||||
"⏳" if payment.status == 'pending'
|
||||
or payment.status == 'pending_yookassa' else "❌")
|
||||
|
||||
user_info = f"User {payment.user_id}"
|
||||
if payment.user and payment.user.username:
|
||||
user_info += f" (@{payment.user.username})"
|
||||
elif payment.user and payment.user.first_name:
|
||||
user_info += f" ({payment.user.first_name})"
|
||||
|
||||
payment_date_str = payment.created_at.strftime(
|
||||
'%Y-%m-%d') if payment.created_at else "N/A"
|
||||
|
||||
stats_text_parts.append(
|
||||
_("admin_stats_payment_item",
|
||||
status_emoji=status_emoji,
|
||||
amount=payment['amount'],
|
||||
currency=payment['currency'],
|
||||
amount=payment.amount,
|
||||
currency=payment.currency,
|
||||
user_info=user_info,
|
||||
p_status=payment['status'],
|
||||
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:
|
||||
sync_status_model: Optional[
|
||||
PanelSyncStatus] = await panel_sync_dal.get_panel_sync_status(session)
|
||||
if sync_status_model and sync_status_model.status != "never_run":
|
||||
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_val = sync_status_model.last_sync_time
|
||||
sync_time_str = sync_time_val.strftime(
|
||||
'%Y-%m-%d %H:%M:%S UTC') if sync_time_val else "N/A"
|
||||
|
||||
details_val = sync_status['details']
|
||||
details_val = sync_status_model.details
|
||||
details_str = (details_val[:100] +
|
||||
"...") if details_val and len(details_val) > 100 else (
|
||||
details_val or "N/A")
|
||||
@@ -80,12 +86,12 @@ async def show_statistics_handler(callback: types.CallbackQuery,
|
||||
stats_text_parts.append(
|
||||
f" {_('admin_stats_sync_time')}: {sync_time_str}")
|
||||
stats_text_parts.append(
|
||||
f" {_('admin_stats_sync_status')}: {sync_status['status']}")
|
||||
f" {_('admin_stats_sync_status')}: {sync_status_model.status}")
|
||||
stats_text_parts.append(
|
||||
f" {_('admin_stats_sync_users_processed')}: {sync_status['users_processed_from_panel']}"
|
||||
f" {_('admin_stats_sync_users_processed')}: {sync_status_model.users_processed_from_panel}"
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f" {_('admin_stats_sync_subs_synced')}: {sync_status['subscriptions_synced']}"
|
||||
f" {_('admin_stats_sync_subs_synced')}: {sync_status_model.subscriptions_synced}"
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f" {_('admin_stats_sync_details_label')}: {details_str}")
|
||||
@@ -94,26 +100,30 @@ async def show_statistics_handler(callback: types.CallbackQuery,
|
||||
|
||||
final_text = "\n".join(stats_text_parts)
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n),
|
||||
parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"Error editing message for statistics: {e}",
|
||||
exc_info=True)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_text,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||||
parse_mode="HTML")
|
||||
except Exception as e_edit:
|
||||
logging.error(f"Error editing message for statistics: {e_edit}",
|
||||
exc_info=True)
|
||||
|
||||
for chunk in [
|
||||
final_text[i:i + 4000]
|
||||
for i in range(0, len(final_text), 4000)
|
||||
]:
|
||||
max_chunk_size = 4000
|
||||
for i in range(0, len(final_text), max_chunk_size):
|
||||
chunk = final_text[i:i + max_chunk_size]
|
||||
is_last_chunk = (i + max_chunk_size) >= len(final_text)
|
||||
try:
|
||||
await callback.message.answer(
|
||||
chunk,
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n)
|
||||
if chunk == final_text[-len(chunk):] else None,
|
||||
current_lang, i18n) if is_last_chunk else None,
|
||||
parse_mode="HTML")
|
||||
else:
|
||||
logging.error("Cannot send statistics, callback.message is None.")
|
||||
except Exception as e_chunk:
|
||||
logging.error(f"Failed to send statistics chunk: {e_chunk}")
|
||||
if i == 0:
|
||||
await callback.message.answer(
|
||||
_("error_displaying_statistics"),
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
break
|
||||
|
||||
@@ -1,36 +1,56 @@
|
||||
import logging
|
||||
from aiogram import Router, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
|
||||
from db.database import update_sync_status, get_last_sync_status, sync_panel_user_data
|
||||
from db.dal import user_dal, subscription_dal, panel_sync_dal
|
||||
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
|
||||
|
||||
router = Router(name="admin_sync_router")
|
||||
|
||||
|
||||
@router.message(Command("sync"))
|
||||
async def sync_command_handler(message: types.Message, bot: Bot,
|
||||
settings: Settings, i18n_data: dict,
|
||||
panel_service: PanelApiService):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
async def sync_command_handler(message_event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
bot: Bot, settings: Settings, i18n_data: dict,
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await message.answer("Language error.")
|
||||
logging.error("i18n missing in sync_command_handler")
|
||||
|
||||
if isinstance(message_event, types.Message):
|
||||
await message_event.answer("Language error.")
|
||||
elif isinstance(message_event, types.CallbackQuery):
|
||||
await message_event.answer("Language error.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
await message.answer(_("sync_started"))
|
||||
logging.info("Admin triggered panel sync.")
|
||||
target_chat_id = message_event.chat.id if isinstance(
|
||||
message_event, types.Message) else (
|
||||
message_event.message.chat.id if message_event.message else None)
|
||||
if not target_chat_id:
|
||||
logging.error("Sync handler: could not determine target_chat_id.")
|
||||
if isinstance(message_event, types.CallbackQuery):
|
||||
await message_event.answer("Error initiating sync.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if isinstance(message_event, types.Message):
|
||||
await message_event.answer(_("sync_started"))
|
||||
|
||||
logging.info(f"Admin ({message_event.from_user.id}) triggered panel sync.")
|
||||
|
||||
users_processed_count = 0
|
||||
users_synced_successfully = 0
|
||||
subscriptions_synced_count = 0
|
||||
sync_errors = []
|
||||
|
||||
try:
|
||||
@@ -39,14 +59,20 @@ async def sync_command_handler(message: types.Message, bot: Bot,
|
||||
if panel_users_data is None:
|
||||
error_msg = "Failed to fetch users from panel or panel API issue."
|
||||
sync_errors.append(error_msg)
|
||||
await update_sync_status("failed", error_msg)
|
||||
await message.answer(_("sync_failed", details=error_msg))
|
||||
await panel_sync_dal.update_panel_sync_status(
|
||||
session, "failed", error_msg)
|
||||
await session.commit()
|
||||
await bot.send_message(target_chat_id,
|
||||
_("sync_failed", details=error_msg))
|
||||
return
|
||||
|
||||
if not panel_users_data:
|
||||
status_msg = "No users found in the panel to sync."
|
||||
await update_sync_status("success", status_msg, 0, 0)
|
||||
await message.answer(
|
||||
await panel_sync_dal.update_panel_sync_status(
|
||||
session, "success", status_msg, 0, 0)
|
||||
await session.commit()
|
||||
await bot.send_message(
|
||||
target_chat_id,
|
||||
_("sync_completed", status="Success", details=status_msg))
|
||||
return
|
||||
|
||||
@@ -55,94 +81,233 @@ async def sync_command_handler(message: types.Message, bot: Bot,
|
||||
|
||||
for panel_user_dict in panel_users_data:
|
||||
users_processed_count += 1
|
||||
telegram_id_from_panel = panel_user_dict.get('telegramId')
|
||||
panel_uuid = panel_user_dict.get('uuid')
|
||||
|
||||
if not telegram_id_from_panel:
|
||||
logging.info(
|
||||
f"Panel user {panel_uuid} (username: {panel_user_dict.get('username')}) has no 'telegramId'. Skipping TG ID based sync."
|
||||
)
|
||||
|
||||
continue
|
||||
telegram_id_from_panel_str = panel_user_dict.get('telegramId')
|
||||
panel_username = panel_user_dict.get('username')
|
||||
|
||||
if not panel_uuid:
|
||||
logging.warning(
|
||||
f"Panel user (TG ID: {telegram_id_from_panel}) missing 'uuid'. Skipping."
|
||||
f"Sync: Panel user data missing 'uuid'. Data: {str(panel_user_dict)[:200]}. Skipping."
|
||||
)
|
||||
sync_errors.append(
|
||||
f"Panel user data for TG ID {telegram_id_from_panel} missing UUID."
|
||||
f"Panel user data (username: {panel_username or 'N/A'}) 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})"
|
||||
telegram_id_from_panel: Optional[int] = None
|
||||
if telegram_id_from_panel_str:
|
||||
try:
|
||||
telegram_id_from_panel = int(telegram_id_from_panel_str)
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Sync: Panel user {panel_uuid} (username: {panel_username}) has invalid 'telegramId': {telegram_id_from_panel_str}. Skipping TG ID based sync."
|
||||
)
|
||||
|
||||
if not telegram_id_from_panel:
|
||||
|
||||
logging.info(
|
||||
f"Sync: Panel user {panel_uuid} (username: {panel_username}) has no valid 'telegramId'. Skipping full sync for this user."
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
bot_user = await user_dal.get_user_by_id(session,
|
||||
telegram_id_from_panel)
|
||||
if not bot_user:
|
||||
user_data_to_create = {
|
||||
"user_id":
|
||||
telegram_id_from_panel,
|
||||
"username":
|
||||
panel_username,
|
||||
"panel_user_uuid":
|
||||
panel_uuid,
|
||||
"language_code":
|
||||
settings.DEFAULT_LANGUAGE,
|
||||
"registration_date":
|
||||
datetime.fromisoformat(
|
||||
panel_user_dict['createdAt'].replace("Z", "+00:00"))
|
||||
if panel_user_dict.get('createdAt') else datetime.now(
|
||||
timezone.utc)
|
||||
}
|
||||
bot_user = await user_dal.create_user(session,
|
||||
user_data_to_create)
|
||||
logging.info(
|
||||
f"Sync: Created new local user {telegram_id_from_panel} from panel data {panel_uuid}."
|
||||
)
|
||||
else:
|
||||
if bot_user.panel_user_uuid != panel_uuid:
|
||||
if bot_user.panel_user_uuid is not None:
|
||||
logging.warning(
|
||||
f"Sync: Local user {telegram_id_from_panel} was linked to {bot_user.panel_user_uuid}, panel now gives {panel_uuid}. Updating."
|
||||
)
|
||||
|
||||
conflicting_user = await user_dal.get_user_by_panel_uuid(
|
||||
session, panel_uuid)
|
||||
if conflicting_user and conflicting_user.user_id != telegram_id_from_panel:
|
||||
sync_errors.append(
|
||||
f"Panel UUID {panel_uuid} for TG {telegram_id_from_panel} already linked to another TG user {conflicting_user.user_id}."
|
||||
)
|
||||
logging.error(sync_errors[-1])
|
||||
continue
|
||||
|
||||
await user_dal.update_user(
|
||||
session, telegram_id_from_panel, {
|
||||
"panel_user_uuid": panel_uuid,
|
||||
"username": panel_username
|
||||
})
|
||||
logging.info(
|
||||
f"Sync: Updated panel_uuid for local user {telegram_id_from_panel} to {panel_uuid}."
|
||||
)
|
||||
|
||||
panel_sub_link_id = panel_user_dict.get(
|
||||
'subscriptionUuid') or panel_user_dict.get('shortUuid')
|
||||
if panel_sub_link_id:
|
||||
end_date_str = panel_user_dict.get('expireAt')
|
||||
start_date_str = panel_user_dict.get('createdAt')
|
||||
|
||||
if end_date_str:
|
||||
try:
|
||||
end_date_obj = datetime.fromisoformat(
|
||||
end_date_str.replace("Z", "+00:00"))
|
||||
start_date_obj = datetime.fromisoformat(
|
||||
start_date_str.replace(
|
||||
"Z",
|
||||
"+00:00")) if start_date_str else datetime.now(
|
||||
timezone.utc)
|
||||
|
||||
status_from_panel = panel_user_dict.get(
|
||||
'status', 'UNKNOWN').upper()
|
||||
is_active_flag = 1 if status_from_panel == 'ACTIVE' and end_date_obj > datetime.now(
|
||||
timezone.utc) else 0
|
||||
|
||||
sub_payload = {
|
||||
"user_id":
|
||||
telegram_id_from_panel,
|
||||
"panel_user_uuid":
|
||||
panel_uuid,
|
||||
"panel_subscription_uuid":
|
||||
panel_sub_link_id,
|
||||
"start_date":
|
||||
start_date_obj,
|
||||
"end_date":
|
||||
end_date_obj,
|
||||
"is_active":
|
||||
is_active_flag,
|
||||
"status_from_panel":
|
||||
status_from_panel,
|
||||
"traffic_limit_bytes":
|
||||
panel_user_dict.get('trafficLimitBytes'),
|
||||
"traffic_used_bytes":
|
||||
panel_user_dict.get('usedTrafficBytes')
|
||||
}
|
||||
|
||||
await subscription_dal.deactivate_other_active_subscriptions(
|
||||
session, panel_uuid, panel_sub_link_id)
|
||||
await subscription_dal.upsert_subscription(
|
||||
session, sub_payload)
|
||||
subscriptions_synced_count += 1
|
||||
users_synced_successfully += 1
|
||||
except ValueError as e_date:
|
||||
logging.warning(
|
||||
f"Sync: Bad date format for panel user {panel_uuid} (TG ID: {telegram_id_from_panel}). Sub data: {str(panel_user_dict)[:100]}. Error: {e_date}"
|
||||
)
|
||||
sync_errors.append(
|
||||
f"Bad date for panel user {panel_uuid} (TG ID: {telegram_id_from_panel})."
|
||||
)
|
||||
except Exception as e_sub_sync:
|
||||
logging.error(
|
||||
f"Sync: Error syncing subscription for panel user {panel_uuid} (TG ID: {telegram_id_from_panel}): {e_sub_sync}",
|
||||
exc_info=True)
|
||||
sync_errors.append(
|
||||
f"Sub sync error for panel user {panel_uuid} (TG ID: {telegram_id_from_panel})."
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Sync: Panel user {panel_uuid} (TG ID: {telegram_id_from_panel}) has sub link but no expireAt date. Skipping subscription sync."
|
||||
)
|
||||
else:
|
||||
|
||||
await subscription_dal.deactivate_other_active_subscriptions(
|
||||
session, panel_uuid, None)
|
||||
logging.info(
|
||||
f"Sync: Panel user {panel_uuid} (TG ID: {telegram_id_from_panel}) has no subscription link on panel. Deactivated local subs if any."
|
||||
)
|
||||
users_synced_successfully += 1
|
||||
|
||||
if users_processed_count % 20 == 0:
|
||||
logging.info(
|
||||
f"Sync progress: {users_processed_count}/{total_panel_users} users processed."
|
||||
f"Sync progress: {users_processed_count}/{total_panel_users} users processed from panel."
|
||||
)
|
||||
|
||||
status_msg = f"Panel users checked: {total_panel_users}. Users/Subscriptions synced via TG ID: {users_synced_successfully}."
|
||||
if sync_errors:
|
||||
status_msg += f" Errors encountered: {len(sync_errors)}. See logs for details."
|
||||
error_preview = "\n".join(sync_errors[:3])
|
||||
await update_sync_status("partial_success",
|
||||
status_msg + " " + error_preview,
|
||||
total_panel_users,
|
||||
users_synced_successfully)
|
||||
await message.answer(
|
||||
_("sync_completed",
|
||||
status="Partial Success",
|
||||
details=status_msg))
|
||||
else:
|
||||
await update_sync_status("success", status_msg, total_panel_users,
|
||||
users_synced_successfully)
|
||||
await message.answer(
|
||||
_("sync_completed", status="Success", details=status_msg))
|
||||
status_msg_key = "sync_completed_details"
|
||||
final_status_type = "success"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error during /sync command: {e}", exc_info=True)
|
||||
error_detail = f"An unexpected error occurred during sync: {str(e)}"
|
||||
await update_sync_status("failed", error_detail, users_processed_count,
|
||||
users_synced_successfully)
|
||||
await message.answer(_("sync_failed", details=error_detail))
|
||||
if sync_errors:
|
||||
final_status_type = "partial_success"
|
||||
status_msg_key = "sync_completed_with_errors_details"
|
||||
error_preview = "\n".join(sync_errors[:3])
|
||||
details_for_db = f"Users processed: {users_processed_count}. Subs synced: {subscriptions_synced_count}. Errors: {len(sync_errors)}. First few: {error_preview}"
|
||||
else:
|
||||
details_for_db = f"Successfully processed {users_processed_count} users. Synced {subscriptions_synced_count} subscriptions."
|
||||
|
||||
await panel_sync_dal.update_panel_sync_status(
|
||||
session, final_status_type, details_for_db, users_processed_count,
|
||||
subscriptions_synced_count)
|
||||
await session.commit()
|
||||
|
||||
final_user_message = _(status_msg_key,
|
||||
total_checked=total_panel_users,
|
||||
users_synced=users_synced_successfully,
|
||||
subs_synced=subscriptions_synced_count,
|
||||
errors_count=len(sync_errors),
|
||||
error_details_preview=error_preview
|
||||
if sync_errors else _("no_errors_placeholder"))
|
||||
await bot.send_message(target_chat_id, final_user_message)
|
||||
|
||||
except Exception as e_sync_global:
|
||||
await session.rollback()
|
||||
logging.error(f"Global error during /sync command: {e_sync_global}",
|
||||
exc_info=True)
|
||||
error_detail_for_db = f"An unexpected error occurred during sync: {str(e_sync_global)[:200]}"
|
||||
await panel_sync_dal.update_panel_sync_status(
|
||||
session, "failed", error_detail_for_db, users_processed_count,
|
||||
subscriptions_synced_count)
|
||||
|
||||
await bot.send_message(target_chat_id,
|
||||
_("sync_failed", details=error_detail_for_db))
|
||||
|
||||
|
||||
@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'))
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
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()
|
||||
|
||||
status_record_model = await panel_sync_dal.get_panel_sync_status(session)
|
||||
response_text = ""
|
||||
if status_record:
|
||||
last_time_val = status_record['last_sync_time']
|
||||
if status_record_model:
|
||||
last_time_val = status_record_model.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']
|
||||
'%Y-%m-%d %H:%M:%S UTC') if last_time_val else "N/A"
|
||||
|
||||
details_val = status_record_model.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_status')}: {status_record_model.status}\n"
|
||||
f" {_('admin_stats_sync_users_processed')}: {status_record_model.users_processed_from_panel}\n"
|
||||
f" {_('admin_stats_sync_subs_synced')}: {status_record_model.subscriptions_synced}\n"
|
||||
f" {_('admin_stats_sync_details_label')}: {details_str}")
|
||||
else:
|
||||
response_text = _("admin_sync_status_never_run")
|
||||
|
||||
await message.answer(response_text, parse_mode="HTML")
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import logging
|
||||
import re
|
||||
import math
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Tuple, List, Any
|
||||
import math
|
||||
import aiosqlite
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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.dal import user_dal, subscription_dal
|
||||
from db.models import User, Subscription
|
||||
|
||||
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,
|
||||
@@ -21,35 +23,49 @@ from bot.keyboards.inline.admin_keyboards import (
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="admin_user_management_router")
|
||||
|
||||
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
|
||||
|
||||
|
||||
async def _get_user_model_by_input(session: AsyncSession,
|
||||
input_text: str) -> Optional[User]:
|
||||
|
||||
user_model: Optional[User] = None
|
||||
if input_text.isdigit():
|
||||
try:
|
||||
user_model = await user_dal.get_user_by_id(session,
|
||||
int(input_text))
|
||||
except ValueError:
|
||||
pass
|
||||
elif input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]):
|
||||
user_model = await user_dal.get_user_by_username(
|
||||
session, input_text[1:])
|
||||
elif USERNAME_REGEX.match(input_text):
|
||||
user_model = await user_dal.get_user_by_username(session, input_text)
|
||||
return user_model
|
||||
|
||||
|
||||
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'))
|
||||
settings: Settings, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error processing ban prompt.", 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))
|
||||
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 for ban_user_prompt: {e}. Sending new message.")
|
||||
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)
|
||||
|
||||
@@ -58,103 +74,90 @@ async def ban_user_prompt_handler(callback: types.CallbackQuery,
|
||||
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'))
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await message.reply("Language error.")
|
||||
await message.reply("Language service 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:
|
||||
|
||||
input_text = message.text.strip() if message.text else ""
|
||||
user_to_ban_model = await _get_user_model_by_input(session, input_text)
|
||||
|
||||
if not user_to_ban_model:
|
||||
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:
|
||||
|
||||
user_id_to_ban = user_to_ban_model.user_id
|
||||
user_display_for_msg = user_to_ban_model.username or str(user_id_to_ban)
|
||||
|
||||
if message.from_user and (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']:
|
||||
|
||||
if user_to_ban_model.is_banned:
|
||||
await message.answer(
|
||||
_("admin_user_already_banned",
|
||||
user_id_or_username=user_id_or_username_for_msg))
|
||||
user_id_or_username=user_display_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)
|
||||
|
||||
ban_success_local = await user_dal.set_user_ban_status(
|
||||
session, user_id_to_ban, True)
|
||||
|
||||
if ban_success_local:
|
||||
|
||||
panel_ban_message_part = ""
|
||||
if user_to_ban_model.panel_user_uuid:
|
||||
panel_ban_api_success = await panel_service.update_user_status_on_panel(
|
||||
user_to_ban_model.panel_user_uuid, enable=False)
|
||||
if panel_ban_api_success:
|
||||
panel_ban_message_part = _("admin_panel_ban_success_part")
|
||||
else:
|
||||
panel_ban_message_part = _("admin_panel_ban_fail_part")
|
||||
|
||||
await session.commit()
|
||||
await message.answer(_("admin_user_banned_success_combined",
|
||||
user_id_or_username=user_display_for_msg,
|
||||
panel_status_part=panel_ban_message_part),
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
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 session.rollback()
|
||||
await message.answer(_("admin_user_ban_failed_local_db_error"))
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def unban_user_prompt_handler(callback: types.CallbackQuery,
|
||||
state: FSMContext, i18n_data: dict,
|
||||
settings: Settings):
|
||||
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
settings: Settings, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error processing unban prompt.",
|
||||
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))
|
||||
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 for unban_user_prompt: {e}. Sending new message.")
|
||||
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)
|
||||
|
||||
@@ -164,131 +167,168 @@ 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'))
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await message.reply("Language error.")
|
||||
await message.reply("Language service 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:
|
||||
|
||||
input_text = message.text.strip() if message.text else ""
|
||||
user_to_unban_model = await _get_user_model_by_input(session, input_text)
|
||||
|
||||
if not user_to_unban_model:
|
||||
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']:
|
||||
|
||||
user_id_to_unban = user_to_unban_model.user_id
|
||||
user_display_for_msg = user_to_unban_model.username or str(
|
||||
user_id_to_unban)
|
||||
|
||||
if not user_to_unban_model.is_banned:
|
||||
await message.answer(
|
||||
_("admin_user_not_banned",
|
||||
user_id_or_username=user_id_or_username_for_msg))
|
||||
user_id_or_username=user_display_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)
|
||||
|
||||
unban_success_local = await user_dal.set_user_ban_status(
|
||||
session, user_id_to_unban, False)
|
||||
|
||||
if unban_success_local:
|
||||
panel_unban_message_part = ""
|
||||
if user_to_unban_model.panel_user_uuid:
|
||||
panel_unban_api_success = await panel_service.update_user_status_on_panel(
|
||||
user_to_unban_model.panel_user_uuid, enable=True)
|
||||
if panel_unban_api_success:
|
||||
panel_unban_message_part = _("admin_panel_unban_success_part")
|
||||
else:
|
||||
panel_unban_message_part = _("admin_panel_unban_fail_part")
|
||||
|
||||
await session.commit()
|
||||
await message.answer(_("admin_user_unbanned_success_combined",
|
||||
user_id_or_username=user_display_for_msg,
|
||||
panel_status_part=panel_unban_message_part),
|
||||
reply_markup=get_back_to_admin_panel_keyboard(
|
||||
current_lang, i18n))
|
||||
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 session.rollback()
|
||||
await message.answer(_("admin_user_unban_failed_local_db_error"))
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def view_banned_users_handler(callback: types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings,
|
||||
state: FSMContext):
|
||||
state: FSMContext, i18n_data: dict,
|
||||
settings: Settings, session: AsyncSession):
|
||||
await state.clear()
|
||||
current_page = 0
|
||||
current_page_idx = 0
|
||||
if ":" in callback.data and callback.data.count(":") == 2:
|
||||
try:
|
||||
current_page = int(callback.data.split(":")[-1])
|
||||
current_page_idx = int(callback.data.split(":")[-1])
|
||||
except ValueError:
|
||||
current_page = 0
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
current_page_idx = 0
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error displaying banned users.",
|
||||
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)
|
||||
page_size = settings.LOGS_PAGE_SIZE
|
||||
offset = current_page_idx * page_size
|
||||
|
||||
if not callback.message:
|
||||
await callback.answer("Error.")
|
||||
return
|
||||
if total_banned == 0:
|
||||
banned_user_models, total_banned_count = await user_dal.get_banned_users_paginated(
|
||||
session, limit=page_size, offset=offset)
|
||||
|
||||
if total_banned_count == 0:
|
||||
await callback.message.edit_text(
|
||||
_("admin_no_banned_users"),
|
||||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||||
else:
|
||||
total_pages = math.ceil(
|
||||
total_banned /
|
||||
settings.LOGS_PAGE_SIZE) if settings.LOGS_PAGE_SIZE > 0 else 1
|
||||
total_pages = math.ceil(total_banned_count /
|
||||
page_size) if page_size > 0 else 1
|
||||
await callback.message.edit_text(
|
||||
text=_("admin_banned_list_title",
|
||||
current_page=current_page + 1,
|
||||
current_page=current_page_idx + 1,
|
||||
total_pages=max(1, total_pages)),
|
||||
reply_markup=get_banned_users_keyboard(banned_users, current_page,
|
||||
total_banned, i18n,
|
||||
reply_markup=get_banned_users_keyboard(banned_user_models,
|
||||
current_page_idx,
|
||||
total_banned_count, i18n,
|
||||
current_lang, settings))
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def _show_user_card_actual(target_message: types.Message,
|
||||
user_id_to_show: int,
|
||||
banned_list_page_to_return: int,
|
||||
i18n_data: dict, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n: return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
user_model = await user_dal.get_user_by_id(session, user_id_to_show)
|
||||
if not user_model:
|
||||
await target_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))
|
||||
return
|
||||
|
||||
user_display_name = user_model.first_name or (f"@{user_model.username}"
|
||||
if user_model.username else
|
||||
f"ID: {user_id_to_show}")
|
||||
|
||||
sub_end_date_str = await subscription_dal.get_user_active_subscription_end_date_str(
|
||||
session, user_id_to_show) or _("user_card_sub_na")
|
||||
|
||||
reg_date_display = user_model.registration_date.strftime(
|
||||
'%Y-%m-%d %H:%M') if user_model.registration_date else "N/A"
|
||||
|
||||
card_text = _("user_card_info",
|
||||
user_id=user_model.user_id,
|
||||
username=user_model.username or "N/A",
|
||||
first_name=user_model.first_name or "",
|
||||
last_name=user_model.last_name or "",
|
||||
language_code=user_model.language_code or "N/A",
|
||||
panel_user_uuid=user_model.panel_user_uuid or "N/A",
|
||||
ban_status=_("user_card_banned")
|
||||
if user_model.is_banned else _("user_card_active"),
|
||||
reg_date=reg_date_display,
|
||||
sub_end_date=sub_end_date_str)
|
||||
await target_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_model.is_banned), i18n,
|
||||
current_lang,
|
||||
banned_list_page_to_return),
|
||||
parse_mode="HTML")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_card:"))
|
||||
async def show_user_card_handler(callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
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
|
||||
@@ -300,281 +340,210 @@ async def show_user_card_handler(callback: types.CallbackQuery,
|
||||
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)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error displaying user card.", 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 _show_user_card_actual(
|
||||
target_message=callback.message,
|
||||
user_id_to_show=user_id_to_show,
|
||||
banned_list_page_to_return=banned_list_page_to_return,
|
||||
i18n_data=i18n_data,
|
||||
settings=settings,
|
||||
panel_service=panel_service,
|
||||
session=session)
|
||||
if force_user_id is None:
|
||||
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):
|
||||
|
||||
async def _confirm_action_handler(callback: types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings,
|
||||
session: AsyncSession, action_type: str):
|
||||
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)
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("Invalid confirmation data.", show_alert=True)
|
||||
return
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error processing confirmation.",
|
||||
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_model = await user_dal.get_user_by_id(session, user_id)
|
||||
user_display = (user_model.first_name or
|
||||
(f"@{user_model.username}" if user_model.username else
|
||||
f"ID {user_id}")) if user_model else f"ID {user_id}"
|
||||
|
||||
prompt_key = f"admin_confirm_{action_type}_prompt"
|
||||
yes_callback = f"admin_{action_type}_do:{user_id}:{banned_list_page}"
|
||||
no_callback = f"admin_user_card:{user_id}:{banned_list_page}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=_("admin_confirm_action_title",
|
||||
action_text=_(f"{action_type}_verb_l")) + "\n\n" +
|
||||
_(prompt_key, user_display=user_display, user_id=user_id),
|
||||
reply_markup=get_confirmation_keyboard(yes_callback_data=yes_callback,
|
||||
no_callback_data=no_callback,
|
||||
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):
|
||||
settings: Settings, session: AsyncSession):
|
||||
await _confirm_action_handler(callback, i18n_data, settings, session,
|
||||
"ban")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_unban_confirm:"))
|
||||
async def confirm_unban_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings, session: AsyncSession):
|
||||
await _confirm_action_handler(callback, i18n_data, settings, session,
|
||||
"unban")
|
||||
|
||||
|
||||
async def _do_ban_unban_action_handler(callback: types.CallbackQuery,
|
||||
i18n_data: dict, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession,
|
||||
state: FSMContext, action_type: str):
|
||||
try:
|
||||
|
||||
_, user_id_str, page_str = callback.data.split(":")
|
||||
user_id = int(user_id_str)
|
||||
user_id_target = int(user_id_str)
|
||||
banned_list_page = int(page_str)
|
||||
except:
|
||||
await callback.answer("Invalid data", show_alert=True)
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("Invalid action data.", show_alert=True)
|
||||
return
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error processing action.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_data = await get_user(user_id)
|
||||
user_display = (user_data['first_name'] or
|
||||
(f"@{user_data['username']}" if user_data.get('username')
|
||||
else f"ID {user_id}")) if user_data else f"ID {user_id}"
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=_("admin_confirm_ban_prompt",
|
||||
user_display=user_display,
|
||||
user_id=user_id),
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
yes_callback_data=f"admin_ban_do:{user_id}:{banned_list_page}",
|
||||
no_callback_data=
|
||||
f"admin_user_card:{user_id}:{banned_list_page}",
|
||||
i18n_instance=i18n,
|
||||
lang=current_lang),
|
||||
parse_mode="HTML")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_unban_do:"))
|
||||
async def do_unban_user_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
state: FSMContext):
|
||||
|
||||
try:
|
||||
_, user_id_str, page_str = callback.data.split(":")
|
||||
user_id_to_unban = int(user_id_str)
|
||||
banned_list_page = int(page_str)
|
||||
except:
|
||||
await callback.answer("Invalid data", show_alert=True)
|
||||
return
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_to_unban_data = await get_user(user_id_to_unban)
|
||||
user_display_name = (
|
||||
user_to_unban_data['first_name'] or
|
||||
(f"@{user_to_unban_data['username']}"
|
||||
if user_to_unban_data.get('username') else f"ID {user_id_to_unban}")
|
||||
) if user_to_unban_data else f"ID {user_id_to_unban}"
|
||||
if not user_to_unban_data or not user_to_unban_data['is_banned']:
|
||||
await callback.answer(_("admin_user_not_banned",
|
||||
user_id_or_username=user_display_name),
|
||||
user_model = await user_dal.get_user_by_id(session, user_id_target)
|
||||
if not user_model:
|
||||
await callback.answer(_("admin_user_not_found_in_bot_db",
|
||||
user_id=user_id_target),
|
||||
show_alert=True)
|
||||
if callback.message:
|
||||
await show_user_card_handler(callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
panel_service,
|
||||
state,
|
||||
force_user_id=user_id_to_unban,
|
||||
force_page=banned_list_page)
|
||||
return
|
||||
await set_user_ban_status_db(user_id_to_unban, is_banned=False)
|
||||
panel_user_uuid = user_to_unban_data[
|
||||
'panel_user_uuid'] if user_to_unban_data and 'panel_user_uuid' in user_to_unban_data.keys(
|
||||
) and user_to_unban_data['panel_user_uuid'] else None
|
||||
if panel_user_uuid:
|
||||
if not await panel_service.update_user_status_on_panel(panel_user_uuid,
|
||||
enable=True):
|
||||
logging.warning(
|
||||
f"Panel status update fail for unban {user_id_to_unban}")
|
||||
await callback.answer(_("admin_user_unbanned_from_card",
|
||||
user_display=user_display_name,
|
||||
user_id=user_id_to_unban),
|
||||
show_alert=False)
|
||||
if callback.message:
|
||||
await show_user_card_handler(callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
panel_service,
|
||||
state,
|
||||
force_user_id=user_id_to_unban,
|
||||
force_page=banned_list_page)
|
||||
|
||||
user_display_name = user_model.first_name or (f"@{user_model.username}"
|
||||
if user_model.username else
|
||||
f"ID {user_id_target}")
|
||||
|
||||
is_banning = action_type == "ban"
|
||||
|
||||
if is_banning:
|
||||
if user_model.is_banned:
|
||||
await callback.answer(_("admin_user_already_banned",
|
||||
user_id_or_username=user_display_name),
|
||||
show_alert=True)
|
||||
return
|
||||
if user_id_target == callback.from_user.id or user_id_target in settings.ADMIN_IDS:
|
||||
await callback.answer(_("admin_cannot_ban_self_or_admin"),
|
||||
show_alert=True)
|
||||
return
|
||||
else:
|
||||
if not user_model.is_banned:
|
||||
await callback.answer(_("admin_user_not_banned",
|
||||
user_id_or_username=user_display_name),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
action_success_local = await user_dal.set_user_ban_status(
|
||||
session, user_id_target, is_banning)
|
||||
|
||||
if action_success_local:
|
||||
panel_action_message = ""
|
||||
if user_model.panel_user_uuid:
|
||||
panel_api_success = await panel_service.update_user_status_on_panel(
|
||||
user_model.panel_user_uuid, enable=not is_banning)
|
||||
if not panel_api_success:
|
||||
panel_action_message = _("admin_panel_status_update_fail_part")
|
||||
logging.warning(
|
||||
f"Panel status update failed for {action_type} of user {user_id_target} (panel: {user_model.panel_user_uuid})"
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
alert_message_key = f"admin_user_{action_type}ned_from_card_alert"
|
||||
await callback.answer(_(alert_message_key,
|
||||
user_display=user_display_name,
|
||||
user_id=user_id_target) + " " +
|
||||
panel_action_message,
|
||||
show_alert=False)
|
||||
else:
|
||||
await session.rollback()
|
||||
await callback.answer(_(f"admin_user_{action_type}_failed_db_error"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
await show_user_card_handler(callback,
|
||||
state,
|
||||
i18n_data,
|
||||
settings,
|
||||
panel_service,
|
||||
session,
|
||||
force_user_id=user_id_target,
|
||||
force_page=banned_list_page)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_ban_do:"))
|
||||
async def do_ban_user_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
state: FSMContext):
|
||||
try:
|
||||
_, user_id_str, page_str = callback.data.split(":")
|
||||
user_id_to_ban = int(user_id_str)
|
||||
banned_list_page = int(page_str)
|
||||
except:
|
||||
await callback.answer("Invalid data", show_alert=True)
|
||||
return
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_to_ban_data = await get_user(user_id_to_ban)
|
||||
user_display_name = (
|
||||
user_to_ban_data['first_name'] or
|
||||
(f"@{user_to_ban_data['username']}"
|
||||
if user_to_ban_data.get('username') else f"ID {user_id_to_ban}")
|
||||
) if user_to_ban_data else f"ID {user_id_to_ban}"
|
||||
if not user_to_ban_data:
|
||||
await callback.answer(_("admin_user_not_found_in_bot_db",
|
||||
user_id=user_id_to_ban),
|
||||
show_alert=True)
|
||||
return
|
||||
if user_to_ban_data['is_banned']:
|
||||
await callback.answer(_("admin_user_already_banned",
|
||||
user_id_or_username=user_display_name),
|
||||
show_alert=True)
|
||||
return
|
||||
if user_id_to_ban == callback.from_user.id or user_id_to_ban in settings.ADMIN_IDS:
|
||||
await callback.answer(_("admin_cannot_ban_self_or_admin"),
|
||||
show_alert=True)
|
||||
return
|
||||
session: AsyncSession, state: FSMContext):
|
||||
await _do_ban_unban_action_handler(callback, i18n_data, settings,
|
||||
panel_service, session, state, "ban")
|
||||
|
||||
await set_user_ban_status_db(user_id_to_ban, is_banned=True)
|
||||
panel_user_uuid = user_to_ban_data[
|
||||
'panel_user_uuid'] if user_to_ban_data and 'panel_user_uuid' in user_to_ban_data.keys(
|
||||
) and user_to_ban_data['panel_user_uuid'] else None
|
||||
if panel_user_uuid:
|
||||
if not await panel_service.update_user_status_on_panel(panel_user_uuid,
|
||||
enable=False):
|
||||
logging.warning(
|
||||
f"Panel status update fail for ban {user_id_to_ban}")
|
||||
await callback.answer(_("admin_user_banned_from_card",
|
||||
user_display=user_display_name,
|
||||
user_id=user_id_to_ban),
|
||||
show_alert=False)
|
||||
if callback.message:
|
||||
await show_user_card_handler(callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
panel_service,
|
||||
state,
|
||||
force_user_id=user_id_to_ban,
|
||||
force_page=banned_list_page)
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_unban_do:"))
|
||||
async def do_unban_user_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
session: AsyncSession, state: FSMContext):
|
||||
await _do_ban_unban_action_handler(callback, i18n_data, settings,
|
||||
panel_service, session, state, "unban")
|
||||
|
||||
|
||||
@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'))
|
||||
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", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error cancelling.", 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"))
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(_("admin_action_cancelled_default"),
|
||||
reply_markup=get_admin_panel_keyboard(
|
||||
i18n, current_lang, settings))
|
||||
except Exception:
|
||||
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_alert"))
|
||||
await state.clear()
|
||||
|
||||
+238
-214
@@ -1,35 +1,36 @@
|
||||
import logging
|
||||
import json
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from yookassa.domain.notification import WebhookNotification
|
||||
from yookassa.domain.models import Amount
|
||||
from yookassa.domain.models.amount import Amount as YooKassaAmount
|
||||
|
||||
from db.dal import payment_dal, user_dal
|
||||
|
||||
from db.database import get_db_connection_manager, _setup_db_connection, get_user
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.payment_service import YooKassaService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.payment_service import YooKassaService
|
||||
|
||||
payment_processing_lock = asyncio.Lock()
|
||||
|
||||
YOOKASSA_EVENT_PAYMENT_SUCCEEDED = 'payment.succeeded'
|
||||
YOOKASSA_EVENT_PAYMENT_CANCELED = 'payment.canceled'
|
||||
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture'
|
||||
YOOKASSA_EVENT_REFUND_SUCCEEDED = 'refund.succeeded'
|
||||
|
||||
|
||||
async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
yk_service: YooKassaService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
@@ -43,6 +44,8 @@ async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}"
|
||||
)
|
||||
return
|
||||
|
||||
db_user = None
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
subscription_months = int(subscription_months_str)
|
||||
@@ -50,262 +53,283 @@ async def process_successful_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
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))
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
logging.error(
|
||||
f"User {user_id} not found in DB during successful payment processing for YK ID {payment_info_from_webhook.get('id')}. Payment record {payment_db_id}."
|
||||
)
|
||||
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, payment_db_id, "failed_user_not_found",
|
||||
payment_info_from_webhook.get("id"))
|
||||
|
||||
return
|
||||
|
||||
except (TypeError, ValueError) as e:
|
||||
logging.error(
|
||||
f"Invalid metadata format for payment processing: {metadata} - {e}"
|
||||
)
|
||||
|
||||
if payment_db_id_str and payment_db_id_str.isdigit():
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, int(payment_db_id_str), "failed_metadata_error",
|
||||
payment_info_from_webhook.get("id"))
|
||||
except Exception as e_upd:
|
||||
logging.error(
|
||||
f"Failed to update payment status after metadata error: {e_upd}"
|
||||
)
|
||||
return
|
||||
|
||||
final_end_date_for_user: Optional[datetime] = None
|
||||
applied_referee_bonus_days: Optional[int] = None
|
||||
base_subscription_end_date: Optional[datetime] = None
|
||||
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
try:
|
||||
await db.execute(
|
||||
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",
|
||||
(payment_info_from_webhook.get("status", "succeeded"),
|
||||
payment_db_id, payment_info_from_webhook.get("id")))
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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']
|
||||
|
||||
success_message = _(
|
||||
"payment_successful_with_referral_bonus",
|
||||
months=subscription_months,
|
||||
base_end_date=base_subscription_end_date.strftime(
|
||||
'%Y-%m-%d')
|
||||
if base_subscription_end_date else "N/A",
|
||||
bonus_days=applied_referee_bonus_days,
|
||||
final_end_date=final_end_date_for_user.strftime(
|
||||
'%Y-%m-%d'),
|
||||
inviter_name=inviter_name_for_msg)
|
||||
elif final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful",
|
||||
months=subscription_months,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
else:
|
||||
logging.error(
|
||||
f"Critical error: final_end_date_for_user is None for user {user_id}"
|
||||
)
|
||||
success_message = _("payment_successful_error_details")
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, success_message)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to send final payment success message to user {user_id}: {e}"
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to activate subscription for user {user_id} after payment {payment_info_from_webhook.get('id')}"
|
||||
)
|
||||
await db.rollback()
|
||||
except Exception as e:
|
||||
try:
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
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}"
|
||||
)
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
user_id,
|
||||
subscription_months,
|
||||
payment_value,
|
||||
payment_db_id,
|
||||
promo_code_id_from_payment=promo_code_id)
|
||||
|
||||
if not activation_details or not activation_details.get('end_date'):
|
||||
logging.error(
|
||||
f"Failed to activate subscription for user {user_id} after payment {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"Subscription Error: Failed to activate for user {user_id}")
|
||||
|
||||
base_subscription_end_date = activation_details['end_date']
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
applied_promo_bonus_days = activation_details.get(
|
||||
"applied_promo_bonus_days", 0)
|
||||
|
||||
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
||||
session, user_id, subscription_months)
|
||||
applied_referee_bonus_days_from_referral: Optional[int] = None
|
||||
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_from_referral = referral_bonus_info.get(
|
||||
"referee_bonus_applied_days")
|
||||
|
||||
user_lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
|
||||
success_message = ""
|
||||
if applied_referee_bonus_days_from_referral and final_end_date_for_user:
|
||||
inviter_name_display = _("friend_placeholder")
|
||||
if db_user and db_user.referred_by_id:
|
||||
inviter = await user_dal.get_user_by_id(
|
||||
session, db_user.referred_by_id)
|
||||
if inviter and inviter.first_name:
|
||||
inviter_name_display = inviter.first_name
|
||||
elif inviter and inviter.username:
|
||||
inviter_name_display = f"@{inviter.username}"
|
||||
|
||||
success_message = _(
|
||||
"payment_successful_with_referral_bonus",
|
||||
months=subscription_months,
|
||||
base_end_date=base_subscription_end_date.strftime('%Y-%m-%d'),
|
||||
bonus_days=applied_referee_bonus_days_from_referral,
|
||||
final_end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
inviter_name=inviter_name_display)
|
||||
elif applied_promo_bonus_days > 0 and final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful_with_promo",
|
||||
months=subscription_months,
|
||||
bonus_days=applied_promo_bonus_days,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
elif final_end_date_for_user:
|
||||
success_message = _(
|
||||
"payment_successful",
|
||||
months=subscription_months,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'))
|
||||
else:
|
||||
logging.error(
|
||||
f"Critical error: final_end_date_for_user is None for user {user_id} after successful payment logic."
|
||||
)
|
||||
success_message = _("payment_successful_error_details")
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, success_message)
|
||||
except Exception as e_notify:
|
||||
logging.error(
|
||||
f"Failed to send final payment success message to user {user_id}: {e_notify}"
|
||||
)
|
||||
|
||||
except Exception as e_process:
|
||||
logging.error(
|
||||
f"Error during process_successful_payment main try block for user {user_id}: {e_process}",
|
||||
exc_info=True)
|
||||
|
||||
raise
|
||||
|
||||
|
||||
async def process_cancelled_payment(bot: Bot, payment_info_from_webhook: dict,
|
||||
async def process_cancelled_payment(session: AsyncSession, 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')}"
|
||||
f"Missing metadata in cancelled payment webhook: {payment_info_from_webhook.get('id')}"
|
||||
)
|
||||
return
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
payment_db_id = int(payment_db_id_str)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid metadata in cancelled payment: {metadata}")
|
||||
return
|
||||
async with get_db_connection_manager() as db:
|
||||
await _setup_db_connection(db)
|
||||
await db.execute(
|
||||
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ? AND (yookassa_payment_id = ? OR yookassa_payment_id IS NULL)",
|
||||
(payment_info_from_webhook.get("status", "canceled"),
|
||||
payment_db_id, payment_info_from_webhook.get("id")))
|
||||
await db.commit()
|
||||
user_lang = getattr(settings, 'DEFAULT_LANGUAGE', 'en')
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
try:
|
||||
await bot.send_message(user_id, _("payment_failed"))
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to send payment cancellation message to user {user_id}: {e}"
|
||||
)
|
||||
f"Invalid metadata in cancelled payment webhook: {metadata}")
|
||||
return
|
||||
|
||||
try:
|
||||
updated_payment = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "canceled"),
|
||||
yk_payment_id=payment_info_from_webhook.get("id"))
|
||||
|
||||
if updated_payment:
|
||||
logging.info(
|
||||
f"Payment {payment_db_id} (YK: {payment_info_from_webhook.get('id')}) status updated to cancelled for user {user_id}."
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Could not find payment record {payment_db_id} to update status to cancelled for user {user_id}."
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
user_lang = settings.DEFAULT_LANGUAGE
|
||||
if db_user and db_user.language_code: user_lang = db_user.language_code
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
await bot.send_message(user_id, _("payment_failed"))
|
||||
|
||||
except Exception as e_process_cancel:
|
||||
logging.error(
|
||||
f"Error processing cancelled payment for user {user_id}, payment_db_id {payment_db_id}: {e_process_cancel}",
|
||||
exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def yookassa_webhook_route(request: web.Request):
|
||||
logging.info(
|
||||
f"YooKassa Webhook Route: Available keys in request.app: {list(request.app.keys())}"
|
||||
)
|
||||
|
||||
try:
|
||||
bot: Bot = request.app['bot']
|
||||
i18n_instance: JsonI18n = request.app['i18n']
|
||||
settings: Settings = request.app['settings']
|
||||
yk_service: YooKassaService = request.app['yookassa_service']
|
||||
panel_service: PanelApiService = request.app['panel_service']
|
||||
subscription_service: SubscriptionService = request.app[
|
||||
'subscription_service']
|
||||
referral_service: ReferralService = request.app['referral_service']
|
||||
except KeyError as e:
|
||||
async_session_factory: sessionmaker = request.app[
|
||||
'async_session_factory']
|
||||
except KeyError as e_app_ctx:
|
||||
logging.error(
|
||||
f"KeyError accessing app context in yookassa_webhook_route: {e}.",
|
||||
f"KeyError accessing app context in yookassa_webhook_route: {e_app_ctx}.",
|
||||
exc_info=True)
|
||||
return web.Response(status=500,
|
||||
text="Internal Server Error: Missing app context")
|
||||
return web.Response(
|
||||
status=500,
|
||||
text="Internal Server Error: Missing app context component")
|
||||
|
||||
try:
|
||||
event_json = await request.json()
|
||||
|
||||
notification_object = WebhookNotification(event_json)
|
||||
payment_data_from_notification = notification_object.object
|
||||
|
||||
logging.info(
|
||||
f"YooKassa Webhook Parsed: Event='{notification_object.event}', PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'"
|
||||
f"YooKassa Webhook Parsed: Event='{notification_object.event}', "
|
||||
f"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."
|
||||
f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata. Cannot process."
|
||||
)
|
||||
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 = {
|
||||
"id":
|
||||
str(payment_data_from_notification.id),
|
||||
"status":
|
||||
str(payment_data_from_notification.status),
|
||||
"paid":
|
||||
bool(payment_data_from_notification.paid),
|
||||
"amount": {
|
||||
"value": str(payment_data_from_notification.amount.value),
|
||||
"currency": str(payment_data_from_notification.amount.currency)
|
||||
} if payment_data_from_notification.amount else {},
|
||||
"metadata":
|
||||
dict(payment_data_from_notification.metadata),
|
||||
"description":
|
||||
str(payment_data_from_notification.description)
|
||||
if payment_data_from_notification.description else None,
|
||||
}
|
||||
|
||||
async with payment_processing_lock:
|
||||
if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED:
|
||||
if payment_dict_for_processing.get(
|
||||
"paid") and payment_dict_for_processing.get(
|
||||
"status") == "succeeded":
|
||||
await process_successful_payment(
|
||||
bot, payment_dict_for_processing, i18n_instance,
|
||||
settings, panel_service, yk_service,
|
||||
subscription_service, referral_service)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Payment Succeeded event for {payment_dict_for_processing.get('id')} but data not ok: status='{payment_dict_for_processing.get('status')}', paid='{payment_dict_for_processing.get('paid')}'"
|
||||
)
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
|
||||
await process_cancelled_payment(bot,
|
||||
payment_dict_for_processing,
|
||||
i18n_instance, settings)
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
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(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
f"Payment Succeeded event for {payment_dict_for_processing.get('id')} "
|
||||
f"but data not as expected: status='{payment_dict_for_processing.get('status')}', "
|
||||
f"paid='{payment_dict_for_processing.get('paid')}'"
|
||||
)
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
|
||||
await process_cancelled_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings)
|
||||
await session.commit()
|
||||
except Exception as e_webhook_db_processing:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Error processing YooKassa webhook event '{notification_object.event}' "
|
||||
f"for YK Payment ID {payment_dict_for_processing.get('id')} in DB transaction: {e_webhook_db_processing}",
|
||||
exc_info=True)
|
||||
return web.Response(
|
||||
status=200, text="ok_internal_processing_error_logged")
|
||||
|
||||
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("YooKassa Webhook: Invalid JSON received.")
|
||||
return web.Response(status=400, text="bad_request_invalid_json")
|
||||
except Exception as e_general_webhook:
|
||||
logging.error(
|
||||
f"KeyError in yookassa_webhook_route after initial context access: {e}.",
|
||||
f"YooKassa Webhook general processing error: {e_general_webhook}",
|
||||
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")
|
||||
return web.Response(status=200,
|
||||
text="ok_general_internal_error_logged")
|
||||
|
||||
@@ -3,6 +3,8 @@ import re
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from aiogram.utils.markdown import hcode
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.states.user_states import UserPromoStates
|
||||
@@ -10,26 +12,26 @@ from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from aiogram.utils.markdown import hcode
|
||||
|
||||
from .start import send_main_menu
|
||||
|
||||
router = Router(name="user_promo_router")
|
||||
|
||||
SUSPICIOUS_SQL_KEYWORDS_REGEX = re.compile(
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|;\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|"
|
||||
r";\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
re.IGNORECASE)
|
||||
SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)")
|
||||
MAX_PROMO_CODE_INPUT_LENGTH = 100
|
||||
|
||||
|
||||
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'))
|
||||
settings: Settings, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
await callback.answer("Language error.", show_alert=True)
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
@@ -44,8 +46,10 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
await callback.message.edit_text(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to edit message for promo prompt: {e}")
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for promo prompt: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
@@ -53,73 +57,96 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
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()}"
|
||||
)
|
||||
f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. "
|
||||
f"FSM state: {await state.get_state()}")
|
||||
|
||||
|
||||
@router.message(UserPromoStates.waiting_for_promo_code, F.text)
|
||||
async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
settings: Settings, i18n_data: dict,
|
||||
promo_code_service: PromoCodeService,
|
||||
bot: Bot):
|
||||
bot: Bot, session: AsyncSession):
|
||||
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", settings.DEFAULT_LANGUAGE)
|
||||
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.")
|
||||
logging.error(
|
||||
"Dependencies (i18n or PromoCodeService) missing in process_promo_code_input"
|
||||
)
|
||||
await message.reply("Service error. Please try again later.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
code_input = message.text.strip()
|
||||
code_input = message.text.strip() if message.text else ""
|
||||
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 not code_input:
|
||||
is_suspicious = True
|
||||
logging.warning(f"Empty promo code input by user {user.id}.")
|
||||
elif len(
|
||||
code_input
|
||||
) > MAX_PROMO_CODE_INPUT_LENGTH or SUSPICIOUS_SQL_KEYWORDS_REGEX.search(
|
||||
code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input):
|
||||
is_suspicious = True
|
||||
logging.warning(
|
||||
f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'"
|
||||
f"Suspicious input for promo code 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}"
|
||||
)
|
||||
|
||||
if settings.ADMIN_IDS:
|
||||
admin_notify_key = "admin_suspicious_promo_attempt_notification" if user.username else "admin_suspicious_promo_attempt_notification_no_username"
|
||||
|
||||
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))
|
||||
for admin_id in settings.ADMIN_IDS:
|
||||
try:
|
||||
await bot.send_message(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 {admin_id}: {e_admin_notify}"
|
||||
)
|
||||
|
||||
response_to_user_text = _("promo_code_not_found",
|
||||
code=code_input.upper())
|
||||
code=hcode(code_input.upper()))
|
||||
else:
|
||||
|
||||
success, response_text_from_service = await promo_code_service.apply_promo_code(
|
||||
user.id, code_input, current_lang)
|
||||
session, user.id, code_input, current_lang)
|
||||
response_to_user_text = response_text_from_service
|
||||
if success:
|
||||
await session.commit()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' successfully applied for user {user.id}."
|
||||
)
|
||||
else:
|
||||
await session.rollback()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' application failed for user {user.id}. Reason: {response_text_from_service}"
|
||||
)
|
||||
|
||||
await message.answer(response_to_user_text,
|
||||
reply_markup=get_back_to_main_menu_markup(
|
||||
current_lang, i18n))
|
||||
current_lang, i18n),
|
||||
parse_mode="HTML")
|
||||
await state.clear()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' processing finished for user {message.from_user.id}. State cleared."
|
||||
f"Promo code input '{code_input}' processing finished for user {message.from_user.id}. State cleared."
|
||||
)
|
||||
|
||||
|
||||
@@ -127,9 +154,9 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
UserPromoStates.waiting_for_promo_code)
|
||||
async def cancel_promo_input_via_button(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, subscription_service: SubscriptionService):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n_data: dict, subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in cancel_promo_input_via_button")
|
||||
@@ -140,21 +167,17 @@ async def cancel_promo_input_via_button(
|
||||
f"User {callback.from_user.id} cancelled promo code input via button from state {await state.get_state()}. Clearing state."
|
||||
)
|
||||
await state.clear()
|
||||
logging.info(
|
||||
f"State after clear for user {callback.from_user.id}: {await state.get_state()}"
|
||||
)
|
||||
|
||||
if callback.message:
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
else:
|
||||
|
||||
await callback.answer("Promo code input cancelled.", show_alert=False)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
await callback.answer(_("promo_input_cancelled_short"),
|
||||
show_alert=False)
|
||||
|
||||
@@ -1,67 +1,88 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.services.referral_service import ReferralService
|
||||
from db.database import get_db_connection_manager
|
||||
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard, get_back_to_main_menu_markup
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="user_referral_router")
|
||||
|
||||
|
||||
async def referral_command_handler(event: types.Message | types.CallbackQuery,
|
||||
async def referral_command_handler(event: Union[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'))
|
||||
referral_service: ReferralService, bot: Bot,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
target_message = event.message if isinstance(
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message:
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
"Target message is None in referral_command_handler from callback."
|
||||
"Target message is None in referral_command_handler (possibly from callback without message)."
|
||||
)
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer("Error displaying referral info.")
|
||||
await event.answer("Error displaying referral info.",
|
||||
show_alert=True)
|
||||
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)
|
||||
logging.error(
|
||||
"Dependencies (i18n or ReferralService) missing in referral_command_handler"
|
||||
)
|
||||
await target_message_obj.answer(
|
||||
"Service error. Please try again later.")
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
try:
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
except Exception as e_bot_info:
|
||||
logging.error(
|
||||
f"Failed to get bot info for referral link: {e_bot_info}")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
if not bot_username:
|
||||
logging.error("Bot username is None, cannot generate referral link.")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
return
|
||||
|
||||
inviter_user_id = event.from_user.id
|
||||
referral_link = referral_service.generate_referral_link(
|
||||
bot_username, inviter_user_id)
|
||||
bonus_info_parts = []
|
||||
|
||||
if hasattr(settings,
|
||||
'subscription_options') and settings.subscription_options:
|
||||
for months_period in sorted(settings.subscription_options.keys()):
|
||||
inv_bonus = settings.referral_bonus_inviter.get(months_period)
|
||||
ref_bonus = settings.referral_bonus_referee.get(months_period)
|
||||
bonus_info_parts = []
|
||||
if settings.subscription_options:
|
||||
|
||||
for months_period_key, _price in sorted(
|
||||
settings.subscription_options.items()):
|
||||
|
||||
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
|
||||
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
|
||||
if inv_bonus is not None or ref_bonus is not None:
|
||||
bonus_info_parts.append(
|
||||
_("referral_bonus_per_period",
|
||||
months=months_period,
|
||||
months=months_period_key,
|
||||
inviter_bonus_days=inv_bonus
|
||||
if inv_bonus is not None else _("no_bonus_days"),
|
||||
if inv_bonus is not None else _("no_bonus_placeholder"),
|
||||
referee_bonus_days=ref_bonus
|
||||
if ref_bonus is not None else _("no_bonus_days")))
|
||||
if ref_bonus is not None else _("no_bonus_placeholder")))
|
||||
|
||||
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)
|
||||
@@ -72,29 +93,16 @@ async def referral_command_handler(event: types.Message | types.CallbackQuery,
|
||||
await event.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
elif isinstance(event, types.CallbackQuery):
|
||||
elif isinstance(event, types.CallbackQuery) and event.message:
|
||||
try:
|
||||
await event.message.edit_text(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to edit message for referral info: {e}")
|
||||
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for referral info: {e_edit}. Sending new one."
|
||||
)
|
||||
await event.message.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
await event.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "copy_referral_link_ack")
|
||||
async def copy_referral_link_ack_callback_handler(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
return await callback.answer("Language service error.",
|
||||
show_alert=True)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
await callback.answer(text=_(key="referral_link_for_copying_reminder"),
|
||||
show_alert=False)
|
||||
|
||||
+224
-174
@@ -2,10 +2,12 @@ import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from db.dal import user_dal
|
||||
|
||||
from db.database import add_user_if_not_exists, update_user_language_code
|
||||
from bot.keyboards.inline.user_keyboards import get_main_menu_inline_keyboard, get_language_selection_keyboard
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
@@ -13,228 +15,272 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from config.settings import Settings
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
|
||||
router = Router(name="user_start_router")
|
||||
|
||||
|
||||
async def send_main_menu(message_or_callback: types.Message
|
||||
| types.CallbackQuery,
|
||||
async def send_main_menu(target_event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
show_trial_button_flag: bool,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False):
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
answered_callback_internally = False
|
||||
|
||||
user_id = target_event.from_user.id
|
||||
user_full_name = target_event.from_user.full_name
|
||||
|
||||
if not i18n:
|
||||
logging.error("i18n_instance missing in send_main_menu")
|
||||
target_mc_for_error = message_or_callback if isinstance(
|
||||
message_or_callback,
|
||||
types.Message) else message_or_callback.message
|
||||
error_text_fallback = "Error: Language service unavailable."
|
||||
if target_mc_for_error:
|
||||
logging.error(
|
||||
f"i18n_instance missing in send_main_menu for user {user_id}")
|
||||
err_msg_fallback = "Error: Language service unavailable. Please try again later."
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
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
|
||||
await target_event.answer(err_msg_fallback, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(target_event, types.Message) and hasattr(
|
||||
target_event, 'chat') and target_event.chat:
|
||||
try:
|
||||
await target_event.chat.send_message(err_msg_fallback)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_full_name = message_or_callback.from_user.full_name
|
||||
text = _(key="main_menu_greeting", user_name=user_full_name)
|
||||
reply_markup: Optional[
|
||||
InlineKeyboardMarkup] = get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_flag)
|
||||
target_message: Optional[types.Message] = None
|
||||
if isinstance(message_or_callback, types.Message):
|
||||
target_message = message_or_callback
|
||||
elif isinstance(message_or_callback, types.CallbackQuery):
|
||||
target_message = message_or_callback.message
|
||||
|
||||
if not target_message:
|
||||
show_trial_button_in_menu = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if hasattr(
|
||||
subscription_service, 'has_had_any_subscription') and callable(
|
||||
getattr(subscription_service, 'has_had_any_subscription')):
|
||||
if not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_button_in_menu = True
|
||||
else:
|
||||
logging.error(
|
||||
"Method has_had_any_subscription is missing in SubscriptionService for send_main_menu!"
|
||||
)
|
||||
|
||||
text = _(key="main_menu_greeting", user_name=user_full_name)
|
||||
reply_markup = get_main_menu_inline_keyboard(current_lang, i18n, settings,
|
||||
show_trial_button_in_menu)
|
||||
|
||||
target_message_obj: Optional[types.Message] = None
|
||||
if isinstance(target_event, types.Message):
|
||||
target_message_obj = target_event
|
||||
elif isinstance(target_event,
|
||||
types.CallbackQuery) and target_event.message:
|
||||
target_message_obj = target_event.message
|
||||
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
f"send_main_menu: target_message is None for event from user {message_or_callback.from_user.id}."
|
||||
f"send_main_menu: target_message_obj is None for event from user {user_id}."
|
||||
)
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer("Error displaying menu.")
|
||||
answered_callback_internally = True
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer(_("error_displaying_menu"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
if is_edit:
|
||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||
await target_message_obj.edit_text(text, reply_markup=reply_markup)
|
||||
else:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
await target_message_obj.answer(text, reply_markup=reply_markup)
|
||||
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer()
|
||||
answered_callback_internally = True
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer()
|
||||
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}."
|
||||
f"Failed to send/edit main menu (user: {user_id}, is_edit: {is_edit}): {type(e_send_edit).__name__} - {e_send_edit}."
|
||||
)
|
||||
if is_edit:
|
||||
if is_edit and target_message_obj and hasattr(
|
||||
target_message_obj, 'chat') and target_message_obj.chat:
|
||||
try:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
await target_message_obj.chat.send_message(
|
||||
text, reply_markup=reply_markup)
|
||||
except Exception as e_send_new:
|
||||
logging.error(
|
||||
f"Also failed to send new main menu message: {e_send_new}")
|
||||
if isinstance(
|
||||
message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
await message_or_callback.answer()
|
||||
answered_callback_internally = True
|
||||
|
||||
if isinstance(message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback_internally:
|
||||
logging.warning(
|
||||
f"Callback {message_or_callback.id} was not answered in send_main_menu main logic paths."
|
||||
)
|
||||
await message_or_callback.answer()
|
||||
f"Also failed to send new main menu message for user {user_id}: {e_send_new}"
|
||||
)
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await target_event.answer(
|
||||
_("error_occurred_try_again") if is_edit else None)
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def start_command_handler(message: types.Message, state: FSMContext,
|
||||
settings: Settings, i18n_data: dict,
|
||||
async def start_command_handler(message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
bot: Bot):
|
||||
|
||||
session: AsyncSession,
|
||||
command: Optional[CommandStart] = None):
|
||||
await state.clear()
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n_instance not found")
|
||||
await message.answer("Language service error.")
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
user_id = message.from_user.id
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
user = message.from_user
|
||||
user_id = user.id
|
||||
|
||||
referred_by_user_id: Optional[int] = None
|
||||
args = message.text.split()
|
||||
if len(args) > 1 and args[0] == "/start":
|
||||
if command and command.args:
|
||||
arg_payload = command.args
|
||||
if arg_payload.startswith("ref_"):
|
||||
try:
|
||||
potential_referrer_id_str = arg_payload.split("_")[1]
|
||||
if potential_referrer_id_str.isdigit():
|
||||
potential_referrer_id = int(potential_referrer_id_str)
|
||||
if potential_referrer_id != user_id:
|
||||
referred_by_user_id = potential_referrer_id
|
||||
except (IndexError, ValueError) as e:
|
||||
logging.warning(
|
||||
f"Could not parse referral from /start args '{arg_payload}': {e}"
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
user_data_to_create = {
|
||||
"user_id": user_id,
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"language_code": current_lang,
|
||||
"referred_by_id": referred_by_user_id,
|
||||
"registration_date": datetime.now(timezone.utc)
|
||||
}
|
||||
try:
|
||||
referral_param = args[1]
|
||||
if referral_param.startswith("ref_") and referral_param.split(
|
||||
"_")[1].isdigit():
|
||||
potential_referrer_id = int(referral_param.split("_")[1])
|
||||
if potential_referrer_id != user_id:
|
||||
referred_by_user_id = potential_referrer_id
|
||||
except (ValueError, IndexError) as e:
|
||||
logging.warning(f"Could not parse referral: '{args[1]}' - {e}")
|
||||
db_op_success, was_new_bot_user = await add_user_if_not_exists(
|
||||
user_id=user_id,
|
||||
username=message.from_user.username,
|
||||
first_name=message.from_user.first_name,
|
||||
last_name=message.from_user.last_name,
|
||||
lang_code=current_lang,
|
||||
referred_by_id=referred_by_user_id)
|
||||
if not db_op_success:
|
||||
await message.answer(_("error_occurred_processing_request"))
|
||||
return
|
||||
if referred_by_user_id:
|
||||
logging.info(
|
||||
f"User {user_id} started with referral from {referred_by_user_id}."
|
||||
)
|
||||
await message.answer(
|
||||
_(key="welcome", user_name=message.from_user.full_name))
|
||||
show_trial_button_in_menu = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if not await subscription_service.has_had_any_subscription(user_id):
|
||||
show_trial_button_in_menu = True
|
||||
logging.info(f"User {user_id} is eligible for a trial button.")
|
||||
db_user = await user_dal.create_user(session, user_data_to_create)
|
||||
|
||||
else:
|
||||
logging.info(
|
||||
f"User {user_id} not eligible for trial button (already had a subscription)."
|
||||
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
|
||||
)
|
||||
except Exception as e_create:
|
||||
|
||||
logging.error(
|
||||
f"Failed to add new user {user_id} to session: {e_create}",
|
||||
exc_info=True)
|
||||
await message.answer(_("error_occurred_processing_request"))
|
||||
return
|
||||
else:
|
||||
logging.info(f"Trial period is disabled in settings. No trial button.")
|
||||
update_payload = {}
|
||||
if db_user.language_code != current_lang:
|
||||
update_payload["language_code"] = current_lang
|
||||
if referred_by_user_id and db_user.referred_by_id is None:
|
||||
update_payload["referred_by_id"] = referred_by_user_id
|
||||
if user.username != db_user.username:
|
||||
update_payload["username"] = user.username
|
||||
if user.first_name != db_user.first_name:
|
||||
update_payload["first_name"] = user.first_name
|
||||
if user.last_name != db_user.last_name:
|
||||
update_payload["last_name"] = user.last_name
|
||||
|
||||
if update_payload:
|
||||
try:
|
||||
await user_dal.update_user(session, user_id, update_payload)
|
||||
|
||||
logging.info(
|
||||
f"Updated existing user {user_id} in session: {update_payload}"
|
||||
)
|
||||
except Exception as e_update:
|
||||
|
||||
logging.error(
|
||||
f"Failed to update existing user {user_id} in session: {e_update}",
|
||||
exc_info=True)
|
||||
|
||||
await message.answer(_(key="welcome", user_name=user.full_name))
|
||||
await send_main_menu(message,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_in_menu)
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=False)
|
||||
|
||||
|
||||
@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: Union[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)
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
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.")
|
||||
reply_markup = get_language_selection_keyboard(i18n, current_lang)
|
||||
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
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)
|
||||
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
if event.message:
|
||||
try:
|
||||
await event.message.edit_text(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
await event.answer()
|
||||
else:
|
||||
await target_message_for_reply.answer(
|
||||
text_to_send, reply_markup=reply_markup_to_send)
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
|
||||
@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):
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Language service error.", show_alert=True)
|
||||
await callback.answer("Service error or message context lost.",
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
lang_code = callback.data.split("_")[2]
|
||||
except IndexError:
|
||||
await callback.answer("Error processing language selection.",
|
||||
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
|
||||
_ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
|
||||
try:
|
||||
updated = await user_dal.update_user_language(session, user_id,
|
||||
lang_code)
|
||||
if updated:
|
||||
|
||||
await callback.answer(_(key="language_set_alert"))
|
||||
i18n_data["current_language"] = lang_code
|
||||
_ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
|
||||
await callback.answer(_(key="language_set_alert"))
|
||||
logging.info(
|
||||
f"User {user_id} language updated to {lang_code} in session.")
|
||||
else:
|
||||
await callback.answer("Could not set language.", show_alert=True)
|
||||
return
|
||||
except Exception as e_lang_update:
|
||||
|
||||
show_trial_button_after_lang_change = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
user_id):
|
||||
show_trial_button_after_lang_change = True
|
||||
|
||||
await send_main_menu(
|
||||
callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_after_lang_change,
|
||||
is_edit=True)
|
||||
logging.error(
|
||||
f"Error updating lang for user {user_id}: {e_lang_update}",
|
||||
exc_info=True)
|
||||
await callback.answer("Error setting language.", show_alert=True)
|
||||
return
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("main_action:"))
|
||||
@@ -242,44 +288,48 @@ 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):
|
||||
|
||||
promo_code_service: PromoCodeService, session: AsyncSession):
|
||||
action = callback.data.split(":")[1]
|
||||
user_id = callback.from_user.id
|
||||
|
||||
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.")
|
||||
await callback.answer("Error: message context lost.", show_alert=True)
|
||||
return
|
||||
|
||||
if action == "subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback, i18n_data, settings)
|
||||
callback, i18n_data, settings, session)
|
||||
elif action == "my_subscription":
|
||||
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service)
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
session, bot)
|
||||
elif action == "referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback, settings, i18n_data, referral_service, bot)
|
||||
callback, settings, i18n_data, referral_service, bot, session)
|
||||
elif action == "apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback, state, i18n_data, settings)
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "request_trial":
|
||||
await user_trial_handlers.request_trial_confirmation_handler(
|
||||
callback, settings, i18n_data, subscription_service)
|
||||
callback, settings, i18n_data, subscription_service, session)
|
||||
elif action == "language":
|
||||
|
||||
await language_command_handler(callback, i18n_data, settings)
|
||||
elif action == "back_to_main":
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
else:
|
||||
await callback.answer("Unknown action.", show_alert=True)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(
|
||||
i18n_data.get("current_language"), key, **kw) if i18n else key
|
||||
await callback.answer(_("main_menu_unknown_action"), show_alert=True)
|
||||
|
||||
+259
-224
@@ -1,15 +1,12 @@
|
||||
import logging
|
||||
import aiosqlite
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime, timezone
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from db.database import add_payment_record, get_db_connection_manager, _setup_db_connection
|
||||
from db.dal import payment_dal
|
||||
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)
|
||||
@@ -21,307 +18,345 @@ 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(event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
i18n_data: dict, settings: Settings,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing in display_subscription_options")
|
||||
|
||||
target_msg = message_or_callback.message if isinstance(
|
||||
message_or_callback, types.CallbackQuery) else message_or_callback
|
||||
if target_msg: await target_msg.answer("Language service error.")
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
await message_or_callback.answer()
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n:
|
||||
err_msg = "Language service error."
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(err_msg, show_alert=True)
|
||||
elif isinstance(event, types.Message):
|
||||
await event.answer(err_msg)
|
||||
return
|
||||
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
text_content = get_text("select_subscription_period"
|
||||
) if settings.subscription_options else get_text(
|
||||
"no_subscription_options_available")
|
||||
|
||||
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
|
||||
settings.subscription_options, currency_symbol_val, current_lang, i18n
|
||||
) if settings.subscription_options else get_back_to_main_menu_markup(
|
||||
current_lang, i18n)
|
||||
|
||||
target_message = message_or_callback.message if isinstance(
|
||||
message_or_callback, types.CallbackQuery) else message_or_callback
|
||||
answered_callback = False
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
|
||||
await message_or_callback.answer()
|
||||
answered_callback = True
|
||||
|
||||
if target_message:
|
||||
if isinstance(message_or_callback, types.CallbackQuery):
|
||||
try:
|
||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
else:
|
||||
await target_message.answer(text, reply_markup=reply_markup)
|
||||
elif isinstance(message_or_callback, types.Message):
|
||||
await message_or_callback.answer(text, reply_markup=reply_markup)
|
||||
|
||||
if isinstance(message_or_callback,
|
||||
types.CallbackQuery) and not answered_callback:
|
||||
await message_or_callback.answer()
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
try:
|
||||
await target_message_obj.edit_text(text_content,
|
||||
reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message_obj.answer(text_content,
|
||||
reply_markup=reply_markup)
|
||||
await event.answer()
|
||||
else:
|
||||
await target_message_obj.answer(text_content,
|
||||
reply_markup=reply_markup)
|
||||
|
||||
|
||||
@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'))
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
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.",
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
get_translation = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs)
|
||||
|
||||
try:
|
||||
months = int(callback.data.split(":")[-1])
|
||||
except ValueError:
|
||||
logging.error(f"Invalid sub period: {callback.data}")
|
||||
await callback.answer(get_translation("error_try_again"),
|
||||
show_alert=True)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(
|
||||
f"Invalid subscription period in callback_data: {callback.data}")
|
||||
await callback.answer(get_text("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)
|
||||
price_rub = settings.subscription_options.get(months)
|
||||
if price_rub is None:
|
||||
logging.error(
|
||||
f"Price not found for {months} months subscription period in settings.subscription_options."
|
||||
)
|
||||
await callback.answer(get_text("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,
|
||||
confirmation_text_content = get_text("confirm_subscription_prompt",
|
||||
months=months,
|
||||
price=f"{price_rub:.2f}",
|
||||
currency_symbol=currency_symbol_val)
|
||||
reply_markup = get_confirm_subscription_keyboard(months, price_rub,
|
||||
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_content,
|
||||
reply_markup=reply_markup)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit message for subscription confirmation failed: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(confirmation_text_content,
|
||||
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, settings: Settings, i18n_data: dict,
|
||||
yookassa_service: YooKassaService, session: AsyncSession):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
current_lang = i18n_data.get("current_language",
|
||||
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"),
|
||||
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
|
||||
await callback.answer(get_text("error_occurred_try_again"),
|
||||
show_alert=True)
|
||||
return
|
||||
|
||||
if not yookassa_service or not yookassa_service.configured:
|
||||
logging.error("YooKassa service is not configured or unavailable.")
|
||||
target_msg_edit = callback.message
|
||||
await target_msg_edit.edit_text(get_text("payment_service_unavailable")
|
||||
)
|
||||
await callback.answer(get_text("payment_service_unavailable_alert"),
|
||||
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)
|
||||
price_rub = float(price_str)
|
||||
except (ValueError, IndexError):
|
||||
logging.error(
|
||||
f"Invalid confirmation data in callback: {callback.data}")
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id
|
||||
description = get_translation("payment_description_subscription",
|
||||
months=months)
|
||||
currency = settings.DEFAULT_CURRENCY_SYMBOL
|
||||
payment_metadata = {
|
||||
|
||||
payment_description = get_text("payment_description_subscription",
|
||||
months=months)
|
||||
currency_code_for_yk = "RUB"
|
||||
|
||||
payment_record_data = {
|
||||
"user_id": user_id,
|
||||
"amount": price_rub,
|
||||
"currency": currency_code_for_yk,
|
||||
"status": "pending_yookassa",
|
||||
"description": payment_description,
|
||||
"subscription_duration_months": months,
|
||||
}
|
||||
db_payment_record = None
|
||||
try:
|
||||
db_payment_record = await payment_dal.create_payment_record(
|
||||
session, payment_record_data)
|
||||
await session.commit()
|
||||
logging.info(
|
||||
f"Payment record {db_payment_record.payment_id} created for user {user_id} with status 'pending_yookassa'."
|
||||
)
|
||||
except Exception as e_db_payment:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Failed to create payment record in DB for user {user_id}: {e_db_payment}",
|
||||
exc_info=True)
|
||||
await callback.message.edit_text(
|
||||
get_text("error_creating_payment_record"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
if not db_payment_record:
|
||||
await callback.message.edit_text(
|
||||
get_text("error_creating_payment_record"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
yookassa_metadata = {
|
||||
"user_id": str(user_id),
|
||||
"subscription_months": str(months),
|
||||
"description": description
|
||||
"payment_db_id": str(db_payment_record.payment_id),
|
||||
}
|
||||
payment_db_id = await add_payment_record(user_id, None, None, price,
|
||||
currency, "pending_creation",
|
||||
description, months, None)
|
||||
if not payment_db_id:
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
get_translation("error_creating_payment_record"))
|
||||
await callback.answer(show_alert=True)
|
||||
return
|
||||
payment_metadata["payment_db_id"] = str(payment_db_id)
|
||||
payment_response = await yookassa_service.create_payment(
|
||||
price, currency, description, payment_metadata)
|
||||
receipt_email_for_yk = settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
|
||||
|
||||
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()
|
||||
payment_response_yk = await yookassa_service.create_payment(
|
||||
amount=price_rub,
|
||||
currency=currency_code_for_yk,
|
||||
description=payment_description,
|
||||
metadata=yookassa_metadata,
|
||||
receipt_email=receipt_email_for_yk)
|
||||
|
||||
if payment_response_yk and payment_response_yk.get("confirmation_url"):
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=db_payment_record.payment_id,
|
||||
new_status=payment_response_yk.get("status", "pending"),
|
||||
yk_payment_id=payment_response_yk.get("id"))
|
||||
await session.commit()
|
||||
except Exception as e_db_update_ykid:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Failed to update payment record {db_payment_record.payment_id} with YK ID: {e_db_update_ykid}",
|
||||
exc_info=True)
|
||||
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"))
|
||||
get_text("error_payment_gateway_link_failed"))
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
get_text(key="payment_link_message", months=months),
|
||||
reply_markup=get_payment_url_keyboard(
|
||||
payment_response_yk["confirmation_url"], current_lang, i18n),
|
||||
disable_web_page_preview=False)
|
||||
else:
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, db_payment_record.payment_id, "failed_creation")
|
||||
await session.commit()
|
||||
except Exception as e_db_fail_create:
|
||||
await session.rollback()
|
||||
logging.error(
|
||||
f"Additionally failed to update payment record to 'failed_creation': {e_db_fail_create}",
|
||||
exc_info=True)
|
||||
|
||||
logging.error(
|
||||
f"Failed to create payment in YooKassa for user {user_id}, payment_db_id {db_payment_record.payment_id}. Response: {payment_response_yk}"
|
||||
)
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_action:subscribe")
|
||||
async def reshow_subscription_options_callback(callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings):
|
||||
await display_subscription_options(callback, i18n_data, settings)
|
||||
settings: Settings,
|
||||
session: AsyncSession):
|
||||
await display_subscription_options(callback, i18n_data, settings, session)
|
||||
|
||||
|
||||
async def my_subscription_command_handler(
|
||||
message_event: types.Message | types.CallbackQuery, i18n_data: dict,
|
||||
event: Union[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()
|
||||
subscription_service: SubscriptionService, session: AsyncSession,
|
||||
bot: Bot):
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
user = event.from_user
|
||||
|
||||
current_lang = i18n_data.get("current_language",
|
||||
getattr(settings, 'DEFAULT_LANGUAGE', 'en'))
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer()
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
logging.error("i18n missing")
|
||||
await target_message.answer("Lang error")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not target_message_obj:
|
||||
if isinstance(event, types.Message):
|
||||
await event.answer(get_text("error_occurred_try_again"))
|
||||
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"))
|
||||
logging.error(
|
||||
"PanelService or SubscriptionService is missing in my_subscription_command_handler."
|
||||
)
|
||||
await target_message_obj.answer(get_text("error_service_unavailable"))
|
||||
return
|
||||
|
||||
active_sub = await subscription_service.get_active_subscription(user.id)
|
||||
sub_info_text = ""
|
||||
if active_sub:
|
||||
end_date_obj = active_sub.get('end_date')
|
||||
if isinstance(end_date_obj, str):
|
||||
try:
|
||||
end_date_obj = datetime.fromisoformat(
|
||||
end_date_obj.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
logging.warning(
|
||||
f"Could not parse date string '{end_date_obj}'.")
|
||||
end_date_obj = datetime.now(timezone.utc)
|
||||
active_sub_details = await subscription_service.get_active_subscription_details(
|
||||
session, user.id)
|
||||
|
||||
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)
|
||||
sub_info_text_content = ""
|
||||
if active_sub_details:
|
||||
end_date_obj = active_sub_details.get('end_date')
|
||||
days_left = 0
|
||||
if end_date_obj:
|
||||
if end_date_obj.tzinfo is None:
|
||||
end_date_obj = end_date_obj.replace(tzinfo=timezone.utc)
|
||||
days_left = (end_date_obj.date() - datetime.now().date()).days
|
||||
|
||||
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 = active_sub_details.get('config_link') or get_text(
|
||||
"config_link_not_available")
|
||||
|
||||
actual_config_link = get_translation("config_link_not_available")
|
||||
panel_user_uuid = active_sub.get('panel_user_uuid')
|
||||
if panel_user_uuid:
|
||||
panel_user_data = await panel_service.get_user_by_uuid(
|
||||
panel_user_uuid)
|
||||
if panel_user_data:
|
||||
if panel_user_data.get('subscriptionUrl'):
|
||||
actual_config_link = panel_user_data['subscriptionUrl']
|
||||
elif panel_user_data.get('shortUuid'):
|
||||
link = await panel_service.get_subscription_link(
|
||||
panel_user_data['shortUuid'])
|
||||
if link: actual_config_link = link
|
||||
traffic_limit_bytes = active_sub_details.get('traffic_limit_bytes')
|
||||
traffic_used_bytes = active_sub_details.get('traffic_used_bytes')
|
||||
|
||||
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_str = get_text("traffic_unlimited")
|
||||
if traffic_limit_bytes and traffic_limit_bytes > 0:
|
||||
traffic_limit_gb_str = f"{traffic_limit_bytes / (1024**3):.2f} GB"
|
||||
|
||||
sub_info_text = get_translation(
|
||||
traffic_used_gb_str = get_text("traffic_na")
|
||||
if traffic_used_bytes is not None:
|
||||
traffic_used_gb_str = f"{traffic_used_bytes / (1024**3):.2f} GB"
|
||||
|
||||
sub_info_text_content = get_text(
|
||||
"my_subscription_details",
|
||||
end_date=end_date_obj.strftime("%Y-%m-%d"),
|
||||
end_date=end_date_obj.strftime("%Y-%m-%d")
|
||||
if end_date_obj else "N/A",
|
||||
days_left=max(0, days_left),
|
||||
status=active_sub.get(
|
||||
'status_from_panel',
|
||||
get_translation('status_active')).capitalize(),
|
||||
status=active_sub_details.get(
|
||||
'status_from_panel', get_text('status_active')).capitalize(),
|
||||
config_link=actual_config_link,
|
||||
traffic_limit=traffic_limit_gb,
|
||||
traffic_used=traffic_used_gb)
|
||||
traffic_limit=traffic_limit_gb_str,
|
||||
traffic_used=traffic_used_gb_str)
|
||||
else:
|
||||
sub_info_text = get_translation("subscription_not_active")
|
||||
sub_info_text_content = get_text("subscription_not_active")
|
||||
logging.info(
|
||||
f"User {user.id} no active sub details for 'my_subscription'.")
|
||||
|
||||
reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
if isinstance(message_event,
|
||||
types.CallbackQuery) and message_event.message:
|
||||
|
||||
if isinstance(event, types.CallbackQuery) and 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,
|
||||
await event.message.edit_text(sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit 'my_subscription' failed: {e_edit}. Sending new message to chat {target_message_obj.chat.id}."
|
||||
)
|
||||
await bot.send_message(chat_id=target_message_obj.chat.id,
|
||||
text=sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
else:
|
||||
await target_message_obj.answer(sub_info_text_content,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
else:
|
||||
await target_message.answer(sub_info_text,
|
||||
reply_markup=reply_markup_val,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True)
|
||||
|
||||
|
||||
@router.message(Command("connect"))
|
||||
async def connect_command_handler(message: types.Message, i18n_data: dict,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService):
|
||||
"""Handles the /connect command, showing subscription info."""
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession, bot: Bot):
|
||||
logging.info(f"User {message.from_user.id} used /connect command.")
|
||||
await my_subscription_command_handler(message, i18n_data, settings,
|
||||
panel_service, subscription_service)
|
||||
panel_service, subscription_service,
|
||||
session, bot)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
@@ -16,37 +16,44 @@ router = Router(name="user_trial_router")
|
||||
|
||||
async def request_trial_confirmation_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService):
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
user_id = callback.from_user.id
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error.", show_alert=True)
|
||||
await callback.answer(_("error_occurred_try_again"), 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
|
||||
|
||||
show_trial_btn_in_menu_if_fail = False
|
||||
if settings.TRIAL_ENABLED:
|
||||
if not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_btn_in_menu_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))
|
||||
settings, False))
|
||||
await callback.answer()
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(user_id):
|
||||
|
||||
if await subscription_service.has_had_any_subscription(session, 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,
|
||||
@@ -59,55 +66,58 @@ async def request_trial_confirmation_handler(
|
||||
async def confirm_activate_trial_handler(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
panel_service: PanelApiService, bot: Bot):
|
||||
panel_service: PanelApiService, session: AsyncSession):
|
||||
user_id = callback.from_user.id
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
await callback.answer("Error.", show_alert=True)
|
||||
await callback.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
show_trial_button_after_action = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
user_id):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
if not settings.TRIAL_ENABLED:
|
||||
await callback.answer(_("trial_feature_disabled"), show_alert=True)
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=False,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(user_id):
|
||||
if await subscription_service.has_had_any_subscription(session, 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,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
return
|
||||
|
||||
activation_result = await subscription_service.activate_trial_subscription(
|
||||
user_id)
|
||||
session, user_id)
|
||||
|
||||
final_message_text_in_chat = ""
|
||||
show_trial_button_after_action = False
|
||||
|
||||
if activation_result and activation_result.get("activated"):
|
||||
await callback.answer(_("trial_activated_alert"), show_alert=True)
|
||||
|
||||
end_date = activation_result.get("end_date")
|
||||
config_link_for_trial = _("config_link_not_available")
|
||||
end_date_obj = activation_result.get("end_date")
|
||||
config_link_for_trial = activation_result.get("subscription_url") or _(
|
||||
"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(
|
||||
if config_link_for_trial == _(
|
||||
"config_link_not_available") and activation_result.get(
|
||||
"panel_short_uuid"):
|
||||
generated_link = await panel_service.get_subscription_link(
|
||||
activation_result["panel_short_uuid"])
|
||||
if link: config_link_for_trial = link
|
||||
if generated_link: config_link_for_trial = generated_link
|
||||
|
||||
traffic_gb_val = activation_result.get("traffic_gb",
|
||||
settings.TRIAL_TRAFFIC_LIMIT_GB)
|
||||
@@ -117,30 +127,34 @@ async def confirm_activate_trial_handler(
|
||||
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_obj.strftime('%Y-%m-%d') if isinstance(
|
||||
end_date_obj, datetime) else "N/A",
|
||||
config_link=config_link_for_trial,
|
||||
traffic_gb=traffic_display)
|
||||
show_trial_button_after_action = False
|
||||
else:
|
||||
message_key = activation_result.get(
|
||||
message_key_from_service = activation_result.get(
|
||||
"message_key", "trial_activation_failed"
|
||||
) if activation_result else "trial_activation_failed"
|
||||
final_message_text_in_chat = _(message_key)
|
||||
final_message_text_in_chat = _(message_key_from_service)
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings,
|
||||
show_trial_button_after_action),
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"Could not edit trial result message: {e_edit}")
|
||||
await callback.message.answer(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_after_action),
|
||||
disable_web_page_preview=True)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Could not edit trial result message: {e_edit}. Sending new one.")
|
||||
|
||||
if callback.message and hasattr(callback.message,
|
||||
'chat') and callback.message.chat:
|
||||
await callback.message.chat.send_message(
|
||||
final_message_text_in_chat,
|
||||
parse_mode="HTML",
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
@@ -152,13 +166,11 @@ async def confirm_activate_trial_handler(
|
||||
@router.callback_query(F.data == "main_action:cancel_trial")
|
||||
async def cancel_trial_activation(callback: types.CallbackQuery,
|
||||
settings: Settings, i18n_data: dict,
|
||||
subscription_service: SubscriptionService):
|
||||
show_trial_button_on_back = False
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
callback.from_user.id):
|
||||
show_trial_button_on_back = True
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
show_trial_button_flag=show_trial_button_on_back,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
|
||||
Reference in New Issue
Block a user