Improve visual

This commit is contained in:
Yuriy Yuriev
2026-05-16 15:51:19 +07:00
parent 7d28ba794b
commit 2f163aaa3e
8 changed files with 906 additions and 132 deletions
+711 -96
View File
File diff suppressed because it is too large Load Diff
+28 -3
View File
@@ -94,16 +94,18 @@ class BotApplication:
return return
restored = 0 restored = 0
notify_users: dict = {} # user_id -> chat_id
for task_id, params in saved_tasks.items(): for task_id, params in saved_tasks.items():
if params.stopped: if params.stopped:
continue continue
# Задачи с лимитом времени, завершившиеся нормально — не восстанавливаем
if params.completed and params.monitor_minutes > 0: if params.completed and params.monitor_minutes > 0:
continue continue
# Сбрасываем некорректный completed (баг IRC) для бесконечных задач
if params.completed: if params.completed:
params.completed = False params.completed = False
params.paused = False
# Все восстановленные задачи ставим на паузу
params.paused = True
await self.interface.task_manager.add_task(task_id, params) await self.interface.task_manager.add_task(task_id, params)
@@ -119,6 +121,29 @@ class BotApplication:
metadata={'type': 'twitch_irc', 'channel': params.channel, 'chat_id': params.chat_id} metadata={'type': 'twitch_irc', 'channel': params.channel, 'chat_id': params.chat_id}
) )
restored += 1 restored += 1
elif params.task_type == "user_visit" and params.chat_id:
await self.background_tasks.start_task(
task_id=task_id,
coro=self.interface._run_url_visits(task_id, params, self.bot),
task_type="user_visit",
metadata={'type': 'user_visit', 'url': params.url, 'chat_id': params.chat_id}
)
restored += 1
if params.user_id and params.chat_id:
notify_users[params.user_id] = params.chat_id
# Уведомляем пользователей о рестарте
for uid, chat_id in notify_users.items():
try:
await self.bot.send_message(
chat_id,
"🔄 Бот был перезапущен\n\n"
"Ваши задачи приостановлены.\n"
"Нажмите ▶️ в списке задач для возобновления."
)
except Exception as e:
logger.warning(f"Failed to notify user {uid}: {e}")
logger.info(f"🔄 Restored {restored} tasks") logger.info(f"🔄 Restored {restored} tasks")
+44
View File
@@ -45,6 +45,7 @@ class TaskStorage:
"links_found": params.links_found, "links_found": params.links_found,
"chat_id": params.chat_id, "chat_id": params.chat_id,
"user_id": params.user_id, "user_id": params.user_id,
"visits_percent": params.visits_percent,
"started_at": params.started_at.isoformat() if params.started_at else None, "started_at": params.started_at.isoformat() if params.started_at else None,
} }
@@ -71,6 +72,7 @@ class TaskStorage:
links_found=data.get("links_found", 0), links_found=data.get("links_found", 0),
chat_id=data.get("chat_id"), chat_id=data.get("chat_id"),
user_id=data.get("user_id"), user_id=data.get("user_id"),
visits_percent=data.get("visits_percent", 0.0),
) )
if data.get("started_at"): if data.get("started_at"):
try: try:
@@ -220,6 +222,48 @@ class _IntBalanceStorage:
return await asyncio.to_thread(self._load_sync) return await asyncio.to_thread(self._load_sync)
class ChatHistoryStorage:
"""Хранит ID сообщений бота для очистки чата при рестарте."""
def __init__(self, file_path: str = "data/chat_history.json"):
self.file_path = Path(file_path)
self.file_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = asyncio.Lock()
def _load_sync(self) -> dict:
if not self.file_path.exists():
return {}
try:
with open(self.file_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {}
def _save_sync(self, data: dict) -> None:
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(data, f)
async def add(self, user_id: int, msg_id: int) -> None:
async with self._lock:
data = await asyncio.to_thread(self._load_sync)
key = str(user_id)
ids = data.get(key, [])
ids.append(msg_id)
data[key] = ids[-200:] # храним последние 200
await asyncio.to_thread(self._save_sync, data)
async def get(self, user_id: int) -> list:
async with self._lock:
data = await asyncio.to_thread(self._load_sync)
return data.get(str(user_id), [])
async def clear(self, user_id: int) -> None:
async with self._lock:
data = await asyncio.to_thread(self._load_sync)
data.pop(str(user_id), None)
await asyncio.to_thread(self._save_sync, data)
class BalanceStorage(_IntBalanceStorage): class BalanceStorage(_IntBalanceStorage):
"""Баланс переходов пользователей.""" """Баланс переходов пользователей."""
+4
View File
@@ -36,8 +36,12 @@ class TaskParams:
monitor_minutes: int = 10 monitor_minutes: int = 10
allowed_domains: Optional[List[str]] = None allowed_domains: Optional[List[str]] = None
visits_percent: float = 0.0 # % от зрителей (0 = использовать visits_per_link)
# Состояние # Состояние
paused: bool = False paused: bool = False
stream_offline: bool = False # runtime: стрим оффлайн (не сохраняется)
auto_paused: bool = False # runtime: пауза выставлена автоматически (не сохраняется)
skip_next: bool = False skip_next: bool = False
force_delay: Optional[int] = None force_delay: Optional[int] = None
force_reading: Optional[int] = None force_reading: Optional[int] = None
+1 -1
View File
@@ -84,7 +84,7 @@ class BrowserService:
# Запускаем браузер # Запускаем браузер
if proxy_config: if proxy_config:
async with AsyncCamoufox( async with AsyncCamoufox(
headless=False, headless=True,
geoip=True, geoip=True,
humanize=True, humanize=True,
exclude_addons=[DefaultAddons.UBO], exclude_addons=[DefaultAddons.UBO],
+47
View File
@@ -0,0 +1,47 @@
"""Получение числа зрителей Twitch через публичный GQL эндпоинт."""
import logging
import aiohttp
logger = logging.getLogger(__name__)
_GQL_URL = "https://gql.twitch.tv/gql"
_HEADERS = {
"Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko",
"Content-Type": "application/json",
}
_QUERY = """
query ($login: String!) {
user(login: $login) {
stream {
viewersCount
}
}
}
"""
async def get_viewer_count(channel: str) -> int | None:
"""
Возвращает число зрителей (0 = оффлайн, >0 = онлайн).
Возвращает None при ошибке запроса — статус неизвестен, не менять состояние.
"""
payload = {"query": _QUERY, "variables": {"login": channel.lower()}}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
_GQL_URL,
json=payload,
headers=_HEADERS,
timeout=aiohttp.ClientTimeout(total=8),
) as resp:
data = await resp.json()
stream = data.get("data", {}).get("user", {}).get("stream")
count = stream.get("viewersCount", 0) if stream else 0
logger.info(f"Viewers on {channel}: {count}")
return count
except Exception as e:
logger.warning(f"GQL request failed for {channel}: {e}")
return None # ошибка сети — не трогаем состояние задачи
+48
View File
@@ -0,0 +1,48 @@
"""
Тест получения числа зрителей через Twitch GQL.
Запуск: python test_viewers.py <канал>
"""
import asyncio
import sys
import aiohttp
GQL_URL = "https://gql.twitch.tv/gql"
HEADERS = {
"Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko",
"Content-Type": "application/json",
}
QUERY = """
query ($login: String!) {
user(login: $login) {
stream {
viewersCount
title
game { name }
}
}
}
"""
async def main():
channel = sys.argv[1] if len(sys.argv) > 1 else "shroud"
payload = {"query": QUERY, "variables": {"login": channel.lower()}}
async with aiohttp.ClientSession() as session:
async with session.post(
GQL_URL, json=payload, headers=HEADERS,
timeout=aiohttp.ClientTimeout(total=8)
) as resp:
data = await resp.json()
stream = data.get("data", {}).get("user", {}).get("stream")
if stream:
print(f"Зрителей: {stream['viewersCount']}")
print(f"Игра: {stream.get('game', {}).get('name', '')}")
print(f"Тайтл: {stream.get('title', '')}")
else:
print(f"Канал {channel} оффлайн или не найден")
asyncio.run(main())
+4 -13
View File
@@ -16,23 +16,14 @@ async def send_message_safe(
chat_id: Optional[int], chat_id: Optional[int],
text: str, text: str,
**kwargs **kwargs
) -> None: ):
"""
Safely send message to Telegram.
Args:
bot: Bot instance
chat_id: Target chat ID
text: Message text
**kwargs: Additional arguments for send_message
"""
if not bot or not chat_id: if not bot or not chat_id:
return return None
try: try:
await bot.send_message(chat_id=chat_id, text=text, **kwargs) return await bot.send_message(chat_id=chat_id, text=text, **kwargs)
except Exception as e: except Exception as e:
logger.error(f"Failed to send message: {e}") logger.error(f"Failed to send message: {e}")
return None
async def send_visit_result( async def send_visit_result(