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:
8
bot/filters/admins.py
Normal file
8
bot/filters/admins.py
Normal 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
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
from .ads import router as ads_router
|
from .admins import routers as admin_routers
|
||||||
from .billing import router as billing_router
|
from .client import routers as client_routers
|
||||||
from .buy import router as buy_router
|
from .system import routers as system_routers
|
||||||
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
|
|
||||||
|
|
||||||
routers = [
|
__all__ = [
|
||||||
prehandling_router,
|
"admin_routers",
|
||||||
referal_router,
|
"client_routers",
|
||||||
billing_router,
|
"system_routers",
|
||||||
deposit_router,
|
|
||||||
menus_router,
|
|
||||||
ads_router,
|
|
||||||
support_router,
|
|
||||||
buy_router,
|
|
||||||
common_router,
|
|
||||||
]
|
]
|
||||||
|
|||||||
4
bot/handlers/admins/__init__.py
Normal file
4
bot/handlers/admins/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .admins import router as general_router
|
||||||
|
from .ads import router as ads_router
|
||||||
|
|
||||||
|
routers = [general_router, ads_router]
|
||||||
36
bot/handlers/admins/admins.py
Normal file
36
bot/handlers/admins/admins.py
Normal 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
|
||||||
|
)
|
||||||
52
bot/handlers/admins/ads.py
Normal file
52
bot/handlers/admins/ads.py
Normal 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)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from aiogram import Router
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
# FIXME: Refine newsletter system
|
|
||||||
15
bot/handlers/client/__init__.py
Normal file
15
bot/handlers/client/__init__.py
Normal 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,
|
||||||
|
]
|
||||||
@@ -5,7 +5,8 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.states.buy import BillingStorage
|
||||||
from bot.texts import BILL_CREATED, BILL_CREATION, CALLBACK_FALLBACK
|
from bot.texts import BILL_CREATED, BILL_CREATION, CALLBACK_FALLBACK
|
||||||
from schemas.billing import BillingContext, BillingProviders
|
from schemas.billing import BillingContext, BillingProviders
|
||||||
@@ -6,7 +6,9 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.states.buy import SubscriptionStorage
|
||||||
from bot.texts import (
|
from bot.texts import (
|
||||||
CALLBACK_FALLBACK,
|
CALLBACK_FALLBACK,
|
||||||
@@ -176,7 +178,10 @@ async def proceed_to_checkout(
|
|||||||
|
|
||||||
if effective_balance < amount:
|
if effective_balance < amount:
|
||||||
await cb.message.edit_text(
|
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
|
return
|
||||||
|
|
||||||
@@ -2,7 +2,8 @@ from aiogram import F, Router
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
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.buy import BillingStorage
|
||||||
from bot.states.deposit import DepositStorage
|
from bot.states.deposit import DepositStorage
|
||||||
from bot.texts import (
|
from bot.texts import (
|
||||||
@@ -4,7 +4,8 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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 (
|
from bot.texts import (
|
||||||
CALLBACK_FALLBACK,
|
CALLBACK_FALLBACK,
|
||||||
FAQ_TEXT,
|
FAQ_TEXT,
|
||||||
@@ -149,7 +150,9 @@ async def sub_info(
|
|||||||
deps.user_service.format_subscription_message(
|
deps.user_service.format_subscription_message(
|
||||||
rw_user, link=user.subscription_link, autorenew=sub.autorenew
|
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(
|
deps.user_service.format_subscription_message(
|
||||||
rw_user, link=user.subscription_link, autorenew=sub.autorenew
|
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
|
||||||
|
),
|
||||||
)
|
)
|
||||||
4
bot/handlers/system/__init__.py
Normal file
4
bot/handlers/system/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .common import router as common_router
|
||||||
|
from .prehandling import router as prehandling_router
|
||||||
|
|
||||||
|
routers = [common_router, prehandling_router]
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
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="approve")],
|
||||||
[InlineKeyboardButton(text="❌", callback_data="decline")],
|
[InlineKeyboardButton(text="❌", callback_data="decline")],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
ticket_actions: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|
||||||
[
|
|
||||||
[InlineKeyboardButton(text="🔒", callback_data="close")],
|
|
||||||
[InlineKeyboardButton(text="💸 Пополнить баланс реферала", callback_data="refpay")],
|
|
||||||
]
|
|
||||||
).as_markup()
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from config import settings
|
from config import settings
|
||||||
from misc.utils import convert_duration_to_int
|
from misc.utils import convert_duration_to_int
|
||||||
from schemas.subscriptions import SubscriptionDuration
|
from schemas.subscriptions import SubscriptionDuration
|
||||||
|
from .common import _return_to_menu_btn
|
||||||
|
|
||||||
main_menu = InlineKeyboardBuilder(
|
main_menu = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
@@ -24,12 +25,6 @@ main_menu = InlineKeyboardBuilder(
|
|||||||
]
|
]
|
||||||
).as_markup()
|
).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(
|
payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
[InlineKeyboardButton(text="💸 Pally (СБП / Карта)", callback_data="billing:pally")],
|
[InlineKeyboardButton(text="💸 Pally (СБП / Карта)", callback_data="billing:pally")],
|
||||||
@@ -54,7 +49,9 @@ deposit_kb: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|||||||
).as_markup()
|
).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()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
if show_autorenew:
|
if show_autorenew:
|
||||||
|
|||||||
12
bot/keyboards/common.py
Normal file
12
bot/keyboards/common.py
Normal 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()
|
||||||
10
bot/main.py
10
bot/main.py
@@ -6,7 +6,7 @@ from aiogram.client.default import DefaultBotProperties
|
|||||||
from aiogram.client.session.aiohttp import AiohttpSession
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
from remnawave import RemnawaveSDK
|
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 bot.middlewares import DIMiddleware
|
||||||
from config import settings
|
from config import settings
|
||||||
from db.session import async_session
|
from db.session import async_session
|
||||||
@@ -55,8 +55,12 @@ async def main():
|
|||||||
default = DefaultBotProperties(parse_mode="HTML")
|
default = DefaultBotProperties(parse_mode="HTML")
|
||||||
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
||||||
|
|
||||||
for router in routers:
|
for r in admin_routers:
|
||||||
dp.include_router(router)
|
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)
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from aiogram.fsm.state import State, StatesGroup
|
|
||||||
|
|
||||||
|
|
||||||
class SupportStorage(StatesGroup):
|
|
||||||
prompt = State()
|
|
||||||
28
bot/texts.py
28
bot/texts.py
@@ -19,20 +19,15 @@ MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗")
|
|||||||
MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙")
|
MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙")
|
||||||
MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
|
MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
|
||||||
MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧")
|
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"
|
_DIVIDER = "────────────────────────────\n\n"
|
||||||
|
|
||||||
|
|
||||||
@@ -205,3 +200,20 @@ SUPPORT_INIT = "<b>🎧 Тех. Поддержка Malenia</b> — не здес
|
|||||||
SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\n<i>Вернуться в меню: /cancel</i>"
|
SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\n<i>Вернуться в меню: /cancel</i>"
|
||||||
|
|
||||||
SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia:</b>"
|
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>"
|
||||||
@@ -11,6 +11,7 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
bot_token: str = Field(alias="BOT_TOKEN")
|
bot_token: str = Field(alias="BOT_TOKEN")
|
||||||
|
admins: list[int] = Field(alias="ADMINS")
|
||||||
|
|
||||||
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
||||||
pally_token: str = Field(alias="PALLY_TOKEN")
|
pally_token: str = Field(alias="PALLY_TOKEN")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
BOT_TOKEN: ${BOT_TOKEN}
|
BOT_TOKEN: ${BOT_TOKEN}
|
||||||
|
ADMINS: ${ADMINS}
|
||||||
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db}
|
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db}
|
||||||
PROXY: ${PROXY:-}
|
PROXY: ${PROXY:-}
|
||||||
REMNAWAVE_URL: ${REMNAWAVE_URL}
|
REMNAWAVE_URL: ${REMNAWAVE_URL}
|
||||||
@@ -53,6 +54,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
BOT_TOKEN: ${BOT_TOKEN}
|
BOT_TOKEN: ${BOT_TOKEN}
|
||||||
|
ADMINS: ${ADMINS}
|
||||||
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db}
|
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db}
|
||||||
PROXY: ${PROXY:-}
|
PROXY: ${PROXY:-}
|
||||||
REMNAWAVE_URL: ${REMNAWAVE_URL}
|
REMNAWAVE_URL: ${REMNAWAVE_URL}
|
||||||
|
|||||||
@@ -41,11 +41,33 @@ QUOTES: list[str] = [
|
|||||||
"Весь мир на экране. Без лишних вопросов.",
|
"Весь мир на экране. Без лишних вопросов.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ADM_QUOTES: list[str] = [
|
||||||
|
"Они строят стены. Мы пишем для них двери.",
|
||||||
|
"За каждым коммитом — чьё-то право на выбор.",
|
||||||
|
"Цензура — это баг. И мы выкатили патч.",
|
||||||
|
"Обычный день в админке. Глоток свободы для них.",
|
||||||
|
"Наш труд невидим. Наше влияние безгранично.",
|
||||||
|
"Мы не пишем код. Мы держим сеть открытой.",
|
||||||
|
"Пульт управления свободой. Добро пожаловать.",
|
||||||
|
"Malenia дышит. Благодаря тебе.",
|
||||||
|
"Пока они запрещают, мы масштабируем.",
|
||||||
|
"Тихая работа в бэкенде. Громкие результаты в сети.",
|
||||||
|
"Миллионы терабайт трафика. Одна общая цель.",
|
||||||
|
"Инструменты создают люди. Свободу создаешь ты.",
|
||||||
|
"Никакой магии. Только наш код и упорство.",
|
||||||
|
"Будущее интернета компилируется прямо здесь.",
|
||||||
|
"Для кого-то — интерфейс. Для нас — линия фронта.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def fetch_quote() -> str:
|
def fetch_quote() -> str:
|
||||||
return choice(QUOTES)
|
return choice(QUOTES)
|
||||||
|
|
||||||
|
|
||||||
|
def adm_fetch_quote() -> str:
|
||||||
|
return choice(ADM_QUOTES)
|
||||||
|
|
||||||
|
|
||||||
def format_bytes(num_bytes: float) -> str:
|
def format_bytes(num_bytes: float) -> str:
|
||||||
"""Конвертировать байты в читаемый формат: ГБ / МБ / КБ / Б."""
|
"""Конвертировать байты в читаемый формат: ГБ / МБ / КБ / Б."""
|
||||||
gb_capacity = 1_073_741_824
|
gb_capacity = 1_073_741_824
|
||||||
@@ -236,6 +258,12 @@ def build_main_menu_text(link: str, balance: int) -> str:
|
|||||||
return texts.MAIN_MENU.format(quote=fetch_quote(), link=link, balance=balance)
|
return texts.MAIN_MENU.format(quote=fetch_quote(), link=link, balance=balance)
|
||||||
|
|
||||||
|
|
||||||
|
def build_adm_main_menu_text(user_count: int, user_id: int):
|
||||||
|
return texts.ADM_MAIN_MENU.format(
|
||||||
|
quote=adm_fetch_quote(), user_count=user_count, user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_same_plan(rw_user: RWUserInfo, new_plan: SubscriptionPlan) -> bool:
|
def is_same_plan(rw_user: RWUserInfo, new_plan: SubscriptionPlan) -> bool:
|
||||||
if rw_user.hwid_device_limit != new_plan.devices:
|
if rw_user.hwid_device_limit != new_plan.devices:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import datetime
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.sql.functions import count
|
||||||
|
|
||||||
from db.models import User
|
from db.models import User
|
||||||
from db.models.transactions import BalanceTransaction, BalanceTxType
|
from db.models.transactions import BalanceTransaction, BalanceTxType
|
||||||
@@ -121,3 +122,9 @@ class UserRepository:
|
|||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
async def user_count(self, session: AsyncSession) -> int:
|
||||||
|
stmt = select(count()).select_from(User)
|
||||||
|
res = await session.execute(stmt)
|
||||||
|
|
||||||
|
return int(res.scalar_one_or_none())
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ from aiogram.types import Message
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NewPromotionContext:
|
class NewPromotionContext:
|
||||||
creator_id: int
|
msg: Message
|
||||||
message: Message | None = None
|
promotion: Message | None = None
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from repositories.subscriptions import SubscriptionRepository
|
|||||||
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
|
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
|
||||||
from schemas.users import NewUserDTO
|
from schemas.users import NewUserDTO
|
||||||
from services import rw
|
from services import rw
|
||||||
|
from config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -220,3 +221,22 @@ class UserService:
|
|||||||
|
|
||||||
remaining_days = max(0, (rw_user.expire_at - datetime.datetime.now(datetime.UTC)).days)
|
remaining_days = max(0, (rw_user.expire_at - datetime.datetime.now(datetime.UTC)).days)
|
||||||
return math.floor(db_sub.plan_price_paid * remaining_days / db_sub.duration_days)
|
return math.floor(db_sub.plan_price_paid * remaining_days / db_sub.duration_days)
|
||||||
|
|
||||||
|
async def launch_promotion(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
promo_msg: Message
|
||||||
|
) -> int:
|
||||||
|
users = await self.user_repository.get_users(session)
|
||||||
|
|
||||||
|
sent = 0
|
||||||
|
for user in users:
|
||||||
|
if user.id in settings.admins:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await promo_msg.copy_to(user.id)
|
||||||
|
sent += 1
|
||||||
|
except Exception:
|
||||||
|
logger.exception("caught an exception while launching promo, skipping user...")
|
||||||
|
|
||||||
|
return sent
|
||||||
Reference in New Issue
Block a user