Improve visual
This commit is contained in:
+724
-109
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
"""Баланс переходов пользователей."""
|
"""Баланс переходов пользователей."""
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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],
|
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(
|
||||||
|
|||||||
Reference in New Issue
Block a user