Migrating to Postgres from sqlite3

This commit is contained in:
machka
2025-05-20 18:41:32 +00:00
parent ac2f83c061
commit a56d804b58
38 changed files with 5017 additions and 4389 deletions
+17 -15
View File
@@ -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", )
+79 -75
View File
@@ -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)
+26 -22
View File
@@ -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
View File
@@ -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)
+121 -87
View File
@@ -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()
+68 -58
View File
@@ -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
+235 -70
View File
@@ -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")
+378 -409
View File
@@ -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()