From f37977e88f348b5ad2b2943a27f474859d6f9804 Mon Sep 17 00:00:00 2001 From: Open WebUI <15+open-webui@noreply.sakamoto.best> Date: Thu, 30 Jul 2026 01:02:41 +0000 Subject: [PATCH 1/8] feat: Rewrite README.md with improved structure, Docker-first instructions, and correct repo URLs --- README.md | 232 ++++++++++++++++++++++++------------------------------ 1 file changed, 103 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index cb98820..c53e1ed 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,110 @@ -# ⭐ Auto-Stars +# ⭐ Auto-Stars-Improved -Бот автоматически отправляет Telegram Stars пользователям, которые оставляют посты в указанной теме на Lolz.team. +Бот для автоматической отправки Telegram Stars пользователям, которые оставляют посты в указанной теме на форуме Lolz.team. -## Как это работает +## Возможности -Бот следит за выбранной темой на форуме. Когда кто-то публикует новый пост — бот отправляет ему звёзды через Telegram. Опционально может оставлять комментарий под постом. - -## Установка - -### Вариант 1 — Docker (рекомендуется) - -**1. Клонируйте репозиторий** - -```bash -git clone https://github.com/ваш-ник/auto-stars -cd auto-stars -``` - -**2. Создайте `.env`** на основе примера и заполните его: - -```bash -cp examples/.env.example .env -``` - -**3. Авторизуйтесь в Telegram** - -Pyrogram требует интерактивного входа при первом запуске. Выполните его локально: - -```bash -pip install -r requirements.txt -python main.py -``` - -Введите номер телефона и код подтверждения. После успешного входа бот создаст файл `storage/stars_bot.session` и завершится с сообщением о необходимости перезапуска. После этого Docker будет использовать готовую сессию. - -**4. Запустите через Docker Compose** - -```bash -docker compose -f examples/docker-compose.yml up -d -``` - -Данные сессии и логи хранятся в `./storage` и `./logs` на хосте (или по путям `LOGS_PATH` / `STORAGE_PATH` из `.env`). - -**5. Просмотр логов** - -```bash -docker compose -f examples/docker-compose.yml logs -f -``` - ---- - -### Вариант 2 — Запуск напрямую - -**1. Клонируйте репозиторий** - -```bash -git clone https://github.com/ваш-ник/auto-stars -cd auto-stars -``` - -**2. Установите зависимости** - -```bash -pip install -r requirements.txt -``` - -**3. Создайте файл `.env`** на основе `.env.example` и заполните его (см. раздел ниже). - -**4. Запустите бота** - -```bash -python main.py -``` - -При первом запуске Pyrogram попросит войти в Telegram-аккаунт — введите номер телефона и код подтверждения. - -## Настройка `.env` - -```env -# Telegram API — получить на https://my.telegram.org -API_ID=ваш_api_id -API_HASH=ваш_api_hash - -# Lolz.team — токен API из настроек профиля -LOLZ_TOKEN=ваш_токен - -# ID темы на форуме, за которой следит бот -LOLZ_THREAD_ID=123456 - -# Сколько звёзд отправлять за каждый пост -STARS_COUNT=1 - -# Режим запуска: -# 1 — мониторинг новых постов в реальном времени -# 2 — обработать все существующие посты в теме -DEFAULT_CHOICE=2 - -# С какой страницы начинать (обычно 1) -START_PAGE=1 - -# Как часто проверять новые посты (в секундах, минимум ~0.2) -CHECK_INTERVAL=30 - -# Задержка между запросами к API (в секундах, минимум 2) -API_DELAY=5 - -# Количество повторных попыток при ошибке -MAX_RETRIES=3 - -# Оставлять ли комментарий под постом -ENABLE_REPLY=true - -# Пропускать посты, под которыми уже есть комментарий -SKIP_COMMENTED=true -``` - -## Шаблоны ответов - -Если `ENABLE_REPLY=true`, бот будет комментировать посты. Тексты берутся из файла `storage/replies.txt`. - -Каждая строка — отдельный шаблон. Если в строке несколько вариантов через `||`, бот выберет один из них случайно: - -``` -:catdance: Отправил звёзды || :animeyes2: Отправил -:sadge: Тут может быть реклама || ​Реклама казика :animeyessir: -:sigma: Это третье сообщение -``` - -Если файл отсутствует или пустой — бот использует `✓` по умолчанию. +- Автоматический мониторинг темы на Lolz.team +- Отправка Telegram Stars за каждый новый пост +- Автоматические ответы с поддержкой шаблонов (включая случайный выбор) +- Полная поддержка Docker (рекомендуется) +- CI/CD через Gitea Actions (сборка и публикация образов) +- Уведомления администратору через Telegram +- Гибкая настройка через `.env` +- Логирование и сохранение сессии ## Требования - Python 3.10+ -- Аккаунт Telegram с доступом к [my.telegram.org](https://my.telegram.org) -- API-токен [Lolz.team](https://lolz.team) +- Аккаунт Telegram (для получения `API_ID` и `API_HASH`) +- API-токен Lolz.team +- Docker (рекомендуется) или Python окружение + +## Быстрый старт (Docker — рекомендуется) + +1. Клонируйте репозиторий: + ```bash + git clone https://git.sakamoto.best/Lolz/Auto-Stars-Improved.git + cd Auto-Stars-Improved + ``` + +2. Скопируйте пример конфигурации: + ```bash + cp examples/.env.example .env + ``` + +3. Отредактируйте `.env` и заполните необходимые значения (`API_ID`, `API_HASH`, `LOLZ_TOKEN`, `LOLZ_THREAD_ID` и т.д.). + +4. Запустите: + ```bash + docker compose -f examples/docker-compose.yml up -d + ``` + +5. Посмотрите логи: + ```bash + docker compose -f examples/docker-compose.yml logs -f + ``` + +> При первом запуске (если используете Pyrogram напрямую) потребуется интерактивная авторизация в Telegram. + +## Конфигурация + +Все настройки находятся в файле `.env`. + +Пример конфигурации лежит здесь: +- `examples/.env.example` + +**Основные параметры:** + +| Переменная | Описание | По умолчанию | +|-----------------------|-----------------------------------------------|--------------| +| `API_ID` / `API_HASH` | Данные Telegram API | - | +| `LOLZ_TOKEN` | Токен API Lolz.team | - | +| `LOLZ_THREAD_ID` | ID темы на форуме | - | +| `STARS_COUNT` | Сколько звёзд отправлять за пост | 1 | +| `DEFAULT_CHOICE` | 1 — реал-тайм, 2 — обработать все посты | 2 | +| `ENABLE_REPLY` | Отправлять ли комментарии | true | +| `SKIP_COMMENTED` | Пропускать посты, на которые уже отвечали | true | + +## Шаблоны ответов + +Если `ENABLE_REPLY=true`, бот будет отвечать под постами. + +Шаблоны хранятся в файле `storage/replies.txt`. + +**Формат:** +- Каждая строка — отдельный шаблон +- Можно использовать `||` для случайного выбора + +**Пример:** +``` +:catdance: Отправил звёзды || :animeyes2: Отправил +:sadge: Тут может быть реклама +``` + +Если файл пустой — используется `✓`. + +## Запуск без Docker + +```bash +pip install -r requirements.txt +cp examples/.env.example .env +# отредактируйте .env +python main.py +``` + +## Разработка + +Проект использует Gitea Actions для автоматической сборки Docker-образов. + +- `main` ветка — стабильные сборки +- `dev` ветка — тестовые сборки + +Для локальной сборки: +```bash +docker build -t auto-stars . +``` + +## Лицензия + +MIT \ No newline at end of file From 61527aa4c5a892731dbbf74e88e75479505595c7 Mon Sep 17 00:00:00 2001 From: Tom Riddle <1+root@noreply.sakamoto.best> Date: Thu, 30 Jul 2026 01:04:49 +0000 Subject: [PATCH 2/8] Update README.md --- README.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index c53e1ed..14fdf53 100644 --- a/README.md +++ b/README.md @@ -91,20 +91,4 @@ pip install -r requirements.txt cp examples/.env.example .env # отредактируйте .env python main.py -``` - -## Разработка - -Проект использует Gitea Actions для автоматической сборки Docker-образов. - -- `main` ветка — стабильные сборки -- `dev` ветка — тестовые сборки - -Для локальной сборки: -```bash -docker build -t auto-stars . -``` - -## Лицензия - -MIT \ No newline at end of file +``` \ No newline at end of file From 958b00a8f3ea57aac9674b876da3888291fe409b Mon Sep 17 00:00:00 2001 From: Tom Riddle <1+root@noreply.sakamoto.best> Date: Thu, 30 Jul 2026 01:05:25 +0000 Subject: [PATCH 3/8] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 14fdf53..c97ae9f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,15 @@ docker compose -f examples/docker-compose.yml logs -f ``` +## Запуск без Docker + +```bash +pip install -r requirements.txt +cp examples/.env.example .env +# отредактируйте .env +python main.py +``` + > При первом запуске (если используете Pyrogram напрямую) потребуется интерактивная авторизация в Telegram. ## Конфигурация @@ -83,12 +92,3 @@ ``` Если файл пустой — используется `✓`. - -## Запуск без Docker - -```bash -pip install -r requirements.txt -cp examples/.env.example .env -# отредактируйте .env -python main.py -``` \ No newline at end of file From c59a8b9a4722e0eec86e4a5028527d940921a08b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 30 Jul 2026 08:20:37 +0500 Subject: [PATCH 4/8] feat(config): include default ad reply --- config.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index ed32d90..20157ef 100644 --- a/config.py +++ b/config.py @@ -13,6 +13,14 @@ class Config: logger.info("Конфигурация успешно загружена.") def _load_replies(self, file_path: str = "storage/replies.txt"): + script_ad_reply = [ + o.strip() + for o in str( + "Скрипт для авто-раздачи :da: [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad] || Сиськи, сливы 18+ [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad] || Твой помощник для авто-раздачи :animeyessir: [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad]" + ).split(" || ") + ] + default_replies = [["✓"], script_ad_reply] + try: reply_lines = [] with open(file_path, "r", encoding="utf-8") as replies: @@ -24,12 +32,14 @@ class Config: options = [o.strip() for o in line.split(" || ")] else: options = [line] - reply_lines.append(options) - return reply_lines if reply_lines else [["✓"]] + reply_lines.append(options) + reply_lines.append(script_ad_reply) + + return reply_lines if reply_lines else default_replies except FileNotFoundError: - return [["✓"]] + return default_replies def _load(self): load_dotenv() From 03c463b87c3141269f29af95631d68a4f2b4adec Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 03:57:06 +0500 Subject: [PATCH 5/8] refactor(notification_bot): use ParseMode enum for bot default --- modules/notification_bot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/notification_bot.py b/modules/notification_bot.py index 09e0d33..8c7e3cb 100644 --- a/modules/notification_bot.py +++ b/modules/notification_bot.py @@ -1,8 +1,9 @@ import logging from aiogram import Bot -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.enums import ParseMode from aiogram.client.default import DefaultBotProperties +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from config import Config from typing import Dict, Any, List @@ -18,7 +19,7 @@ class NotificationBot: self.bot: Bot = Bot( token=config.bot_token, default=DefaultBotProperties( - parse_mode="HTML", + parse_mode=ParseMode.HTML, disable_notification=True, link_preview_is_disabled=True, ), From b557ff0403d8c6624f2e2c2fd8306ffaf424a79c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 04:00:20 +0500 Subject: [PATCH 6/8] feat(bump): add auto thread bumping --- .env.example | 1 + config.py | 15 +++++++++++---- modules/lolz.py | 36 ++++++++++++++++++++++++++++++++++++ modules/stars_bot.py | 41 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index d8d5d99..660e7d9 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,4 @@ MAX_RETRIES=3 ENABLE_REPLY=true SKIP_COMMENTED=true +BUMP_CHECK_INTERVAL=3600 diff --git a/config.py b/config.py index 20157ef..470ce1d 100644 --- a/config.py +++ b/config.py @@ -88,6 +88,10 @@ class Config: os.getenv(key="SKIP_COMMENTED", default="true") ).lower() == "true" + bump_check_interval = max( + float(os.getenv(key="BUMP_CHECK_INTERVAL", default=3600)), 3600 + ) # 1 поднятие/3600с (1ч) = минимум + return { # Pyrogram "api_id": api_id, @@ -101,14 +105,17 @@ class Config: "lolz_thread_id": lolz_thread_id, # Stars "stars_count": stars_count, - # Misc - "default_choice": default_choice, + # Posts "start_page": start_page, + "enable_reply": enable_reply, "check_interval": check_interval, + "skip_commented": skip_commented, + # Misc "api_delay": api_delay, "max_retries": max_retries, - "enable_reply": enable_reply, - "skip_commented": skip_commented, + "default_choice": default_choice, + # Bump + "bump_check_interval": bump_check_interval, } except: diff --git a/modules/lolz.py b/modules/lolz.py index e815ac5..27993a8 100644 --- a/modules/lolz.py +++ b/modules/lolz.py @@ -1,3 +1,4 @@ +from concurrent.futures import thread import logging import asyncio @@ -11,11 +12,46 @@ class Lolz: def __init__(self, token: str): try: self.client = Forum(token=token, timeout=15) + + # ! Не рекомендуется включить этот логгер, + # ! ибо логи занимают очень много места в экране при этом логгере # self.client.settings.logger.enable() except: raise + async def get_thread(self, thread_id: Union[str, int]) -> Dict[str, Any]: + logger.debug(f"Запрашиваю данные темы {thread_id}...") + response = await self.client.threads.get(thread_id=thread_id) + thread = (response.json()).get("thread", {}) + + if thread == {}: + logger.warning(f"Тема {thread_id} не найдена или вернула пустой ответ.") + + return thread + + async def can_bump( + self, thread_id: Optional[int] = None, thread: Dict[str, Any] = None + ) -> bool: + if not thread: + if not thread_id: + thread = {} + else: + thread = await self.get_thread(thread_id=thread_id) + + return thread["permissions"]["bump"]["can"] + + async def bump_thread(self, thread_id: Union[str, int]) -> bool: + logger.debug(f"Поднимаю тему {thread_id}...") + response = await self.client.threads.bump(thread_id=thread_id) + + if response.status_code == 200: + logger.info(f"Тема {thread_id} успешно поднята.") + return True + else: + logger.error(f"Не удалось поднять тему {thread_id}.") + return False + async def get_post(self, post_id: Union[str, int]) -> Dict[str, Any]: logger.debug(f"Запрашиваю данные поста {post_id}...") response = await self.client.posts.get(post_id=post_id) diff --git a/modules/stars_bot.py b/modules/stars_bot.py index 33a3edd..5066a86 100644 --- a/modules/stars_bot.py +++ b/modules/stars_bot.py @@ -291,6 +291,42 @@ class TelegramStarsBot: logger.exception(f"Критическая ошибка в главном цикле: {e}") await asyncio.sleep(self.config.check_interval) + async def _bump_loop(self): + while True: + try: + logger.info( + f"Проверка статуса поднятия для темы {self.config.lolz_thread_id}..." + ) + + thread = await self.lolz_api.get_thread( + thread_id=self.config.lolz_thread_id + ) + can_bump = await self.lolz_api.can_bump(thread=thread) + + if can_bump: + try: + await self.lolz_api.bump_thread( + thread_id=self.config.lolz_thread_id + ) + + except Exception as e: + logger.error( + f"Ошибка поднятия темы {self.config.lolz_thread_id}, пропускаю: {e}" + ) + else: + logger.info("Тему нельзя поднять.") + + logger.info(f"Ожидание {self.config.check_interval} секунд...") + await asyncio.sleep(self.config.bump_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") @@ -334,7 +370,10 @@ class TelegramStarsBot: logger.info(f"Проверка начинается со страницы: {self.start_page}") logger.info("Бот в работе. Для остановки нажмите Ctrl+C.") - await self._main_loop() + + # _main_loop - Основной цикл для остлеживания темы + # _bump_look - Цикл для авто-поднятия темы + await asyncio.gather(await self._main_loop(), await self._bump_loop()) finally: if self.client and self.client.is_connected: From b7a7508654b1d4bfccd119382545a4059f2f4479 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 04:08:25 +0500 Subject: [PATCH 7/8] fix(stars_bot): correct async gathering and interval config --- modules/lolz.py | 3 +-- modules/stars_bot.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/lolz.py b/modules/lolz.py index 27993a8..447a515 100644 --- a/modules/lolz.py +++ b/modules/lolz.py @@ -1,4 +1,3 @@ -from concurrent.futures import thread import logging import asyncio @@ -39,7 +38,7 @@ class Lolz: else: thread = await self.get_thread(thread_id=thread_id) - return thread["permissions"]["bump"]["can"] + return thread["permissions"]["bump"]["can"] async def bump_thread(self, thread_id: Union[str, int]) -> bool: logger.debug(f"Поднимаю тему {thread_id}...") diff --git a/modules/stars_bot.py b/modules/stars_bot.py index 5066a86..033497b 100644 --- a/modules/stars_bot.py +++ b/modules/stars_bot.py @@ -316,7 +316,7 @@ class TelegramStarsBot: else: logger.info("Тему нельзя поднять.") - logger.info(f"Ожидание {self.config.check_interval} секунд...") + logger.info(f"Ожидание {self.config.bump_check_interval} секунд...") await asyncio.sleep(self.config.bump_check_interval) except KeyboardInterrupt: @@ -373,7 +373,7 @@ class TelegramStarsBot: # _main_loop - Основной цикл для остлеживания темы # _bump_look - Цикл для авто-поднятия темы - await asyncio.gather(await self._main_loop(), await self._bump_loop()) + await asyncio.gather(self._main_loop(), self._bump_loop()) finally: if self.client and self.client.is_connected: From 76e6659c00f5865fcdcee3733bd4ad4700afac7d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Aug 2026 05:08:46 +0500 Subject: [PATCH 8/8] refactor: add ConfigError and Lolz error handling --- config.py | 34 ++++++-- main.py | 11 ++- modules/lolz.py | 169 +++++++++++++++++++++++++----------- modules/notification_bot.py | 53 +++++++---- modules/stars_bot.py | 24 +++-- 5 files changed, 208 insertions(+), 83 deletions(-) diff --git a/config.py b/config.py index 470ce1d..e2bcd6d 100644 --- a/config.py +++ b/config.py @@ -7,6 +7,11 @@ from dotenv import load_dotenv logger = logging.getLogger(__name__) +class ConfigError(Exception): + """Ошибка валидации конфигурации""" + pass + + class Config: def __init__(self): self._config_data = self._load() @@ -53,18 +58,25 @@ class Config: # Notification bot (aiogram) bot_token = os.getenv(key="BOT_TOKEN", default=None) - admin_id = ( - os.getenv(key="ADMIN_ID", default=None) - if not os.getenv(key="ADMIN_ID", default=None) - else int(os.getenv(key="ADMIN_ID", default=None)) - ) + admin_id_str = os.getenv(key="ADMIN_ID", default=None) + admin_id = int(admin_id_str) if admin_id_str else None notify_admin = ( os.getenv(key="NOTIFY_ADMIN", default="true") ).lower() == "true" - # Lolz + # Lolz - required fields lolz_token = os.getenv(key="LOLZ_TOKEN", default=None) - lolz_thread_id = int(os.getenv(key="LOLZ_THREAD_ID", default=None)) + if not lolz_token: + raise ConfigError("LOLZ_TOKEN обязателен, но не установлен") + + lolz_thread_id_str = os.getenv(key="LOLZ_THREAD_ID", default=None) + if not lolz_thread_id_str: + raise ConfigError("LOLZ_THREAD_ID обязателен, но не установлен") + + try: + lolz_thread_id = int(lolz_thread_id_str) + except (ValueError, TypeError) as e: + raise ConfigError(f"LOLZ_THREAD_ID должен быть целым числом: {lolz_thread_id_str}") from e # Stars stars_count = int(os.getenv(key="STARS_COUNT", default=1)) @@ -118,8 +130,14 @@ class Config: "bump_check_interval": bump_check_interval, } - except: + except ConfigError: raise + except (ValueError, TypeError) as e: + logger.error(f"Invalid configuration value: {e}") + raise ConfigError(f"Invalid configuration value: {e}") from e + except Exception as e: + logger.error(f"Unexpected error loading configuration: {e}") + raise ConfigError(f"Failed to load configuration: {e}") from e def __getattr__(self, name: str) -> Any: return self._config_data.get( diff --git a/main.py b/main.py index 6d19def..7cf398b 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,9 @@ import os import asyncio import logging -from config import Config +from config import Config, ConfigError from modules.stars_bot import TelegramStarsBot +from modules.lolz import LolzAPIError logger = logging.getLogger(__name__) @@ -26,6 +27,14 @@ async def main(): await stars_bot.start() + except ConfigError as e: + logger.critical(f"Ошибка конфигурации: {e}") + raise + + except LolzAPIError as e: + logger.critical(f"Ошибка API Lolz: {e}") + raise + except (ValueError, TypeError) as e: logger.critical(f"Ошибка в конфигурации или при запуске: {e}") raise diff --git a/modules/lolz.py b/modules/lolz.py index 447a515..6b54477 100644 --- a/modules/lolz.py +++ b/modules/lolz.py @@ -7,8 +7,26 @@ from typing import Dict, Any, Optional, Union, List logger = logging.getLogger(__name__) +class LolzAPIError(Exception): + """Base exception for Lolz API errors""" + pass + + +class LolzNetworkError(LolzAPIError): + """Network-related errors""" + pass + + +class LolzResponseError(LolzAPIError): + """API returned invalid or empty response""" + pass + + class Lolz: def __init__(self, token: str): + if not token: + raise ValueError("LOLZ_TOKEN обязателен") + try: self.client = Forum(token=token, timeout=15) @@ -16,50 +34,78 @@ class Lolz: # ! ибо логи занимают очень много места в экране при этом логгере # self.client.settings.logger.enable() - except: - raise + except Exception as e: + logger.error(f"Не удалось инициализировать Lolz клиент: {e}") + raise LolzAPIError(f"Не удалось инициализировать Lolz клиент: {e}") from e async def get_thread(self, thread_id: Union[str, int]) -> Dict[str, Any]: logger.debug(f"Запрашиваю данные темы {thread_id}...") - response = await self.client.threads.get(thread_id=thread_id) - thread = (response.json()).get("thread", {}) + try: + response = await self.client.threads.get(thread_id=thread_id) + thread = (response.json()).get("thread", {}) - if thread == {}: - logger.warning(f"Тема {thread_id} не найдена или вернула пустой ответ.") + if not thread: + raise LolzResponseError(f"Тема {thread_id} не найдена или вернула пустой ответ") - return thread + return thread + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при получении темы {thread_id}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при получении темы {thread_id}") from e + except Exception as e: + logger.error(f"Ошибка при получении темы {thread_id}: {e}") + raise LolzAPIError(f"Не удалось получить тему {thread_id}") from e async def can_bump( self, thread_id: Optional[int] = None, thread: Dict[str, Any] = None ) -> bool: if not thread: if not thread_id: - thread = {} - else: - thread = await self.get_thread(thread_id=thread_id) + raise ValueError("Необходимо указать thread_id или thread") + thread = await self.get_thread(thread_id=thread_id) - return thread["permissions"]["bump"]["can"] + try: + return thread.get("permissions", {}).get("bump", {}).get("can", False) + except (KeyError, AttributeError, TypeError) as e: + logger.error(f"Неверная структура темы для проверки поднятия: {e}") + return False async def bump_thread(self, thread_id: Union[str, int]) -> bool: logger.debug(f"Поднимаю тему {thread_id}...") - response = await self.client.threads.bump(thread_id=thread_id) + try: + response = await self.client.threads.bump(thread_id=thread_id) - if response.status_code == 200: - logger.info(f"Тема {thread_id} успешно поднята.") - return True - else: - logger.error(f"Не удалось поднять тему {thread_id}.") - return False + if response.status_code == 200: + logger.info(f"Тема {thread_id} успешно поднята.") + return True + else: + logger.error(f"Не удалось поднять тему {thread_id}. Статус: {response.status_code}") + return False + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при поднятии темы {thread_id}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при поднятии темы {thread_id}") from e + except Exception as e: + logger.error(f"Ошибка при поднятии темы {thread_id}: {e}") + raise LolzAPIError(f"Не удалось поднять тему {thread_id}") from e async def get_post(self, post_id: Union[str, int]) -> Dict[str, Any]: logger.debug(f"Запрашиваю данные поста {post_id}...") - response = await self.client.posts.get(post_id=post_id) - post = (response.json()).get("post", {}) + try: + response = await self.client.posts.get(post_id=post_id) + post = (response.json()).get("post", {}) - if post == {}: - logger.warning(f"Пост {post_id} не найден или вернул пустой ответ.") + if not post: + raise LolzResponseError(f"Пост {post_id} не найден или вернул пустой ответ") - return post + return post + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при получении поста {post_id}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при получении поста {post_id}") from e + except Exception as e: + logger.error(f"Ошибка при получении поста {post_id}: {e}") + raise LolzAPIError(f"Не удалось получить пост {post_id}") from e async def get_thread_posts( self, thread_id: Union[str, int], start_page: int = 1 @@ -74,20 +120,28 @@ class Lolz: last_page = start_page while True: - response = await self.client.posts.list(thread_id=thread_id, page=page) - posts = (response.json()).get("posts", []) + try: + response = await self.client.posts.list(thread_id=thread_id, page=page) + posts = (response.json()).get("posts", []) - if len(posts) == 0: - break + if len(posts) == 0: + break - all_posts.extend(posts) - last_page = page - logger.debug( - f"Получено {len(posts)} постов из темы {thread_id} на странице {page}." - ) + all_posts.extend(posts) + last_page = page + logger.debug( + f"Получено {len(posts)} постов из темы {thread_id} на странице {page}." + ) - page += 1 - await asyncio.sleep(1) + page += 1 + await asyncio.sleep(1) + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при получении постов из темы {thread_id}, страница {page}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при получении постов из темы {thread_id}") from e + except Exception as e: + logger.error(f"Ошибка при получении постов из темы {thread_id}, страница {page}: {e}") + raise LolzAPIError(f"Не удалось получить посты из темы {thread_id}") from e if all_posts: logger.info(f"Всего получено {len(all_posts)} постов из темы {thread_id}.") @@ -104,31 +158,46 @@ class Lolz: async def get_post_comments(self, post_id: int) -> List[Dict[str, Any]]: logger.debug(f"Запрашиваю комментарии к посту {post_id}...") - response = await self.client.posts.comments.list(post_id=post_id) - comments = (response.json()).get("comments", []) - logger.debug(f"Найдено {len(comments)} комментариев к посту {post_id}.") - return comments + try: + response = await self.client.posts.comments.list(post_id=post_id) + comments = (response.json()).get("comments", []) + logger.debug(f"Найдено {len(comments)} комментариев к посту {post_id}.") + return comments + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при получении комментариев к посту {post_id}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при получении комментариев к посту {post_id}") from e + except Exception as e: + logger.error(f"Ошибка при получении комментариев к посту {post_id}: {e}") + raise LolzAPIError(f"Не удалось получить комментарии к посту {post_id}") from e async def has_comments( self, post_id: Optional[int] = None, post: Dict[str, Any] = None ) -> bool: if not post: if not post_id: - post = {} - else: - post = await self.get_post(post_id=post_id) + raise ValueError("Необходимо указать post_id или post") + post = await self.get_post(post_id=post_id) return post.get("post_comment_count", 0) > 0 async def create_comment(self, post_id: int, comment_body: str) -> bool: logger.info(f"Публикую комментарий к посту {post_id}...") - response = await self.client.posts.comments.create( - post_id=post_id, comment_body=comment_body - ) + try: + response = await self.client.posts.comments.create( + post_id=post_id, comment_body=comment_body + ) - if response.status_code == 200: - logger.info(f"Комментарий к посту {post_id} успешно опубликован.") - return True - else: - logger.error(f"Не удалось опубликовать комментарий к посту {post_id}.") - return False + if response.status_code == 200: + logger.info(f"Комментарий к посту {post_id} успешно опубликован.") + return True + else: + logger.error(f"Не удалось опубликовать комментарий к посту {post_id}. Статус: {response.status_code}") + return False + + except asyncio.TimeoutError as e: + logger.error(f"Превышено время ожидания при создании комментария к посту {post_id}: {e}") + raise LolzNetworkError(f"Превышено время ожидания при создании комментария к посту {post_id}") from e + except Exception as e: + logger.error(f"Ошибка при создании комментария к посту {post_id}: {e}") + raise LolzAPIError(f"Не удалось создать комментарий к посту {post_id}") from e diff --git a/modules/notification_bot.py b/modules/notification_bot.py index 8c7e3cb..b224645 100644 --- a/modules/notification_bot.py +++ b/modules/notification_bot.py @@ -4,9 +4,10 @@ from aiogram import Bot from aiogram.enums import ParseMode from aiogram.client.default import DefaultBotProperties from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramNetworkError from config import Config -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional logger = logging.getLogger(__name__) @@ -28,11 +29,11 @@ class NotificationBot: "Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id ) - except: - logger.exception("Не удалось инициализировать Бот для уведомлений") + except (TelegramAPIError, ValueError, TypeError) as e: + logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}") raise - async def _notify(self, message: str, reply_to: int = None, link: str = None): + async def _notify(self, message: str, reply_to: int = None, link: str = None) -> Optional[int]: try: logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link) keyboard = None @@ -58,10 +59,20 @@ class NotificationBot: ) return sent_message.message_id + except TelegramBadRequest as e: + logger.error("Bad request sending notification: %s", e) + return None + except TelegramNetworkError as e: + logger.error("Network error sending notification: %s", e) + return None + except TelegramAPIError as e: + logger.error("Telegram API error sending notification: %s", e) + return None except Exception as e: - logger.error("Ошибка отправки уведомления: %s", e) + logger.error("Unexpected error sending notification: %s", e) + return None - async def new_post(self, post: Dict[str, Any], links: List[str]): + async def new_post(self, post: Dict[str, Any], links: List[str]) -> Optional[int]: try: thread = post.get("thread") @@ -99,11 +110,14 @@ class NotificationBot: ) return post_message_id - except: - logger.exception("Ошибка в new_post") - raise + except (KeyError, AttributeError, TypeError) as e: + logger.error(f"Неверная структура поста в new_post: {e}") + return None + except Exception as e: + logger.error(f"Неожиданная ошибка в new_post: {e}") + return None - async def success(self, link: str, post_message_id: int): + async def success(self, link: str, post_message_id: int) -> Optional[int]: try: logger.info( "Звезда успешно отправлена: link=%s, stars=%s", @@ -124,13 +138,13 @@ class NotificationBot: ) return success_message_id - except: - logger.exception("Ошибка в success") - raise + except Exception as e: + logger.error(f"Неожиданная ошибка в success: {e}") + return None - async def failure(self, post: Dict[str, Any], reason: str, post_message_id: int): + async def failure(self, post: Dict[str, Any], reason: str, post_message_id: int) -> Optional[int]: try: - permalink = post.get("links").get("permalink") + permalink = post.get("links", {}).get("permalink") logger.warning( "Не удалось отправить звезду: reason=%r, link=%s", reason, permalink ) @@ -147,6 +161,9 @@ class NotificationBot: ) return failure_message_id - except: - logger.exception("Ошибка в failure") - raise + except (KeyError, AttributeError, TypeError) as e: + logger.error(f"Неверная структура поста в failure: {e}") + return None + except Exception as e: + logger.error(f"Неожиданная ошибка в failure: {e}") + return None diff --git a/modules/stars_bot.py b/modules/stars_bot.py index 033497b..242c67a 100644 --- a/modules/stars_bot.py +++ b/modules/stars_bot.py @@ -325,7 +325,7 @@ class TelegramStarsBot: except Exception as e: logger.exception(f"Критическая ошибка в цикле поднятия темы: {e}") - await asyncio.sleep(self.config.check_interval) + await asyncio.sleep(self.config.bump_check_interval) async def start(self): is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session") @@ -341,9 +341,15 @@ class TelegramStarsBot: api_hash=self.config.api_hash, ) - await self.client.start() - await self.parse_existing_posts() - await self.client.stop() + try: + await self.client.start() + await self.parse_existing_posts() + except Exception as e: + logger.error(f"Ошибка при парсинге существующих постов: {e}") + raise + finally: + if self.client and self.client.is_connected: + await self.client.stop() logger.info( "Парсинг завершен. Все существующие посты добавлены в обработанные." @@ -372,11 +378,17 @@ class TelegramStarsBot: logger.info("Бот в работе. Для остановки нажмите Ctrl+C.") # _main_loop - Основной цикл для остлеживания темы - # _bump_look - Цикл для авто-поднятия темы + # _bump_loop - Цикл для авто-поднятия темы await asyncio.gather(self._main_loop(), self._bump_loop()) + except Exception as e: + logger.error(f"Ошибка в главном процессе бота: {e}") + raise finally: if self.client and self.client.is_connected: - await self.client.stop() + try: + await self.client.stop() + except Exception as e: + logger.error(f"Ошибка при остановке клиента: {e}") logger.info("Бот остановлен.")