Refactor bot handlers and keyboards; implement admin features

- Removed unused prehandling, referrals, support, and states files.
- Updated admin and client handlers to improve structure and functionality.
- Introduced admin filters and routers for better access control.
- Added new billing and deposit functionalities for client interactions.
- Enhanced keyboard layouts for admin and client menus.
- Implemented promotion handling for admin users.
- Updated configuration to include admin settings.
- Improved utility functions for quote fetching and menu text building.
- Added common handlers for undefined commands and message deletions.
This commit is contained in:
2026-05-13 10:34:06 +07:00
parent e9f84d9377
commit acb8662a66
28 changed files with 257 additions and 65 deletions

8
bot/filters/admins.py Normal file
View File

@@ -0,0 +1,8 @@
from aiogram.filters import Filter
from config import settings
class AdminFilter(Filter):
async def __call__(self, event) -> bool:
return event.from_user.id in settings.admins

View File

@@ -1,21 +1,9 @@
from .ads import router as ads_router
from .billing import router as billing_router
from .buy import router as buy_router
from .common import router as common_router
from .deposit import router as deposit_router
from .menus import router as menus_router
from .prehandling import router as prehandling_router
from .referals import router as referal_router
from .support import router as support_router
from .admins import routers as admin_routers
from .client import routers as client_routers
from .system import routers as system_routers
routers = [
prehandling_router,
referal_router,
billing_router,
deposit_router,
menus_router,
ads_router,
support_router,
buy_router,
common_router,
__all__ = [
"admin_routers",
"client_routers",
"system_routers",
]

View File

@@ -0,0 +1,4 @@
from .admins import router as general_router
from .ads import router as ads_router
routers = [general_router, ads_router]

View File

@@ -0,0 +1,36 @@
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from bot.filters.admins import AdminFilter
from bot.keyboards.admins import main_menu
from misc.utils import build_adm_main_menu_text
from schemas.di import DependenciesDTO
router = Router()
@router.message(AdminFilter(), Command(commands=["start", "cancel"]))
async def standard_start(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
users = await deps.user_repository.user_count(session)
await msg.answer(
build_adm_main_menu_text(user_count=users, user_id=msg.from_user.id), reply_markup=main_menu
)
@router.callback_query(AdminFilter(), F.data == "menu:main")
async def main_menu_cb(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
users = await deps.user_repository.user_count(session)
await cb.message.edit_text(
build_adm_main_menu_text(user_count=users, user_id=cb.from_user.id), reply_markup=main_menu
)

View File

@@ -0,0 +1,52 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.di import DependenciesDTO
from schemas.promotions import NewPromotionContext
from bot.texts import ADM_PROMO_INIT, CALLBACK_FALLBACK, GENERAL_PROCESSING, ADM_VERIFY_PROMOTION, SUCCESSFULLY_SENT_PROMO
from bot.keyboards.common import return_to_menu
from bot.keyboards.admins import action_confirmation
from bot.states.admins import AdminStorage
router = Router()
@router.callback_query(F.data == "promotion")
async def promo_init(cb: CallbackQuery, state: FSMContext):
await state.clear()
await cb.message.edit_text(ADM_PROMO_INIT, reply_markup=return_to_menu)
ctx = NewPromotionContext(cb.message)
await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx})
@router.message(AdminStorage.promotion_msg)
async def promo_text(msg: Message, state: FSMContext):
data = await state.get_data()
await state.clear()
edited_msg = await msg.answer(GENERAL_PROCESSING)
ctx: NewPromotionContext | None = data.get("ctx")
if ctx:
await ctx.msg.delete()
ctx.promotion = msg
await edited_msg.edit_text(ADM_VERIFY_PROMOTION, reply_markup=action_confirmation)
await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx})
@router.callback_query(AdminStorage.promotion_msg, F.data.in_(("approve", "decline")))
async def promo_send(cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO):
data = await state.get_data()
await state.clear()
ctx: NewPromotionContext | None = data.get("ctx")
if not (ctx and ctx.promotion and ctx.msg):
await cb.message.edit_text(CALLBACK_FALLBACK, reply_markup=return_to_menu)
return
successful = await deps.user_service.launch_promotion(session, ctx.promotion)
await cb.message.edit_text(SUCCESSFULLY_SENT_PROMO.format(successful=successful), reply_markup=return_to_menu)

