diff --git a/api/main.py b/api/main.py index 82d8b3f1..82684327 100644 --- a/api/main.py +++ b/api/main.py @@ -3,7 +3,7 @@ from api.routes import users, keys, coupons, servers, tariffs, gifts, referrals, app = FastAPI( title="SoloBot API (Alpha)", - version="0.5.0", + version="0.5.1", docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", diff --git a/api/routes/base_crud.py b/api/routes/base_crud.py index 52535ff8..0d9a6d73 100644 --- a/api/routes/base_crud.py +++ b/api/routes/base_crud.py @@ -1,6 +1,6 @@ from typing import Any -from fastapi import APIRouter, Depends, HTTPException, Path, Query +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm.attributes import InstrumentedAttribute @@ -104,7 +104,7 @@ def generate_crud_router( @router.post("/", response_model=schema_response) async def create( - payload: Any, + payload: Any = Body(...), admin: Admin = Depends(verify_admin_token), session: AsyncSession = Depends(get_session), ): @@ -122,7 +122,7 @@ def generate_crud_router( @router.patch(f"/{{{parameter_name}}}", response_model=schema_response) async def update( - payload: Any, + payload: Any = Body(...), value: int | str = Path(..., alias=parameter_name), admin: Admin = Depends(verify_admin_token), session: AsyncSession = Depends(get_session), diff --git a/api/schemas/coupons.py b/api/schemas/coupons.py index b7bbbfbf..348e6163 100644 --- a/api/schemas/coupons.py +++ b/api/schemas/coupons.py @@ -11,15 +11,34 @@ class CouponBase(BaseModel): is_used: bool = False days: int | None = Field(default=None) + percent: int | None = Field(default=None) + max_discount_amount: int | None = Field(default=None) + min_order_amount: int | None = Field(default=None) + + new_users_only: bool = False + @model_validator(mode="after") - def check_exactly_one_of_amount_or_days(self) -> "CouponBase": + def check_coupon_type(self) -> "CouponBase": has_amount = self.amount not in (None, 0) has_days = self.days is not None + has_percent = self.percent is not None + + provided = int(has_amount) + int(has_days) + int(has_percent) + if provided != 1: + raise ValueError("Coupon must have exactly one of: 'amount', 'days', 'percent'") + + if has_days and self.days is not None and self.days <= 0: + raise ValueError("'days' must be > 0") + + if has_percent and self.percent is not None and not (1 <= self.percent <= 100): + raise ValueError("'percent' must be between 1 and 100") + + if has_percent: + if self.min_order_amount is not None and self.min_order_amount < 0: + raise ValueError("'min_order_amount' must be >= 0") + if self.max_discount_amount is not None and self.max_discount_amount < 0: + raise ValueError("'max_discount_amount' must be >= 0") - if has_amount and has_days: - raise ValueError("Coupon must have exactly one of: 'amount' or 'days'") - if not has_amount and not has_days: - raise ValueError("Coupon must have exactly one of: 'amount' or 'days'") return self @@ -38,12 +57,34 @@ class CouponUpdate(BaseModel): is_used: bool | None = None days: int | None = Field(default=None) + percent: int | None = None + max_discount_amount: int | None = None + min_order_amount: int | None = None + + new_users_only: bool | None = None + @model_validator(mode="after") - def validate_amount_or_days(self) -> "CouponUpdate": - if self.amount is None and self.days is None: - return self - if self.amount is not None and self.days is not None: - raise ValueError("Specify only one of: 'amount' or 'days'") + def validate_coupon_update(self) -> "CouponUpdate": + has_amount = self.amount not in (None, 0) + has_days = self.days is not None + has_percent = self.percent is not None + + provided = int(has_amount) + int(has_days) + int(has_percent) + if provided > 1: + raise ValueError("Specify only one of: 'amount', 'days', 'percent'") + + if has_days and self.days is not None and self.days <= 0: + raise ValueError("'days' must be > 0") + + if has_percent and self.percent is not None and not (1 <= self.percent <= 100): + raise ValueError("'percent' must be between 1 and 100") + + if has_percent: + if self.min_order_amount is not None and self.min_order_amount < 0: + raise ValueError("'min_order_amount' must be >= 0") + if self.max_discount_amount is not None and self.max_discount_amount < 0: + raise ValueError("'max_discount_amount' must be >= 0") + return self diff --git a/docker-compose.yml b/docker-compose.yml index 4c1e3a48..0b6610b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,5 @@ services: restart: unless-stopped network_mode: host volumes: - - /etc/machine-id:/etc/machine-id:ro - - /var/lib/dbus/machine-id:/var/lib/dbus/machine-id:ro + - /:/host:ro diff --git a/filters/admin.py b/filters/admin.py index 769fe3fc..84a3e4ba 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -13,9 +13,7 @@ class IsAdminFilter(BaseFilter): try: async with async_session_maker() as session: - result = await session.execute( - select(exists().where(Admin.tg_id == event.from_user.id)) - ) + result = await session.execute(select(exists().where(Admin.tg_id == event.from_user.id))) return result.scalar() except (Exception,): return False diff --git a/handlers/admin/ads/keyboard.py b/handlers/admin/ads/keyboard.py index 2333076a..fe0aed29 100644 --- a/handlers/admin/ads/keyboard.py +++ b/handlers/admin/ads/keyboard.py @@ -3,6 +3,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import build_admin_back_btn +from handlers.buttons import BACK class AdminAdsCallback(CallbackData, prefix="admin_ads"): @@ -43,7 +44,7 @@ def build_ads_list_kb(ads: list, current_page: int, total_pages: int) -> InlineK if current_page > 1: pagination_buttons.append( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminAdsCallback(action="list", code=f"{current_page - 1}").pack(), ) ) diff --git a/handlers/admin/bans/keyboard.py b/handlers/admin/bans/keyboard.py index a203ad04..aabac00a 100644 --- a/handlers/admin/bans/keyboard.py +++ b/handlers/admin/bans/keyboard.py @@ -1,6 +1,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import AdminPanelCallback +from handlers.buttons import BACK def build_bans_kb(): @@ -19,7 +20,7 @@ def build_bans_kb(): callback_data=AdminPanelCallback(action="bans_manual_menu").pack(), ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=AdminPanelCallback(action="management").pack(), ) @@ -43,7 +44,7 @@ def build_blocked_users_kb(): callback_data=AdminPanelCallback(action="bans_clear_blocked").pack(), ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=AdminPanelCallback(action="bans").pack(), ) @@ -67,7 +68,7 @@ def build_shadow_bans_kb(): callback_data=AdminPanelCallback(action="bans_clear_shadow").pack(), ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=AdminPanelCallback(action="bans").pack(), ) @@ -87,7 +88,7 @@ def build_manual_bans_kb(): callback_data=AdminPanelCallback(action="bans_clear_manual").pack(), ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=AdminPanelCallback(action="bans").pack(), ) diff --git a/handlers/admin/clusters/cluster_tariffs.py b/handlers/admin/clusters/cluster_tariffs.py index 6b0d0423..5e55938c 100644 --- a/handlers/admin/clusters/cluster_tariffs.py +++ b/handlers/admin/clusters/cluster_tariffs.py @@ -186,9 +186,7 @@ async def choose_tariffs( await callback.message.edit_text("❌ Для этой группы нет доступных тарифов.") return - servers_q = await session.execute( - select(Server.id).where(Server.server_name.in_(selected_servers)) - ) + servers_q = await session.execute(select(Server.id).where(Server.server_name.in_(selected_servers))) server_ids = [row[0] for row in servers_q.fetchall()] current_bindings_q = await session.execute( @@ -244,7 +242,7 @@ async def toggle_tariff_selection( ) -@router.callback_query(AdminClusterCallback.filter(F.action == "apply_tariffs"),IsAdminFilter()) +@router.callback_query(AdminClusterCallback.filter(F.action == "apply_tariffs"), IsAdminFilter()) async def apply_tariffs( callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext ): diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index 734af93d..5d82e023 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -4,6 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn from ..servers.keyboard import AdminServerCallback +from handlers.buttons import BACK class AdminClusterCallback(CallbackData, prefix="admin_cluster"): @@ -65,7 +66,7 @@ def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineK ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), ) ) @@ -97,7 +98,7 @@ def build_attach_tariff_kb(cluster_name: str) -> InlineKeyboardMarkup: ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), ) ) @@ -114,7 +115,7 @@ def build_legacy_reset_kb(cluster_name: str) -> InlineKeyboardMarkup: ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminClusterCallback(action="attach_tariff_menu", data=cluster_name).pack(), ) ) @@ -157,7 +158,7 @@ def build_select_subgroup_servers_kb( ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminClusterCallback(action="attach_tariff_menu", data=cluster_name).pack(), ) ) @@ -273,7 +274,7 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup: callback_data=AdminClusterCallback(action="set_tariff", data=cluster_name).pack(), ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack())) + builder.row(InlineKeyboardButton(text=BACK, callback_data=AdminPanelCallback(action="clusters").pack())) return builder.as_markup() @@ -321,7 +322,7 @@ def build_tariff_group_selection_kb(cluster_name: str, groups: list[tuple[int, s ) builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), ) ) @@ -363,7 +364,7 @@ def build_select_group_servers_kb(cluster_name: str, cluster_servers: list, sele ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), ) ) @@ -383,7 +384,7 @@ def build_tariff_group_selection_for_servers_kb( ) builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminClusterCallback(action="set_group", data=cluster_name).pack(), ) ) @@ -402,7 +403,7 @@ def build_availability_kb(cluster_name: str) -> InlineKeyboardMarkup: ], [ InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack() + text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack() ) ], ] diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index e04812db..9d60f9ff 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -64,9 +64,7 @@ async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext await state.set_state(AdminCouponsState.waiting_for_coupon_type) -async def show_coupon_audience_step(callback_query: CallbackQuery, state: FSMContext, coupon_type: str): - await state.update_data(coupon_type=coupon_type) - +async def show_coupon_audience_step(callback_query: CallbackQuery, state: FSMContext): text = "🎯 Кому доступен купон?" kb = InlineKeyboardBuilder() kb.button(text="👤 Всем", callback_data="coupon_audience_all") @@ -80,52 +78,34 @@ async def show_coupon_audience_step(callback_query: CallbackQuery, state: FSMCon @router.callback_query(F.data == "coupon_type_balance", IsAdminFilter()) async def handle_balance_coupon_selection(callback_query: CallbackQuery, state: FSMContext): - await show_coupon_audience_step(callback_query, state, "balance") + await state.update_data(coupon_type="balance") + await show_coupon_audience_step(callback_query, state) @router.callback_query(F.data == "coupon_type_days", IsAdminFilter()) async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSMContext): - await show_coupon_audience_step(callback_query, state, "days") - - -@router.callback_query(F.data == "coupon_type_percent", IsAdminFilter()) -async def handle_percent_coupon_selection(callback_query: CallbackQuery, state: FSMContext): - await show_coupon_audience_step(callback_query, state, "percent") - - -@router.callback_query(F.data.in_(("coupon_audience_all", "coupon_audience_new")), IsAdminFilter()) -async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContext): - data = await state.get_data() - coupon_type = data.get("coupon_type") - if coupon_type not in ("balance", "days", "percent"): - await callback_query.answer("Ошибка: тип купона не найден", show_alert=True) - return - - await state.update_data(new_users_only=callback_query.data == "coupon_audience_new") + await state.update_data(coupon_type="days", new_users_only=False) kb = InlineKeyboardBuilder() kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack()) kb.adjust(1) - if coupon_type == "balance": - text = ( - "🎫 Введите данные для создания купона в формате:\n\n" - "📝 код 💰 сумма 🔢 лимит\n\n" - "Пример: 'COUPON1 50 5'\n\n" - ) - await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup()) - await state.set_state(AdminCouponsState.waiting_for_balance_data) - return + text = ( + "🎫 Введите данные для создания купона в формате:\n\n" + "📝 коддни 🔢 лимит\n\n" + "Пример: 'DAYS10 10 50'\n\n" + ) + await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup()) + await state.set_state(AdminCouponsState.waiting_for_days_data) - if coupon_type == "days": - text = ( - "🎫 Введите данные для создания купона в формате:\n\n" - "📝 коддни 🔢 лимит\n\n" - "Пример: 'DAYS10 10 50'\n\n" - ) - await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup()) - await state.set_state(AdminCouponsState.waiting_for_days_data) - return + +@router.callback_query(F.data == "coupon_type_percent", IsAdminFilter()) +async def handle_percent_coupon_selection(callback_query: CallbackQuery, state: FSMContext): + await state.update_data(coupon_type="percent", new_users_only=False) + + kb = InlineKeyboardBuilder() + kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack()) + kb.adjust(1) text = ( "🎫 Введите данные для создания купона в формате:\n\n" @@ -137,6 +117,29 @@ async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContex await state.set_state(AdminCouponsState.waiting_for_percent_data) +@router.callback_query(F.data.in_(("coupon_audience_all", "coupon_audience_new")), IsAdminFilter()) +async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContext): + data = await state.get_data() + coupon_type = data.get("coupon_type") + if coupon_type != "balance": + await callback_query.answer("Ошибка: режим доступен только для купонов на баланс", show_alert=True) + return + + await state.update_data(new_users_only=callback_query.data == "coupon_audience_new") + + kb = InlineKeyboardBuilder() + kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack()) + kb.adjust(1) + + text = ( + "🎫 Введите данные для создания купона в формате:\n\n" + "📝 код 💰 сумма 🔢 лимит\n\n" + "Пример: 'COUPON1 50 5'\n\n" + ) + await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup()) + await state.set_state(AdminCouponsState.waiting_for_balance_data) + + @router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter()) async def handle_balance_coupon_input(message: Message, state: FSMContext, session: Any): text = message.text.strip() @@ -239,16 +242,13 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session: return try: - data = await state.get_data() - new_users_only = bool(data.get("new_users_only")) - ok = await create_coupon( session, coupon_code, 0, usage_limit, days=days, - new_users_only=new_users_only, + new_users_only=False, percent=None, ) if not ok: @@ -256,13 +256,11 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session: return coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}" - audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем" text = ( f"✅ Купон {coupon_code} создан!\n" f"⏳ {format_days(days)}\n" f"🔢 Лимит: {usage_limit} раз\n" - f"🎯 Доступ: {audience_txt}\n" f"🔗 Ссылка: {coupon_link}\n" ) @@ -311,29 +309,21 @@ async def handle_percent_coupon_input(message: Message, state: FSMContext, sessi return try: - data = await state.get_data() - new_users_only = bool(data.get("new_users_only")) - ok = await create_coupon( session, coupon_code, 0, usage_limit, days=None, - new_users_only=new_users_only, + new_users_only=False, percent=percent, ) if not ok: await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup()) return - audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем" - text = ( - f"✅ Купон {coupon_code} создан!\n" - f"📉 Скидка: {percent}%\n" - f"🔢 Лимит: {usage_limit} раз\n" - f"🎯 Доступ: {audience_txt}\n" + f"✅ Купон {coupon_code} создан!\n📉 Скидка: {percent}%\n🔢 Лимит: {usage_limit} раз\n" ) kb = InlineKeyboardBuilder() @@ -431,7 +421,7 @@ async def update_coupons_list(message, session: Any, page: int = 1): await message.edit_text(text=text, reply_markup=kb) -@router.inline_query(F.query.startswith("coupon_"),) +@router.inline_query(F.query.startswith("coupon_")) async def inline_coupon_handler(inline_query: InlineQuery, session: Any): if not INLINE_MODE: return diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py index 66e66f81..62a8154a 100644 --- a/handlers/admin/gifts/gifts_handler.py +++ b/handlers/admin/gifts/gifts_handler.py @@ -16,6 +16,7 @@ from logger import logger from ..panel.keyboard import AdminPanelCallback from .keyboard import build_admin_gifts_kb, build_gifts_list_kb +from handlers.buttons import BACK router = Router() @@ -41,7 +42,7 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession if not tariffs: builder = InlineKeyboardBuilder() - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="gifts").pack()) await callback.message.edit_text("❌ Нет активных тарифов в группе 'gifts'.", reply_markup=builder.as_markup()) return @@ -74,7 +75,7 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession ) ) - builder.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())) + builder.row(types.InlineKeyboardButton(text=BACK, callback_data=AdminPanelCallback(action="gifts").pack())) await callback.message.edit_text("🎁 Выберите тариф для подарка:", reply_markup=builder.as_markup()) @@ -109,7 +110,7 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session: ) ) - builder.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_gift_create")) + builder.row(types.InlineKeyboardButton(text=BACK, callback_data="admin_gift_create")) await edit_or_send_message( target_message=callback.message, @@ -129,7 +130,7 @@ async def handle_tariff_selection(callback: CallbackQuery, state: FSMContext): await state.set_state(GiftCreationState.waiting_for_limit_input_or_unlimited) kb = InlineKeyboardBuilder() - kb.button(text="🔙 Назад", callback_data="admin_gift_create") + kb.button(text=BACK, callback_data="admin_gift_create") await callback.message.edit_text( "🔢 Введите максимальное количество активаций подарка:", reply_markup=kb.as_markup() ) @@ -184,7 +185,7 @@ async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: i if not gifts: builder = InlineKeyboardBuilder() - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="gifts").pack()) await callback.message.edit_text("❌ Подарки не найдены.", reply_markup=builder.as_markup()) return @@ -229,7 +230,7 @@ async def view_gift(callback: CallbackQuery, session: AsyncSession): builder = InlineKeyboardBuilder() builder.button(text="🗑 Удалить", callback_data=f"gift_delete|{gift_id}") - builder.button(text="🔙 Назад", callback_data="admin_gifts_all") + builder.button(text=BACK, callback_data="admin_gifts_all") await callback.message.edit_text(text, reply_markup=builder.as_markup()) diff --git a/handlers/admin/gifts/keyboard.py b/handlers/admin/gifts/keyboard.py index f38eb52c..e4007f18 100644 --- a/handlers/admin/gifts/keyboard.py +++ b/handlers/admin/gifts/keyboard.py @@ -27,7 +27,7 @@ def build_gift_tariffs_kb(tariffs: list[Tariff]) -> InlineKeyboardMarkup: text=f"{tariff.name} — {tariff.duration_days // 30} мес.", callback_data=f"admin_gift_confirm|{tariff.id}", ) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="gifts").pack()) builder.adjust(1) return builder.as_markup() @@ -61,6 +61,6 @@ def build_gifts_list_kb(gifts: list[Gift], page: int, total: int) -> InlineKeybo if nav: builder.row(*nav) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())) + builder.row(InlineKeyboardButton(text=BACK, callback_data=AdminPanelCallback(action="gifts").pack())) return builder.as_markup() diff --git a/handlers/admin/management/keyboard.py b/handlers/admin/management/keyboard.py index 0c2f4578..82dae946 100644 --- a/handlers/admin/management/keyboard.py +++ b/handlers/admin/management/keyboard.py @@ -2,6 +2,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from core.bootstrap import MANAGEMENT_CONFIG +from handlers.buttons import BACK from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn @@ -74,7 +75,7 @@ def build_database_kb() -> InlineKeyboardMarkup: def build_back_to_db_menu() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="⬅️ Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) builder.adjust(1) return builder.as_markup() @@ -84,7 +85,7 @@ def build_export_db_sources_kb() -> InlineKeyboardMarkup: builder.button(text="🌀 Remnawave", callback_data=AdminPanelCallback(action="export_remnawave").pack()) builder.button(text="🧩 3x-ui", callback_data=AdminPanelCallback(action="request_3xui_file").pack()) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) builder.adjust(1) return builder.as_markup() @@ -115,7 +116,7 @@ def build_single_admin_menu(tg_id: int, role: str = "moderator") -> InlineKeyboa text="🎟 Выпустить токен", callback_data=AdminPanelCallback(action=f"generate_token|{tg_id}").pack() ) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="admins").pack()) builder.adjust(1) return builder.as_markup() @@ -125,14 +126,14 @@ def build_role_selection_kb(tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="👑 superadmin", callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|superadmin").pack()) builder.button(text="🛡 moderator", callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|moderator").pack()) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack()) builder.adjust(1) return builder.as_markup() def build_admin_back_kb_to_admins() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="admins").pack()) builder.adjust(1) return builder.as_markup() @@ -140,7 +141,7 @@ def build_admin_back_kb_to_admins() -> InlineKeyboardMarkup: def build_token_result_kb(token: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="📋 Скопировать токен", switch_inline_query_current_chat=token) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="admins").pack()) builder.adjust(1) return builder.as_markup() @@ -150,6 +151,6 @@ def build_post_import_kb() -> InlineKeyboardMarkup: builder.button( text="🔁 Перевыпустить подписки", callback_data=AdminPanelCallback(action="resync_after_import").pack() ) - builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) + builder.button(text=BACK, callback_data=AdminPanelCallback(action="back_to_db_menu").pack()) builder.adjust(1) return builder.as_markup() diff --git a/handlers/admin/module/keyboard.py b/handlers/admin/module/keyboard.py index cbe10b4d..006835c9 100644 --- a/handlers/admin/module/keyboard.py +++ b/handlers/admin/module/keyboard.py @@ -29,7 +29,7 @@ def build_modules_kb(page: int, total_pages: int, items: list[tuple[str, str | N if page > 1: nav.append( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminPanelCallback(action="modules", page=page - 1).pack(), ) ) diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 18249e93..a908eb86 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -37,7 +37,7 @@ def build_manage_server_kb(server_name: str, cluster_name: str, enabled: bool) - ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=f"cluster_servers|{cluster_name}", ) @@ -70,9 +70,7 @@ def build_edit_server_fields_kb(server_name: str, server_data: dict) -> InlineKe builder.row(InlineKeyboardButton(text="⚙️ Тип панели", callback_data=f"select_panel_type|{server_name}")) builder.row( - InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminServerCallback(action="manage", data=server_name).pack() - ) + InlineKeyboardButton(text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()) ) return builder.as_markup() @@ -83,7 +81,7 @@ def build_panel_type_selection_kb(server_name: str) -> InlineKeyboardMarkup: inline_keyboard=[ [InlineKeyboardButton(text="🌐 3x-ui", callback_data=f"set_panel_type|{server_name}|3x-ui")], [InlineKeyboardButton(text="🌀 remnawave", callback_data=f"set_panel_type|{server_name}|remnawave")], - [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"edit_server|{server_name}")], + [InlineKeyboardButton(text=BACK, callback_data=f"edit_server|{server_name}")], ] ) @@ -94,7 +92,7 @@ def build_cluster_selection_kb(server_name: str, clusters: list[str]) -> InlineK for cluster in clusters: builder.row(InlineKeyboardButton(text=cluster, callback_data=f"set_cluster|{server_name}|{cluster}")) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"edit_server|{server_name}")) + builder.row(InlineKeyboardButton(text=BACK, callback_data=f"edit_server|{server_name}")) return builder.as_markup() diff --git a/handlers/admin/settings/keyboard.py b/handlers/admin/settings/keyboard.py index 8c0c0bd6..36a80ac5 100644 --- a/handlers/admin/settings/keyboard.py +++ b/handlers/admin/settings/keyboard.py @@ -2,6 +2,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from core.settings.money_config import get_currency_mode +from handlers.buttons import BACK from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn from .settings_config import ( @@ -44,7 +45,7 @@ def build_toggle_section_keyboard( builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action=back_action).pack(), ) ) @@ -117,12 +118,13 @@ def build_providers_order_kb(sorted_names: list[str]) -> InlineKeyboardMarkup: for idx, name in enumerate(sorted_names): title = PAYMENT_PROVIDER_TITLES.get(name, name) - pos = idx + 1 + pos = idx + 1 builder.row( InlineKeyboardButton( text="⬆️", callback_data=AdminPanelCallback( - action="settings_order_up", page=pos, + action="settings_order_up", + page=pos, ).pack(), ), InlineKeyboardButton( @@ -134,7 +136,8 @@ def build_providers_order_kb(sorted_names: list[str]) -> InlineKeyboardMarkup: InlineKeyboardButton( text="⬇️", callback_data=AdminPanelCallback( - action="settings_order_down", page=pos, + action="settings_order_down", + page=pos, ).pack(), ), ) @@ -147,7 +150,7 @@ def build_providers_order_kb(sorted_names: list[str]) -> InlineKeyboardMarkup: ) builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings_cashboxes").pack(), ) ) @@ -192,7 +195,7 @@ def build_settings_notifications_intervals_kb(notifications_state: dict[str, obj builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings_notifications").pack(), ) ) @@ -261,7 +264,7 @@ def build_settings_money_kb(money_state: dict[str, object]) -> InlineKeyboardMar builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings").pack(), ) ) diff --git a/handlers/admin/settings/settings_cashboxes.py b/handlers/admin/settings/settings_cashboxes.py index 4e476a82..437d9f34 100644 --- a/handlers/admin/settings/settings_cashboxes.py +++ b/handlers/admin/settings/settings_cashboxes.py @@ -68,6 +68,7 @@ async def toggle_cashbox_setting( ) await callback.answer("Настройка обновлена") + @router.callback_query(AdminPanelCallback.filter(F.action == "settings_providers_order")) async def open_providers_order_menu(callback: CallbackQuery, session: AsyncSession) -> None: sorted_names = _get_sorted_provider_names() diff --git a/handlers/admin/settings/settings_money.py b/handlers/admin/settings/settings_money.py index 8c3adc7e..a1b0f7e2 100644 --- a/handlers/admin/settings/settings_money.py +++ b/handlers/admin/settings/settings_money.py @@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from core.bootstrap import MONEY_CONFIG, update_money_config from filters.admin import IsAdminFilter +from handlers.buttons import BACK from ..panel.keyboard import AdminPanelCallback from .keyboard import MONEY_FIELDS, build_settings_money_kb @@ -182,7 +183,7 @@ async def open_currency_mode_menu(callback: CallbackQuery) -> None: ], [ InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings_money").pack(), ) ], diff --git a/handlers/admin/settings/settings_tariffs.py b/handlers/admin/settings/settings_tariffs.py index 5705fe4a..227a115e 100644 --- a/handlers/admin/settings/settings_tariffs.py +++ b/handlers/admin/settings/settings_tariffs.py @@ -4,6 +4,7 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy.ext.asyncio import AsyncSession +from handlers.buttons import BACK from core.settings.tariffs_config import TARIFFS_CONFIG, update_tariffs_config from filters.admin import IsAdminFilter @@ -61,7 +62,7 @@ def build_tariffs_settings_kb() -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings").pack(), ) ) @@ -137,7 +138,7 @@ def build_tariffs_packs_kb() -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminPanelCallback(action="settings_tariffs").pack(), ) ) diff --git a/handlers/admin/tariffs/keyboard.py b/handlers/admin/tariffs/keyboard.py index 2420754d..dd4f28b9 100644 --- a/handlers/admin/tariffs/keyboard.py +++ b/handlers/admin/tariffs/keyboard.py @@ -3,6 +3,7 @@ from collections import defaultdict from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder +from handlers.buttons import BACK from database.tariffs import create_subgroup_hash @@ -33,7 +34,7 @@ def build_tariff_menu_kb() -> InlineKeyboardMarkup: callback_data=AdminTariffCallback(action="arrange").pack(), ) ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminPanelCallback(action="admin").pack())) + builder.row(InlineKeyboardButton(text=BACK, callback_data=AdminPanelCallback(action="admin").pack())) return builder.as_markup() @@ -60,7 +61,7 @@ def build_tariff_arrangement_groups_kb(groups: list[str]) -> InlineKeyboardMarku builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminTariffCallback(action="list").pack(), ) ) @@ -116,7 +117,7 @@ def build_tariffs_arrangement_kb(group_code: str, tariffs: list) -> InlineKeyboa builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminPanelCallback(action="tariffs").pack(), ) ) @@ -146,7 +147,7 @@ def build_tariff_groups_kb(groups: list[str]) -> InlineKeyboardMarkup: row = [] builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminPanelCallback(action="tariffs").pack(), ) ) @@ -196,7 +197,7 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminTariffCallback(action="list").pack(), ) ) @@ -251,7 +252,7 @@ def build_single_tariff_kb( ], [ InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() if group_code else AdminTariffCallback(action="list").pack(), @@ -297,10 +298,6 @@ def build_edit_tariff_fields_kb(tariff_id: int) -> InlineKeyboardMarkup: ) ], [InlineKeyboardButton(text="🔘 Активность", callback_data=f"toggle_active|{tariff_id}")], - [ - InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() - ) - ], + [InlineKeyboardButton(text=BACK, callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack())], ] ) diff --git a/handlers/admin/tariffs/tariff_manage.py b/handlers/admin/tariffs/tariff_manage.py index 75e09cf4..5aa3d532 100644 --- a/handlers/admin/tariffs/tariff_manage.py +++ b/handlers/admin/tariffs/tariff_manage.py @@ -18,6 +18,7 @@ from database.models import Gift, Key, Server, Tariff from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback +from handlers.buttons import BACK from . import router from .keyboard import ( AdminTariffCallback, @@ -331,7 +332,7 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT inline_keyboard=[ [ InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() + text=BACK, callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() ) ] ] @@ -465,7 +466,7 @@ async def ask_new_value(callback: CallbackQuery, state: FSMContext): ], [ InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack(), ) ], diff --git a/handlers/admin/tariffs/tariff_subgroups.py b/handlers/admin/tariffs/tariff_subgroups.py index bfa784ac..1c2e5396 100644 --- a/handlers/admin/tariffs/tariff_subgroups.py +++ b/handlers/admin/tariffs/tariff_subgroups.py @@ -9,6 +9,7 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder +from handlers.buttons import BACK from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -40,7 +41,7 @@ async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: inline_keyboard=[ [ InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + text=BACK, callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] @@ -211,7 +212,7 @@ async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminTariffCallback(action=f"group|{group_code}").pack(), ) ) @@ -296,7 +297,7 @@ async def save_new_subgroup_title(message: Message, state: FSMContext, session: inline_keyboard=[ [ InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + text=BACK, callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] @@ -355,7 +356,7 @@ async def perform_subgroup_deletion(callback: CallbackQuery, state: FSMContext, inline_keyboard=[ [ InlineKeyboardButton( - text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + text=BACK, callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] @@ -387,7 +388,7 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext "❌ Нет доступных тарифов для редактирования.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] + [InlineKeyboardButton(text=BACK, callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] ), ) diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py index bfad1b01..b1456c13 100644 --- a/handlers/admin/users/keyboard.py +++ b/handlers/admin/users/keyboard.py @@ -5,6 +5,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from handlers.buttons import BACK from config import HWID_RESET_BUTTON from database import get_clusters @@ -349,7 +350,7 @@ def build_hwid_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup: callback_data=AdminUserEditorCallback(action="users_hwid_reset", data=email, tg_id=tg_id).pack(), ) builder.button( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(), ) builder.adjust(1) @@ -431,7 +432,7 @@ def build_user_ban_type_kb(tg_id: int) -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( - text="⬅️ Назад", + text=BACK, callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=True).pack(), ) ) diff --git a/handlers/admin/users/users_bans.py b/handlers/admin/users/users_bans.py index bcd6edf4..ca1a224c 100644 --- a/handlers/admin/users/users_bans.py +++ b/handlers/admin/users/users_bans.py @@ -7,6 +7,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import delete from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession +from handlers.buttons import BACK from database.models import ManualBan from filters.admin import IsAdminFilter @@ -41,7 +42,7 @@ async def handle_ban_forever_start(callback: CallbackQuery, callback_data: Admin await state.update_data(tg_id=callback_data.tg_id) kb = InlineKeyboardBuilder() - kb.row(build_editor_btn("⬅️ Назад", tg_id=callback_data.tg_id, edit=True)) + kb.row(build_editor_btn(BACK, tg_id=callback_data.tg_id, edit=True)) await callback.message.edit_text( text="✏️ Введите причину постоянной блокировки (или -, чтобы пропустить):", @@ -97,7 +98,7 @@ async def handle_ban_temporary(callback: CallbackQuery, callback_data: AdminUser await state.update_data(tg_id=callback_data.tg_id) kb = InlineKeyboardBuilder() - kb.row(build_editor_btn("⬅️ Назад", tg_id=callback_data.tg_id, edit=True)) + kb.row(build_editor_btn(BACK, tg_id=callback_data.tg_id, edit=True)) await callback.message.edit_text( text="✏️ Введите причину временной блокировки (или -, чтобы пропустить):", @@ -114,7 +115,7 @@ async def handle_ban_reason_input(message: Message, state: FSMContext): tg_id = user_data.get("tg_id") kb = InlineKeyboardBuilder() - kb.row(build_editor_btn("⬅️ Назад", tg_id=tg_id, edit=True)) + kb.row(build_editor_btn(BACK, tg_id=tg_id, edit=True)) await message.answer( "⏳ Введите срок блокировки в днях (0 — навсегда):", diff --git a/handlers/admin/users/users_keys.py b/handlers/admin/users/users_keys.py index 4a5b9ce0..2539ab52 100644 --- a/handlers/admin/users/users_keys.py +++ b/handlers/admin/users/users_keys.py @@ -3,7 +3,7 @@ import time import uuid from datetime import datetime, timedelta, timezone -from core.settings.tariffs_config import normalize_tariff_config +from handlers.buttons import BACK import pytz @@ -419,7 +419,7 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback( action="users_key_edit", tg_id=tg_id, @@ -450,7 +450,7 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn ) builder.row( InlineKeyboardButton( - text="Назад", + text=BACK, callback_data=f"users_key_edit|{email}", ) ) @@ -503,7 +503,7 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback( action="users_key_edit", tg_id=tg_id, @@ -588,7 +588,7 @@ async def handle_recreate_key_start( ) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) ) @@ -735,7 +735,7 @@ async def handle_recreate_key_confirm( builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback( action="users_key_edit", tg_id=tg_id, @@ -1478,7 +1478,7 @@ async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, s builder.row(InlineKeyboardButton(text="💾 Сохранить", callback_data="cfg_save")) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(), ) ) @@ -1499,7 +1499,7 @@ async def handle_cfg_edit_base(callback_query: CallbackQuery, state: FSMContext, builder.row(InlineKeyboardButton(text="📱 Устройства", callback_data="cfg_base_devices")) if traffic_options: builder.row(InlineKeyboardButton(text="📊 Трафик", callback_data="cfg_base_traffic")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="cfg_back_menu")) await callback_query.message.edit_text( "📦 Редактирование базы тарифа\n\nВыберите параметр:", @@ -1519,7 +1519,7 @@ async def handle_cfg_edit_addon(callback_query: CallbackQuery, state: FSMContext builder.row(InlineKeyboardButton(text="📱 Устройства", callback_data="cfg_addon_devices")) if traffic_options: builder.row(InlineKeyboardButton(text="📊 Трафик", callback_data="cfg_addon_traffic")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="cfg_back_menu")) await callback_query.message.edit_text( "➕ Редактирование докупки\n\nВыберите параметр:", @@ -1544,7 +1544,7 @@ async def handle_cfg_base_devices(callback_query: CallbackQuery, state: FSMConte mark = " ✅" if int(opt) == int(base_devices) else "" builder.button(text=f"{opt} устр.{mark}", callback_data=f"cfg_set_base_dev:{opt}") builder.adjust(3) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="cfg_back_menu")) await state.set_state(UserEditorState.config_select_base) await state.update_data(cfg_param="devices") @@ -1568,7 +1568,7 @@ async def handle_cfg_base_traffic(callback_query: CallbackQuery, state: FSMConte label = "безлимит" if opt == 0 else f"{opt} ГБ" builder.button(text=f"{label}{mark}", callback_data=f"cfg_set_base_traf:{opt}") builder.adjust(2) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="cfg_back_menu")) await state.set_state(UserEditorState.config_select_base) await state.update_data(cfg_param="traffic") @@ -1698,7 +1698,7 @@ async def handle_cfg_input_addon(message: Message, state: FSMContext, session: A builder.row(InlineKeyboardButton(text="💾 Сохранить", callback_data="cfg_save")) builder.row( InlineKeyboardButton( - text="🔙 Назад", + text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(), ) ) diff --git a/handlers/admin/users/users_tariffs.py b/handlers/admin/users/users_tariffs.py index c53f31e3..3dc24f13 100644 --- a/handlers/admin/users/users_tariffs.py +++ b/handlers/admin/users/users_tariffs.py @@ -7,6 +7,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession +from handlers.buttons import BACK from core.settings.tariffs_config import normalize_tariff_config from database import get_tariff_by_id @@ -70,7 +71,7 @@ async def handle_user_choose_tariff_group( builder = InlineKeyboardBuilder() for group_code in groups: builder.button(text=group_code, callback_data=f"group:{group_code}") - builder.button(text="🔙 Назад", callback_data="back:renew") + builder.button(text=BACK, callback_data="back:renew") builder.adjust(1) await callback_query.message.edit_text( @@ -101,7 +102,7 @@ async def handle_user_choose_tariff( builder = InlineKeyboardBuilder() for tariff in tariffs: builder.button(text=f"{tariff.name} – {int(tariff.price_rub)}₽", callback_data=f"confirm:{tariff.id}") - builder.button(text="🔙 Назад", callback_data="back:group") + builder.button(text=BACK, callback_data="back:group") builder.adjust(1) await callback_query.message.edit_text( @@ -271,7 +272,7 @@ async def handle_user_renew_confirm( builder.row(b) builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="back:group")) devices_label = ( "Безлимит устройств" @@ -456,7 +457,7 @@ async def handle_cfg_renew_devices(callback_query: CallbackQuery, state: FSMCont builder.row(b) builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="back:group")) devices_label = "Безлимит устройств" if selected_devices <= 0 else f"{selected_devices} устройств" traffic_label = ( @@ -573,7 +574,7 @@ async def handle_cfg_renew_traffic(callback_query: CallbackQuery, state: FSMCont builder.row(b) builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="back:group")) devices_label = ( "Безлимит устройств" @@ -708,7 +709,7 @@ async def handle_back_to_group( builder = InlineKeyboardBuilder() for group_code in groups: builder.button(text=group_code, callback_data=f"group:{group_code}") - builder.button(text="🔙 Назад", callback_data="back:renew") + builder.button(text=BACK, callback_data="back:renew") builder.adjust(1) await callback_query.message.edit_text( diff --git a/handlers/buttons.py b/handlers/buttons.py index be137aca..a5fa0ca0 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -21,6 +21,7 @@ TRIAL_SUB = "🎁 Пробная подписка" TRIAL_BONUS = "🚀 Активировать пробный период" MY_SUB = "🔐 Моя подписка" RENEW_SUB = "🔄 Обновить подписку" +ADMIN_BTN = "📊 Администратор" ADDONS_BUTTON_DEVICES = "➕ Добавить устройства" ADDONS_BUTTON_TRAFFIC = "➕ Добавить трафик" diff --git a/handlers/coupons.py b/handlers/coupons.py index a3a8cec1..dee47811 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -238,8 +238,12 @@ async def handle_key_extension( tariff_gb = int(tariff["traffic_limit"]) if tariff and tariff.get("traffic_limit") else 0 tariff_devices = int(tariff["device_limit"]) if tariff and tariff.get("device_limit") else 0 - total_gb = int(key.current_traffic_limit) if getattr(key, "current_traffic_limit", None) is not None else tariff_gb - device_limit = int(key.current_device_limit) if getattr(key, "current_device_limit", None) is not None else tariff_devices + total_gb = ( + int(key.current_traffic_limit) if getattr(key, "current_traffic_limit", None) is not None else tariff_gb + ) + device_limit = ( + int(key.current_device_limit) if getattr(key, "current_device_limit", None) is not None else tariff_devices + ) key_subgroup = None if tariff: diff --git a/handlers/donate.py b/handlers/donate.py index 7194a819..2d856df2 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -11,7 +11,7 @@ from aiogram.types import ( from aiogram.utils.keyboard import InlineKeyboardBuilder from config import RUB_TO_XTR, STARS_BOT_URL -from handlers.buttons import BACK, MAIN_MENU +from handlers.buttons import BACK, MAIN_MENU, STARS_BOT from logger import logger from .utils import edit_or_send_message @@ -31,7 +31,7 @@ async def process_donate(callback_query: CallbackQuery, state: FSMContext): await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url=STARS_BOT_URL)) + builder.row(InlineKeyboardButton(text=STARS_BOT, url=STARS_BOT_URL)) builder.row( InlineKeyboardButton( text="💰 Ввести сумму доната", diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 78144f50..9d8481a9 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -167,17 +167,20 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data.startswith("connect_tv|")) async def process_connect_tv(callback_query: CallbackQuery, session: Any): - key_name = callback_query.data.split("|")[1] + key_name = callback_query.data.split("|", 1)[1] record = await get_key_details(session, key_name) final_link = None is_full_remnawave = False use_webapp = False + is_remnawave_webapp = False if record: - server_name = record["server_id"] + server_name = record.get("server_id") final_link = record.get("key") or record.get("remnawave_link") - is_full_remnawave = await is_full_remnawave_cluster(server_name, session) + + if server_name: + is_full_remnawave = await is_full_remnawave_cluster(server_name, session) remnawave_webapp_enabled = bool(MODES_CONFIG.get("REMNAWAVE_WEBAPP_ENABLED", REMNAWAVE_WEBAPP)) happ_cryptolink_enabled = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)) @@ -191,22 +194,23 @@ async def process_connect_tv(callback_query: CallbackQuery, session: Any): ) is_remnawave_webapp = bool(is_full_remnawave and final_link and use_webapp and not happ_cryptolink_enabled) - else: - is_remnawave_webapp = False - if is_remnawave_webapp: - back_callback = f"view_key|{key_name}" - else: - back_callback = f"connect_device|{key_name}" + if not final_link: + await callback_query.answer("❌ Ссылка подписки не найдена", show_alert=True) + return + + back_callback = f"view_key|{key_name}" if is_remnawave_webapp else f"connect_device|{key_name}" builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=TV_CONTINUE, callback_data=f"continue_tv|{key_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data=back_callback)) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) + text = CONNECT_TV_TEXT.format(subscription_link=final_link) + await edit_or_send_message( target_message=callback_query.message, - text=CONNECT_TV_TEXT, + text=text, reply_markup=builder.as_markup(), media_path=None, disable_web_page_preview=True, diff --git a/handlers/keys/key_create.py b/handlers/keys/key_create.py index 4e7539e5..34065db6 100644 --- a/handlers/keys/key_create.py +++ b/handlers/keys/key_create.py @@ -28,7 +28,7 @@ from database.models import Admin from database.notifications import check_hot_lead_discount from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs from handlers.admin.panel.keyboard import AdminPanelCallback -from handlers.buttons import MAIN_MENU +from handlers.buttons import MAIN_MENU, BACK from handlers.texts import ( CREATING_CONNECTION_MSG, DISCOUNT_OFFER_MESSAGE, @@ -362,7 +362,7 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte callback_prefix="select_tariff_plan", ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_tariff_group_list")) + builder.row(InlineKeyboardButton(text=BACK, callback_data="back_to_tariff_group_list")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index eac7f743..f0c3bf31 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -368,7 +368,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont callback_prefix="renew_plan", ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"renew_key|{key_name}")) + builder.row(InlineKeyboardButton(text=BACK, callback_data=f"renew_key|{key_name}")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) hook_builder = InlineKeyboardBuilder() diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py index 889f533c..4aabb84b 100644 --- a/handlers/keys/operations/renewal.py +++ b/handlers/keys/operations/renewal.py @@ -74,6 +74,7 @@ async def renew_on_remnawave( if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): logger.error(f"{PANEL_REMNA} Не удалось войти в Remnawave API") return False + hwid_device_limit = int(hwid_device_limit or 0) if old_device_limit is not None and hwid_device_limit < old_device_limit: try: diff --git a/handlers/payments/pay.py b/handlers/payments/pay.py index fa064a1c..14ce340c 100644 --- a/handlers/payments/pay.py +++ b/handlers/payments/pay.py @@ -19,7 +19,12 @@ from handlers.payments.currency_rates import format_for_user from handlers.payments.providers import get_providers_with_hooks from handlers.payments.stars.handlers import process_callback_pay_stars from handlers.payments.tribute.handlers import process_callback_pay_tribute -from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG +from handlers.texts import ( + FAST_PAY_CHOOSE_CURRENCY, + BALANCE_MANAGEMENT_TEXT, + PAYMENT_METHODS_MSG, + BALANCE_HISTORY_HEADER, +) from hooks.hook_buttons import insert_hook_buttons from hooks.hooks import run_hooks @@ -211,7 +216,7 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any): if records: language_code = getattr(callback_query.from_user, "language_code", None) - history_text = "💳 История операций:\n\n
" + history_text = f"{BALANCE_HISTORY_HEADER}\n\n
" for record in records: amount_rub = record["amount"] or 0 formatted_amount = await format_for_user(session, callback_query.from_user.id, amount_rub, language_code) diff --git a/handlers/payments/providers.py b/handlers/payments/providers.py index 4db18c55..34e078cf 100644 --- a/handlers/payments/providers.py +++ b/handlers/payments/providers.py @@ -79,9 +79,7 @@ def _get_effective_order(name: str, cfg: dict[str, Any]) -> int: def _sort_providers(providers: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: """Сортирует провайдеров по полю 'order' (меньше = выше).""" - return dict( - sorted(providers.items(), key=lambda item: _get_effective_order(item[0], item[1])) - ) + return dict(sorted(providers.items(), key=lambda item: _get_effective_order(item[0], item[1]))) def sort_provider_names(names: list[str], providers_map: dict[str, dict[str, Any]]) -> list[str]: diff --git a/handlers/profile.py b/handlers/profile.py index eba92b26..3b15fa7e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -28,6 +28,7 @@ from handlers.buttons import ( MY_SUB, MY_SUBS, TRIAL_SUB, + ADMIN_BTN, ) from handlers.payments.currency_rates import format_for_user from handlers.texts import ADD_SUBSCRIPTION_HINT @@ -135,7 +136,7 @@ async def process_callback_view_profile( if admin: builder.row( InlineKeyboardButton( - text="📊 Администратор", + text=ADMIN_BTN, callback_data=AdminPanelCallback(action="admin").pack(), ) ) diff --git a/handlers/start.py b/handlers/start.py index 9ab8db31..62a54dd3 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -39,6 +39,7 @@ from handlers.buttons import ( SUB_CHANELL_DONE, SUPPORT, TRIAL_SUB, + ADMIN_BTN, ) from handlers.captcha import generate_captcha from handlers.coupons import activate_coupon @@ -313,7 +314,7 @@ async def show_start_menu( kb.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL)) if admin: - kb.row(InlineKeyboardButton(text="📊 Администратор", callback_data=AdminPanelCallback(action="admin").pack())) + kb.row(InlineKeyboardButton(text=ADMIN_BTN, callback_data=AdminPanelCallback(action="admin").pack())) try: module_buttons = await run_hooks("start_menu", chat_id=message.chat.id, session=session)