Compare commits

..

30 Commits

Author SHA1 Message Date
Egor b8e664aad8 Update Dockerfile 2025-09-16 03:18:56 +03:00
Egor b365394ff4 Update docker-registry.yml 2025-09-16 03:18:46 +03:00
Egor e53549d7cc Update docker-hub.yml 2025-09-16 03:18:28 +03:00
Egor 743e99af85 Merge pull request #78 from yazhog/main
Шаблон описания пользователя в панели Remnawave, с плейсхолдерами
2025-09-16 01:38:53 +03:00
Egor 9616fe8d44 Merge pull request #79 from Fr1ngg/dev
Dev
2025-09-16 01:32:10 +03:00
Egor 4ae6361599 Merge pull request #76 from FireWookie/codex/required-sub-channel
Codex/required sub channel
2025-09-16 01:29:40 +03:00
yazhog 89c12e6ba9 Merge pull request #24 from yazhog/codex/add-user-description-template-to-.env.example
Add configurable RemnaWave user description template
2025-09-15 22:02:04 +03:00
yazhog 5d6bc82d8a Support username_clean placeholder for RemnaWave descriptions 2025-09-15 21:54:13 +03:00
yazhog a4f3fe5a49 Run RemnaWave middleware updates asynchronously 2025-09-15 21:43:30 +03:00
yazhog 3a58a3490a Update RemnaWave description from auth middleware 2025-09-15 21:31:49 +03:00
yazhog e2408702ea Update description when refreshing RemnaWave users 2025-09-15 21:22:01 +03:00
yazhog 3f1f27e5aa Merge pull request #23 from yazhog/revert-22-codex/find-user-mapping-between-telegram-bot-and-remnawave-api
Revert "Format RemnaWave username with Telegram names"
2025-09-15 20:41:01 +03:00
yazhog bbd0ec2edd Revert "Format RemnaWave username with Telegram names" 2025-09-15 20:40:49 +03:00
yazhog eb22b19d98 Merge pull request #22 from yazhog/codex/find-user-mapping-between-telegram-bot-and-remnawave-api
Format RemnaWave username with Telegram names
2025-09-15 16:30:50 +03:00
yazhog 45b9d44fd0 Transliterate names for RemnaWave usernames 2025-09-15 16:16:43 +03:00
yazhog a92003f0f5 Sanitize RemnaWave username for API 2025-09-15 16:09:08 +03:00
yazhog 4ac9387ef2 Relax RemnaWave username format 2025-09-15 16:01:23 +03:00
yazhog a24b5e8145 Format RemnaWave username with Telegram names 2025-09-15 15:52:04 +03:00
firewookie 7244e3978b upd channel check 2025-09-15 16:37:21 +05:00
firewookie 41cabf5e0e add middleware and keyboard 2025-09-14 23:42:47 +05:00
Egor 338bc03cd6 Update messages.py 2025-09-14 14:26:29 +03:00
Egor 985260144e Update messages.py 2025-09-14 13:07:30 +03:00
Egor b487925fbf Add files via upload 2025-09-14 09:21:22 +03:00
Egor 07a89443b3 Merge pull request #74 from Fr1ngg/dev
Фиксы сервиса мониторинга
2025-09-14 09:13:11 +03:00
Egor 54ebb2d96d Update admin.py 2025-09-14 09:11:25 +03:00
Egor 34a7cd1f6c Add files via upload 2025-09-14 09:09:16 +03:00
Egor 455b8fc709 Add files via upload 2025-09-14 09:06:13 +03:00
Egor 591b422302 Add files via upload 2025-09-14 09:02:39 +03:00
Egor fc73c34a03 Update admin.py 2025-09-14 08:32:30 +03:00
Egor cc1c6e3088 Update admin.py 2025-09-14 08:05:44 +03:00
19 changed files with 616 additions and 101 deletions
+13
View File
@@ -12,6 +12,11 @@ ADMIN_NOTIFICATIONS_ENABLED=true
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
# Обязательная подписка на канал
CHANNEL_SUB_ID= # Опционально ID твоего канала (-100)
CHANNEL_IS_SUB_REQUIRED=false # Обязательна ли подписка на канал
CHANNEL_LINK= # Опционально ссылка на канал
# ===== DATABASE CONFIGURATION =====
# Режим базы данных: "auto", "postgresql", "sqlite"
DATABASE_MODE=auto
@@ -46,6 +51,14 @@ REMNAWAVE_PASSWORD=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD
REMNAWAVE_SECRET_KEY=
# Шаблон описания пользователя в панели Remnawave
# Доступные плейсхолдеры:
# {full_name} — Имя, Фамилия из Telegram
# {username} — @логин из Telegram (c @)
# {username_clean} — логин из Telegram (без @)
# {telegram_id} — ID Telegram
REMNAWAVE_USER_DESCRIPTION_TEMPLATE="Bot user: {full_name} {username}"
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
+3 -3
View File
@@ -36,15 +36,15 @@ jobs:
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.3.2-$(git rev-parse --short HEAD)"
VERSION="v2.3.3-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.3.2-dev-$(git rev-parse --short HEAD)"
VERSION="v2.3.3-dev-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.3.2-pr-$(git rev-parse --short HEAD)"
VERSION="v2.3.3-pr-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
+3 -3
View File
@@ -52,13 +52,13 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/}
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.3.2"
VERSION="v2.3.3"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.3.2-dev-$(git rev-parse --short HEAD)"
VERSION="v2.3.3-dev-$(git rev-parse --short HEAD)"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.3.2-pr-$(git rev-parse --short HEAD)"
VERSION="v2.3.3-pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.11-slim
ARG VERSION="v2.3.2"
ARG VERSION="v2.3.3"
ARG BUILD_DATE
ARG VCS_REF
+4 -1
View File
@@ -5,6 +5,7 @@ from aiogram.fsm.storage.memory import MemoryStorage
import redis.asyncio as redis
from app.config import settings
from app.middlewares.channel_checker import ChannelCheckerMiddleware
from app.middlewares.global_error import GlobalErrorMiddleware
from app.middlewares.auth import AuthMiddleware
from app.middlewares.logging import LoggingMiddleware
@@ -75,7 +76,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
dp.message.middleware(GlobalErrorMiddleware())
dp.callback_query.middleware(GlobalErrorMiddleware())
dp.pre_checkout_query.middleware(GlobalErrorMiddleware())
@@ -90,6 +91,8 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(ThrottlingMiddleware())
dp.message.middleware(SubscriptionStatusMiddleware())
dp.callback_query.middleware(SubscriptionStatusMiddleware())
dp.message.middleware(ChannelCheckerMiddleware())
dp.callback_query.middleware(ChannelCheckerMiddleware())
start.register_handlers(dp)
menu.register_handlers(dp)
subscription.register_handlers(dp)
+34
View File
@@ -1,4 +1,6 @@
import os
import re
from collections import defaultdict
from typing import List, Optional, Union, Dict
from pydantic_settings import BaseSettings
from pydantic import field_validator, Field
@@ -14,6 +16,10 @@ class Settings(BaseSettings):
ADMIN_NOTIFICATIONS_ENABLED: bool = False
ADMIN_NOTIFICATIONS_CHAT_ID: Optional[str] = None
ADMIN_NOTIFICATIONS_TOPIC_ID: Optional[int] = None
CHANNEL_SUB_ID: Optional[str] = None
CHANNEL_LINK: Optional[str] = None
CHANNEL_IS_REQUIRED_SUB: bool = False
DATABASE_URL: str = ""
@@ -36,6 +42,7 @@ class Settings(BaseSettings):
REMNAWAVE_USERNAME: Optional[str] = None
REMNAWAVE_PASSWORD: Optional[str] = None
REMNAWAVE_AUTH_TYPE: str = "api_key"
REMNAWAVE_USER_DESCRIPTION_TEMPLATE: str = "Bot user: {full_name} {username}"
TRIAL_DURATION_DAYS: int = 3
TRIAL_TRAFFIC_LIMIT_GB: int = 10
@@ -252,6 +259,33 @@ class Settings(BaseSettings):
"password": self.REMNAWAVE_PASSWORD,
"auth_type": self.REMNAWAVE_AUTH_TYPE
}
def format_remnawave_user_description(
self,
*,
full_name: str,
username: Optional[str],
telegram_id: int
) -> str:
template = self.REMNAWAVE_USER_DESCRIPTION_TEMPLATE or "Bot user: {full_name} {username}"
template_for_formatting = template.replace("@{username}", "{username}")
username_clean = (username or "").lstrip("@")
values = defaultdict(str, {
"full_name": full_name,
"username": f"@{username_clean}" if username_clean else "",
"username_clean": username_clean,
"telegram_id": str(telegram_id)
})
description = template_for_formatting.format_map(values)
if not username_clean:
description = re.sub(r'@(?=\W|$)', '', description)
description = re.sub(r'\(\s*\)', '', description)
description = re.sub(r'\s+', ' ', description).strip()
return description
def get_autopay_warning_days(self) -> List[int]:
try:
+11 -7
View File
@@ -173,13 +173,16 @@ async def show_messages_history(
message_preview = broadcast.message_text[:100] + "..." if len(broadcast.message_text) > 100 else broadcast.message_text
import html
message_preview = html.escape(message_preview)
text += f"""
{status_emoji} <b>{broadcast.created_at.strftime('%d.%m.%Y %H:%M')}</b>
📊 Отправлено: {broadcast.sent_count}/{broadcast.total_count} ({success_rate}%)
🎯 Аудитория: {get_target_name(broadcast.target_type)}
👤 Админ: {broadcast.admin_name}
📝 Сообщение: <i>{message_preview}</i>
━━━━━━━━━━━━━━━━━━━━
📝 Сообщение: {message_preview}
━━━━━━━━━━━━━━━━━━━━━━━
"""
keyboard = get_broadcast_history_keyboard(page, total_pages, db_user.language).inline_keyboard
@@ -187,7 +190,7 @@ async def show_messages_history(
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
parse_mode="HTML"
)
await callback.answer()
@@ -344,13 +347,13 @@ async def handle_media_selection(
db_user: User,
state: FSMContext
):
media_type = callback.data.replace('add_media_', '')
if media_type == "skip":
if callback.data == "skip_media":
await state.update_data(has_media=False)
await show_button_selector_callback(callback, db_user, state)
return
media_type = callback.data.replace('add_media_', '')
media_instructions = {
"photo": "📷 Отправьте фотографию для рассылки:",
"video": "🎥 Отправьте видео для рассылки:",
@@ -985,7 +988,8 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(toggle_button_selection, F.data.startswith("btn_"))
dp.callback_query.register(confirm_button_selection, F.data == "buttons_confirm")
dp.callback_query.register(show_button_selector_callback, F.data == "edit_buttons")
dp.callback_query.register(handle_media_selection, F.data.startswith("add_media_") | F.data.in_(["skip_media"]))
dp.callback_query.register(handle_media_selection, F.data.startswith("add_media_"))
dp.callback_query.register(handle_media_selection, F.data == "skip_media")
dp.callback_query.register(handle_media_confirmation, F.data.in_(["confirm_media", "replace_media"]))
dp.callback_query.register(handle_change_media, F.data == "change_media")
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
+95 -38
View File
@@ -9,6 +9,7 @@ from app.config import settings
from app.database.database import get_db
from app.services.monitoring_service import monitoring_service
from app.utils.decorators import admin_required
from app.utils.pagination import paginate_list
from app.keyboards.admin import get_monitoring_keyboard, get_admin_main_keyboard
from app.localization.texts import get_texts
@@ -19,7 +20,6 @@ router = Router()
@router.callback_query(F.data == "admin_monitoring")
@admin_required
async def admin_monitoring_menu(callback: CallbackQuery):
"""Главное меню мониторинга"""
try:
async for db in get_db():
status = await monitoring_service.get_monitoring_status(db)
@@ -127,50 +127,57 @@ async def force_check_callback(callback: CallbackQuery):
await callback.answer(f"❌ Ошибка проверки: {str(e)}", show_alert=True)
@router.callback_query(F.data == "admin_mon_logs")
@router.callback_query(F.data.startswith("admin_mon_logs"))
@admin_required
async def monitoring_logs_callback(callback: CallbackQuery):
try:
page = 1
if "_page_" in callback.data:
page = int(callback.data.split("_page_")[1])
async for db in get_db():
logs = await monitoring_service.get_monitoring_logs(db, limit=15)
all_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
if not logs:
text = "📝 <b>Логи мониторинга пусты</b>\n\nСистема еще не выполняла проверки."
else:
text = "📝 <b>Последние логи мониторинга:</b>\n\n"
if not all_logs:
text = "📋 <b>Логи мониторинга пусты</b>\n\nСистема еще не выполнила проверки."
keyboard = get_monitoring_logs_back_keyboard()
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
return
per_page = 8
paginated_logs = paginate_list(all_logs, page=page, per_page=per_page)
text = f"📋 <b>Логи мониторинга</b> (стр. {page}/{paginated_logs.total_pages})\n\n"
for log in paginated_logs.items:
icon = "" if log['is_success'] else ""
time_str = log['created_at'].strftime('%m-%d %H:%M')
event_type = log['event_type'].replace('_', ' ').title()
for log in logs:
icon = "" if log['is_success'] else ""
time_str = log['created_at'].strftime('%m-%d %H:%M')
event_type = log['event_type'].replace('_', ' ').title()
text += f"{icon} <code>{time_str}</code> {event_type}\n"
message = log['message']
if len(message) > 60:
message = message[:60] + "..."
text += f" 📄 {message}\n\n"
if len(text) > 3500:
text += "...\n\n<i>Показаны последние записи. Для просмотра всех логов используйте файл логов.</i>"
break
message = log['message']
if len(message) > 45:
message = message[:45] + "..."
text += f"{icon} <code>{time_str}</code> {event_type}\n"
text += f" 📄 {message}\n\n"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="🗑️ Очистить", callback_data="admin_mon_clear_logs")
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")]
])
total_success = sum(1 for log in all_logs if log['is_success'])
total_failed = len(all_logs) - total_success
success_rate = round(total_success / len(all_logs) * 100, 1) if all_logs else 0
text += f"📊 <b>Общая статистика:</b>\n"
text += f"• Всего событий: {len(all_logs)}\n"
text += f"• Успешных: {total_success}\n"
text += f"• Ошибок: {total_failed}\n"
text += f"• Успешность: {success_rate}%"
keyboard = get_monitoring_logs_keyboard(page, paginated_logs.total_pages)
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
break
except Exception as e:
logger.error(f"Ошибка получения логов: {e}")
await callback.answer(f"❌ Ошибка получения логов: {str(e)}", show_alert=True)
await callback.answer("❌ Ошибка получения логов", show_alert=True)
@router.callback_query(F.data == "admin_mon_clear_logs")
@@ -178,12 +185,12 @@ async def monitoring_logs_callback(callback: CallbackQuery):
async def clear_logs_callback(callback: CallbackQuery):
try:
async for db in get_db():
deleted_count = await monitoring_service.cleanup_old_logs(db, days=7)
deleted_count = await monitoring_service.cleanup_old_logs(db, days=0)
if deleted_count > 0:
await callback.answer(f"🗑️ Удалено {deleted_count} старых записей логов")
await callback.answer(f"🗑️ Удалено {deleted_count} записей логов")
else:
await callback.answer("Нет старых логов для удаления")
await callback.answer("Логи уже пусты")
await monitoring_logs_callback(callback)
break
@@ -196,7 +203,6 @@ async def clear_logs_callback(callback: CallbackQuery):
@router.callback_query(F.data == "admin_mon_test_notifications")
@admin_required
async def test_notifications_callback(callback: CallbackQuery):
"""Тест системы уведомлений"""
try:
test_message = f"""
🧪 <b>Тестовое уведомление системы мониторинга</b>
@@ -280,10 +286,62 @@ async def monitoring_statistics_callback(callback: CallbackQuery):
await callback.answer(f"❌ Ошибка получения статистики: {str(e)}", show_alert=True)
def get_monitoring_logs_keyboard(current_page: int, total_pages: int):
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = []
if total_pages > 1:
nav_row = []
if current_page > 1:
nav_row.append(InlineKeyboardButton(
text="⬅️",
callback_data=f"admin_mon_logs_page_{current_page - 1}"
))
nav_row.append(InlineKeyboardButton(
text=f"{current_page}/{total_pages}",
callback_data="current_page"
))
if current_page < total_pages:
nav_row.append(InlineKeyboardButton(
text="➡️",
callback_data=f"admin_mon_logs_page_{current_page + 1}"
))
keyboard.append(nav_row)
keyboard.extend([
[
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="🗑️ Очистить", callback_data="admin_mon_clear_logs")
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")]
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_monitoring_logs_back_keyboard():
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_mon_logs_filters")
],
[
InlineKeyboardButton(text="🗑️ Очистить логи", callback_data="admin_mon_clear_logs")
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")]
])
@router.message(Command("monitoring"))
@admin_required
async def monitoring_command(message: Message):
"""Команда /monitoring для быстрого доступа"""
try:
async for db in get_db():
status = await monitoring_service.get_monitoring_status(db)
@@ -309,5 +367,4 @@ async def monitoring_command(message: Message):
def register_handlers(dp):
"""Регистрация обработчиков мониторинга"""
dp.include_router(router)
+18 -3
View File
@@ -1644,7 +1644,12 @@ async def toggle_user_server(
async with remnawave_service.api as api:
await api.update_user(
uuid=user.remnawave_uuid,
active_internal_squads=current_squads
active_internal_squads=current_squads,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
)
)
logger.info(f"✅ Обновлены серверы в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
@@ -2023,7 +2028,12 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm
async with remnawave_service.api as api:
await api.update_user(
uuid=user.remnawave_uuid,
hwid_device_limit=devices
hwid_device_limit=devices,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
)
)
logger.info(f"✅ Обновлен лимит устройств в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
@@ -2061,7 +2071,12 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int,
await api.update_user(
uuid=user.remnawave_uuid,
traffic_limit_bytes=traffic_gb * (1024**3) if traffic_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
)
)
logger.info(f"✅ Обновлен лимит трафика в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
+26
View File
@@ -1,6 +1,7 @@
import logging
from datetime import datetime
from aiogram import Dispatcher, types, F, Bot
from aiogram.enums import ChatMemberStatus
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -849,6 +850,25 @@ async def get_main_menu_text_simple(user_name, texts, db: AsyncSession):
return base_text
async def required_sub_channel_check(
query: types.CallbackQuery,
bot: Bot,
state: FSMContext,
db: AsyncSession,
db_user=None
):
chat_member = await bot.get_chat_member(
chat_id=settings.CHANNEL_SUB_ID,
user_id=query.from_user.id
)
if chat_member.status not in [ChatMemberStatus.MEMBER]:
return await query.answer("❌ Вы не подписались на канал!", show_alert=True)
await query.answer("✅ Спасибо за подписку", show_alert=True)
await query.message.delete()
await cmd_start(query.message, state, db, db_user)
def register_handlers(dp: Dispatcher):
logger.info("🔧 === НАЧАЛО регистрации обработчиков start.py ===")
@@ -887,6 +907,12 @@ def register_handlers(dp: Dispatcher):
)
)
logger.info("✅ Зарегистрирован handle_potential_referral_code")
dp.callback_query.register(
required_sub_channel_check,
F.data.in_(["sub_channel_check"])
)
logger.info("✅ Зарегистрирован required_sub_channel_check")
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
+171 -15
View File
@@ -8,20 +8,12 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="👥 Юзеры и Подписки", callback_data="admin_submenu_users"),
InlineKeyboardButton(text="💰 Промокоды и статистика", callback_data="admin_submenu_promo")
],
[
InlineKeyboardButton(text="📨 Коммуникации", callback_data="admin_submenu_communications"),
InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_submenu_settings")
],
[
InlineKeyboardButton(text="🛠️ Системные функции", callback_data="admin_submenu_system")
],
[
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
]
[InlineKeyboardButton(text="👥 Юзеры/Подписки", callback_data="admin_submenu_users")],
[InlineKeyboardButton(text="💰 Промокоды/Статистика", callback_data="admin_submenu_promo")],
[InlineKeyboardButton(text="📨 Сообщения", callback_data="admin_submenu_communications")],
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_submenu_settings")],
[InlineKeyboardButton(text="🛠️ Система", callback_data="admin_submenu_system")],
[InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")]
])
@@ -548,7 +540,7 @@ def get_monitoring_keyboard() -> InlineKeyboardMarkup:
],
[
InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="admin_mon_force_check"),
InlineKeyboardButton(text="📝 Логи", callback_data="admin_mon_logs")
InlineKeyboardButton(text="📋 Логи", callback_data="admin_mon_logs")
],
[
InlineKeyboardButton(text="🧪 Тест уведомлений", callback_data="admin_mon_test_notifications"),
@@ -570,6 +562,170 @@ def get_monitoring_logs_keyboard() -> InlineKeyboardMarkup:
]
])
def get_monitoring_logs_navigation_keyboard(
current_page: int,
total_pages: int,
has_logs: bool = True
) -> InlineKeyboardMarkup:
keyboard = []
if total_pages > 1:
nav_row = []
if current_page > 1:
nav_row.append(InlineKeyboardButton(
text="⬅️",
callback_data=f"admin_mon_logs_page_{current_page - 1}"
))
nav_row.append(InlineKeyboardButton(
text=f"{current_page}/{total_pages}",
callback_data="current_page_info"
))
if current_page < total_pages:
nav_row.append(InlineKeyboardButton(
text="➡️",
callback_data=f"admin_mon_logs_page_{current_page + 1}"
))
keyboard.append(nav_row)
management_row = []
if has_logs:
management_row.extend([
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="🗑️ Очистить", callback_data="admin_mon_clear_logs")
])
else:
management_row.append(
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_mon_logs")
)
keyboard.append(management_row)
keyboard.append([
InlineKeyboardButton(text="⬅️ Назад к мониторингу", callback_data="admin_monitoring")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_log_detail_keyboard(log_id: int, current_page: int = 1) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🗑️ Удалить этот лог",
callback_data=f"admin_mon_delete_log_{log_id}"
)
],
[
InlineKeyboardButton(
text="⬅️ К списку логов",
callback_data=f"admin_mon_logs_page_{current_page}"
)
]
])
def get_monitoring_clear_confirm_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, очистить", callback_data="admin_mon_clear_logs_confirm"),
InlineKeyboardButton(text="❌ Отмена", callback_data="admin_mon_logs")
],
[
InlineKeyboardButton(text="🗑️ Очистить ВСЕ логи", callback_data="admin_mon_clear_all_logs")
]
])
def get_monitoring_status_keyboard(
is_running: bool,
last_check_ago_minutes: int = 0
) -> InlineKeyboardMarkup:
keyboard = []
control_row = []
if is_running:
control_row.extend([
InlineKeyboardButton(text="⏹️ Остановить", callback_data="admin_mon_stop"),
InlineKeyboardButton(text="🔄 Перезапустить", callback_data="admin_mon_restart")
])
else:
control_row.append(
InlineKeyboardButton(text="▶️ Запустить", callback_data="admin_mon_start")
)
keyboard.append(control_row)
monitoring_row = []
if not is_running or last_check_ago_minutes > 10:
monitoring_row.append(
InlineKeyboardButton(
text="⚡ Срочная проверка",
callback_data="admin_mon_force_check"
)
)
else:
monitoring_row.append(
InlineKeyboardButton(
text="🔄 Проверить сейчас",
callback_data="admin_mon_force_check"
)
)
keyboard.append(monitoring_row)
info_row = [
InlineKeyboardButton(text="📋 Логи", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="📊 Статистика", callback_data="admin_mon_statistics")
]
keyboard.append(info_row)
test_row = [
InlineKeyboardButton(text="🧪 Тест уведомлений", callback_data="admin_mon_test_notifications")
]
keyboard.append(test_row)
keyboard.append([
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_settings")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_monitoring_settings_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="⏱️ Интервал проверки", callback_data="admin_mon_set_interval"),
InlineKeyboardButton(text="🔔 Уведомления", callback_data="admin_mon_toggle_notifications")
],
[
InlineKeyboardButton(text="💳 Настройки автооплаты", callback_data="admin_mon_autopay_settings"),
InlineKeyboardButton(text="🧹 Автоочистка логов", callback_data="admin_mon_auto_cleanup")
],
[
InlineKeyboardButton(text="⬅️ К мониторингу", callback_data="admin_monitoring")
]
])
def get_log_type_filter_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Успешные", callback_data="admin_mon_logs_filter_success"),
InlineKeyboardButton(text="❌ Ошибки", callback_data="admin_mon_logs_filter_error")
],
[
InlineKeyboardButton(text="🔄 Циклы мониторинга", callback_data="admin_mon_logs_filter_cycle"),
InlineKeyboardButton(text="💳 Автооплаты", callback_data="admin_mon_logs_filter_autopay")
],
[
InlineKeyboardButton(text="📋 Все логи", callback_data="admin_mon_logs"),
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")
]
])
def get_admin_servers_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
+16
View File
@@ -21,6 +21,22 @@ def get_rules_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
]
])
def get_channel_sub_keyboard(channel_link: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="🔗 Подписаться", url=channel_link
)
],
[
InlineKeyboardButton(
text="✅ Я подписался", callback_data="sub_channel_check"
)
]
]
)
def get_post_registration_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
+41 -17
View File
@@ -1,3 +1,4 @@
import asyncio
import logging
from datetime import datetime
from typing import Callable, Dict, Any, Awaitable
@@ -8,11 +9,31 @@ from aiogram.fsm.context import FSMContext
from app.config import settings
from app.database.database import get_db
from app.database.crud.user import get_user_by_telegram_id, create_user
from app.services.remnawave_service import RemnaWaveService
from app.states import RegistrationStates
from app.utils.check_reg_process import is_registration_process
logger = logging.getLogger(__name__)
async def _refresh_remnawave_description(
remnawave_uuid: str,
description: str,
telegram_id: int
) -> None:
try:
remnawave_service = RemnaWaveService()
async with remnawave_service.api as api:
await api.update_user(uuid=remnawave_uuid, description=description)
logger.info(
f"✅ [Middleware] Описание пользователя {telegram_id} обновлено в RemnaWave"
)
except Exception as remnawave_error:
logger.error(
f"❌ [Middleware] Ошибка обновления RemnaWave для {telegram_id}: {remnawave_error}"
)
class AuthMiddleware(BaseMiddleware):
async def __call__(
@@ -21,6 +42,8 @@ class AuthMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
user: TgUser = None
if isinstance(event, (Message, CallbackQuery)):
@@ -42,23 +65,10 @@ class AuthMiddleware(BaseMiddleware):
if state:
current_state = await state.get_state()
registration_states = [
RegistrationStates.waiting_for_rules_accept.state,
RegistrationStates.waiting_for_referral_code.state
]
is_registration_process = (
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
or (current_state in registration_states)
or (
isinstance(event, CallbackQuery)
and event.data
and (event.data in ['rules_accept', 'rules_decline', 'referral_skip'])
)
)
is_reg_process = is_registration_process(event, current_state)
if is_registration_process:
if is_reg_process:
logger.info(f"🔍 Пропускаем пользователя {user.id} в процессе регистрации")
data['db'] = db
data['db_user'] = None
@@ -151,11 +161,25 @@ class AuthMiddleware(BaseMiddleware):
profile_updated = True
db_user.last_activity = datetime.utcnow()
if profile_updated:
db_user.updated_at = datetime.utcnow()
logger.info(f"💾 [Middleware] Профиль пользователя {user.id} обновлен в middleware")
if db_user.remnawave_uuid:
description = settings.format_remnawave_user_description(
full_name=db_user.full_name,
username=db_user.username,
telegram_id=db_user.telegram_id
)
asyncio.create_task(
_refresh_remnawave_description(
remnawave_uuid=db_user.remnawave_uuid,
description=description,
telegram_id=db_user.telegram_id
)
)
await db.commit()
data['db'] = db
+77
View File
@@ -0,0 +1,77 @@
import logging
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware, Bot
from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.types import TelegramObject, Update, Message, CallbackQuery
from app.config import settings
from app.keyboards.inline import get_channel_sub_keyboard
from app.utils.check_reg_process import is_registration_process
logger = logging.getLogger(__name__)
class ChannelCheckerMiddleware(BaseMiddleware):
def __init__(self):
self.BAD_MEMBER_STATUS = ("left", "kicked")
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
telegram_id = None
if isinstance(event, (Message, CallbackQuery)):
telegram_id = event.from_user.id
elif isinstance(event, Update):
if event.message:
telegram_id = event.message.from_user.id
elif event.callback_query:
telegram_id = event.callback_query.from_user.id
if telegram_id is None:
return await handler(event, data)
state: FSMContext = data.get('state')
current_state = None
if state:
current_state = await state.get_state()
is_reg_process = is_registration_process(event, current_state)
# Пропускаем пользователя на разрешенные ивенты
if is_reg_process:
return await handler(event, data)
bot: Bot = data["bot"]
channel_id = settings.CHANNEL_SUB_ID
if not channel_id:
return await handler(event, data)
channel_link = settings.CHANNEL_LINK
try:
member = await bot.get_chat_member(chat_id=channel_id, user_id=telegram_id)
if member.status in self.BAD_MEMBER_STATUS:
return await self._deny_message(event, bot, channel_link)
except (TelegramForbiddenError, TelegramBadRequest):
# бот не админ или нет доступа к каналу
return await self._deny_message(event, bot, channel_link)
# если все каналы пройдены
return await handler(event, data)
@staticmethod
async def _deny_message(event: TelegramObject, bot: Bot, channel_link: str):
channel_sub_kb = get_channel_sub_keyboard(channel_link)
text = f"""🔔 Для использования бота подпишитесь на новостной канал, чтобы получать уведомления о новых возможностях и обновлениях бота. Спасибо!"""
if isinstance(event, Message):
return await event.answer(text, reply_markup=channel_sub_kb)
elif isinstance(event, CallbackQuery):
return await event.message.edit_text(text, reply_markup=channel_sub_kb)
elif isinstance(event, Update) and event.message:
return await bot.send_message(event.message.chat.id, text, reply_markup=channel_sub_kb)
+50 -10
View File
@@ -158,8 +158,13 @@ class MonitoringService:
status=UserStatus.ACTIVE if is_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
hwid_device_limit=subscription.device_limit,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
@@ -693,7 +698,9 @@ class MonitoringService:
self,
db: AsyncSession,
limit: int = 50,
event_type: Optional[str] = None
event_type: Optional[str] = None,
page: int = 1,
per_page: int = 20
) -> List[Dict[str, Any]]:
try:
from sqlalchemy import select, desc
@@ -703,7 +710,11 @@ class MonitoringService:
if event_type:
query = query.where(MonitoringLog.event_type == event_type)
query = query.limit(limit)
if page > 1 or per_page != 20:
offset = (page - 1) * per_page
query = query.offset(offset).limit(per_page)
else:
query = query.limit(limit)
result = await db.execute(query)
logs = result.scalars().all()
@@ -723,25 +734,54 @@ class MonitoringService:
except Exception as e:
logger.error(f"Ошибка получения логов мониторинга: {e}")
return []
async def get_monitoring_logs_count(
self,
db: AsyncSession,
event_type: Optional[str] = None
) -> int:
try:
from sqlalchemy import select, func
query = select(func.count(MonitoringLog.id))
if event_type:
query = query.where(MonitoringLog.event_type == event_type)
result = await db.execute(query)
count = result.scalar()
return count or 0
except Exception as e:
logger.error(f"Ошибка получения количества логов: {e}")
return 0
async def cleanup_old_logs(self, db: AsyncSession, days: int = 30) -> int:
try:
from sqlalchemy import delete
from sqlalchemy import delete, select
cutoff_date = datetime.utcnow() - timedelta(days=days)
result = await db.execute(
delete(MonitoringLog).where(MonitoringLog.created_at < cutoff_date)
)
if days == 0:
result = await db.execute(delete(MonitoringLog))
else:
cutoff_date = datetime.utcnow() - timedelta(days=days)
result = await db.execute(
delete(MonitoringLog).where(MonitoringLog.created_at < cutoff_date)
)
deleted_count = result.rowcount
await db.commit()
logger.info(f"Удалено {deleted_count} старых записей логов")
if days == 0:
logger.info(f"🗑️ Удалены все логи мониторинга ({deleted_count} записей)")
else:
logger.info(f"🗑️ Удалено {deleted_count} старых записей логов (старше {days} дней)")
return deleted_count
except Exception as e:
logger.error(f"Ошибка очистки логов: {e}")
await db.rollback()
return 0
+10 -1
View File
@@ -779,6 +779,11 @@ class RemnaWaveService:
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
hwid_device_limit=subscription.device_limit,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
stats["updated"] += 1
@@ -793,7 +798,11 @@ class RemnaWaveService:
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
telegram_id=user.telegram_id,
hwid_device_limit=subscription.device_limit,
description=f"Bot user: {user.full_name}",
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
+16 -2
View File
@@ -68,6 +68,11 @@ class SubscriptionService:
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
hwid_device_limit=subscription.device_limit,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
@@ -82,7 +87,11 @@ class SubscriptionService:
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
telegram_id=user.telegram_id,
hwid_device_limit=subscription.device_limit,
description=f"Bot user: {user.full_name}",
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
@@ -135,8 +144,13 @@ class SubscriptionService:
status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
hwid_device_limit=subscription.device_limit,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads
)
+27
View File
@@ -0,0 +1,27 @@
from typing import Optional
from aiogram.fsm.context import FSMContext
from aiogram.types import TelegramObject, Message, CallbackQuery
from app.states import RegistrationStates
def is_registration_process(event: TelegramObject, current_state: Optional[str]) -> bool:
registration_states = [
RegistrationStates.waiting_for_rules_accept.state,
RegistrationStates.waiting_for_referral_code.state
]
is_registration_process = (
(isinstance(event, Message) and event.text and event.text.startswith("/start"))
or (current_state in registration_states)
or (
isinstance(event, CallbackQuery)
and event.data
and (
event.data in ["rules_accept", "rules_decline", "referral_skip"]
or event.data in ["sub_channel_check"]
)
)
)
return is_registration_process
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 716 KiB