View File

@@ -1,5 +0,0 @@
from aiogram import Router
router = Router()
# FIXME: Refine newsletter system

View File

@@ -0,0 +1,15 @@
from .billing import router as billing_router
from .buy import router as buy_router
from .deposit import router as deposit_router
from .menus import router as menus_router
from .referals import router as referal_router
from .support import router as support_router
routers = [
referal_router,
billing_router,
deposit_router,
menus_router,
support_router,
buy_router,
]

View File

@@ -5,7 +5,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.client import pay_url, return_to_menu
from bot.keyboards.client import pay_url
from bot.keyboards.common import return_to_menu
from bot.states.buy import BillingStorage
from bot.texts import BILL_CREATED, BILL_CREATION, CALLBACK_FALLBACK
from schemas.billing import BillingContext, BillingProviders

View File

@@ -6,7 +6,9 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.client import deposit_kb, devices_selector, duration_selector, return_to_menu
from bot.keyboards.client import deposit_kb, devices_selector, duration_selector
from bot.keyboards.common import return_to_menu
from bot.states.buy import SubscriptionStorage
from bot.texts import (
CALLBACK_FALLBACK,
@@ -176,7 +178,10 @@ async def proceed_to_checkout(
if effective_balance < amount:
await cb.message.edit_text(
INSUFFICIENT_FUNDS.format(balance=user.balance, amount=amount, diff=abs(amount-user.balance)), reply_markup=deposit_kb
INSUFFICIENT_FUNDS.format(
balance=user.balance, amount=amount, diff=abs(amount - user.balance)
),
reply_markup=deposit_kb,
)
return

View File

@@ -2,7 +2,8 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from bot.keyboards.client import payment_gateways, return_to_menu
from bot.keyboards.client import payment_gateways
from bot.keyboards.common import return_to_menu
from bot.states.buy import BillingStorage
from bot.states.deposit import DepositStorage
from bot.texts import (

View File

@@ -4,7 +4,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.client import close_kb, main_menu, return_to_menu, subscription_controls
from bot.keyboards.client import close_kb, main_menu, subscription_controls
from bot.keyboards.common import return_to_menu
from bot.texts import (
CALLBACK_FALLBACK,
FAQ_TEXT,
@@ -149,7 +150,9 @@ async def sub_info(
deps.user_service.format_subscription_message(
rw_user, link=user.subscription_link, autorenew=sub.autorenew
),
reply_markup=subscription_controls(show_autorenew=bool(rw_user), current_autorenewal_status=sub.autorenew),
reply_markup=subscription_controls(
show_autorenew=bool(rw_user), current_autorenewal_status=sub.autorenew
),
)
@@ -183,5 +186,7 @@ async def toggle_autorenewal(
deps.user_service.format_subscription_message(
rw_user, link=user.subscription_link, autorenew=sub.autorenew
),
reply_markup=subscription_controls(show_autorenew=bool(rw_user), current_autorenewal_status=sub.autorenew),
reply_markup=subscription_controls(
show_autorenew=bool(rw_user), current_autorenewal_status=sub.autorenew
),
)

View File

@@ -0,0 +1,4 @@
from .common import router as common_router
from .prehandling import router as prehandling_router
routers = [common_router, prehandling_router]

View File

@@ -1,16 +1,16 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
promotion_confirmation: InlineKeyboardMarkup = InlineKeyboardBuilder(
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[InlineKeyboardButton(text="📢 Рассылка", callback_data="promotion")],
[InlineKeyboardButton(text="👤 Управление пользователем", callback_data="usermgmt")],
]
).as_markup()
action_confirmation: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[InlineKeyboardButton(text="", callback_data="approve")],
[InlineKeyboardButton(text="", callback_data="decline")],
]
).as_markup()
ticket_actions: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[InlineKeyboardButton(text="🔒", callback_data="close")],
[InlineKeyboardButton(text="💸 Пополнить баланс реферала", callback_data="refpay")],
]
).as_markup()

