This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Тупой ef908e0946 bug fixes
2025-07-06 10:43:05 +03:00

436 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import json
import re
import os
import logging
import random
import argparse
import sys
from typing import Set, Optional, Dict, Any, List, Union
import aiohttp
from pyrogram import Client
from pyrogram.errors import FloodWait
class ColoredFormatter(logging.Formatter):
COLORS = {
"GREY": "\x1b[38;20m",
"BLUE": "\x1b[34;20m",
"GREEN": "\x1b[32;20m",
"YELLOW": "\x1b[33;20m",
"RED": "\x1b[31;20m",
"BOLD_RED": "\x1b[31;1m",
"RESET": "\x1b[0m",
}
def __init__(self, fmt: str):
super().__init__()
self.fmt = fmt
self.FORMATS = {
logging.DEBUG: self.COLORS["GREY"] + self.fmt + self.COLORS["RESET"],
logging.INFO: self.COLORS["BLUE"] + "" + self.fmt + self.COLORS["RESET"],
logging.WARNING: self.COLORS["YELLOW"] + "⚠️ " + self.fmt + self.COLORS["RESET"],
logging.ERROR: self.COLORS["RED"] + "" + self.fmt + self.COLORS["RESET"],
logging.CRITICAL: self.COLORS["BOLD_RED"] + "🔥 " + self.fmt + self.COLORS["RESET"],
"SUCCESS": self.COLORS["GREEN"] + "" + self.fmt + self.COLORS["RESET"],
}
def format(self, record):
log_fmt = self.FORMATS.get(getattr(record, "levelformat", record.levelno))
formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S")
return formatter.format(record)
def setup_logger():
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(ColoredFormatter("%(message)s"))
logger.addHandler(handler)
def success(message, *args, **kws):
logger._log(logging.INFO, message, args, extra={'levelformat': 'SUCCESS'}, **kws)
logger.success = success
return logger
logger = setup_logger()
class Config:
def __init__(self, config_file: str = "config.json", cli_thread_id: Optional[str] = None, reset: bool = False):
self.config_file = config_file
self.cli_thread_id = cli_thread_id
if reset or not self._config_exists_and_is_valid():
self._run_interactive_setup()
with open(self.config_file, 'r', encoding='utf-8') as f:
self._config_data = json.load(f)
logger.success("Конфигурация успешно загружена.")
def _config_exists_and_is_valid(self) -> bool:
if not os.path.exists(self.config_file):
return False
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("api_id") != "YOUR_API_ID"
except (json.JSONDecodeError, KeyError):
return False
def _run_interactive_setup(self):
logger.info("Запускаю мастер первоначальной настройки...")
config = {
"api_id": input("Введите ваш API ID: "),
"api_hash": input("Введите ваш API Hash: "),
"phone_number": input("Введите ваш номер телефона (в международном формате, +...): "),
"lolz_token": input("Введите ваш Lolzteam API токен: "),
"forum_thread_id": input("Введите ID темы на форуме для отслеживания: "),
"stars_count": 3,
"check_interval": 30,
"api_delay": 5,
"max_retries": 3,
"processed_posts_file": "processed_posts.json",
"enable_reply": True,
"reply_templates": ["Готово! Отправил звезды. ⭐", "Выполнено.", "Сделал.", "+rep"],
"skip_posts_with_comments": True,
}
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
logger.success(f"Конфигурация сохранена в файл '{self.config_file}'.")
logger.info("Теперь перезапустите скрипт для входа в аккаунт Telegram.")
sys.exit()
def __getattr__(self, name: str) -> Any:
if name == "forum_thread_id" and self.cli_thread_id:
return self.cli_thread_id
return self._config_data.get(name, True if name == "skip_posts_with_comments" else None)
class ProcessedPostsManager:
def __init__(self, file_path: str):
self.file_path = file_path
self.processed_posts: Set[int] = self._load()
def _load(self) -> Set[int]:
try:
with open(self.file_path, 'r', encoding='utf-8') as f:
return set(json.load(f))
except (FileNotFoundError, json.JSONDecodeError):
return set()
def _save(self):
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(list(self.processed_posts), f, indent=4)
def is_processed(self, post_id: int) -> bool:
return post_id in self.processed_posts
def mark_processed(self, post_id: int):
self.processed_posts.add(post_id)
self._save()
def add_existing_posts(self, post_ids: List[int]):
for post_id in post_ids:
self.processed_posts.add(post_id)
self._save()
class LolzAPI:
def __init__(self, token: str):
self.base_url = "https://prod-api.lolz.live"
self.headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
async def _request(self, method: str, endpoint: str, is_retry: bool = False, **kwargs) -> Optional[Dict[str, Any]]:
url = f"{self.base_url}{endpoint}"
try:
async with aiohttp.ClientSession() as session:
async with session.request(method, url, headers=self.headers, **kwargs) as response:
if response.status == 200:
return await response.json()
error_text = await response.text()
if response.status == 403 and "Необходимо подождать" in error_text and not is_retry:
logger.warning("Обнаружен флуд-контроль API. Ожидание 3 секунды перед повторной попыткой...")
await asyncio.sleep(3)
return await self._request(method, endpoint, is_retry=True, **kwargs)
if response.status == 429:
logger.warning("Превышен лимит запросов к API, ожидание 10 секунд...")
await asyncio.sleep(10)
else:
logger.error(f"Ошибка API {response.status} для {url}: {error_text}")
return None
except aiohttp.ClientError as e:
logger.error(f"Ошибка соединения с API: {e}")
return None
async def get_thread_posts(self, thread_id: Union[str, int], start_page: int = 1) -> List[Dict[str, Any]]:
all_posts = []
page = start_page
while True:
params = {"thread_id": thread_id, "page": page, "order": "post_date_reverse"}
data = await self._request("GET", "/posts", params=params)
posts = data.get("posts", []) if data else []
if not posts:
break
all_posts.extend(posts)
logger.info(f"Получено {len(posts)} постов из темы {thread_id} на странице {page}.")
page += 1
await asyncio.sleep(1)
if all_posts:
logger.info(f"Всего получено {len(all_posts)} постов из темы {thread_id}.")
else:
logger.info(f"Постов в теме {thread_id} не найдено.")
return all_posts
async def get_all_thread_posts(self, thread_id: Union[str, int]) -> List[Dict[str, Any]]:
all_posts = []
page = 1
while True:
params = {"thread_id": thread_id, "page": page}
data = await self._request("GET", "/posts", params=params)
posts = data.get("posts", []) if data else []
if not posts:
break
all_posts.extend(posts)
logger.info(f"Получено {len(posts)} постов из темы {thread_id} на странице {page}.")
page += 1
await asyncio.sleep(1)
return all_posts
async def get_post_comments(self, post_id: int) -> List[Dict[str, Any]]:
params = {"post_id": post_id}
data = await self._request("GET", "/posts/comments", params=params)
if data and "comments" in data:
return data["comments"]
return []
async def has_comments(self, post_id: int) -> bool:
comments = await self.get_post_comments(post_id)
return len(comments) > 0
async def create_comment(self, post_id: int, comment_body: str) -> bool:
payload = {"comment_body": comment_body}
logger.info(f"Публикую комментарий к посту {post_id}...")
response = await self._request("POST", f"/posts/{post_id}/comments", json=payload)
if response and response.get("comment"):
logger.success(f"Комментарий к посту {post_id} успешно опубликован.")
return True
else:
logger.error(f"Не удалось опубликовать комментарий к посту {post_id}.")
return False
class TelegramLinkExtractor:
@staticmethod
def extract(text: str) -> List[str]:
patterns = [
r'https?://(?:www\.)?(?:t\.me|telegram\.me)/([a-zA-Z0-9_]+(?:/\d+)?)',
r'\[MEDIA=telegram\]([a-zA-Z0-9_]+(?:/\d+)?)\[/MEDIA\]',
r'data-telegram-post="([a-zA-Z0-9_]+/\d+)"'
]
all_matches = {f"https://t.me/{match}" for p in patterns for match in re.findall(p, text, re.I)}
return list(all_matches)
@staticmethod
def parse(link: str) -> Optional[tuple[str, Optional[int]]]:
match = re.search(r't\.me/([^/]+)(?:/(\d+))?', link)
if match:
channel = match.group(1)
message_id = int(match.group(2)) if match.group(2) else None
return channel, message_id
return None
class TelegramStarsBot:
SESSION_NAME = "stars_bot_session"
def __init__(self, config: Config):
self.config = config
self.lolz_api = LolzAPI(config.lolz_token)
self.processed_manager = ProcessedPostsManager(config.processed_posts_file)
self.client: Optional[Client] = None
self.start_page: int = 1
async def parse_existing_posts(self):
logger.info("Начинаю парсинг всех существующих постов в теме...")
all_posts = await self.lolz_api.get_all_thread_posts(self.config.forum_thread_id)
if all_posts:
post_ids = [post.get("post_id") for post in all_posts if post.get("post_id")]
self.processed_manager.add_existing_posts(post_ids)
logger.success(f"Добавлено {len(post_ids)} постов в список обработанных.")
else:
logger.info("Не найдено постов для добавления в обработанные.")
async def send_stars_reaction(self, channel: str, message_id: Optional[int] = None) -> bool:
if not hasattr(self.client, 'send_paid_reaction'):
logger.error("Платные реакции недоступны. Отправка 'звезд' невозможна.")
return False
try:
for attempt in range(self.config.max_retries):
try:
if message_id is None:
async for message in self.client.get_chat_history(f"@{channel}", limit=1):
message_id = message.id
break
if message_id is None:
logger.error(f"Не удалось найти сообщения в канале @{channel}")
return False
await self.client.send_paid_reaction(f"@{channel}", message_id, self.config.stars_count)
logger.success(f"Отправлено {self.config.stars_count} звезд в @{channel}/{message_id}")
return True
except FloodWait as e:
logger.warning(f"FloodWait: необходимо подождать {e.x + 2} секунд.")
await asyncio.sleep(e.x + 2)
except Exception as e:
logger.error(f"Попытка {attempt + 1} отправки звезд не удалась: {e}")
if attempt < self.config.max_retries - 1:
await asyncio.sleep(3 * (attempt + 1))
return False
except Exception as e:
logger.error(f"Критическая ошибка при отправке звезд: {e}")
return False
async def _process_single_post(self, post: Dict[str, Any]):
post_id = post.get("post_id")
post_user_id = post.get("poster_user_id")
if not post_id or self.processed_manager.is_processed(post_id):
return
logger.info(f"Найден новый пост для обработки: ID {post_id}")
if self.config.skip_posts_with_comments:
if await self.lolz_api.has_comments(post_id):
logger.info(f"Пост {post_id} уже имеет комментарии. Пропускаю обработку.")
self.processed_manager.mark_processed(post_id)
return
post_content = post.get('post_body_html') or post.get('post_body')
if not post_content:
logger.warning(f"У поста {post_id} отсутствует содержимое. Пропускаем.")
self.processed_manager.mark_processed(post_id)
return
links = TelegramLinkExtractor.extract(post_content)
if not links:
logger.info(f"В посте {post_id} не найдено ссылок Telegram.")
self.processed_manager.mark_processed(post_id)
return
successful_reactions = 0
for link in links:
parsed_link = TelegramLinkExtractor.parse(link)
if parsed_link:
channel, message_id = parsed_link
if await self.send_stars_reaction(channel, message_id):
successful_reactions += 1
await asyncio.sleep(1)
if successful_reactions > 0 and self.config.enable_reply:
await asyncio.sleep(self.config.api_delay)
reply_message = random.choice(self.config.reply_templates)
if post_user_id:
reply_message = f"[userids={post_user_id};align=left]{reply_message}[/userids]"
await self.lolz_api.create_comment(post_id, reply_message)
logger.info(f"Пост {post_id} полностью обработан.")
self.processed_manager.mark_processed(post_id)
async def _main_loop(self):
while True:
try:
logger.info(f"Проверка новых постов в теме {self.config.forum_thread_id} начиная со страницы {self.start_page}...")
posts = await self.lolz_api.get_thread_posts(self.config.forum_thread_id, self.start_page)
if posts:
for post in reversed(posts):
await self._process_single_post(post)
else:
logger.info("Новых постов для обработки не найдено.")
logger.info(f"Ожидание {self.config.check_interval} секунд...")
await asyncio.sleep(self.config.check_interval)
except KeyboardInterrupt:
logger.info("Получен сигнал прерывания (Ctrl+C).")
break
except Exception as e:
logger.exception(f"Критическая ошибка в главном цикле: {e}")
await asyncio.sleep(self.config.check_interval)
async def start(self):
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
if is_first_login:
logger.info("Сессия Telegram не найдена. Запускаю процесс входа...")
else:
choice = input("1 - Начать мониторинг\n2 - Спарсить все существующие посты\nВыберите действие (1 или 2): ")
if choice == "2":
logger.info("Выбран режим парсинга существующих постов.")
self.client = Client(self.SESSION_NAME, self.config.api_id, self.config.api_hash)
await self.client.start()
await self.parse_existing_posts()
await self.client.stop()
logger.success("Парсинг завершен. Все существующие посты добавлены в обработанные.")
return
start_page_input = input("Введите номер страницы для начала проверки (или нажмите Enter для проверки с первой страницы): ")
try:
self.start_page = int(start_page_input) if start_page_input.strip() else 1
if self.start_page < 1:
logger.error("Номер страницы должен быть положительным. Использую первую страницу.")
self.start_page = 1
except ValueError:
logger.error("Введено некорректное значение. Использую первую страницу.")
self.start_page = 1
self.client = Client(self.SESSION_NAME, self.config.api_id, self.config.api_hash)
try:
await self.client.start()
logger.success("Клиент Telegram успешно запущен.")
if is_first_login:
logger.success("Аккаунт Telegram успешно подключен.")
logger.info("Пожалуйста, перезапустите скрипт для начала работы.")
return
logger.info("=" * 40)
logger.info(f"Комментарии на форуме: {'ВКЛЮЧЕНЫ' if self.config.enable_reply else 'ВЫКЛЮЧЕНЫ'}")
logger.info(f"Пропуск постов с комментариями: {'ВКЛЮЧЕН' if self.config.skip_posts_with_comments else 'ВЫКЛЮЧЕН'}")
logger.info(f"Проверка начинается со страницы: {self.start_page}")
logger.info("Бот в работе. Для остановки нажмите Ctrl+C.")
logger.info("=" * 40)
await self._main_loop()
finally:
if self.client and self.client.is_connected:
await self.client.stop()
logger.info("Бот остановлен.")
def main():
parser = argparse.ArgumentParser(description='Telegram Stars Bot для Lolzteam')
parser.add_argument('--thread-id', type=str, help='ID темы форума (переопределяет config.json)')
parser.add_argument('--reset-config', action='store_true', help='Запустить мастер настройки заново')
args = parser.parse_args()
try:
config = Config(cli_thread_id=args.thread_id, reset=args.reset_config)
bot = TelegramStarsBot(config)
asyncio.run(bot.start())
except (ValueError, TypeError) as e:
logger.critical(f"Ошибка в конфигурации или при запуске: {e}")
except Exception as e:
logger.critical(f"Фатальная ошибка при запуске бота: {e}")
if __name__ == "__main__":
main()