init auth and main function
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Background task manager for async task tracking."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Callable, Coroutine, Any
|
||||
|
||||
from core.constants import TaskStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskInfo:
|
||||
"""Information about a running task."""
|
||||
task_id: str
|
||||
task_type: str
|
||||
coroutine: Coroutine = None
|
||||
status: TaskStatus = TaskStatus.RUNNING
|
||||
created_at: float = field(default_factory=lambda: asyncio.get_running_loop().time())
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class BackgroundTaskManager:
|
||||
"""
|
||||
Manages background async tasks with tracking and cancellation support.
|
||||
|
||||
Features:
|
||||
- Start and track async tasks
|
||||
- Cancel individual or all tasks
|
||||
- Get task status and metadata
|
||||
- Prevent duplicate tasks
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._tasks: Dict[str, asyncio.Task] = {}
|
||||
self._task_info: Dict[str, TaskInfo] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def start_task(
|
||||
self,
|
||||
task_id: str,
|
||||
coro: Coroutine,
|
||||
task_type: str = "unknown",
|
||||
metadata: dict = None
|
||||
) -> bool:
|
||||
"""
|
||||
Start a background task.
|
||||
|
||||
Args:
|
||||
task_id: Unique task identifier
|
||||
coro: Coroutine to run
|
||||
task_type: Type of task
|
||||
metadata: Additional task metadata
|
||||
|
||||
Returns:
|
||||
True if started successfully, False if already running
|
||||
"""
|
||||
async with self._lock:
|
||||
if task_id in self._tasks and not self._tasks[task_id].done():
|
||||
logger.warning(f"Task {task_id} is already running")
|
||||
return False
|
||||
|
||||
task = asyncio.create_task(coro)
|
||||
self._tasks[task_id] = task
|
||||
|
||||
self._task_info[task_id] = TaskInfo(
|
||||
task_id=task_id,
|
||||
task_type=task_type,
|
||||
coroutine=coro,
|
||||
metadata=metadata or {}
|
||||
)
|
||||
|
||||
task.add_done_callback(lambda t, tid=task_id: self._on_task_done(tid, t))
|
||||
|
||||
logger.info(f"Started background task: {task_id} ({task_type})")
|
||||
return True
|
||||
|
||||
def _on_task_done(self, task_id: str, task: asyncio.Task) -> None:
|
||||
"""Callback когда задача завершилась."""
|
||||
try:
|
||||
exc = task.exception()
|
||||
if exc:
|
||||
if isinstance(exc, asyncio.CancelledError):
|
||||
logger.info(f"Task {task_id} cancelled (normal)")
|
||||
# Не считаем ошибкой
|
||||
else:
|
||||
logger.error(f"Task {task_id} failed: {exc}")
|
||||
else:
|
||||
logger.info(f"Task {task_id} completed successfully")
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"Task {task_id} was cancelled")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in task callback: {e}")
|
||||
|
||||
async def cancel_task(self, task_id: str) -> bool:
|
||||
"""
|
||||
Cancel a specific task.
|
||||
|
||||
Args:
|
||||
task_id: Task identifier to cancel
|
||||
|
||||
Returns:
|
||||
True if task was cancelled, False if not found
|
||||
"""
|
||||
async with self._lock:
|
||||
task = self._tasks.get(task_id)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info(f"Task cancelled: {task_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def cancel_all(self) -> int:
|
||||
"""
|
||||
Cancel all running tasks.
|
||||
|
||||
Returns:
|
||||
Number of tasks cancelled
|
||||
"""
|
||||
cancelled = 0
|
||||
|
||||
async with self._lock:
|
||||
for task_id, task in list(self._tasks.items()):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
cancelled += 1
|
||||
|
||||
self._tasks.clear()
|
||||
self._task_info.clear()
|
||||
|
||||
logger.info(f"All tasks cancelled: {cancelled}")
|
||||
return cancelled
|
||||
|
||||
def get_active_tasks(self) -> Dict[str, dict]:
|
||||
"""
|
||||
Get all active tasks with their metadata.
|
||||
|
||||
Returns:
|
||||
Dictionary of active tasks
|
||||
"""
|
||||
active = {}
|
||||
for task_id, task in self._tasks.items():
|
||||
if not task.done():
|
||||
info = self._task_info.get(task_id)
|
||||
active[task_id] = {
|
||||
"info": info.metadata if info else {},
|
||||
"status": TaskStatus.RUNNING.value,
|
||||
"type": info.task_type if info else "unknown",
|
||||
"created_at": info.created_at if info else 0
|
||||
}
|
||||
return active
|
||||
|
||||
@property
|
||||
def active_count(self) -> int:
|
||||
"""Number of currently active tasks."""
|
||||
return sum(1 for t in self._tasks.values() if not t.done())
|
||||
|
||||
def has_active_tasks(self) -> bool:
|
||||
"""Check if there are any active tasks."""
|
||||
return self.active_count > 0
|
||||
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
Менеджер прокси с поддержкой SOCKS5 и HTTP.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Dict
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
from config.settings import settings
|
||||
from core.exceptions import ProxyTimeoutError, NoProxiesAvailableError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProxyType(str, Enum):
|
||||
"""Типы прокси."""
|
||||
SOCKS5 = "socks5"
|
||||
SOCKS4 = "socks4"
|
||||
HTTP = "http"
|
||||
HTTPS = "https"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Proxy:
|
||||
"""Прокси с поддержкой разных типов."""
|
||||
ip: str
|
||||
port: str
|
||||
proxy_type: ProxyType = ProxyType.SOCKS5
|
||||
login: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return f"{self.proxy_type.value}://{self.ip}:{self.port}"
|
||||
|
||||
@property
|
||||
def server(self) -> str:
|
||||
"""URL сервера для Camoufox/Playwright."""
|
||||
protocol = "socks5" if self.proxy_type in [ProxyType.SOCKS5, ProxyType.SOCKS4] else "http"
|
||||
return f"{protocol}://{self.ip}:{self.port}"
|
||||
|
||||
@property
|
||||
def proxy_config(self) -> dict:
|
||||
"""Конфигурация для браузера."""
|
||||
config = {
|
||||
'server': self.server,
|
||||
}
|
||||
if self.login and self.password:
|
||||
config.update({
|
||||
'username': self.login,
|
||||
'password': self.password
|
||||
})
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line: str, default_type: ProxyType = ProxyType.SOCKS5) -> Optional['Proxy']:
|
||||
"""
|
||||
Парсит строку прокси.
|
||||
|
||||
Форматы:
|
||||
- ip:port (SOCKS5 по умолчанию)
|
||||
- ip:port:login:password
|
||||
- socks5://ip:port:login:password
|
||||
- http://ip:port:login:password
|
||||
"""
|
||||
line = line.strip()
|
||||
|
||||
# Определяем тип прокси по префиксу
|
||||
proxy_type = default_type
|
||||
if "://" in line:
|
||||
protocol, rest = line.split("://", 1)
|
||||
protocol = protocol.lower()
|
||||
|
||||
if protocol in ["socks5", "socks"]:
|
||||
proxy_type = ProxyType.SOCKS5
|
||||
elif protocol == "socks4":
|
||||
proxy_type = ProxyType.SOCKS4
|
||||
elif protocol == "http":
|
||||
proxy_type = ProxyType.HTTP
|
||||
elif protocol == "https":
|
||||
proxy_type = ProxyType.HTTPS
|
||||
|
||||
line = rest
|
||||
|
||||
# Парсим части
|
||||
parts = line.split(":")
|
||||
|
||||
if len(parts) == 2:
|
||||
return cls(ip=parts[0], port=parts[1], proxy_type=proxy_type)
|
||||
elif len(parts) == 4:
|
||||
return cls(
|
||||
ip=parts[0],
|
||||
port=parts[1],
|
||||
login=parts[2],
|
||||
password=parts[3],
|
||||
proxy_type=proxy_type
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ProxyManager:
|
||||
"""
|
||||
Менеджер прокси с поддержкой SOCKS5 и HTTP.
|
||||
"""
|
||||
|
||||
def __init__(self, proxy_file: Optional[str] = None):
|
||||
self.proxy_file = proxy_file or settings.PROXY_FILE
|
||||
self.cooldown_time = settings.PROXY_COOLDOWN_TIME
|
||||
self.max_usage = settings.PROXY_MAX_USAGE_BEFORE_COOLDOWN
|
||||
|
||||
self._proxies: List[Proxy] = []
|
||||
self._lock = asyncio.Lock()
|
||||
self._in_use: Dict[str, asyncio.Event] = {}
|
||||
self._usage_count: Dict[str, int] = defaultdict(int)
|
||||
self._cooldown_until: Dict[str, float] = {}
|
||||
self._working_status: Dict[str, bool] = {}
|
||||
|
||||
self._load_proxies()
|
||||
|
||||
def _load_proxies(self) -> None:
|
||||
"""Загружает прокси из файла."""
|
||||
if not os.path.exists(self.proxy_file):
|
||||
logger.warning(f"Proxy file not found: {self.proxy_file}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.proxy_file, 'r', encoding='utf-8') as f:
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in f
|
||||
if line.strip() and not line.startswith('#')
|
||||
]
|
||||
|
||||
loaded = []
|
||||
for line in lines:
|
||||
proxy = Proxy.from_line(line, default_type=ProxyType.SOCKS5)
|
||||
if proxy:
|
||||
self._proxies.append(proxy)
|
||||
self._working_status[proxy.id] = True
|
||||
loaded.append(proxy)
|
||||
|
||||
logger.info(f"Loaded {len(loaded)} proxies from {self.proxy_file}")
|
||||
|
||||
# Статистика по типам
|
||||
type_counts = defaultdict(int)
|
||||
for p in loaded:
|
||||
type_counts[p.proxy_type.value] += 1
|
||||
for ptype, count in type_counts.items():
|
||||
logger.info(f" {ptype}: {count}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load proxies: {e}", exc_info=True)
|
||||
|
||||
def _is_available(self, proxy: Proxy) -> bool:
|
||||
"""Проверка доступности прокси."""
|
||||
proxy_id = proxy.id
|
||||
|
||||
if proxy_id in self._in_use:
|
||||
return False
|
||||
|
||||
if proxy_id in self._cooldown_until:
|
||||
loop = asyncio.get_running_loop()
|
||||
if loop.time() < self._cooldown_until[proxy_id]:
|
||||
return False
|
||||
del self._cooldown_until[proxy_id]
|
||||
|
||||
if self._usage_count[proxy_id] >= self.max_usage:
|
||||
return False
|
||||
|
||||
return self._working_status.get(proxy_id, False)
|
||||
|
||||
async def acquire_proxy(self, timeout: float = 60) -> Optional[Proxy]:
|
||||
"""Получает доступный прокси."""
|
||||
if not self._proxies:
|
||||
return None
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
start_time = loop.time()
|
||||
attempt = 0
|
||||
|
||||
while True:
|
||||
attempt += 1
|
||||
|
||||
async with self._lock:
|
||||
available = [
|
||||
p for p in self._proxies
|
||||
if self._is_available(p)
|
||||
]
|
||||
|
||||
if available:
|
||||
proxy = min(available, key=lambda p: self._usage_count[p.id])
|
||||
|
||||
self._in_use[proxy.id] = asyncio.Event()
|
||||
self._usage_count[proxy.id] += 1
|
||||
|
||||
logger.info(
|
||||
f"Proxy acquired: {proxy.id} "
|
||||
f"(type: {proxy.proxy_type.value}, "
|
||||
f"used: {self._usage_count[proxy.id]}x)"
|
||||
)
|
||||
return proxy
|
||||
|
||||
logger.debug(f"No proxies available (attempt {attempt})")
|
||||
|
||||
elapsed = loop.time() - start_time
|
||||
if elapsed >= timeout:
|
||||
logger.warning(f"Proxy acquire timeout ({timeout}s)")
|
||||
return None
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def release_proxy(self, proxy: Proxy, success: bool = True) -> None:
|
||||
"""Освобождает прокси."""
|
||||
if not proxy:
|
||||
return
|
||||
|
||||
proxy_id = proxy.id
|
||||
|
||||
async with self._lock:
|
||||
if proxy_id in self._in_use:
|
||||
del self._in_use[proxy_id]
|
||||
|
||||
if self._usage_count[proxy_id] >= self.max_usage:
|
||||
loop = asyncio.get_running_loop()
|
||||
self._cooldown_until[proxy_id] = (
|
||||
loop.time() + self.cooldown_time
|
||||
)
|
||||
self._usage_count[proxy_id] = 0
|
||||
|
||||
if not success:
|
||||
self._working_status[proxy_id] = False
|
||||
|
||||
logger.debug(f"Proxy released: {proxy_id}")
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Статистика прокси."""
|
||||
loop = asyncio.get_running_loop()
|
||||
now = loop.time()
|
||||
|
||||
return {
|
||||
"total": len(self._proxies),
|
||||
"working": sum(1 for s in self._working_status.values() if s),
|
||||
"in_use": len(self._in_use),
|
||||
"available": self.available_count,
|
||||
"in_cooldown": sum(1 for t in self._cooldown_until.values() if now < t),
|
||||
}
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self._proxies)
|
||||
|
||||
@property
|
||||
def available_count(self) -> int:
|
||||
working = sum(1 for s in self._working_status.values() if s)
|
||||
in_use = len(self._in_use)
|
||||
in_cooldown = sum(
|
||||
1 for t in self._cooldown_until.values()
|
||||
if asyncio.get_event_loop().time() < t
|
||||
)
|
||||
return working - in_use - in_cooldown
|
||||
|
||||
def has_proxies(self) -> bool:
|
||||
return len(self._proxies) > 0
|
||||
|
||||
def get_socks5_proxies(self) -> List[Proxy]:
|
||||
"""Список SOCKS5 прокси."""
|
||||
return [p for p in self._proxies if p.proxy_type == ProxyType.SOCKS5]
|
||||
|
||||
def get_http_proxies(self) -> List[Proxy]:
|
||||
"""Список HTTP прокси."""
|
||||
return [p for p in self._proxies if p.proxy_type in [ProxyType.HTTP, ProxyType.HTTPS]]
|
||||
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Сохранение и загрузка задач между перезапусками.
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from managers.task_manager import TaskParams
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskStorage:
|
||||
"""Хранилище задач в JSON файле."""
|
||||
|
||||
def __init__(self, file_path: str = "data/tasks.json"):
|
||||
self.file_path = Path(file_path)
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def _params_to_dict(self, params: TaskParams) -> dict:
|
||||
"""Конвертирует TaskParams в словарь."""
|
||||
return {
|
||||
"url": params.url,
|
||||
"task_type": params.task_type,
|
||||
"min_delay": params.min_delay,
|
||||
"max_delay": params.max_delay,
|
||||
"min_reading": params.min_reading,
|
||||
"max_reading": params.max_reading,
|
||||
"max_visits": params.max_visits,
|
||||
"current_visit": params.current_visit,
|
||||
"channel": params.channel,
|
||||
"target_username": params.target_username,
|
||||
"visits_per_link": params.visits_per_link,
|
||||
"monitor_minutes": params.monitor_minutes,
|
||||
"allowed_domains": params.allowed_domains,
|
||||
"paused": params.paused,
|
||||
"completed": params.completed,
|
||||
"stopped": params.stopped,
|
||||
"total_visits": params.total_visits,
|
||||
"successful_visits": params.successful_visits,
|
||||
"links_found": params.links_found,
|
||||
"chat_id": params.chat_id,
|
||||
"started_at": params.started_at.isoformat() if params.started_at else None,
|
||||
}
|
||||
|
||||
def _dict_to_params(self, data: dict) -> TaskParams:
|
||||
"""Конвертирует словарь в TaskParams."""
|
||||
params = TaskParams(
|
||||
url=data.get("url", ""),
|
||||
task_type=data.get("task_type", "visit"),
|
||||
min_delay=data.get("min_delay", 10),
|
||||
max_delay=data.get("max_delay", 30),
|
||||
min_reading=data.get("min_reading", 5),
|
||||
max_reading=data.get("max_reading", 15),
|
||||
max_visits=data.get("max_visits"),
|
||||
current_visit=data.get("current_visit", 0),
|
||||
channel=data.get("channel", ""),
|
||||
target_username=data.get("target_username", "*"),
|
||||
visits_per_link=data.get("visits_per_link", 5),
|
||||
monitor_minutes=data.get("monitor_minutes", 10),
|
||||
allowed_domains=data.get("allowed_domains"),
|
||||
paused=data.get("paused", False),
|
||||
completed=data.get("completed", False),
|
||||
stopped=data.get("stopped", False),
|
||||
total_visits=data.get("total_visits", 0),
|
||||
successful_visits=data.get("successful_visits", 0),
|
||||
links_found=data.get("links_found", 0),
|
||||
chat_id=data.get("chat_id"),
|
||||
)
|
||||
|
||||
if data.get("started_at"):
|
||||
try:
|
||||
params.started_at = datetime.fromisoformat(data["started_at"])
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Failed to parse started_at: {e}")
|
||||
|
||||
return params
|
||||
|
||||
async def save_tasks(self, tasks: Dict[str, TaskParams]) -> None:
|
||||
"""Сохраняет все задачи в файл."""
|
||||
async with self._lock:
|
||||
try:
|
||||
data = {}
|
||||
for task_id, params in tasks.items():
|
||||
data[task_id] = self._params_to_dict(params)
|
||||
|
||||
with open(self.file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"💾 Saved {len(data)} tasks to {self.file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Save error: {e}")
|
||||
|
||||
async def load_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Загружает задачи из файла."""
|
||||
if not self.file_path.exists():
|
||||
logger.info("No saved tasks found")
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(self.file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
tasks = {}
|
||||
for task_id, params_data in data.items():
|
||||
tasks[task_id] = self._dict_to_params(params_data)
|
||||
|
||||
logger.info(f"📂 Loaded {len(tasks)} tasks from {self.file_path}")
|
||||
return tasks
|
||||
except Exception as e:
|
||||
logger.error(f"Load error: {e}")
|
||||
return {}
|
||||
|
||||
async def save_task(self, task_id: str, params: TaskParams) -> None:
|
||||
"""Сохраняет одну задачу (добавляет к существующим)."""
|
||||
tasks = await self.load_tasks()
|
||||
tasks[task_id] = params
|
||||
await self.save_tasks(tasks)
|
||||
|
||||
async def delete_task(self, task_id: str) -> None:
|
||||
"""Удаляет задачу из файла."""
|
||||
tasks = await self.load_tasks()
|
||||
if task_id in tasks:
|
||||
del tasks[task_id]
|
||||
await self.save_tasks(tasks)
|
||||
|
||||
|
||||
class ChatStorage:
|
||||
"""Хранилище привязки стримеров к чатам."""
|
||||
|
||||
def __init__(self, file_path: str = "data/chats.json"):
|
||||
self.file_path = Path(file_path)
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def get_chat_streamers(self, chat_id: int) -> list:
|
||||
"""Получает список стримеров для чата."""
|
||||
data = await self._load()
|
||||
return data.get(str(chat_id), [])
|
||||
|
||||
async def add_streamer(self, chat_id: int, channel: str) -> None:
|
||||
"""Добавляет стримера в чат."""
|
||||
data = await self._load()
|
||||
key = str(chat_id)
|
||||
if key not in data:
|
||||
data[key] = []
|
||||
if channel not in data[key]:
|
||||
data[key].append(channel)
|
||||
await self._save(data)
|
||||
|
||||
async def remove_streamer(self, chat_id: int, channel: str) -> None:
|
||||
"""Удаляет стримера из чата."""
|
||||
data = await self._load()
|
||||
key = str(chat_id)
|
||||
if key in data and channel in data[key]:
|
||||
data[key].remove(channel)
|
||||
await self._save(data)
|
||||
|
||||
async def _load(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) as e:
|
||||
logger.warning(f"Failed to load chats: {e}")
|
||||
return {}
|
||||
|
||||
async def _save(self, data: dict) -> None:
|
||||
async with self._lock:
|
||||
with open(self.file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
Менеджер задач по стримерам.
|
||||
Простой список с возможностью изменять параметры на лету.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
import random
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskParams:
|
||||
"""Динамические параметры задачи."""
|
||||
url: str = ""
|
||||
task_type: str = "visit"
|
||||
|
||||
# Задержки
|
||||
min_delay: int = 10
|
||||
max_delay: int = 30
|
||||
min_reading: int = 5
|
||||
max_reading: int = 15
|
||||
|
||||
# Лимиты
|
||||
max_visits: Optional[int] = None
|
||||
current_visit: int = 0
|
||||
|
||||
# Twitch параметры
|
||||
channel: str = ""
|
||||
target_username: str = ""
|
||||
visits_per_link: int = 5
|
||||
monitor_minutes: int = 10
|
||||
allowed_domains: Optional[List[str]] = None
|
||||
|
||||
# Состояние
|
||||
paused: bool = False
|
||||
skip_next: bool = False
|
||||
force_delay: Optional[int] = None
|
||||
force_reading: Optional[int] = None
|
||||
|
||||
# Статистика
|
||||
started_at: Optional[datetime] = None
|
||||
total_visits: int = 0
|
||||
successful_visits: int = 0
|
||||
links_found: int = 0
|
||||
chat_id: Optional[int] = None
|
||||
|
||||
completed: bool = False # Задача завершена
|
||||
stopped: bool = False # Задача остановлена пользователем
|
||||
|
||||
@property
|
||||
def monitor_duration(self) -> int:
|
||||
return self.monitor_minutes * 60
|
||||
|
||||
def update(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
def get_delay(self) -> int:
|
||||
if self.skip_next:
|
||||
self.skip_next = False
|
||||
return 0
|
||||
if self.force_delay:
|
||||
delay = self.force_delay
|
||||
self.force_delay = None
|
||||
return delay
|
||||
return random.randint(self.min_delay, self.max_delay)
|
||||
|
||||
def get_reading_time(self) -> int:
|
||||
if self.force_reading:
|
||||
reading = self.force_reading
|
||||
self.force_reading = None
|
||||
return reading
|
||||
return random.randint(self.min_reading, self.max_reading)
|
||||
|
||||
def get_status_emoji(self) -> str:
|
||||
if self.stopped:
|
||||
return "🛑"
|
||||
if self.completed:
|
||||
return "✅"
|
||||
if self.paused:
|
||||
return "⏸️"
|
||||
return "🔄"
|
||||
|
||||
def get_status_text(self) -> str:
|
||||
if self.stopped:
|
||||
return "🛑 Остановлена"
|
||||
if self.completed:
|
||||
return "✅ Завершена"
|
||||
if self.paused:
|
||||
return "⏸️ Пауза"
|
||||
return "🔄 Активна"
|
||||
|
||||
|
||||
def get_stats_text(self) -> str:
|
||||
"""Текст статистики."""
|
||||
if self.task_type == "twitch_irc":
|
||||
status = self.get_status_text()
|
||||
duration = ""
|
||||
if self.started_at and (self.completed or self.stopped):
|
||||
delta = datetime.now() - self.started_at
|
||||
duration = f"├ ⏱️ Длилась: {delta.seconds // 60} мин\n"
|
||||
|
||||
# Домены
|
||||
domains_text = ""
|
||||
if self.allowed_domains:
|
||||
domains_text = f"├ 🌐 Домены: {', '.join(self.allowed_domains)}\n"
|
||||
else:
|
||||
domains_text = "├ 🌐 Домены: все\n"
|
||||
|
||||
return (
|
||||
f"📺 **{self.channel}**\n"
|
||||
f"├ Статус: {status}\n"
|
||||
f"├ 🔗 Ссылок найдено: {self.links_found}\n"
|
||||
f"├ 📊 Всего визитов: {self.total_visits}\n"
|
||||
f"├ ✅ Успешно: {self.successful_visits}\n"
|
||||
f"├ ⏱️ Задержка: {self.min_delay}-{self.max_delay}с\n"
|
||||
f"├ 📖 Чтение: {self.min_reading}-{self.max_reading}с\n"
|
||||
f"├ 🔗 На ссылку: {self.visits_per_link} визитов\n"
|
||||
f"├ ⏰ Мониторинг: {self.monitor_minutes} мин\n"
|
||||
f"{domains_text}"
|
||||
f"{duration}"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"🌐 `{self.url[:50]}`\n"
|
||||
f"├ Статус: {self.get_status_text()}\n"
|
||||
f"├ 📊 Визитов: {self.total_visits}\n"
|
||||
f"├ ✅ Успешно: {self.successful_visits}\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""
|
||||
Менеджер задач.
|
||||
Хранит список стримеров и позволяет управлять каждым.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._tasks: Dict[str, TaskParams] = {} # task_id -> params
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def add_task(self, task_id: str, params: TaskParams) -> None:
|
||||
"""Добавляет задачу."""
|
||||
async with self._lock:
|
||||
self._tasks[task_id] = params
|
||||
logger.info(f"➕ Задача: {task_id[:12]} ({params.channel or params.url})")
|
||||
|
||||
async def remove_task(self, task_id: str) -> bool:
|
||||
"""Удаляет задачу."""
|
||||
async with self._lock:
|
||||
if task_id in self._tasks:
|
||||
del self._tasks[task_id]
|
||||
logger.info(f"➖ Задача удалена: {task_id[:12]}")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_task(self, task_id: str) -> Optional[TaskParams]:
|
||||
"""Получает задачу."""
|
||||
async with self._lock:
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
async def update_task(self, task_id: str, **kwargs) -> bool:
|
||||
"""Обновляет параметры задачи."""
|
||||
params = await self.get_task(task_id)
|
||||
if params:
|
||||
params.update(**kwargs)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def complete_task(self, task_id: str) -> bool:
|
||||
"""Пометить задачу как завершенную."""
|
||||
return await self.update_task(task_id, completed=True, paused=True)
|
||||
|
||||
async def stop_task(self, task_id: str) -> bool:
|
||||
"""Пометить задачу как остановленную."""
|
||||
return await self.update_task(task_id, stopped=True, paused=True)
|
||||
|
||||
async def get_active_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Только активные задачи (не завершенные и не остановленные)."""
|
||||
return {
|
||||
tid: p for tid, p in self._tasks.items()
|
||||
if not p.completed and not p.stopped
|
||||
}
|
||||
|
||||
async def get_completed_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Завершенные задачи (для просмотра статистики)."""
|
||||
return {
|
||||
tid: p for tid, p in self._tasks.items()
|
||||
if p.completed or p.stopped
|
||||
}
|
||||
|
||||
|
||||
async def pause_task(self, task_id: str) -> bool:
|
||||
"""Пауза задачи."""
|
||||
return await self.update_task(task_id, paused=True)
|
||||
|
||||
async def resume_task(self, task_id: str) -> bool:
|
||||
"""Снять с паузы."""
|
||||
return await self.update_task(task_id, paused=False)
|
||||
|
||||
async def skip_next_task(self, task_id: str) -> bool:
|
||||
"""Пропустить следующее действие."""
|
||||
return await self.update_task(task_id, skip_next=True)
|
||||
|
||||
async def restart_task(self, task_id: str) -> bool:
|
||||
"""Сбросить счетчики для перезапуска."""
|
||||
return await self.update_task(
|
||||
task_id,
|
||||
current_visit=0,
|
||||
total_visits=0,
|
||||
successful_visits=0,
|
||||
links_found=0
|
||||
)
|
||||
|
||||
async def get_all_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Все задачи."""
|
||||
async with self._lock:
|
||||
return dict(self._tasks)
|
||||
|
||||
async def get_tasks_by_type(self, task_type: str) -> Dict[str, TaskParams]:
|
||||
"""Задачи по типу."""
|
||||
return {
|
||||
tid: p for tid, p in self._tasks.items()
|
||||
if p.task_type == task_type
|
||||
}
|
||||
|
||||
async def get_twitch_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Только Twitch задачи."""
|
||||
return await self.get_tasks_by_type("twitch_irc")
|
||||
|
||||
async def get_visit_tasks(self) -> Dict[str, TaskParams]:
|
||||
"""Только задачи посещения."""
|
||||
return await self.get_tasks_by_type("visit")
|
||||
|
||||
async def get_stats(self) -> dict:
|
||||
"""Общая статистика."""
|
||||
twitch_tasks = await self.get_twitch_tasks()
|
||||
visit_tasks = await self.get_visit_tasks()
|
||||
|
||||
return {
|
||||
"total": len(self._tasks),
|
||||
"twitch": len(twitch_tasks),
|
||||
"visit": len(visit_tasks),
|
||||
"paused": sum(1 for p in self._tasks.values() if p.paused),
|
||||
"active": sum(1 for p in self._tasks.values() if not p.paused),
|
||||
}
|
||||
|
||||
def _get_task_info(self, task_id: str, params: TaskParams) -> str:
|
||||
"""Информация о задаче для отображения."""
|
||||
status = params.get_status_emoji()
|
||||
|
||||
if params.task_type == "twitch_irc":
|
||||
return (
|
||||
f"{status} 📺 **{params.channel}**\n"
|
||||
f" `{task_id[:12]}...`\n"
|
||||
f" 👤 @{params.target_username} | "
|
||||
f"🔗{params.links_found} | "
|
||||
f"📊{params.total_visits}\n"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"{status} 🌐 `{params.url[:40]}`\n"
|
||||
f" `{task_id[:12]}...`\n"
|
||||
f" 📊{params.total_visits} визитов\n"
|
||||
)
|
||||
|
||||
async def format_task_list(self, max_items: int = 15) -> str:
|
||||
"""Форматирует список задач для сообщения."""
|
||||
async with self._lock:
|
||||
if not self._tasks:
|
||||
return "📊 Нет активных задач"
|
||||
|
||||
# Сначала Twitch, потом визиты
|
||||
twitch = await self.get_twitch_tasks()
|
||||
visits = await self.get_visit_tasks()
|
||||
|
||||
async with self._lock:
|
||||
total_tasks = len(self._tasks)
|
||||
text = f"📊 **ЗАДАЧИ: {total_tasks}**\n\n"
|
||||
|
||||
if twitch:
|
||||
text += "📺 **Twitch мониторинг:**\n"
|
||||
for i, (task_id, params) in enumerate(list(twitch.items())[:5]):
|
||||
text += self._get_task_info(task_id, params)
|
||||
if len(twitch) > 5:
|
||||
text += f" _...и еще {len(twitch) - 5}_\n"
|
||||
text += "\n"
|
||||
|
||||
if visits:
|
||||
text += "🌐 **Посещение страниц:**\n"
|
||||
for i, (task_id, params) in enumerate(list(visits.items())[:5]):
|
||||
text += self._get_task_info(task_id, params)
|
||||
if len(visits) > 5:
|
||||
text += f" _...и еще {len(visits) - 5}_\n"
|
||||
|
||||
return text
|
||||
Reference in New Issue
Block a user