@@ -222,3 +222,7 @@ WEBHOOK_PATH=/webhook
|
||||
APP_CONFIG_PATH=app-config.json
|
||||
ENABLE_DEEP_LINKS=true
|
||||
APP_CONFIG_CACHE_TTL=3600
|
||||
|
||||
VERSION_CHECK_ENABLED=true
|
||||
VERSION_CHECK_REPO=fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
VERSION_CHECK_INTERVAL_HOURS=1
|
||||
|
||||
+21
-1
@@ -1,9 +1,15 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ARG VERSION="2.2.3"
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
wget \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
@@ -13,16 +19,30 @@ RUN groupadd -g 1000 app && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p logs data && \
|
||||
chown -R app:app /app
|
||||
chown -R app:app /app && \
|
||||
chown -R 1000:1000 ./logs ./data
|
||||
|
||||
USER app
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV VERSION=${VERSION}
|
||||
ENV BUILD_DATE=${BUILD_DATE}
|
||||
ENV VCS_REF=${VCS_REF}
|
||||
|
||||
EXPOSE 8081 8082
|
||||
|
||||
LABEL org.opencontainers.image.title="Bedolaga RemnaWave Bot"
|
||||
LABEL org.opencontainers.image.description="Telegram bot for RemnaWave VPN service"
|
||||
LABEL org.opencontainers.image.version="${VERSION}"
|
||||
LABEL org.opencontainers.image.created="${BUILD_DATE}"
|
||||
LABEL org.opencontainers.image.revision="${VCS_REF}"
|
||||
LABEL org.opencontainers.image.source="https://github.com/fr1ngg/remnawave-bedolaga-telegram-bot"
|
||||
LABEL org.opencontainers.image.url="https://github.com/fr1ngg/remnawave-bedolaga-telegram-bot"
|
||||
LABEL org.opencontainers.image.vendor="fr1ngg"
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
|
||||
|
||||
|
||||
+8
-3
@@ -24,7 +24,8 @@ from app.handlers.admin import (
|
||||
rules as admin_rules, remnawave as admin_remnawave,
|
||||
statistics as admin_statistics, servers as admin_servers,
|
||||
maintenance as admin_maintenance,
|
||||
user_messages as admin_user_messages
|
||||
user_messages as admin_user_messages,
|
||||
updates as admin_updates
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -68,6 +69,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
storage = MemoryStorage()
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
|
||||
dp.message.middleware(LoggingMiddleware())
|
||||
dp.callback_query.middleware(LoggingMiddleware())
|
||||
dp.message.middleware(AuthMiddleware())
|
||||
@@ -79,6 +81,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
dp.callback_query.middleware(ThrottlingMiddleware())
|
||||
dp.message.middleware(SubscriptionStatusMiddleware())
|
||||
dp.callback_query.middleware(SubscriptionStatusMiddleware())
|
||||
|
||||
start.register_handlers(dp)
|
||||
menu.register_handlers(dp)
|
||||
subscription.register_handlers(dp)
|
||||
@@ -86,6 +89,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
promocode.register_handlers(dp)
|
||||
referral.register_handlers(dp)
|
||||
support.register_handlers(dp)
|
||||
|
||||
admin_main.register_handlers(dp)
|
||||
admin_users.register_handlers(dp)
|
||||
admin_subscriptions.register_handlers(dp)
|
||||
@@ -98,12 +102,13 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_remnawave.register_handlers(dp)
|
||||
admin_statistics.register_handlers(dp)
|
||||
admin_maintenance.register_handlers(dp)
|
||||
admin_user_messages.register_handlers(dp)
|
||||
admin_user_messages.register_handlers(dp)
|
||||
admin_updates.register_handlers(dp)
|
||||
|
||||
common.register_handlers(dp)
|
||||
|
||||
register_stars_handlers(dp)
|
||||
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
|
||||
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
|
||||
|
||||
try:
|
||||
await maintenance_service.start_monitoring()
|
||||
|
||||
@@ -141,6 +141,10 @@ class Settings(BaseSettings):
|
||||
APP_CONFIG_PATH: str = "app-config.json"
|
||||
ENABLE_DEEP_LINKS: bool = True
|
||||
APP_CONFIG_CACHE_TTL: int = 3600
|
||||
|
||||
VERSION_CHECK_ENABLED: bool = True
|
||||
VERSION_CHECK_REPO: str = "fr1ngg/remnawave-bedolaga-telegram-bot"
|
||||
VERSION_CHECK_INTERVAL_HOURS: int = 1
|
||||
|
||||
@field_validator('LOG_FILE', mode='before')
|
||||
@classmethod
|
||||
@@ -427,6 +431,15 @@ class Settings(BaseSettings):
|
||||
except Exception as e:
|
||||
logger.info(f"ERROR PARSING CONFIG: {e}")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
def is_version_check_enabled(self) -> bool:
|
||||
return self.VERSION_CHECK_ENABLED
|
||||
|
||||
def get_version_check_repo(self) -> str:
|
||||
return self.VERSION_CHECK_REPO
|
||||
|
||||
def get_version_check_interval(self) -> int:
|
||||
return self.VERSION_CHECK_INTERVAL_HOURS
|
||||
|
||||
def _get_fallback_traffic_packages(self) -> List[Dict]:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.services.version_service import version_service
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_updates_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Проверить обновления",
|
||||
callback_data="admin_updates_check"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📋 Информация о версии",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔗 Открыть репозиторий",
|
||||
url=f"https://github.com/{version_service.repo}/releases"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="◀️ Назад",
|
||||
callback_data="admin_panel"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
def get_version_info_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Обновить",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="◀️ К обновлениям",
|
||||
callback_data="admin_updates"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_updates_menu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
version_info = await version_service.get_version_info()
|
||||
|
||||
current_version = version_info['current_version']
|
||||
has_updates = version_info['has_updates']
|
||||
total_newer = version_info['total_newer']
|
||||
last_check = version_info['last_check']
|
||||
|
||||
status_icon = "🆕" if has_updates else "✅"
|
||||
status_text = f"Доступно {total_newer} обновлений" if has_updates else "Актуальная версия"
|
||||
|
||||
last_check_text = ""
|
||||
if last_check:
|
||||
last_check_text = f"\n🕐 Последняя проверка: {last_check.strftime('%d.%m.%Y %H:%M')}"
|
||||
|
||||
message = f"""🔄 <b>СИСТЕМА ОБНОВЛЕНИЙ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
{status_icon} <b>Статус:</b> {status_text}
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}{last_check_text}
|
||||
|
||||
ℹ️ Система автоматически проверяет обновления каждый час и отправляет уведомления о новых версиях."""
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=get_updates_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка показа меню обновлений: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки меню обновлений", show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def check_updates(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("🔄 Проверяю обновления...")
|
||||
|
||||
try:
|
||||
has_updates, newer_releases = await version_service.check_for_updates(force=True)
|
||||
|
||||
if not has_updates:
|
||||
message = f"""✅ <b>ОБНОВЛЕНИЯ НЕ НАЙДЕНЫ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>
|
||||
🎯 <b>Статус:</b> У вас установлена последняя версия
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}"""
|
||||
|
||||
else:
|
||||
updates_list = []
|
||||
for i, release in enumerate(newer_releases[:5]):
|
||||
icon = version_service.format_version_display(release).split()[0]
|
||||
updates_list.append(
|
||||
f"{i+1}. {icon} <code>{release.tag_name}</code> • {release.formatted_date}"
|
||||
)
|
||||
|
||||
updates_text = "\n".join(updates_list)
|
||||
more_text = f"\n\n📋 И еще {len(newer_releases) - 5} обновлений..." if len(newer_releases) > 5 else ""
|
||||
|
||||
message = f"""🆕 <b>НАЙДЕНЫ ОБНОВЛЕНИЯ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>
|
||||
🎯 <b>Доступно обновлений:</b> {len(newer_releases)}
|
||||
|
||||
📋 <b>Последние версии:</b>
|
||||
{updates_text}{more_text}
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}"""
|
||||
|
||||
keyboard = get_updates_keyboard(db_user.language)
|
||||
|
||||
if has_updates:
|
||||
keyboard.inline_keyboard.insert(-2, [
|
||||
InlineKeyboardButton(
|
||||
text="📋 Подробнее о версиях",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки обновлений: {e}")
|
||||
await callback.message.edit_text(
|
||||
f"❌ <b>ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ</b>\n\n"
|
||||
f"Не удалось связаться с сервером GitHub.\n"
|
||||
f"Попробуйте позже.\n\n"
|
||||
f"📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>",
|
||||
reply_markup=get_updates_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_version_info(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("📋 Загружаю информацию о версиях...")
|
||||
|
||||
try:
|
||||
version_info = await version_service.get_version_info()
|
||||
|
||||
current_version = version_info['current_version']
|
||||
current_release = version_info['current_release']
|
||||
newer_releases = version_info['newer_releases']
|
||||
has_updates = version_info['has_updates']
|
||||
last_check = version_info['last_check']
|
||||
repo_url = version_info['repo_url']
|
||||
|
||||
current_info = f"📦 <b>ТЕКУЩАЯ ВЕРСИЯ</b>\n\n"
|
||||
|
||||
if current_release:
|
||||
current_info += f"🏷️ <b>Версия:</b> <code>{current_release.tag_name}</code>\n"
|
||||
current_info += f"📅 <b>Дата релиза:</b> {current_release.formatted_date}\n"
|
||||
if current_release.short_description:
|
||||
current_info += f"📝 <b>Описание:</b>\n{current_release.short_description}\n"
|
||||
else:
|
||||
current_info += f"🏷️ <b>Версия:</b> <code>{current_version}</code>\n"
|
||||
current_info += f"ℹ️ <b>Статус:</b> Информация о релизе недоступна\n"
|
||||
|
||||
message_parts = [current_info]
|
||||
|
||||
if has_updates and newer_releases:
|
||||
updates_info = f"\n🆕 <b>ДОСТУПНЫЕ ОБНОВЛЕНИЯ</b>\n\n"
|
||||
|
||||
for i, release in enumerate(newer_releases):
|
||||
icon = "🔥" if i == 0 else "📦"
|
||||
if release.prerelease:
|
||||
icon = "🧪"
|
||||
elif release.is_dev:
|
||||
icon = "🔧"
|
||||
|
||||
updates_info += f"{icon} <b>{release.tag_name}</b>\n"
|
||||
updates_info += f" 📅 {release.formatted_date}\n"
|
||||
if release.short_description:
|
||||
updates_info += f" 📝 {release.short_description}\n"
|
||||
updates_info += "\n"
|
||||
|
||||
message_parts.append(updates_info.rstrip())
|
||||
|
||||
system_info = f"\n🔧 <b>СИСТЕМА ОБНОВЛЕНИЙ</b>\n\n"
|
||||
system_info += f"🔗 <b>Репозиторий:</b> {version_service.repo}\n"
|
||||
system_info += f"⚡ <b>Автопроверка:</b> {'Включена' if version_service.enabled else 'Отключена'}\n"
|
||||
system_info += f"🕐 <b>Интервал:</b> Каждый час\n"
|
||||
|
||||
if last_check:
|
||||
system_info += f"🕐 <b>Последняя проверка:</b> {last_check.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
|
||||
message_parts.append(system_info.rstrip())
|
||||
|
||||
final_message = "\n".join(message_parts)
|
||||
|
||||
if len(final_message) > 4000:
|
||||
final_message = final_message[:3900] + "\n\n... (информация обрезана)"
|
||||
|
||||
await callback.message.edit_text(
|
||||
final_message,
|
||||
reply_markup=get_version_info_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения информации о версиях: {e}")
|
||||
await callback.message.edit_text(
|
||||
f"❌ <b>ОШИБКА ЗАГРУЗКИ</b>\n\n"
|
||||
f"Не удалось получить информацию о версиях.\n\n"
|
||||
f"📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>",
|
||||
reply_markup=get_version_info_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
show_updates_menu,
|
||||
F.data == "admin_updates"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
check_updates,
|
||||
F.data == "admin_updates_check"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_version_info,
|
||||
F.data == "admin_updates_info"
|
||||
)
|
||||
@@ -29,7 +29,8 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel")
|
||||
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel"),
|
||||
InlineKeyboardButton(text="🔄 Обновления", callback_data="admin_updates")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
|
||||
@@ -130,6 +130,78 @@ class AdminNotificationService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
return False
|
||||
|
||||
async def send_version_update_notification(
|
||||
self,
|
||||
current_version: str,
|
||||
latest_version,
|
||||
total_updates: int
|
||||
) -> bool:
|
||||
"""Отправляет уведомление о новых обновлениях"""
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
if latest_version.prerelease:
|
||||
update_type = "🧪 ПРЕДВАРИТЕЛЬНАЯ ВЕРСИЯ"
|
||||
type_icon = "🧪"
|
||||
elif latest_version.is_dev:
|
||||
update_type = "🔧 DEV ВЕРСИЯ"
|
||||
type_icon = "🔧"
|
||||
else:
|
||||
update_type = "📦 НОВАЯ ВЕРСИЯ"
|
||||
type_icon = "📦"
|
||||
|
||||
description = latest_version.short_description
|
||||
if len(description) > 200:
|
||||
description = description[:197] + "..."
|
||||
|
||||
message = f"""{type_icon} <b>{update_type} ДОСТУПНА</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
🆕 <b>Новая версия:</b> <code>{latest_version.tag_name}</code>
|
||||
📅 <b>Дата релиза:</b> {latest_version.formatted_date}
|
||||
|
||||
📝 <b>Описание:</b>
|
||||
{description}
|
||||
|
||||
🔢 <b>Всего доступно обновлений:</b> {total_updates}
|
||||
🔗 <b>Репозиторий:</b> https://github.com/{getattr(self, 'repo', 'fr1ngg/remnawave-bedolaga-telegram-bot')}
|
||||
|
||||
ℹ️ Для обновления перезапустите контейнер с новым тегом или обновите код из репозитория.
|
||||
|
||||
⚙️ <i>Автоматическая проверка обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об обновлении: {e}")
|
||||
return False
|
||||
|
||||
async def send_version_check_error_notification(
|
||||
self,
|
||||
error_message: str,
|
||||
current_version: str
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
message = f"""⚠️ <b>ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
❌ <b>Ошибка:</b> {error_message}
|
||||
|
||||
🔄 Следующая попытка через час.
|
||||
⚙️ Проверьте доступность GitHub API и настройки сети.
|
||||
|
||||
⚙️ <i>Система автоматических обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об ошибке проверки версий: {e}")
|
||||
return False
|
||||
|
||||
async def send_balance_topup_notification(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import aiohttp
|
||||
from packaging import version
|
||||
import re
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersionInfo:
|
||||
def __init__(self, tag_name: str, published_at: str, name: str, body: str, prerelease: bool = False):
|
||||
self.tag_name = tag_name
|
||||
self.published_at = datetime.fromisoformat(published_at.replace('Z', '+00:00'))
|
||||
self.name = name or tag_name
|
||||
self.body = body
|
||||
self.prerelease = prerelease
|
||||
self.is_dev = 'dev' in tag_name.lower()
|
||||
|
||||
@property
|
||||
def clean_version(self) -> str:
|
||||
return re.sub(r'^v', '', self.tag_name)
|
||||
|
||||
@property
|
||||
def version_obj(self):
|
||||
"""Объект версии для сравнения"""
|
||||
try:
|
||||
clean_ver = self.clean_version
|
||||
# Обработка dev версий
|
||||
if 'dev' in clean_ver:
|
||||
base_ver = clean_ver.split('-dev')[0]
|
||||
return version.parse(f"{base_ver}.dev")
|
||||
return version.parse(clean_ver)
|
||||
except Exception:
|
||||
return version.parse("0.0.0")
|
||||
|
||||
@property
|
||||
def formatted_date(self) -> str:
|
||||
return self.published_at.strftime('%d.%m.%Y %H:%M')
|
||||
|
||||
@property
|
||||
def short_description(self) -> str:
|
||||
"""Краткое описание релиза"""
|
||||
if not self.body:
|
||||
return "Без описания"
|
||||
|
||||
# Берем первые 150 символов
|
||||
description = self.body.strip()
|
||||
if len(description) > 150:
|
||||
description = description[:147] + "..."
|
||||
|
||||
return description
|
||||
|
||||
|
||||
class VersionService:
|
||||
def __init__(self, bot=None):
|
||||
self.bot = bot
|
||||
self.repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot')
|
||||
self.enabled = getattr(settings, 'VERSION_CHECK_ENABLED', True)
|
||||
self.current_version = self._get_current_version()
|
||||
self.cache_ttl = 3600 # 1 час кеш
|
||||
self._cache: Dict = {}
|
||||
self._last_check: Optional[datetime] = None
|
||||
self._notification_service = None
|
||||
|
||||
def _get_current_version(self) -> str:
|
||||
# Сначала пробуем получить из env переменных (Docker build args)
|
||||
import os
|
||||
current = os.getenv('VERSION', '').strip()
|
||||
|
||||
if current:
|
||||
return current
|
||||
|
||||
# Пробуем получить из git (для dev окружения)
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['git', 'describe', '--tags', '--always'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback версия
|
||||
return "v2.2.3-unknown"
|
||||
|
||||
def set_notification_service(self, notification_service):
|
||||
self._notification_service = notification_service
|
||||
|
||||
async def check_for_updates(self, force: bool = False) -> Tuple[bool, List[VersionInfo]]:
|
||||
if not self.enabled:
|
||||
return False, []
|
||||
|
||||
try:
|
||||
releases = await self._fetch_releases(force)
|
||||
if not releases:
|
||||
return False, []
|
||||
|
||||
current_ver = self._parse_version(self.current_version)
|
||||
newer_releases = []
|
||||
|
||||
for release in releases:
|
||||
release_ver = release.version_obj
|
||||
if release_ver > current_ver:
|
||||
newer_releases.append(release)
|
||||
|
||||
# Сортируем по версии (новые сверху)
|
||||
newer_releases.sort(key=lambda x: x.version_obj, reverse=True)
|
||||
|
||||
has_updates = len(newer_releases) > 0
|
||||
|
||||
if has_updates and not force:
|
||||
await self._send_update_notification(newer_releases)
|
||||
|
||||
return has_updates, newer_releases
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки обновлений: {e}")
|
||||
return False, []
|
||||
|
||||
async def _fetch_releases(self, force: bool = False) -> List[VersionInfo]:
|
||||
if not force and self._cache and self._last_check:
|
||||
if datetime.now() - self._last_check < timedelta(seconds=self.cache_ttl):
|
||||
return self._cache.get('releases', [])
|
||||
|
||||
url = f"https://api.github.com/repos/{self.repo}/releases"
|
||||
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
releases = []
|
||||
|
||||
for release_data in data[:20]: # Берем последние 20 релизов
|
||||
release = VersionInfo(
|
||||
tag_name=release_data['tag_name'],
|
||||
published_at=release_data['published_at'],
|
||||
name=release_data['name'],
|
||||
body=release_data['body'] or '',
|
||||
prerelease=release_data['prerelease']
|
||||
)
|
||||
releases.append(release)
|
||||
|
||||
self._cache['releases'] = releases
|
||||
self._last_check = datetime.now()
|
||||
|
||||
logger.info(f"Получено {len(releases)} релизов из GitHub")
|
||||
return releases
|
||||
else:
|
||||
logger.warning(f"GitHub API вернул статус {response.status}")
|
||||
return []
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Таймаут при запросе к GitHub API")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запроса к GitHub API: {e}")
|
||||
return []
|
||||
|
||||
def _parse_version(self, version_str: str):
|
||||
try:
|
||||
clean_ver = re.sub(r'^v', '', version_str)
|
||||
# Обработка dev версий
|
||||
if 'dev' in clean_ver:
|
||||
base_ver = clean_ver.split('-dev')[0]
|
||||
return version.parse(f"{base_ver}.dev")
|
||||
if 'unknown' in clean_ver:
|
||||
return version.parse("0.0.0")
|
||||
return version.parse(clean_ver)
|
||||
except Exception:
|
||||
return version.parse("0.0.0")
|
||||
|
||||
async def _send_update_notification(self, newer_releases: List[VersionInfo]):
|
||||
if not self._notification_service or not newer_releases:
|
||||
return
|
||||
|
||||
try:
|
||||
# Проверяем, не отправляли ли уже уведомление о последней версии
|
||||
latest_version = newer_releases[0]
|
||||
cache_key = f"notified_{latest_version.tag_name}"
|
||||
|
||||
if self._cache.get(cache_key):
|
||||
return
|
||||
|
||||
# Формируем сообщение
|
||||
await self._notification_service.send_version_update_notification(
|
||||
current_version=self.current_version,
|
||||
latest_version=latest_version,
|
||||
total_updates=len(newer_releases)
|
||||
)
|
||||
|
||||
# Помечаем, что уведомление отправлено
|
||||
self._cache[cache_key] = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об обновлении: {e}")
|
||||
|
||||
async def get_version_info(self) -> Dict:
|
||||
try:
|
||||
has_updates, newer_releases = await self.check_for_updates()
|
||||
all_releases = await self._fetch_releases()
|
||||
|
||||
# Находим текущую версию в списке релизов
|
||||
current_release = None
|
||||
current_ver = self._parse_version(self.current_version)
|
||||
|
||||
for release in all_releases:
|
||||
if release.version_obj == current_ver:
|
||||
current_release = release
|
||||
break
|
||||
|
||||
return {
|
||||
'current_version': self.current_version,
|
||||
'current_release': current_release,
|
||||
'has_updates': has_updates,
|
||||
'newer_releases': newer_releases[:5], # Показываем только 5 последних
|
||||
'total_newer': len(newer_releases),
|
||||
'last_check': self._last_check,
|
||||
'repo_url': f"https://github.com/{self.repo}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения информации о версиях: {e}")
|
||||
return {
|
||||
'current_version': self.current_version,
|
||||
'current_release': None,
|
||||
'has_updates': False,
|
||||
'newer_releases': [],
|
||||
'total_newer': 0,
|
||||
'last_check': None,
|
||||
'repo_url': f"https://github.com/{self.repo}",
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def start_periodic_check(self):
|
||||
if not self.enabled:
|
||||
logger.info("Проверка версий отключена")
|
||||
return
|
||||
|
||||
logger.info(f"Запуск периодической проверки обновлений для {self.repo}")
|
||||
logger.info(f"Текущая версия: {self.current_version}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(3600) # Проверяем каждый час
|
||||
await self.check_for_updates()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Остановка проверки обновлений")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в периодической проверке обновлений: {e}")
|
||||
await asyncio.sleep(300) # При ошибке ждем 5 минут
|
||||
|
||||
def format_version_display(self, version_info: VersionInfo) -> str:
|
||||
status_icon = ""
|
||||
if version_info.prerelease:
|
||||
status_icon = "🧪"
|
||||
elif version_info.is_dev:
|
||||
status_icon = "🔧"
|
||||
else:
|
||||
status_icon = "📦"
|
||||
|
||||
return f"{status_icon} {version_info.tag_name}"
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса
|
||||
version_service = VersionService()
|
||||
@@ -13,6 +13,7 @@ from app.database.database import init_db
|
||||
from app.services.monitoring_service import monitoring_service
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.services.version_service import version_service
|
||||
from app.external.webhook_server import WebhookServer
|
||||
from app.external.yookassa_webhook import start_yookassa_webhook_server
|
||||
from app.database.universal_migration import run_universal_migration
|
||||
@@ -49,6 +50,7 @@ async def main():
|
||||
yookassa_server_task = None
|
||||
monitoring_task = None
|
||||
maintenance_task = None
|
||||
version_check_task = None
|
||||
polling_task = None
|
||||
|
||||
try:
|
||||
@@ -77,7 +79,15 @@ async def main():
|
||||
bot, dp = await setup_bot()
|
||||
|
||||
monitoring_service.bot = bot
|
||||
maintenance_service.set_bot(bot)
|
||||
maintenance_service.set_bot(bot)
|
||||
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
admin_notification_service = AdminNotificationService(bot)
|
||||
version_service.bot = bot
|
||||
version_service.set_notification_service(admin_notification_service)
|
||||
logger.info(f"🔄 Сервис версий настроен для репозитория: {version_service.repo}")
|
||||
logger.info(f"📦 Текущая версия: {version_service.current_version}")
|
||||
|
||||
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
|
||||
|
||||
payment_service = PaymentService(bot)
|
||||
@@ -108,7 +118,13 @@ async def main():
|
||||
logger.info("🔧 Служба техработ уже запущена")
|
||||
maintenance_task = None
|
||||
|
||||
logger.info("🔄 Запуск polling...")
|
||||
if settings.is_version_check_enabled():
|
||||
logger.info("🔄 Запуск сервиса проверки версий...")
|
||||
version_check_task = asyncio.create_task(version_service.start_periodic_check())
|
||||
else:
|
||||
logger.info("ℹ️ Проверка версий отключена")
|
||||
|
||||
logger.info("📄 Запуск polling...")
|
||||
polling_task = asyncio.create_task(dp.start_polling(bot, skip_updates=True))
|
||||
|
||||
logger.info("=" * 50)
|
||||
@@ -117,6 +133,10 @@ async def main():
|
||||
logger.info(f" Tribute: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
if settings.is_yookassa_enabled():
|
||||
logger.info(f" YooKassa: {settings.WEBHOOK_URL}:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
|
||||
logger.info("🔄 Активные фоновые сервисы:")
|
||||
logger.info(f" Мониторинг: {'Включен' if monitoring_task else 'Отключен'}")
|
||||
logger.info(f" Техработы: {'Включен' if maintenance_task else 'Отключен'}")
|
||||
logger.info(f" Проверка версий: {'Включен' if version_check_task else 'Отключен'}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
try:
|
||||
@@ -143,6 +163,14 @@ async def main():
|
||||
if exception:
|
||||
logger.error(f"Служба техработ завершилась с ошибкой: {exception}")
|
||||
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
|
||||
|
||||
if version_check_task and version_check_task.done():
|
||||
exception = version_check_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Сервис проверки версий завершился с ошибкой: {exception}")
|
||||
if settings.is_version_check_enabled():
|
||||
logger.info("🔄 Перезапуск сервиса проверки версий...")
|
||||
version_check_task = asyncio.create_task(version_service.start_periodic_check())
|
||||
|
||||
if polling_task.done():
|
||||
exception = polling_task.exception()
|
||||
@@ -186,6 +214,14 @@ async def main():
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if version_check_task and not version_check_task.done():
|
||||
logger.info("ℹ️ Остановка сервиса проверки версий...")
|
||||
version_check_task.cancel()
|
||||
try:
|
||||
await version_check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if polling_task and not polling_task.done():
|
||||
logger.info("ℹ️ Остановка polling...")
|
||||
polling_task.cancel()
|
||||
|
||||
@@ -26,3 +26,6 @@ python-dateutil==2.8.2
|
||||
pytz==2023.4
|
||||
cryptography>=41.0.0
|
||||
qrcode[pil]==7.4.2
|
||||
|
||||
# Для работы с версиями
|
||||
packaging==23.2
|
||||
|
||||
Reference in New Issue
Block a user