Merge branch 'dev'
This commit is contained in:
@@ -27,3 +27,4 @@ MAX_RETRIES=3
|
||||
ENABLE_REPLY=true
|
||||
SKIP_COMMENTED=true
|
||||
|
||||
BUMP_CHECK_INTERVAL=3600
|
||||
|
||||
@@ -1,136 +1,94 @@
|
||||
# ⭐ 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
|
||||
```
|
||||
|
||||
## Запуск без Docker
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
cp examples/.env.example .env
|
||||
# отредактируйте .env
|
||||
python main.py
|
||||
```
|
||||
|
||||
> При первом запуске (если используете 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: Тут может быть реклама
|
||||
```
|
||||
|
||||
Если файл пустой — используется `✓`.
|
||||
|
||||
@@ -7,12 +7,25 @@ from dotenv import load_dotenv
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Ошибка валидации конфигурации"""
|
||||
pass
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self._config_data = self._load()
|
||||
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 +37,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()
|
||||
@@ -43,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))
|
||||
@@ -78,6 +100,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,
|
||||
@@ -91,18 +117,27 @@ 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:
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
+138
-34
@@ -7,24 +7,105 @@ 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)
|
||||
|
||||
# ! Не рекомендуется включить этот логгер,
|
||||
# ! ибо логи занимают очень много места в экране при этом логгере
|
||||
# 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}...")
|
||||
try:
|
||||
response = await self.client.threads.get(thread_id=thread_id)
|
||||
thread = (response.json()).get("thread", {})
|
||||
|
||||
if not thread:
|
||||
raise LolzResponseError(f"Тема {thread_id} не найдена или вернула пустой ответ")
|
||||
|
||||
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:
|
||||
raise ValueError("Необходимо указать thread_id или thread")
|
||||
thread = await self.get_thread(thread_id=thread_id)
|
||||
|
||||
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}...")
|
||||
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}. Статус: {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
|
||||
@@ -39,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}.")
|
||||
@@ -69,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
|
||||
|
||||
+38
-20
@@ -1,11 +1,13 @@
|
||||
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 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__)
|
||||
|
||||
@@ -18,7 +20,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,
|
||||
),
|
||||
@@ -27,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
|
||||
@@ -57,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")
|
||||
|
||||
@@ -98,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",
|
||||
@@ -123,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
|
||||
)
|
||||
@@ -146,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
|
||||
|
||||
+56
-5
@@ -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.bump_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.bump_check_interval)
|
||||
|
||||
async def start(self):
|
||||
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
|
||||
|
||||
@@ -305,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(
|
||||
"Парсинг завершен. Все существующие посты добавлены в обработанные."
|
||||
@@ -334,10 +376,19 @@ class TelegramStarsBot:
|
||||
|
||||
logger.info(f"Проверка начинается со страницы: {self.start_page}")
|
||||
logger.info("Бот в работе. Для остановки нажмите Ctrl+C.")
|
||||
await self._main_loop()
|
||||
|
||||
# _main_loop - Основной цикл для остлеживания темы
|
||||
# _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("Бот остановлен.")
|
||||
|
||||
Reference in New Issue
Block a user