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