diff --git a/database/models.py b/database/models.py index a386effd..e35dd1d9 100644 --- a/database/models.py +++ b/database/models.py @@ -111,6 +111,7 @@ class Server(DictLikeMixin, Base): enabled = Column(Boolean, default=True) subgroups = relationship("ServerSubgroup", back_populates="server", cascade="all, delete-orphan") + groups = relationship("ServerSpecialgroup", back_populates="server", cascade="all, delete-orphan") class ServerSubgroup(DictLikeMixin, Base): @@ -126,6 +127,18 @@ class ServerSubgroup(DictLikeMixin, Base): __table_args__ = (UniqueConstraint("server_id", "subgroup_title", name="uq_server_subgroup"),) +class ServerSpecialgroup(DictLikeMixin, Base): + __tablename__ = "server_specialgroups" + + id = Column(Integer, primary_key=True, autoincrement=True) + server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), index=True, nullable=False) + group_code = Column(String, nullable=False) + + server = relationship("Server") + + __table_args__ = (UniqueConstraint("server_id", "group_code", name="uq_server_group"),) + + class Payment(DictLikeMixin, Base): __tablename__ = "payments" diff --git a/database/servers.py b/database/servers.py index a9a296c1..792d9680 100644 --- a/database/servers.py +++ b/database/servers.py @@ -2,7 +2,7 @@ from sqlalchemy import delete, func, insert, select, update from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession -from database.models import Key, Server, ServerSubgroup, Tariff +from database.models import Key, Server, ServerSpecialgroup, ServerSubgroup, Tariff from logger import logger @@ -58,11 +58,24 @@ async def get_servers(session: AsyncSession, include_enabled: bool = False) -> d for sid, sg in r.all(): subs_map.setdefault(sid, []).append(sg) + groups_map = {} + if ids: + r2 = await session.execute( + select(ServerSpecialgroup.server_id, ServerSpecialgroup.group_code).where( + ServerSpecialgroup.server_id.in_(ids) + ) + ) + for sid, gc in r2.all(): + groups_map.setdefault(sid, []).append(gc) + + allowed = {"trial", "discounts", "discounts_max"} + grouped = {} for s in servers: if not include_enabled and not s.enabled: continue cluster = s.cluster_name + special = sorted({g for g in groups_map.get(s.id, []) if g in allowed}) grouped.setdefault(cluster, []).append({ "server_name": s.server_name, "api_url": s.api_url, @@ -73,6 +86,7 @@ async def get_servers(session: AsyncSession, include_enabled: bool = False) -> d "max_keys": s.max_keys, "tariff_group": s.tariff_group, "tariff_subgroups": subs_map.get(s.id, []), + "special_groups": special, "cluster_name": cluster, }) return grouped diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index ad14610a..7b79d701 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -8,7 +8,7 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from py3xui import AsyncApi -from sqlalchemy import delete, func, select, update +from sqlalchemy import and_, delete, func, select, update from sqlalchemy.ext.asyncio import AsyncSession from config import ( @@ -19,7 +19,7 @@ from config import ( USE_COUNTRY_SELECTION, ) from database import check_unique_server_name, get_servers, update_key_expiry -from database.models import Key, Server, ServerSubgroup, Tariff +from database.models import Key, Server, ServerSpecialgroup, ServerSubgroup, Tariff from filters.admin import IsAdminFilter from handlers.keys.operations import ( create_client_on_server, @@ -27,6 +27,7 @@ from handlers.keys.operations import ( delete_key_from_cluster, renew_key_in_cluster, ) +from handlers.utils import ALLOWED_GROUP_CODES from logger import logger from panels.remnawave import RemnawaveAPI from utils.backup import create_backup_and_send_to_admins @@ -35,12 +36,15 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import ( AdminClusterCallback, AdminServerCallback, + build_attach_tariff_kb, build_cluster_management_kb, build_clusters_editor_kb, build_manage_cluster_kb, build_panel_type_kb, + build_select_group_servers_kb, build_select_subgroup_servers_kb, build_sync_cluster_kb, + build_tariff_group_selection_for_servers_kb, build_tariff_group_selection_kb, build_tariff_subgroup_selection_kb, ) @@ -322,16 +326,24 @@ async def handle_cluster_servers(callback: CallbackQuery, session: AsyncSession) servers = await get_servers(session=session, include_enabled=True) cluster_servers = servers.get(cluster_name, []) + allowed = {"trial", "discounts", "discounts_max"} lines = [] for s in cluster_servers: subs = s.get("tariff_subgroups") or [] subs_str = ", ".join(sorted(subs)) if subs else "—" - lines.append(f"• {s.get('server_name', '?')} — {subs_str}") + + grps = s.get("special_groups") or [] + grps = [g for g in grps if g in allowed] + grps_str = ", ".join(sorted(grps)) if grps else "—" + + lines.append(f"• {s.get('server_name', '?')} — {subs_str} | {grps_str}") details = "\n".join(lines) if lines else "нет серверов" await callback.message.edit_text( - text=(f"📡 Серверы в кластере {cluster_name}\nподгруппы:\n
{details}"), + text=( + f"📡 Серверы в кластере {cluster_name}\nподгруппы | спецгруппы:\n
{details}" + ), reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), ) @@ -1309,3 +1321,211 @@ async def reset_cluster_subgroups(callback: CallbackQuery, callback_data: AdminC except Exception as e: logger.error(f"Ошибка при сбросе подгрупп для кластера {cluster_name}: {e}") await callback.message.edit_text("❌ Не удалось сбросить подгруппы.") + + +def render_attach_tariff_menu_text(cluster_name: str, cluster_servers: list[dict]) -> str: + sub_map: dict[str, list[str]] = {} + for s in cluster_servers: + for sg in s.get("tariff_subgroups") or []: + sub_map.setdefault(sg, []).append(s["server_name"]) + + allowed = ("trial", "discounts", "discounts_max") + spec_map: dict[str, list[str]] = {k: [] for k in allowed} + for s in cluster_servers: + for g in s.get("special_groups") or []: + if g in spec_map: + spec_map[g].append(s["server_name"]) + + lines = [f"🧩 Привязки тарифов • {cluster_name}"] + + lines.append("Подгруппы:") + if sub_map: + subs_lines = [] + for k in sorted(sub_map): + servers_list = ", ".join(sorted(set(sub_map[k]))) + subs_lines.append(f"• {k}: {servers_list}") + lines.append("
\n" + "\n".join(subs_lines) + "\n") + else: + lines.append("
— нет привязок") + + lines.append("Спецгруппы:") + has_spec = any(spec_map[k] for k in allowed) + if has_spec: + spec_lines = [] + for k in allowed: + vals = sorted(set(spec_map[k])) + spec_lines.append(f"• {k}: {', '.join(vals) if vals else '—'}") + lines.append("
\n" + "\n".join(spec_lines) + "\n") + else: + lines.append("
— нет привязок") + + return "\n".join(lines) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "attach_tariff_menu"), IsAdminFilter()) +async def handle_attach_tariff_menu(callback: CallbackQuery, session: AsyncSession): + packed = AdminClusterCallback.unpack(callback.data) + cluster_name = packed.data + + servers = await get_servers(session, include_enabled=True) + cluster_servers = servers.get(cluster_name, []) + + text = render_attach_tariff_menu_text(cluster_name, cluster_servers) + await callback.message.edit_text( + text=text, + reply_markup=build_attach_tariff_kb(cluster_name), + disable_web_page_preview=True, + ) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "set_group")) +async def show_servers_for_group( + callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext +): + cluster_name = callback_data.data + servers = await get_servers(session=session, include_enabled=True) + cluster_servers = servers.get(cluster_name, []) + data = await state.get_data() + selected = set(data.get(f"grp_sel:{cluster_name}", [])) + await callback.message.edit_text( + f"🗂 Выберите серверы в кластере
{cluster_name} для назначения тарифной группы:",
+ reply_markup=build_select_group_servers_kb(cluster_name, cluster_servers, selected),
+ )
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "toggle_server_group"))
+async def toggle_server_for_group(
+ callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext
+):
+ cluster_name, idx_str = callback_data.data.split("|", 1)
+ i = int(idx_str)
+ servers = await get_servers(session=session, include_enabled=True)
+ cluster_servers = servers.get(cluster_name, [])
+ names = []
+ for s in cluster_servers:
+ if isinstance(s, str):
+ names.append(s)
+ elif isinstance(s, dict):
+ names.append(s.get("server_name") or s.get("name") or str(s))
+ else:
+ names.append(getattr(s, "server_name", None) or getattr(s, "name", None) or str(s))
+ if i < 0 or i >= len(names):
+ await callback.answer("Сервер не найден", show_alert=True)
+ return
+ server_name = names[i]
+ key = f"grp_sel:{cluster_name}"
+ data = await state.get_data()
+ selected = set(data.get(key, []))
+ if server_name in selected:
+ selected.remove(server_name)
+ else:
+ selected.add(server_name)
+ await state.update_data({key: list(selected)})
+ await callback.message.edit_text(
+ f"🗂 Выберите серверы в кластере {cluster_name} для назначения тарифной группы:",
+ reply_markup=build_select_group_servers_kb(cluster_name, cluster_servers, selected),
+ )
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "reset_group_selection"))
+async def reset_group_selection(
+ callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext
+):
+ cluster_name = callback_data.data
+ servers = await get_servers(session=session, include_enabled=True)
+ cluster_servers = servers.get(cluster_name, [])
+ await state.update_data({f"grp_sel:{cluster_name}": []})
+ await callback.message.edit_text(
+ f"🗂 Выберите серверы в кластере {cluster_name} для назначения тарифной группы:",
+ reply_markup=build_select_group_servers_kb(cluster_name, cluster_servers, set()),
+ )
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "choose_group"))
+async def choose_group(
+ callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext
+):
+ cluster_name = callback_data.data
+ key = f"grp_sel:{cluster_name}"
+ data = await state.get_data()
+ selected = set(data.get(key, []))
+ if not selected:
+ await callback.answer("Сначала выберите хотя бы один сервер", show_alert=True)
+ return
+ groups = [(i, code) for i, code in enumerate(ALLOWED_GROUP_CODES)]
+ await callback.message.edit_text(
+ f"📚 Выберите группу для {len(selected)} сервер(а/ов) кластера {cluster_name}:",
+ reply_markup=build_tariff_group_selection_for_servers_kb(cluster_name, groups),
+ )
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "apply_group_to_servers"))
+async def apply_group_to_servers(
+ callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession, state: FSMContext
+):
+ try:
+ cluster_name, idx_str = callback_data.data.split("|", 1)
+ i = int(idx_str)
+ groups = ALLOWED_GROUP_CODES
+ if i < 0 or i >= len(groups):
+ await callback.answer("Группа не найдена", show_alert=True)
+ return
+ group_code = groups[i]
+
+ key = f"grp_sel:{cluster_name}"
+ data = await state.get_data()
+ selected = set(data.get(key, []))
+ if not selected:
+ await callback.message.edit_text("❌ Не выбраны серверы для назначения группы.")
+ return
+
+ rows = await session.execute(select(Server.id, Server.server_name).where(Server.server_name.in_(selected)))
+ id_by_name = {name: sid for sid, name in rows.fetchall()}
+ server_ids = [id_by_name[n] for n in selected if n in id_by_name]
+ if not server_ids:
+ await callback.answer("Серверы не найдены", show_alert=True)
+ return
+
+ exist_rows = await session.execute(
+ select(ServerSpecialgroup.server_id).where(
+ and_(ServerSpecialgroup.server_id.in_(server_ids), ServerSpecialgroup.group_code == group_code)
+ )
+ )
+ already = {r[0] for r in exist_rows.fetchall()}
+ to_insert = [sid for sid in server_ids if sid not in already]
+
+ if to_insert:
+ session.add_all([ServerSpecialgroup(server_id=sid, group_code=group_code) for sid in to_insert])
+ await session.commit()
+
+ await state.update_data({key: []})
+ applied = ", ".join(sorted(selected))
+ await callback.message.edit_text(
+ f"✅ Группа {group_code} назначена серверам:\n{applied}", + reply_markup=build_cluster_management_kb(cluster_name), + ) + except Exception as e: + logger.error(f"Ошибка при назначении группы тарифов: {e}") + await callback.message.edit_text("❌ Произошла ошибка при назначении группы.") + + +@router.callback_query(AdminClusterCallback.filter(F.action == "reset_cluster_groups")) +async def reset_cluster_groups(callback: CallbackQuery, callback_data: AdminClusterCallback, session: AsyncSession): + try: + cluster_name = callback_data.data + res = await session.execute(select(Server.id).where(Server.cluster_name == cluster_name)) + server_ids = [row[0] for row in res.fetchall()] + if not server_ids: + await callback.answer("В кластере нет серверов", show_alert=True) + return + await session.execute(delete(ServerSpecialgroup).where(ServerSpecialgroup.server_id.in_(server_ids))) + await session.commit() + servers = await get_servers(session=session, include_enabled=True) + cluster_servers = servers.get(cluster_name, []) + await callback.message.edit_text( + f"✅ Все привязки групп сброшены для кластера {cluster_name}.", + reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), + ) + except Exception as e: + logger.error(f"Ошибка при сбросе групп для кластера {cluster_name}: {e}") + await callback.message.edit_text("❌ Не удалось сбросить привязки групп.") diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index b7262940..af83c268 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -57,21 +57,12 @@ def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineK callback_data=AdminServerCallback(action="add", data=cluster_name).pack(), ) ) - builder.row( InlineKeyboardButton( - text="🗂 Выбрать подгруппу тарифов", - callback_data=AdminClusterCallback(action="set_subgroup", data=cluster_name).pack(), + text="💸 Тариф(Установить/изменить)", + callback_data=AdminClusterCallback(action="attach_tariff_menu", data=cluster_name).pack(), ) ) - - builder.row( - InlineKeyboardButton( - text="🧹 Сбросить все подгруппы", - callback_data=AdminClusterCallback(action="reset_cluster_subgroups", data=cluster_name).pack(), - ) - ) - builder.row( InlineKeyboardButton( text="🔙 Назад", @@ -82,6 +73,37 @@ def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineK return builder.as_markup() +def build_attach_tariff_kb(cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🗂 Спецгруппы", + callback_data=AdminClusterCallback(action="set_group", data=cluster_name).pack(), + ), + InlineKeyboardButton( + text="🧹 Сбросить", + callback_data=AdminClusterCallback(action="reset_cluster_groups", data=cluster_name).pack(), + ), + ) + builder.row( + InlineKeyboardButton( + text="🗂 Подгруппы", + callback_data=AdminClusterCallback(action="set_subgroup", data=cluster_name).pack(), + ), + InlineKeyboardButton( + text="🧹 Сбросить", + callback_data=AdminClusterCallback(action="reset_cluster_subgroups", data=cluster_name).pack(), + ), + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), + ) + ) + return builder.as_markup() + + def build_select_subgroup_servers_kb( cluster_name: str, cluster_servers: list, selected: set[str] ) -> InlineKeyboardMarkup: @@ -242,3 +264,65 @@ def build_tariff_group_selection_kb(cluster_name: str, groups: list[tuple[int, s ) builder.adjust(2, 1) return builder.as_markup() + + +def build_select_group_servers_kb(cluster_name: str, cluster_servers: list, selected: set[str]) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + names = [] + for s in cluster_servers: + if isinstance(s, str): + names.append(s) + elif isinstance(s, dict): + names.append(s.get("server_name") or s.get("name") or str(s)) + else: + names.append(getattr(s, "server_name", None) or getattr(s, "name", None) or str(s)) + + for i, name in enumerate(names): + mark = "✅" if name in selected else "⬜️" + builder.row( + InlineKeyboardButton( + text=f"{mark} {name}", + callback_data=AdminClusterCallback(action="toggle_server_group", data=f"{cluster_name}|{i}").pack(), + ) + ) + + builder.row( + InlineKeyboardButton( + text="📚 Выбрать спецгруппу", + callback_data=AdminClusterCallback(action="choose_group", data=cluster_name).pack(), + ) + ) + builder.row( + InlineKeyboardButton( + text="♻️ Сбросить выбор", + callback_data=AdminClusterCallback(action="reset_group_selection", data=cluster_name).pack(), + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), + ) + ) + return builder.as_markup() + + +def build_tariff_group_selection_for_servers_kb( + cluster_name: str, groups: list[tuple[int, str]] +) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + for group_id, group_code in groups: + builder.button( + text=group_code, + callback_data=AdminClusterCallback( + action="apply_group_to_servers", data=f"{cluster_name}|{group_id}" + ).pack(), + ) + builder.row( + InlineKeyboardButton( + text="⬅️ Назад", + callback_data=AdminClusterCallback(action="set_group", data=cluster_name).pack(), + ) + ) + builder.adjust(2, 1) + return builder.as_markup() diff --git a/handlers/admin/module/module_handler.py b/handlers/admin/module/module_handler.py index acba56e8..203f1ad0 100644 --- a/handlers/admin/module/module_handler.py +++ b/handlers/admin/module/module_handler.py @@ -56,11 +56,13 @@ async def handle_modules(callback_query: CallbackQuery, state: FSMContext, sessi total_pages = max(1, (len(items) + per_page - 1) // per_page) page = min(page, total_pages) start = (page - 1) * per_page - chunk = items[start:start + per_page] + chunk = items[start : start + per_page] if chunk: + def fmt(n, v): return f"{n} v{v}" if v else n + lines = "\n".join(f"• {fmt(n, v)}" for n, v in chunk) text = f"🧩 Мои модули\n\nНайдено: {len(items)}\n
{lines}" else: diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 2cfdd0d3..d397d857 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -45,24 +45,24 @@ async def send_broadcast_batch(bot, messages, batch_size=15, session=None): try: if photo: - result = await bot.send_photo( + await bot.send_photo( chat_id=tg_id, photo=photo, caption=text, parse_mode="HTML", reply_markup=keyboard ) else: - result = await bot.send_message(chat_id=tg_id, text=text, parse_mode="HTML", reply_markup=keyboard) + await bot.send_message(chat_id=tg_id, text=text, parse_mode="HTML", reply_markup=keyboard) results.append(True) - + except TelegramRetryAfter as e: retry_in = int(e.retry_after) + 1 logger.warning(f"⚠️ Flood control: повтор через {retry_in} сек. для пользователя {tg_id}") await asyncio.sleep(e.retry_after) try: if photo: - result = await bot.send_photo( + await bot.send_photo( chat_id=tg_id, photo=photo, caption=text, parse_mode="HTML", reply_markup=keyboard ) else: - result = await bot.send_message(chat_id=tg_id, text=text, parse_mode="HTML", reply_markup=keyboard) + await bot.send_message(chat_id=tg_id, text=text, parse_mode="HTML", reply_markup=keyboard) results.append(True) except TelegramForbiddenError: logger.warning(f"🚫 Бот заблокирован пользователем {tg_id}.") @@ -79,7 +79,7 @@ async def send_broadcast_batch(bot, messages, batch_size=15, session=None): logger.error(f"❌ Ошибка повторной отправки пользователю {tg_id}: {retry_error}") await try_add_blocked_user(tg_id, session) results.append(False) - + except TelegramForbiddenError: logger.warning(f"🚫 Бот заблокирован пользователем {tg_id}.") await try_add_blocked_user(tg_id, session) @@ -147,9 +147,7 @@ async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str .where(~User.tg_id.in_(banned_tg_ids)) ) elif send_to == "hotleads": - subquery_active_keys = ( - select(Key.tg_id).where(Key.expiry_time > now_ms).distinct() - ) + subquery_active_keys = select(Key.tg_id).where(Key.expiry_time > now_ms).distinct() query = ( select(distinct(User.tg_id)) .join(Payment, User.tg_id == Payment.tg_id) diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 237538c7..65451a20 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -458,7 +458,7 @@ async def handle_balance_add( if amount is not None: amount = int(amount) old_balance = await get_balance(session, tg_id) - + if amount >= 0: await update_balance(session, tg_id, amount) new_balance = old_balance + amount diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index e74e1173..b40c8ec7 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -13,7 +13,7 @@ from sqlalchemy import or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from bot import bot -from config import DISCOUNT_ACTIVE_HOURS, USE_NEW_PAYMENT_FLOW, RENEW_BUTTON_BEFORE_DAYS +from config import DISCOUNT_ACTIVE_HOURS, RENEW_BUTTON_BEFORE_DAYS, USE_NEW_PAYMENT_FLOW from database import ( check_tariff_exists, get_balance, diff --git a/handlers/keys/operations/creation.py b/handlers/keys/operations/creation.py index 56e1b80e..d0539750 100644 --- a/handlers/keys/operations/creation.py +++ b/handlers/keys/operations/creation.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from config import HAPP_CRYPTOLINK, PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE from database import get_servers, get_tariff_by_id, store_key from database.models import User -from handlers.utils import check_server_key_limit +from handlers.utils import ALLOWED_GROUP_CODES, check_server_key_limit from logger import logger from panels._3xui import ( ClientConfig, @@ -73,6 +73,26 @@ async def create_key_on_cluster( subgroup_servers = [s for s in enabled_servers if subgroup_title in s.get("tariff_subgroups", [])] if subgroup_servers: enabled_servers = subgroup_servers + else: + logger.warning( + f"[Key Creation] В кластере {cluster_id} не найдено серверов для подгруппы '{subgroup_title}'. Использую весь кластер." + ) + + special = None + if is_trial: + special = "trial" + elif tariff: + gc = (tariff.get("group_code") or "").lower() + if gc in ALLOWED_GROUP_CODES: + special = gc + if special: + bound_servers = [s for s in enabled_servers if special in (s.get("special_groups") or [])] + if bound_servers: + enabled_servers = bound_servers + else: + logger.warning( + f"[Key Creation] В кластере {cluster_id} нет серверов со спецгруппой '{special}'. Использую весь кластер." + ) remnawave_servers = [ s diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index a624b4ea..7ba8de2b 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -9,16 +9,16 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from config import ( NOTIFICATION_TIME, + NOTIFY_10H_ENABLED, + NOTIFY_10H_HOURS, + NOTIFY_24H_ENABLED, + NOTIFY_24H_HOURS, NOTIFY_DELETE_DELAY, NOTIFY_DELETE_KEY, NOTIFY_HOT_LEADS, NOTIFY_INACTIVE_TRAFFIC, NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, - NOTIFY_24H_ENABLED, - NOTIFY_24H_HOURS, - NOTIFY_10H_ENABLED, - NOTIFY_10H_HOURS, TRIAL_TIME_DISABLE, ) from database import ( @@ -94,14 +94,18 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): if NOTIFY_24H_ENABLED: try: - threshold_24h = int((datetime.now(moscow_tz) + timedelta(hours=NOTIFY_24H_HOURS)).timestamp() * 1000) + threshold_24h = int( + (datetime.now(moscow_tz) + timedelta(hours=NOTIFY_24H_HOURS)).timestamp() * 1000 + ) await notify_24h_keys(bot, session, current_time, threshold_24h, keys) except Exception as e: logger.error(f"Ошибка в notify_24h_keys: {e}") if NOTIFY_10H_ENABLED: try: - threshold_10h = int((datetime.now(moscow_tz) + timedelta(hours=NOTIFY_10H_HOURS)).timestamp() * 1000) + threshold_10h = int( + (datetime.now(moscow_tz) + timedelta(hours=NOTIFY_10H_HOURS)).timestamp() * 1000 + ) await notify_10h_keys(bot, session, current_time, threshold_10h, keys) except Exception as e: logger.error(f"Ошибка в notify_10h_keys: {e}") @@ -165,7 +169,7 @@ async def notify_24h_keys( continue expiry_data = await prepare_key_expiry_data(key, session, current_time) - + notification_text = KEY_EXPIRY.format( email=email, hours_left_formatted=expiry_data["hours_left_formatted"], @@ -249,7 +253,7 @@ async def notify_10h_keys( continue expiry_data = await prepare_key_expiry_data(key, session, current_time) - + notification_text = KEY_EXPIRY.format( email=email, hours_left_formatted=expiry_data["hours_left_formatted"], @@ -486,15 +490,17 @@ async def process_auto_renew_or_notify( if not selected_tariff: expiry_data = await prepare_key_expiry_data(key, conn, int(datetime.now(moscow_tz).timestamp() * 1000)) - + use_change_tariff_kb = False - + if tariff_id and await check_tariff_exists(conn, tariff_id): current_tariff = await get_tariff_by_id(conn, tariff_id) if current_tariff: forbidden_groups = ["discounts", "discounts_max", "gifts", "trial"] try: - hook_results = await run_hooks("renewal_forbidden_groups", chat_id=tg_id, admin=False, session=conn) + hook_results = await run_hooks( + "renewal_forbidden_groups", chat_id=tg_id, admin=False, session=conn + ) for hook_result in hook_results: additional_groups = hook_result.get("additional_groups", []) forbidden_groups.extend(additional_groups) @@ -526,7 +532,7 @@ async def process_auto_renew_or_notify( keyboard = build_change_tariff_kb(email) else: keyboard = build_notification_kb(email) - + await add_notification(conn, tg_id, notification_id) await send_notification(bot, tg_id, standard_photo, message_text, keyboard) return diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py index c3ba929b..69f6b4b7 100644 --- a/handlers/notifications/notify_utils.py +++ b/handlers/notifications/notify_utils.py @@ -1,5 +1,6 @@ import asyncio import os + from datetime import datetime import aiofiles @@ -176,7 +177,7 @@ async def prepare_key_expiry_data(key, session: AsyncSession, current_time: int) tariff_name = "—" tariff_details = "" - + if getattr(key, "tariff_id", None): tariff = await get_tariff_by_id(session, key.tariff_id) if tariff: @@ -192,7 +193,7 @@ async def prepare_key_expiry_data(key, session: AsyncSession, current_time: int) lines.append(f"Трафик: {traffic_text}") lines.append(f"Устройств: {devices_text}") tariff_details = "\n" + "\n".join(lines) - + return { "hours_left_formatted": hours_left_formatted, "formatted_expiry_date": formatted_expiry_date, diff --git a/handlers/utils.py b/handlers/utils.py index 5a3e25da..777793ff 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -25,6 +25,9 @@ from hooks.hooks import run_hooks from logger import logger +ALLOWED_GROUP_CODES = ["trial", "discounts", "discounts_max"] + + async def generate_random_email( length: int = 8, session: AsyncSession | None = None, diff --git a/middlewares/subscription.py b/middlewares/subscription.py index 914e6196..ca5eb619 100644 --- a/middlewares/subscription.py +++ b/middlewares/subscription.py @@ -37,7 +37,7 @@ class SubscriptionMiddleware(BaseMiddleware): return await handler(event, data) if event.message.from_user.is_bot: return await handler(event, data) - + tg_id = event.message.from_user.id message = event.message from_user = event.message.from_user @@ -48,7 +48,7 @@ class SubscriptionMiddleware(BaseMiddleware): return await handler(event, data) if event.callback_query.from_user.is_bot: return await handler(event, data) - + tg_id = event.callback_query.from_user.id message = event.callback_query.message from_user = event.callback_query.from_user