mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
268 lines
9.9 KiB
Python
268 lines
9.9 KiB
Python
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
|
||
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 "Без описания"
|
||
|
||
description = self.body.strip()
|
||
if len(description) > 350:
|
||
description = description[:347] + "..."
|
||
|
||
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
|
||
self._cache: Dict = {}
|
||
self._last_check: Optional[datetime] = None
|
||
self._notification_service = None
|
||
|
||
async def get_latest_stable_version(self) -> str:
|
||
try:
|
||
url = f"https://api.github.com/repos/{self.repo}/releases/latest"
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(url) as response:
|
||
if response.status == 200:
|
||
data = await response.json()
|
||
return data['tag_name']
|
||
except Exception:
|
||
pass
|
||
return "UNKNOW"
|
||
|
||
def _get_current_version(self) -> str:
|
||
import os
|
||
|
||
current = os.getenv('VERSION', '').strip()
|
||
|
||
if current:
|
||
if '-' in current and current.startswith('v'):
|
||
base_version = current.split('-')[0]
|
||
if base_version.count('.') == 2:
|
||
return base_version
|
||
return current
|
||
|
||
return "UNKNOW"
|
||
|
||
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]:
|
||
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)
|
||
if 'dev' in clean_ver:
|
||
base_ver = clean_ver.split('-dev')[0]
|
||
return version.parse(f"{base_ver}.dev")
|
||
if 'unknow' in clean_ver.lower():
|
||
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],
|
||
'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)
|
||
|
||
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()
|