Revert "Resolve duplicate handlers and cleanup imports"

This commit is contained in:
Egor
2025-09-30 21:07:37 +03:00
committed by GitHub
parent 43e63f864e
commit 46eb93dabe
12 changed files with 217 additions and 34 deletions

View File

@@ -571,7 +571,8 @@ class Subscription(Base):
return 0.0
def extend_subscription(self, days: int):
from datetime import timedelta, datetime
if self.end_date > datetime.utcnow():
self.end_date = self.end_date + timedelta(days=days)
else:

View File

@@ -4,6 +4,7 @@ import ssl
import base64
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union, Any
from urllib.parse import urlparse
import aiohttp
import logging
from dataclasses import dataclass

View File

@@ -128,6 +128,7 @@ async def show_support_submenu(
# Moderators have access only to tickets and not to settings
is_moderator_only = (not settings.is_admin(callback.from_user.id) and SupportSettingsService.is_moderator(callback.from_user.id))
from app.keyboards.admin import get_admin_support_submenu_keyboard
kb = get_admin_support_submenu_keyboard(db_user.language)
if is_moderator_only:
# Rebuild keyboard to include only tickets and back to main menu

View File

@@ -4,7 +4,6 @@ import html
import contextlib
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
@@ -153,6 +152,24 @@ async def toggle_sla(callback: types.CallbackQuery, db_user: User, db: AsyncSess
await show_support_settings(callback, db_user, db)
from app.states import SupportSettingsStates
@admin_required
@error_handler
async def start_set_sla_minutes(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
await callback.message.edit_text(
"⏳ <b>Настройка SLA</b>\n\nВведите количество минут ожидания ответа (целое число > 0):",
parse_mode="HTML",
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_support_settings")]]
)
)
await state.set_state(SupportSettingsStates.waiting_for_desc) # temporary reuse replaced below
# we'll manage separate state below
from aiogram.fsm.state import State, StatesGroup
class SupportAdvancedStates(StatesGroup):
waiting_for_sla_minutes = State()
waiting_for_moderator_id = State()

View File

@@ -3,6 +3,7 @@ from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.states import AdminStates
from app.keyboards.admin import get_welcome_text_keyboard, get_admin_main_keyboard
@@ -43,16 +44,16 @@ async def show_welcome_text_panel(
db_user: User,
db: AsyncSession
):
welcome_settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
status_text = "включено" if settings['is_enabled'] else "отключено"
await callback.message.edit_text(
f"👋 Управление приветственным текстом\n\n"
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
f"Здесь вы можете управлять текстом, который показывается новым пользователям после регистрации.\n\n"
f"💡 Доступные плейсхолдеры для автозамены:",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
await callback.answer()
@@ -88,11 +89,11 @@ async def show_current_welcome_text(
db_user: User,
db: AsyncSession
):
welcome_settings = await get_current_welcome_text_settings(db)
current_text = welcome_settings['text']
is_enabled = welcome_settings['is_enabled']
if not welcome_settings['id']:
settings = await get_current_welcome_text_settings(db)
current_text = settings['text']
is_enabled = settings['is_enabled']
if not settings['id']:
status = "📝 Используется стандартный текст:"
else:
status = "📝 Текущий приветственный текст:"
@@ -120,7 +121,7 @@ async def show_placeholders_help(
db_user: User,
db: AsyncSession
):
welcome_settings = await get_current_welcome_text_settings(db)
settings = await get_current_welcome_text_settings(db)
placeholders = get_available_placeholders()
placeholders_text = "\n".join([f"• <code>{key}</code>\n {desc}" for key, desc in placeholders.items()])
@@ -136,7 +137,7 @@ async def show_placeholders_help(
await callback.message.edit_text(
help_text,
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
await callback.answer()
@@ -148,12 +149,12 @@ async def show_formatting_help(
db_user: User,
db: AsyncSession
):
welcome_settings = await get_current_welcome_text_settings(db)
settings = await get_current_welcome_text_settings(db)
formatting_info = get_telegram_formatting_info()
await callback.message.edit_text(
formatting_info,
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
await callback.answer()
@@ -166,8 +167,8 @@ async def start_edit_welcome_text(
db_user: User,
db: AsyncSession
):
welcome_settings = await get_current_welcome_text_settings(db)
current_text = welcome_settings['text']
settings = await get_current_welcome_text_settings(db)
current_text = settings['text']
placeholders = get_available_placeholders()
placeholders_text = "\n".join([f"• <code>{key}</code> - {desc}" for key, desc in placeholders.items()])
@@ -205,9 +206,9 @@ async def process_welcome_text_edit(
success = await set_welcome_text(db, new_text, db_user.id)
if success:
welcome_settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
status_text = "включено" if settings['is_enabled'] else "отключено"
placeholders = get_available_placeholders()
placeholders_text = "\n".join([f"• <code>{key}</code>" for key in placeholders.keys()])
@@ -218,14 +219,14 @@ async def process_welcome_text_edit(
f"Новый текст:\n"
f"<code>{new_text}</code>\n\n"
f"💡 Будут заменяться плейсхолдеры: {placeholders_text}",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
else:
welcome_settings = await get_current_welcome_text_settings(db)
settings = await get_current_welcome_text_settings(db)
await message.answer(
"❌ Ошибка при сохранении текста. Попробуйте еще раз.",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled'])
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled'])
)
await state.clear()
@@ -241,9 +242,9 @@ async def reset_welcome_text(
success = await set_welcome_text(db, default_text, db_user.id)
if success:
welcome_settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
settings = await get_current_welcome_text_settings(db)
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
status_text = "включено" if settings['is_enabled'] else "отключено"
await callback.message.edit_text(
f"✅ Приветственный текст сброшен на стандартный!\n\n"
@@ -251,14 +252,14 @@ async def reset_welcome_text(
f"Стандартный текст:\n"
f"<code>{default_text}</code>\n\n"
f"💡 Плейсхолдер <code>{{user_name}}</code> будет заменяться на имя пользователя",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
else:
welcome_settings = await get_current_welcome_text_settings(db)
settings = await get_current_welcome_text_settings(db)
await callback.message.edit_text(
"❌ Ошибка при сбросе текста. Попробуйте еще раз.",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled'])
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled'])
)
await callback.answer()
@@ -280,14 +281,14 @@ async def show_preview_welcome_text(
test_user = TestUser()
preview_text = await get_welcome_text_for_user(db, test_user)
welcome_settings = await get_current_welcome_text_settings(db)
settings = await get_current_welcome_text_settings(db)
if preview_text:
await callback.message.edit_text(
f"👁️ Предварительный просмотр\n\n"
f"Как будет выглядеть текст для пользователя 'Иван' (@test_user):\n\n"
f"<code>{preview_text}</code>",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)
else:
@@ -295,7 +296,7 @@ async def show_preview_welcome_text(
f"👁️ Предварительный просмотр\n\n"
f"🔴 Приветственные сообщения отключены.\n"
f"Новые пользователи не будут получать приветственный текст после регистрации.",
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
parse_mode="HTML"
)

View File

@@ -10,6 +10,7 @@ from app.database.crud.user import get_user_by_telegram_id, update_user
from app.keyboards.inline import get_main_menu_keyboard, get_language_selection_keyboard
from app.localization.texts import get_texts, get_rules
from app.database.models import User
from app.utils.user_utils import mark_user_as_had_paid_subscription
from app.database.crud.user_message import get_random_active_message
from app.services.subscription_checkout_service import (
has_subscription_checkout_draft,
@@ -64,6 +65,17 @@ async def show_main_menu(
await callback.answer()
async def mark_user_as_had_paid_subscription(
db: AsyncSession,
user: User
) -> None:
if not user.has_had_paid_subscription:
user.has_had_paid_subscription = True
user.updated_at = datetime.utcnow()
await db.commit()
logger.info(f"🎯 Пользователь {user.telegram_id} отмечен как имевший платную подписку")
async def show_service_rules(
callback: types.CallbackQuery,
db_user: User,

View File

@@ -146,6 +146,12 @@ async def _prepare_subscription_summary(
data: Dict[str, Any],
texts,
) -> Tuple[str, Dict[str, Any]]:
from app.utils.pricing_utils import (
calculate_months_from_days,
format_period_description,
validate_pricing_calculation,
apply_percentage_discount,
)
summary_data = dict(data)
countries = await _get_available_countries(db_user.promo_group_id)
@@ -1122,6 +1128,7 @@ async def return_to_saved_cart(
)
return
from app.utils.pricing_utils import calculate_months_from_days, format_period_description
countries = await _get_available_countries(db_user.promo_group_id)
selected_countries_names = []
@@ -1326,6 +1333,7 @@ async def apply_countries_changes(
db: AsyncSession,
state: FSMContext
):
from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price
logger.info(f"🔧 Применение изменений стран")
@@ -1626,6 +1634,7 @@ async def confirm_change_devices(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price
new_devices_count = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
@@ -1739,6 +1748,7 @@ async def execute_change_devices(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price
callback_parts = callback.data.split('_')
new_devices_count = int(callback_parts[3])
@@ -1869,6 +1879,7 @@ async def show_devices_page(
page: int = 1
):
from app.utils.pagination import paginate_list
texts = get_texts(db_user.language)
devices_per_page = 5
@@ -1967,6 +1978,7 @@ async def handle_single_device_reset(
if response and 'response' in response:
devices_list = response['response'].get('devices', [])
from app.utils.pagination import paginate_list
devices_per_page = 5
pagination = paginate_list(devices_list, page=page, per_page=devices_per_page)
@@ -2272,6 +2284,7 @@ async def confirm_add_devices(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price
devices_count = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
@@ -2407,6 +2420,11 @@ async def confirm_extend_subscription(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import (
calculate_months_from_days,
validate_pricing_calculation,
apply_percentage_discount,
)
from app.services.admin_notification_service import AdminNotificationService
days = int(callback.data.split('_')[2])
@@ -3016,6 +3034,7 @@ async def select_country(
return
period_base_price = PERIOD_PRICES[data['period_days']]
from app.utils.pricing_utils import apply_percentage_discount
discounted_base_price, _ = apply_percentage_discount(
period_base_price,
@@ -3151,6 +3170,7 @@ async def confirm_purchase(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import calculate_months_from_days, validate_pricing_calculation
from app.services.admin_notification_service import AdminNotificationService
data = await state.get_data()
@@ -3870,6 +3890,7 @@ async def create_paid_subscription_with_traffic_mode(
traffic_gb: Optional[int] = None
):
from app.config import settings
from app.database.crud.subscription import create_paid_subscription
if traffic_gb is None:
if settings.is_traffic_fixed():
@@ -5330,6 +5351,7 @@ async def confirm_switch_traffic(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price
new_traffic_gb = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
@@ -5443,6 +5465,7 @@ async def execute_switch_traffic(
db_user: User,
db: AsyncSession
):
from app.utils.pricing_utils import get_remaining_months
callback_parts = callback.data.split('_')
new_traffic_gb = int(callback_parts[3])
@@ -5527,6 +5550,7 @@ def get_traffic_switch_keyboard(
subscription_end_date: datetime = None,
discount_percent: int = 0,
) -> InlineKeyboardMarkup:
from app.utils.pricing_utils import get_remaining_months
from app.config import settings
months_multiplier = 1

View File

@@ -412,6 +412,7 @@ async def show_my_tickets(
# Добавим кнопку перехода к закрытым
keyboard.inline_keyboard.insert(0, [types.InlineKeyboardButton(text=texts.t("VIEW_CLOSED_TICKETS", "🟢 Закрытые тикеты"), callback_data="my_tickets_closed")])
# Всегда используем фото-рендер с логотипом (утилита сама сделает фоллбек при необходимости)
from app.utils.photo_message import edit_or_answer_photo
await edit_or_answer_photo(
callback=callback,
caption=texts.t("MY_TICKETS_TITLE", "📋 Ваши тикеты:"),
@@ -455,6 +456,7 @@ async def show_my_tickets_closed(
data = [{'id': t.id, 'title': t.title, 'status_emoji': t.status_emoji} for t in tickets]
kb = get_my_tickets_keyboard(data, current_page=current_page, total_pages=total_pages, language=db_user.language, page_prefix="my_tickets_closed_page_")
kb.inline_keyboard.insert(0, [types.InlineKeyboardButton(text=texts.t("BACK_TO_OPEN_TICKETS", "🔴 Открытые тикеты"), callback_data="my_tickets")])
from app.utils.photo_message import edit_or_answer_photo
await edit_or_answer_photo(
callback=callback,
caption=texts.t("CLOSED_TICKETS_TITLE", "🟢 Закрытые тикеты:"),

View File

@@ -701,7 +701,7 @@ class AdminNotificationService:
if not payment_method:
return '💰 С баланса'
return method_names.get(payment_method, '💰 С баланса')
return method_names.get(payment_method, f'💰 С баланса')
def _format_traffic(self, traffic_gb: int) -> str:
if traffic_gb == 0:
@@ -748,32 +748,40 @@ class AdminNotificationService:
if details.get("auto_enabled", False):
icon = "⚠️"
title = "АВТОМАТИЧЕСКОЕ ВКЛЮЧЕНИЕ ТЕХРАБОТ"
alert_type = "warning"
else:
icon = "🔧"
title = "ВКЛЮЧЕНИЕ ТЕХРАБОТ"
alert_type = "info"
elif event_type == "disable":
icon = ""
title = "ОТКЛЮЧЕНИЕ ТЕХРАБОТ"
alert_type = "success"
elif event_type == "api_status":
if status == "online":
icon = "🟢"
title = "API REMNAWAVE ВОССТАНОВЛЕНО"
alert_type = "success"
else:
icon = "🔴"
title = "API REMNAWAVE НЕДОСТУПНО"
alert_type = "error"
elif event_type == "monitoring":
if status == "started":
icon = "🔍"
title = "МОНИТОРИНГ ЗАПУЩЕН"
alert_type = "info"
else:
icon = "⏹️"
title = "МОНИТОРИНГ ОСТАНОВЛЕН"
alert_type = "info"
else:
icon = ""
title = "СИСТЕМА ТЕХРАБОТ"
alert_type = "info"
message_parts = [f"{icon} <b>{title}</b>", ""]
@@ -963,6 +971,103 @@ class AdminNotificationService:
logger.error(f"Ошибка отправки уведомления о статусе панели Remnawave: {e}")
return False
async def send_remnawave_panel_status_notification(
self,
status: str,
details: Dict[str, Any] = None
) -> bool:
if not self._is_enabled():
return False
try:
details = details or {}
status_config = {
"online": {"icon": "🟢", "title": "ПАНЕЛЬ REMNAWAVE ДОСТУПНА", "alert_type": "success"},
"offline": {"icon": "🔴", "title": "ПАНЕЛЬ REMNAWAVE НЕДОСТУПНА", "alert_type": "error"},
"degraded": {"icon": "🟡", "title": "ПАНЕЛЬ REMNAWAVE РАБОТАЕТ СО СБОЯМИ", "alert_type": "warning"},
"maintenance": {"icon": "🔧", "title": "ПАНЕЛЬ REMNAWAVE НА ОБСЛУЖИВАНИИ", "alert_type": "info"}
}
config = status_config.get(status, status_config["offline"])
message_parts = [
f"{config['icon']} <b>{config['title']}</b>",
""
]
if details.get("api_url"):
message_parts.append(f"🔗 <b>URL:</b> {details['api_url']}")
if details.get("response_time"):
message_parts.append(f"⚡ <b>Время отклика:</b> {details['response_time']} сек")
if details.get("last_check"):
last_check = details["last_check"]
if isinstance(last_check, str):
from datetime import datetime
last_check = datetime.fromisoformat(last_check)
message_parts.append(f"🕐 <b>Последняя проверка:</b> {last_check.strftime('%H:%M:%S')}")
if status == "online":
if details.get("uptime"):
message_parts.append(f"⏱️ <b>Время работы:</b> {details['uptime']}")
if details.get("users_online"):
message_parts.append(f"👥 <b>Пользователей онлайн:</b> {details['users_online']}")
message_parts.append("")
message_parts.append("Все системы работают нормально.")
elif status == "offline":
if details.get("error"):
error_msg = str(details["error"])[:150]
message_parts.append(f"❌ <b>Ошибка:</b> {error_msg}")
if details.get("consecutive_failures"):
message_parts.append(f"🔄 <b>Неудачных попыток:</b> {details['consecutive_failures']}")
message_parts.append("")
message_parts.append("⚠️ Панель недоступна. Проверьте соединение и статус сервера.")
elif status == "degraded":
if details.get("issues"):
issues = details["issues"]
if isinstance(issues, list):
message_parts.append("⚠️ <b>Обнаруженные проблемы:</b>")
for issue in issues[:3]:
message_parts.append(f"{issue}")
else:
message_parts.append(f"⚠️ <b>Проблема:</b> {issues}")
message_parts.append("")
message_parts.append("Панель работает, но возможны задержки или сбои.")
elif status == "maintenance":
if details.get("maintenance_reason"):
message_parts.append(f"🔧 <b>Причина:</b> {details['maintenance_reason']}")
if details.get("estimated_duration"):
message_parts.append(f"⏰ <b>Ожидаемая длительность:</b> {details['estimated_duration']}")
if details.get("manual_message"):
message_parts.append(f"💬 <b>Сообщение:</b> {details['manual_message']}")
message_parts.append("")
message_parts.append("Панель временно недоступна для обслуживания.")
from datetime import datetime
message_parts.append("")
message_parts.append(f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>")
message = "\n".join(message_parts)
return await self._send_message(message)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о статусе панели Remnawave: {e}")
return False
async def send_subscription_update_notification(
self,
db: AsyncSession,

View File

@@ -1,4 +1,5 @@
import json
import json
import logging
from copy import deepcopy
from pathlib import Path

View File

@@ -987,6 +987,15 @@ class RemnaWaveService:
logger.error(f"Error removing users from squad: {e}")
return False
async def delete_squad(self, squad_uuid: str) -> bool:
try:
async with self.get_api_client() as api:
response = await api.delete_internal_squad(squad_uuid)
return response
except Exception as e:
logger.error(f"Error deleting squad: {e}")
return False
async def get_all_inbounds(self) -> List[Dict]:
try:
async with self.get_api_client() as api:
@@ -1021,6 +1030,15 @@ class RemnaWaveService:
logger.error(f"Error renaming squad: {e}")
return False
async def create_squad(self, name: str, inbound_uuids: List[str]) -> bool:
try:
async with self.get_api_client() as api:
squad = await api.create_internal_squad(name, inbound_uuids)
return squad is not None
except Exception as e:
logger.error(f"Error creating squad: {e}")
return False
async def get_node_user_usage_by_range(self, node_uuid: str, start_date, end_date) -> List[Dict[str, Any]]:
try:
async with self.get_api_client() as api:

View File

@@ -189,7 +189,7 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int
and_(
Transaction.user_id == referral.id,
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed.is_(True)
Transaction.is_completed == True
)
)
)