diff --git a/.env.example b/.env.example
index b91653b0..d0cb06b8 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/Dockerfile b/Dockerfile
index 6e251300..e5b84d9f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/app/bot.py b/app/bot.py
index 5d215f40..dba6b636 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -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()
diff --git a/app/config.py b/app/config.py
index c5df5e90..fbec3c79 100644
--- a/app/config.py
+++ b/app/config.py
@@ -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:
diff --git a/app/handlers/admin/updates.py b/app/handlers/admin/updates.py
new file mode 100644
index 00000000..53716ff1
--- /dev/null
+++ b/app/handlers/admin/updates.py
@@ -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"""🔄 СИСТЕМА ОБНОВЛЕНИЙ
+
+📦 Текущая версия: {current_version}
+{status_icon} Статус: {status_text}
+
+🔗 Репозиторий: {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"""✅ ОБНОВЛЕНИЯ НЕ НАЙДЕНЫ
+
+📦 Текущая версия: {version_service.current_version}
+🎯 Статус: У вас установлена последняя версия
+
+🔗 Репозиторий: {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} {release.tag_name} • {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"""🆕 НАЙДЕНЫ ОБНОВЛЕНИЯ
+
+📦 Текущая версия: {version_service.current_version}
+🎯 Доступно обновлений: {len(newer_releases)}
+
+📋 Последние версии:
+{updates_text}{more_text}
+
+🔗 Репозиторий: {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"❌ ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ\n\n"
+ f"Не удалось связаться с сервером GitHub.\n"
+ f"Попробуйте позже.\n\n"
+ f"📦 Текущая версия: {version_service.current_version}",
+ 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"📦 ТЕКУЩАЯ ВЕРСИЯ\n\n"
+
+ if current_release:
+ current_info += f"🏷️ Версия: {current_release.tag_name}\n"
+ current_info += f"📅 Дата релиза: {current_release.formatted_date}\n"
+ if current_release.short_description:
+ current_info += f"📝 Описание:\n{current_release.short_description}\n"
+ else:
+ current_info += f"🏷️ Версия: {current_version}\n"
+ current_info += f"ℹ️ Статус: Информация о релизе недоступна\n"
+
+ message_parts = [current_info]
+
+ if has_updates and newer_releases:
+ updates_info = f"\n🆕 ДОСТУПНЫЕ ОБНОВЛЕНИЯ\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} {release.tag_name}\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🔧 СИСТЕМА ОБНОВЛЕНИЙ\n\n"
+ system_info += f"🔗 Репозиторий: {version_service.repo}\n"
+ system_info += f"⚡ Автопроверка: {'Включена' if version_service.enabled else 'Отключена'}\n"
+ system_info += f"🕐 Интервал: Каждый час\n"
+
+ if last_check:
+ system_info += f"🕐 Последняя проверка: {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"❌ ОШИБКА ЗАГРУЗКИ\n\n"
+ f"Не удалось получить информацию о версиях.\n\n"
+ f"📦 Текущая версия: {version_service.current_version}",
+ 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"
+ )
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index 63207d41..05d3aee7 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -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")
diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py
index 3e64a903..7f0bca14 100644
--- a/app/services/admin_notification_service.py
+++ b/app/services/admin_notification_service.py
@@ -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} {update_type} ДОСТУПНА
+
+ 📦 Текущая версия: {current_version}
+ 🆕 Новая версия: {latest_version.tag_name}
+ 📅 Дата релиза: {latest_version.formatted_date}
+
+ 📝 Описание:
+ {description}
+
+ 🔢 Всего доступно обновлений: {total_updates}
+ 🔗 Репозиторий: https://github.com/{getattr(self, 'repo', 'fr1ngg/remnawave-bedolaga-telegram-bot')}
+
+ ℹ️ Для обновления перезапустите контейнер с новым тегом или обновите код из репозитория.
+
+ ⚙️ Автоматическая проверка обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}"""
+
+ 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"""⚠️ ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ
+
+ 📦 Текущая версия: {current_version}
+ ❌ Ошибка: {error_message}
+
+ 🔄 Следующая попытка через час.
+ ⚙️ Проверьте доступность GitHub API и настройки сети.
+
+ ⚙️ Система автоматических обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}"""
+
+ return await self._send_message(message)
+
+ except Exception as e:
+ logger.error(f"Ошибка отправки уведомления об ошибке проверки версий: {e}")
+ return False
async def send_balance_topup_notification(
self,
diff --git a/app/services/version_service.py b/app/services/version_service.py
new file mode 100644
index 00000000..9a7d3fec
--- /dev/null
+++ b/app/services/version_service.py
@@ -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()
diff --git a/main.py b/main.py
index 1b7b5935..c3292b2b 100644
--- a/main.py
+++ b/main.py
@@ -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()
diff --git a/requirements.txt b/requirements.txt
index c8635525..31f3fd5c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -26,3 +26,6 @@ python-dateutil==2.8.2
pytz==2023.4
cryptography>=41.0.0
qrcode[pil]==7.4.2
+
+# Для работы с версиями
+packaging==23.2