Improve visual
This commit is contained in:
+711
-96
File diff suppressed because it is too large
Load Diff
@@ -94,16 +94,18 @@ class BotApplication:
|
||||
return
|
||||
|
||||
restored = 0
|
||||
notify_users: dict = {} # user_id -> chat_id
|
||||
|
||||
for task_id, params in saved_tasks.items():
|
||||
if params.stopped:
|
||||
continue
|
||||
# Задачи с лимитом времени, завершившиеся нормально — не восстанавливаем
|
||||
if params.completed and params.monitor_minutes > 0:
|
||||
continue
|
||||
# Сбрасываем некорректный completed (баг IRC) для бесконечных задач
|
||||
if params.completed:
|
||||
params.completed = False
|
||||
params.paused = False
|
||||
|
||||
# Все восстановленные задачи ставим на паузу
|
||||
params.paused = True
|
||||
|
||||
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}
|
||||
)
|
||||
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")
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ class TaskStorage:
|
||||
"links_found": params.links_found,
|
||||
"chat_id": params.chat_id,
|
||||
"user_id": params.user_id,
|
||||
"visits_percent": params.visits_percent,
|
||||
"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),
|
||||
chat_id=data.get("chat_id"),
|
||||
user_id=data.get("user_id"),
|
||||
visits_percent=data.get("visits_percent", 0.0),
|
||||
)
|
||||
if data.get("started_at"):
|
||||
try:
|
||||
@@ -220,6 +222,48 @@ class _IntBalanceStorage:
|
||||
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):
|
||||
"""Баланс переходов пользователей."""
|
||||
|
||||
|
||||
@@ -36,8 +36,12 @@ class TaskParams:
|
||||
monitor_minutes: int = 10
|
||||
allowed_domains: Optional[List[str]] = None
|
||||
|
||||
visits_percent: float = 0.0 # % от зрителей (0 = использовать visits_per_link)
|
||||
|
||||
# Состояние
|
||||
paused: bool = False
|
||||
stream_offline: bool = False # runtime: стрим оффлайн (не сохраняется)
|
||||
auto_paused: bool = False # runtime: пауза выставлена автоматически (не сохраняется)
|
||||
skip_next: bool = False
|
||||
force_delay: Optional[int] = None
|
||||
force_reading: Optional[int] = None
|
||||
|
||||
@@ -84,7 +84,7 @@ class BrowserService:
|
||||
# Запускаем браузер
|
||||
if proxy_config:
|
||||
async with AsyncCamoufox(
|
||||
headless=False,
|
||||
headless=True,
|
||||
geoip=True,
|
||||
humanize=True,
|
||||
exclude_addons=[DefaultAddons.UBO],
|
||||
|
||||
@@ -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 # ошибка сети — не трогаем состояние задачи
|
||||
@@ -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
@@ -16,23 +16,14 @@ async def send_message_safe(
|
||||
chat_id: Optional[int],
|
||||
text: str,
|
||||
**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:
|
||||
return
|
||||
|
||||
return None
|
||||
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:
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def send_visit_result(
|
||||
|
||||
Reference in New Issue
Block a user