mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Новый фильтр Готовы к продлению
This commit is contained in:
@@ -172,6 +172,7 @@ class Settings(BaseSettings):
|
|||||||
DEFAULT_AUTOPAY_ENABLED: bool = False
|
DEFAULT_AUTOPAY_ENABLED: bool = False
|
||||||
DEFAULT_AUTOPAY_DAYS_BEFORE: int = 3
|
DEFAULT_AUTOPAY_DAYS_BEFORE: int = 3
|
||||||
MIN_BALANCE_FOR_AUTOPAY_KOPEKS: int = 10000
|
MIN_BALANCE_FOR_AUTOPAY_KOPEKS: int = 10000
|
||||||
|
SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS: int = 20000
|
||||||
|
|
||||||
MONITORING_INTERVAL: int = 60
|
MONITORING_INTERVAL: int = 60
|
||||||
INACTIVE_USER_DELETE_MONTHS: int = 3
|
INACTIVE_USER_DELETE_MONTHS: int = 3
|
||||||
|
|||||||
@@ -299,6 +299,137 @@ async def show_users_list_by_balance(
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
@error_handler
|
||||||
|
async def show_users_ready_to_renew(
|
||||||
|
callback: types.CallbackQuery,
|
||||||
|
db_user: User,
|
||||||
|
db: AsyncSession,
|
||||||
|
state: FSMContext,
|
||||||
|
page: int = 1
|
||||||
|
):
|
||||||
|
"""Показывает пользователей с истекшей подпиской и балансом >= порога."""
|
||||||
|
await state.set_state(AdminStates.viewing_user_from_ready_to_renew_list)
|
||||||
|
|
||||||
|
texts = get_texts(db_user.language)
|
||||||
|
threshold = getattr(
|
||||||
|
settings,
|
||||||
|
"SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS",
|
||||||
|
20000,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_service = UserService()
|
||||||
|
users_data = await user_service.get_users_ready_to_renew(
|
||||||
|
db,
|
||||||
|
min_balance_kopeks=threshold,
|
||||||
|
page=page,
|
||||||
|
limit=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
amount_text = settings.format_price(threshold)
|
||||||
|
header = texts.t(
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_TITLE",
|
||||||
|
"♻️ Пользователи готовы к продлению",
|
||||||
|
)
|
||||||
|
description = texts.t(
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_DESC",
|
||||||
|
"Подписка истекла, а на балансе осталось {amount} или больше.",
|
||||||
|
).format(amount=amount_text)
|
||||||
|
|
||||||
|
if not users_data["users"]:
|
||||||
|
empty_text = texts.t(
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY",
|
||||||
|
"Сейчас нет пользователей, которые подходят под этот фильтр.",
|
||||||
|
)
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"{header}\n\n{description}\n\n{empty_text}",
|
||||||
|
reply_markup=get_admin_users_keyboard(db_user.language),
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"{header}\n\n{description}\n\n"
|
||||||
|
text += "Нажмите на пользователя для управления:"
|
||||||
|
|
||||||
|
keyboard = []
|
||||||
|
current_time = datetime.utcnow()
|
||||||
|
|
||||||
|
for user in users_data["users"]:
|
||||||
|
subscription = user.subscription
|
||||||
|
status_emoji = "✅" if user.status == UserStatus.ACTIVE.value else "🚫"
|
||||||
|
subscription_emoji = "❌"
|
||||||
|
expired_days = "?"
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
if subscription.is_trial:
|
||||||
|
subscription_emoji = "🎁"
|
||||||
|
elif subscription.is_active:
|
||||||
|
subscription_emoji = "💎"
|
||||||
|
else:
|
||||||
|
subscription_emoji = "⏰"
|
||||||
|
|
||||||
|
if subscription.end_date:
|
||||||
|
delta = current_time - subscription.end_date
|
||||||
|
expired_days = delta.days
|
||||||
|
|
||||||
|
button_text = (
|
||||||
|
f"{status_emoji} {subscription_emoji} {user.full_name}"
|
||||||
|
f" | 💰 {settings.format_price(user.balance_kopeks)}"
|
||||||
|
f" | ⏰ {expired_days}д ист."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(button_text) > 60:
|
||||||
|
short_name = user.full_name
|
||||||
|
if len(short_name) > 20:
|
||||||
|
short_name = short_name[:17] + "..."
|
||||||
|
button_text = (
|
||||||
|
f"{status_emoji} {subscription_emoji} {short_name}"
|
||||||
|
f" | 💰 {settings.format_price(user.balance_kopeks)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.append([
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text=button_text,
|
||||||
|
callback_data=f"admin_user_manage_{user.id}",
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
if users_data["total_pages"] > 1:
|
||||||
|
pagination_row = get_admin_pagination_keyboard(
|
||||||
|
users_data["current_page"],
|
||||||
|
users_data["total_pages"],
|
||||||
|
"admin_users_ready_to_renew_list",
|
||||||
|
"admin_users_ready_to_renew_filter",
|
||||||
|
db_user.language,
|
||||||
|
).inline_keyboard[0]
|
||||||
|
keyboard.append(pagination_row)
|
||||||
|
|
||||||
|
keyboard.extend([
|
||||||
|
[
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="🔍 Поиск",
|
||||||
|
callback_data="admin_users_search",
|
||||||
|
),
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="📊 Статистика",
|
||||||
|
callback_data="admin_users_stats",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="⬅️ Назад",
|
||||||
|
callback_data="admin_users",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@admin_required
|
@admin_required
|
||||||
@error_handler
|
@error_handler
|
||||||
async def show_users_list_by_traffic(
|
async def show_users_list_by_traffic(
|
||||||
@@ -859,6 +990,22 @@ async def handle_users_purchases_list_pagination(
|
|||||||
await show_users_list_by_purchases(callback, db_user, db, state, 1)
|
await show_users_list_by_purchases(callback, db_user, db, state, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
@error_handler
|
||||||
|
async def handle_users_ready_to_renew_pagination(
|
||||||
|
callback: types.CallbackQuery,
|
||||||
|
db_user: User,
|
||||||
|
db: AsyncSession,
|
||||||
|
state: FSMContext
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
page = int(callback.data.split('_')[-1])
|
||||||
|
await show_users_ready_to_renew(callback, db_user, db, state, page)
|
||||||
|
except (ValueError, IndexError) as e:
|
||||||
|
logger.error(f"Ошибка парсинга номера страницы: {e}")
|
||||||
|
await show_users_ready_to_renew(callback, db_user, db, state, 1)
|
||||||
|
|
||||||
|
|
||||||
@admin_required
|
@admin_required
|
||||||
@error_handler
|
@error_handler
|
||||||
async def handle_users_campaign_list_pagination(
|
async def handle_users_campaign_list_pagination(
|
||||||
@@ -1502,6 +1649,8 @@ async def show_user_management(
|
|||||||
back_callback = "admin_users_purchases_filter"
|
back_callback = "admin_users_purchases_filter"
|
||||||
elif current_state == AdminStates.viewing_user_from_campaign_list:
|
elif current_state == AdminStates.viewing_user_from_campaign_list:
|
||||||
back_callback = "admin_users_campaign_filter"
|
back_callback = "admin_users_campaign_filter"
|
||||||
|
elif current_state == AdminStates.viewing_user_from_ready_to_renew_list:
|
||||||
|
back_callback = "admin_users_ready_to_renew_filter"
|
||||||
|
|
||||||
# Базовая клавиатура профиля
|
# Базовая клавиатура профиля
|
||||||
kb = get_user_management_keyboard(user.id, user.status, db_user.language, back_callback)
|
kb = get_user_management_keyboard(user.id, user.status, db_user.language, back_callback)
|
||||||
@@ -4860,6 +5009,11 @@ def register_handlers(dp: Dispatcher):
|
|||||||
F.data.startswith("admin_users_purchases_list_page_")
|
F.data.startswith("admin_users_purchases_list_page_")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dp.callback_query.register(
|
||||||
|
handle_users_ready_to_renew_pagination,
|
||||||
|
F.data.startswith("admin_users_ready_to_renew_list_page_")
|
||||||
|
)
|
||||||
|
|
||||||
dp.callback_query.register(
|
dp.callback_query.register(
|
||||||
handle_users_campaign_list_pagination,
|
handle_users_campaign_list_pagination,
|
||||||
F.data.startswith("admin_users_campaign_list_page_")
|
F.data.startswith("admin_users_campaign_list_page_")
|
||||||
@@ -5131,6 +5285,11 @@ def register_handlers(dp: Dispatcher):
|
|||||||
show_users_list_by_purchases,
|
show_users_list_by_purchases,
|
||||||
F.data == "admin_users_purchases_filter"
|
F.data == "admin_users_purchases_filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dp.callback_query.register(
|
||||||
|
show_users_ready_to_renew,
|
||||||
|
F.data == "admin_users_ready_to_renew_filter"
|
||||||
|
)
|
||||||
|
|
||||||
dp.callback_query.register(
|
dp.callback_query.register(
|
||||||
show_users_list_by_campaign,
|
show_users_list_by_campaign,
|
||||||
|
|||||||
@@ -372,6 +372,12 @@ def get_admin_users_filters_keyboard(language: str = "ru") -> InlineKeyboardMark
|
|||||||
callback_data="admin_users_purchases_filter"
|
callback_data="admin_users_purchases_filter"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=_t(texts, "ADMIN_USERS_FILTER_RENEW_READY", "♻️ Готовы к продлению"),
|
||||||
|
callback_data="admin_users_ready_to_renew_filter"
|
||||||
|
)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_t(texts, "ADMIN_USERS_FILTER_CAMPAIGN", "📢 По кампании"),
|
text=_t(texts, "ADMIN_USERS_FILTER_CAMPAIGN", "📢 По кампании"),
|
||||||
|
|||||||
@@ -711,6 +711,10 @@
|
|||||||
"ADMIN_USERS_FILTERS": "⚙️ Filters",
|
"ADMIN_USERS_FILTERS": "⚙️ Filters",
|
||||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 By activity",
|
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 By activity",
|
||||||
"ADMIN_USERS_FILTER_BALANCE": "💰 By balance",
|
"ADMIN_USERS_FILTER_BALANCE": "💰 By balance",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Ready to renew",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Users ready to renew",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Their subscription expired and the balance still has {amount} or more.",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "No users match this filter right now.",
|
||||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 By campaign",
|
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 By campaign",
|
||||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 By purchases",
|
"ADMIN_USERS_FILTER_PURCHASES": "🛒 By purchases",
|
||||||
"ADMIN_USERS_FILTER_SPENDING": "💳 By spending",
|
"ADMIN_USERS_FILTER_SPENDING": "💳 By spending",
|
||||||
|
|||||||
@@ -712,6 +712,10 @@
|
|||||||
"ADMIN_USERS_FILTERS": "⚙️ Фильтры",
|
"ADMIN_USERS_FILTERS": "⚙️ Фильтры",
|
||||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 По активности",
|
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 По активности",
|
||||||
"ADMIN_USERS_FILTER_BALANCE": "💰 По балансу",
|
"ADMIN_USERS_FILTER_BALANCE": "💰 По балансу",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Готовы к продлению",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Пользователи готовы к продлению",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Подписка истекла, а на балансе осталось {amount} или больше.",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "Сейчас нет пользователей, которые подходят под этот фильтр.",
|
||||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 По кампании",
|
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 По кампании",
|
||||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 По количеству покупок",
|
"ADMIN_USERS_FILTER_PURCHASES": "🛒 По количеству покупок",
|
||||||
"ADMIN_USERS_FILTER_SPENDING": "💳 По сумме трат",
|
"ADMIN_USERS_FILTER_SPENDING": "💳 По сумме трат",
|
||||||
|
|||||||
@@ -710,8 +710,12 @@
|
|||||||
"ADMIN_USERS_ALL": "👥 Всі користувачі",
|
"ADMIN_USERS_ALL": "👥 Всі користувачі",
|
||||||
"ADMIN_USERS_FILTERS": "⚙️ Фільтри",
|
"ADMIN_USERS_FILTERS": "⚙️ Фільтри",
|
||||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 За активністю",
|
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 За активністю",
|
||||||
"ADMIN_USERS_FILTER_BALANCE": "💰 За балансом",
|
"ADMIN_USERS_FILTER_BALANCE": "💰 За балансом",
|
||||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 За кампанією",
|
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Готові до продовження",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Користувачі, готові до продовження",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Підписка вже закінчилась, а на балансі залишилось {amount} або більше.",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "Наразі немає користувачів, які підходять під цей фільтр.",
|
||||||
|
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 За кампанією",
|
||||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 За кількістю покупок",
|
"ADMIN_USERS_FILTER_PURCHASES": "🛒 За кількістю покупок",
|
||||||
"ADMIN_USERS_FILTER_SPENDING": "💳 За сумою витрат",
|
"ADMIN_USERS_FILTER_SPENDING": "💳 За сумою витрат",
|
||||||
"ADMIN_USERS_FILTER_TRAFFIC": "📶 За трафіком",
|
"ADMIN_USERS_FILTER_TRAFFIC": "📶 За трафіком",
|
||||||
@@ -1532,4 +1536,4 @@
|
|||||||
"POLL_ERROR": "Не вдалося обробити опитування. Спробуйте пізніше.",
|
"POLL_ERROR": "Не вдалося обробити опитування. Спробуйте пізніше.",
|
||||||
"POLL_COMPLETED": "🙏 Дякуємо за участь в опитуванні!",
|
"POLL_COMPLETED": "🙏 Дякуємо за участь в опитуванні!",
|
||||||
"POLL_REWARD_GRANTED": "Нагороду {amount} зараховано на ваш баланс."
|
"POLL_REWARD_GRANTED": "Нагороду {amount} зараховано на ваш баланс."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -710,6 +710,10 @@
|
|||||||
"ADMIN_USERS_FILTERS":"⚙️筛选器",
|
"ADMIN_USERS_FILTERS":"⚙️筛选器",
|
||||||
"ADMIN_USERS_FILTER_ACTIVITY":"🕒按活跃度",
|
"ADMIN_USERS_FILTER_ACTIVITY":"🕒按活跃度",
|
||||||
"ADMIN_USERS_FILTER_BALANCE":"💰按余额",
|
"ADMIN_USERS_FILTER_BALANCE":"💰按余额",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY":"♻️准备续费",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_TITLE":"♻️准备续费的用户",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_DESC":"订阅已到期,但余额仍不少于{amount}。",
|
||||||
|
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY":"目前没有符合条件的用户。",
|
||||||
"ADMIN_USERS_FILTER_CAMPAIGN":"📢按活动",
|
"ADMIN_USERS_FILTER_CAMPAIGN":"📢按活动",
|
||||||
"ADMIN_USERS_FILTER_PURCHASES":"🛒按购买次数",
|
"ADMIN_USERS_FILTER_PURCHASES":"🛒按购买次数",
|
||||||
"ADMIN_USERS_FILTER_SPENDING":"💳按消费金额",
|
"ADMIN_USERS_FILTER_SPENDING":"💳按消费金额",
|
||||||
@@ -1860,4 +1864,4 @@
|
|||||||
"POLL_REWARD_GRANTED":"奖励{amount}已存入您的余额。",
|
"POLL_REWARD_GRANTED":"奖励{amount}已存入您的余额。",
|
||||||
"DEVICE_GUIDE_WINDOWS":"💻Windows",
|
"DEVICE_GUIDE_WINDOWS":"💻Windows",
|
||||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO":"🕐活跃:很久以前"
|
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO":"🕐活跃:很久以前"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Optional, List, Dict, Any, Tuple
|
from typing import Optional, List, Dict, Any, Tuple
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import delete, select, update, func
|
from sqlalchemy import delete, select, update, func
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
from aiogram import Bot, types
|
from aiogram import Bot, types
|
||||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||||
from app.database.crud.user import (
|
from app.database.crud.user import (
|
||||||
@@ -218,6 +219,60 @@ class UserService:
|
|||||||
"has_prev": False
|
"has_prev": False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def get_users_ready_to_renew(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
min_balance_kopeks: int,
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 20,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Возвращает пользователей с истекшей подпиской и достаточным балансом."""
|
||||||
|
try:
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
base_filters = [
|
||||||
|
User.balance_kopeks >= min_balance_kopeks,
|
||||||
|
Subscription.end_date.isnot(None),
|
||||||
|
Subscription.end_date <= now,
|
||||||
|
]
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(User)
|
||||||
|
.options(selectinload(User.subscription))
|
||||||
|
.join(Subscription, Subscription.user_id == User.id)
|
||||||
|
.where(*base_filters)
|
||||||
|
.order_by(User.balance_kopeks.desc(), Subscription.end_date.asc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
result = await db.execute(query)
|
||||||
|
users = result.scalars().all()
|
||||||
|
|
||||||
|
count_query = (
|
||||||
|
select(func.count(User.id))
|
||||||
|
.join(Subscription, Subscription.user_id == User.id)
|
||||||
|
.where(*base_filters)
|
||||||
|
)
|
||||||
|
total_count = (await db.execute(count_query)).scalar() or 0
|
||||||
|
total_pages = (total_count + limit - 1) // limit if total_count else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"users": users,
|
||||||
|
"current_page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"total_count": total_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения пользователей для продления: {e}")
|
||||||
|
return {
|
||||||
|
"users": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"total_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
async def get_user_spending_stats_map(
|
async def get_user_spending_stats_map(
|
||||||
self,
|
self,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class AdminStates(StatesGroup):
|
|||||||
viewing_user_from_spending_list = State()
|
viewing_user_from_spending_list = State()
|
||||||
viewing_user_from_purchases_list = State()
|
viewing_user_from_purchases_list = State()
|
||||||
viewing_user_from_campaign_list = State()
|
viewing_user_from_campaign_list = State()
|
||||||
|
viewing_user_from_ready_to_renew_list = State()
|
||||||
|
|
||||||
class SupportStates(StatesGroup):
|
class SupportStates(StatesGroup):
|
||||||
waiting_for_message = State()
|
waiting_for_message = State()
|
||||||
|
|||||||
Reference in New Issue
Block a user