View File

@@ -4,6 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import settings
from misc.utils import convert_duration_to_int
from schemas.subscriptions import SubscriptionDuration
from .common import _return_to_menu_btn
main_menu = InlineKeyboardBuilder(
[
@@ -24,12 +25,6 @@ main_menu = InlineKeyboardBuilder(
]
).as_markup()
_return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
text="⬅️ Назад", callback_data="menu:main"
)
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()
payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[InlineKeyboardButton(text="💸 Pally (СБП / Карта)", callback_data="billing:pally")],
@@ -54,7 +49,9 @@ deposit_kb: InlineKeyboardMarkup = InlineKeyboardBuilder(
).as_markup()
def subscription_controls(show_autorenew: bool, current_autorenewal_status: bool = None) -> InlineKeyboardMarkup:
def subscription_controls(
show_autorenew: bool, current_autorenewal_status: bool | None = None
) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
if show_autorenew:
@@ -64,7 +61,7 @@ def subscription_controls(show_autorenew: bool, current_autorenewal_status: bool
else "🔴 Отключить автопродление"
)
builder.row(InlineKeyboardButton(text=autorenewal_text, callback_data="toggle:autorenewal"))
builder.row(_return_to_menu_btn)
return builder.as_markup()

12
bot/keyboards/common.py Normal file
View File

@@ -0,0 +1,12 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import settings
from misc.utils import convert_duration_to_int
from schemas.subscriptions import SubscriptionDuration
_return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
text="⬅️ Назад", callback_data="menu:main"
)
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()

View File

@@ -6,7 +6,7 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.client.session.aiohttp import AiohttpSession
from remnawave import RemnawaveSDK
from bot.handlers import routers
from bot.handlers import admin_routers, client_routers, system_routers
from bot.middlewares import DIMiddleware
from config import settings
from db.session import async_session
@@ -55,8 +55,12 @@ async def main():
default = DefaultBotProperties(parse_mode="HTML")
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
for router in routers:
dp.include_router(router)
for r in admin_routers:
dp.include_router(r)
for r in client_routers:
dp.include_router(r)
for r in system_routers:
dp.include_router(r)
await dp.start_polling(bot)

View File

@@ -1,5 +0,0 @@
from aiogram.fsm.state import State, StatesGroup
class SupportStorage(StatesGroup):
prompt = State()

View File

@@ -19,20 +19,15 @@ MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗")
MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙")
MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧")
MALENIA_SHIELD = PremiumEmoji(5444924740596702937, "☀️")
MALENIA_SILHOUETTE = PremiumEmoji(5447117295631506779, "☀️")
MALENIA_CARD = PremiumEmoji(5447145526451543197, "☀️")
# ============================================================
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
# ============================================================
_SUPPORT_USER_DESCRIPTION = (
"👤 <b>Профиль:</b> {full_name}\n"
"🔗 <b>Alias:</b> {username_display}\n"
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
"❤️ <b>Referal UID:</b> <code>{referal_id}</code>\n\n"
"💰 <b>Баланс:</b> <code>{balance}</code>"
)
_DIVIDER = "────────────────────────────\n\n"
@@ -205,3 +200,20 @@ SUPPORT_INIT = "<b>🎧 Тех. Поддержка Malenia</b> — не здес
SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\n<i>Вернуться в меню: /cancel</i>"
SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia:</b>"
# ========================================================================================================================
# ============================================================
# ADMIN
# ============================================================
ADM_MAIN_MENU = (
f"<b>{MALENIA_SHIELD} Malenia. Админ-панель.</b>\n"
"<i>— {quote}</i>\n\n"
f"<b>{MALENIA_SILHOUETTE} Пользователей в боте:</b> <code>{{user_count}}</code>\n"
f"<b>{MALENIA_CARD} Ваш ID:</b> <code>{{user_id}}</code>\n"
)
ADM_PROMO_INIT = "<b>📢 Введите/перешлите текст рассылки:</b>"
ADM_VERIFY_PROMOTION = "<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>"
SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>"