Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9502c7a38 | |||
| 31bf50aaca | |||
| 8423912dbb | |||
| 64508438e7 | |||
| f5cf63bac0 | |||
| 37d4400513 | |||
| 5dece14c8b | |||
| 5c325bd8b9 | |||
| 62873c7f8c | |||
| 4883320aaa | |||
| 0f9d6470b8 | |||
| 76f37673d4 | |||
| 5772df644f | |||
| f5fd0187ec | |||
| cb83fc1608 | |||
| f82f9ef752 | |||
| 96179c5715 | |||
| 39defecb2f | |||
| 8a1e467a15 | |||
| 7e85a1af4c | |||
| 0cc2440736 | |||
| 9ee9e7b731 | |||
| 62c44e885b | |||
| 2919e95c4c | |||
| 620115d89a | |||
| d6d2752341 | |||
| f76c74ce3f | |||
| 4abb5d6acc | |||
| adf963a1a7 | |||
| 6bcb89ffd0 | |||
| 74b8dddd9d | |||
| 76e6659c00 | |||
| b7a7508654 | |||
| b557ff0403 | |||
| 03c463b87c | |||
| c59a8b9a47 | |||
| 958b00a8f3 | |||
| 61527aa4c5 | |||
| f37977e88f |
+27
-5
@@ -14,16 +14,38 @@ LOLZ_TOKEN=
|
|||||||
LOLZ_THREAD_ID=
|
LOLZ_THREAD_ID=
|
||||||
|
|
||||||
STARS_COUNT=1
|
STARS_COUNT=1
|
||||||
|
|
||||||
# 1 - Начать мониторинг
|
|
||||||
# 2 - Спарсить все существующие посты
|
|
||||||
DEFAULT_CHOICE=2
|
|
||||||
START_PAGE=1
|
START_PAGE=1
|
||||||
|
|
||||||
CHECK_INTERVAL=30
|
CHECK_INTERVAL=10
|
||||||
API_DELAY=5
|
API_DELAY=5
|
||||||
|
API_TIMEOUT=15
|
||||||
MAX_RETRIES=3
|
MAX_RETRIES=3
|
||||||
|
|
||||||
ENABLE_REPLY=true
|
ENABLE_REPLY=true
|
||||||
SKIP_COMMENTED=true
|
SKIP_COMMENTED=true
|
||||||
|
|
||||||
|
ENABLE_AUTO_BUMP=true
|
||||||
|
BUMP_CHECK_INTERVAL=3600
|
||||||
|
|
||||||
|
# По умолчанию капча выключена
|
||||||
|
# Можно использовать BBCode для оформления капчи
|
||||||
|
# Капча будет в самом нижнем спойлере: Капча (обязательно)
|
||||||
|
ENABLE_CAPTCHA=false
|
||||||
|
CAPTCHA_ANSWER=40
|
||||||
|
# Интервал проверки обновления капчи в секундах (минимум 300 секунд = 5 минут)
|
||||||
|
# Капча будет автоматически обновляться из файла storage/captcha.txt
|
||||||
|
CAPTCHA_CHECK_INTERVAL=1800
|
||||||
|
|
||||||
|
# Прокси: применяется в зависимости от выбранных опций PROXY_TELEGRAM и PROXY_LOLZ
|
||||||
|
PROXY_TELEGRAM=false
|
||||||
|
PROXY_LOLZ=false
|
||||||
|
# Прокси
|
||||||
|
# Формат protocol://username:password@host:port или protocol://host:port
|
||||||
|
PROXY=
|
||||||
|
|
||||||
|
# Можно задать данные для прокси отдельно
|
||||||
|
PROXY_PROTOCOL=socks5 # поддерживаются socks5 и https форматы
|
||||||
|
PROXY_HOST=
|
||||||
|
PROXY_PORT=
|
||||||
|
PROXY_USERNAME= # Можно не ставить, если для прокси авторизация не нужна
|
||||||
|
PROXY_PASSWORD= # Можно не ставить, если для прокси авторизация не нужна
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -40,7 +42,9 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
git.sakamoto.best/lolz/auto-stars:${{ steps.vars.outputs.SHORT_SHA }}
|
git.sakamoto.best/lolz/auto-stars:${{ steps.vars.outputs.SHORT_SHA }}
|
||||||
git.sakamoto.best/lolz/auto-stars:dev
|
git.sakamoto.best/lolz/auto-stars:dev
|
||||||
cache-from: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache
|
cache-from: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache-dev
|
||||||
cache-to: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache,mode=max
|
cache-to: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache-dev,mode=max
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_INLINE_CACHE=1
|
BUILDKIT_INLINE_CACHE=1
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -42,5 +44,7 @@ jobs:
|
|||||||
git.sakamoto.best/lolz/auto-stars:latest
|
git.sakamoto.best/lolz/auto-stars:latest
|
||||||
cache-from: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache
|
cache-from: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache
|
||||||
cache-to: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache,mode=max
|
cache-to: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache,mode=max
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_INLINE_CACHE=1
|
BUILDKIT_INLINE_CACHE=1
|
||||||
|
|||||||
+16
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
# Prevent .pyc files and enable unbuffered stdout/stderr
|
# Prevent .pyc files and enable unbuffered stdout/stderr
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
@@ -6,14 +6,27 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install build tools for C extensions (e.g. TgCrypto), then clean up
|
# Install build tools in builder stage only
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends gcc libc6-dev && \
|
apt-get install -y --no-install-recommends gcc libc6-dev && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install deps first — layer cache reuse on code-only changes
|
# Install deps first — layer cache reuse on code-only changes
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Runtime stage - clean slate
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only installed packages from builder
|
||||||
|
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY main.py config.py ./
|
COPY main.py config.py ./
|
||||||
|
|||||||
@@ -1,136 +1,198 @@
|
|||||||
# ⭐ Auto-Stars
|
# ⭐ Auto-Stars-Improved
|
||||||
|
|
||||||
Бот автоматически отправляет Telegram Stars пользователям, которые оставляют посты в указанной теме на Lolz.team.
|
Бот для автоматической отправки Telegram Stars пользователям, которые оставляют посты в указанной теме на форуме Lolz.team.
|
||||||
|
|
||||||
## Как это работает
|
## Возможности
|
||||||
|
|
||||||
Бот следит за выбранной темой на форуме. Когда кто-то публикует новый пост — бот отправляет ему звёзды через Telegram. Опционально может оставлять комментарий под постом.
|
- Автоматический мониторинг темы на Lolz.team
|
||||||
|
- Отправка Telegram Stars за каждый новый пост
|
||||||
## Установка
|
- Автоматические ответы с поддержкой шаблонов (включая случайный выбор)
|
||||||
|
- Автоматическое поднятие темы (bump) с настраиваемым интервалом
|
||||||
### Вариант 1 — Docker (рекомендуется)
|
- Система капчи для фильтрации пользователей
|
||||||
|
- Поддержка прокси для Telegram и Lolz API (SOCKS5/HTTPS)
|
||||||
**1. Клонируйте репозиторий**
|
- Полная поддержка Docker (рекомендуется)
|
||||||
|
- CI/CD через Gitea Actions (сборка и публикация образов)
|
||||||
```bash
|
- Уведомления администратору через Telegram
|
||||||
git clone https://github.com/ваш-ник/auto-stars
|
- Гибкая настройка через `.env`
|
||||||
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: Это третье сообщение
|
|
||||||
```
|
|
||||||
|
|
||||||
Если файл отсутствует или пустой — бот использует `✓` по умолчанию.
|
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- Аккаунт Telegram с доступом к [my.telegram.org](https://my.telegram.org)
|
- Аккаунт Telegram (для получения `API_ID` и `API_HASH`)
|
||||||
- API-токен [Lolz.team](https://lolz.team)
|
- 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 |
|
||||||
|
| `START_PAGE` | С какой страницы начинать парсинг | 1 |
|
||||||
|
| `ENABLE_REPLY` | Отправлять ли комментарии | true |
|
||||||
|
| `SKIP_COMMENTED` | Пропускать посты, на которые уже отвечали | true |
|
||||||
|
|
||||||
|
### Параметры производительности
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| ---------------- | ---------------------------------------- | ------------ |
|
||||||
|
| `CHECK_INTERVAL` | Интервал проверки новых постов (секунды) | 10 |
|
||||||
|
| `API_DELAY` | Задержка между API запросами (секунды) | 5 |
|
||||||
|
| `API_TIMEOUT` | Таймаут для API запросов (секунды) | 15 |
|
||||||
|
| `MAX_RETRIES` | Количество повторных попыток при ошибках | 3 |
|
||||||
|
|
||||||
|
### Уведомления
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| -------------- | ------------------------------------------ | ------------ |
|
||||||
|
| `BOT_TOKEN` | Токен бота для уведомлений (от @BotFather) | - |
|
||||||
|
| `ADMIN_ID` | Telegram ID администратора для уведомлений | - |
|
||||||
|
| `NOTIFY_ADMIN` | Включить уведомления администратору | true |
|
||||||
|
|
||||||
|
### Автоматическое поднятие темы (Bump)
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| --------------------- | ------------------------------------- | ------------ |
|
||||||
|
| `ENABLE_AUTO_BUMP` | Включить автоматическое поднятие темы | true |
|
||||||
|
| `BUMP_CHECK_INTERVAL` | Интервал между поднятиями (секунды) | 3600 |
|
||||||
|
|
||||||
|
> **Примечание**: Минимальный интервал поднятия — 3600 секунд (1 час) согласно правилам Lolz.team.
|
||||||
|
|
||||||
|
### Система капчи
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| ------------------------ | -------------------------------------------- | ------------ |
|
||||||
|
| `ENABLE_CAPTCHA` | Включить систему капчи | false |
|
||||||
|
| `CAPTCHA_ANSWER` | Правильный ответ на капчу | - |
|
||||||
|
| `CAPTCHA_CHECK_INTERVAL` | Интервал проверки обновления капчи (секунды) | 1800 |
|
||||||
|
|
||||||
|
Капча добавляется в нижний спойлер первого поста темы: `Капча (обязательно)`.
|
||||||
|
|
||||||
|
Текст капчи хранится в файле `storage/captcha.txt` и поддерживает BBCode форматирование.
|
||||||
|
|
||||||
|
**Пример `storage/captcha.txt`:**
|
||||||
|
|
||||||
|
```
|
||||||
|
[CENTER][B]Напишите ответ:[/B]
|
||||||
|
сколько будет 20 + 20? [I][COLOR=rgb(80, 80, 78)](Капча меняется раз в месяц)[/COLOR][/I][/CENTER]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Прокси
|
||||||
|
|
||||||
|
Бот поддерживает использование прокси отдельно для Telegram и Lolz API.
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| ---------------- | -------------------------------- | ------------ |
|
||||||
|
| `PROXY_TELEGRAM` | Использовать прокси для Telegram | false |
|
||||||
|
| `PROXY_LOLZ` | Использовать прокси для Lolz API | false |
|
||||||
|
|
||||||
|
#### Вариант 1: Единая строка прокси
|
||||||
|
|
||||||
|
```env
|
||||||
|
PROXY=socks5://username:password@host:port
|
||||||
|
```
|
||||||
|
|
||||||
|
Или без авторизации:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PROXY=socks5://host:port
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Вариант 2: Отдельные параметры
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
| ---------------- | ---------------------------------- | ------------ |
|
||||||
|
| `PROXY_PROTOCOL` | Протокол прокси (socks5 или https) | socks5 |
|
||||||
|
| `PROXY_HOST` | Хост прокси-сервера | - |
|
||||||
|
| `PROXY_PORT` | Порт прокси-сервера | - |
|
||||||
|
| `PROXY_USERNAME` | Логин (опционально) | - |
|
||||||
|
| `PROXY_PASSWORD` | Пароль (опционально) | - |
|
||||||
|
|
||||||
|
**Поддерживаемые форматы прокси:**
|
||||||
|
|
||||||
|
- SOCKS5: `socks5://host:port`
|
||||||
|
- HTTPS: `https://host:port`
|
||||||
|
|
||||||
|
**Поддерживаемые возможности:**
|
||||||
|
|
||||||
|
- ✓ Пароли с символом `:` внутри
|
||||||
|
- ✓ IPv6 адреса
|
||||||
|
- ✓ Авторизация с логином и паролем
|
||||||
|
- ✓ Независимая настройка для Telegram и Lolz API
|
||||||
|
|
||||||
|
## Шаблоны ответов
|
||||||
|
|
||||||
|
Если `ENABLE_REPLY=true`, бот будет отвечать под постами.
|
||||||
|
|
||||||
|
Шаблоны хранятся в файле `storage/replies.txt`.
|
||||||
|
|
||||||
|
**Формат:**
|
||||||
|
|
||||||
|
- Каждая строка — отдельный шаблон
|
||||||
|
- Можно использовать `||` для случайного выбора
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```
|
||||||
|
:catdance: Отправил звёзды || :animeyes2: Отправил
|
||||||
|
:sadge: Тут может быть реклама
|
||||||
|
```
|
||||||
|
|
||||||
|
Если файл пустой — используется `✓`.
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
- Автор: [Дриада](https://lolz.team/members/7272152/)
|
||||||
|
- Создайте Issue на Gitea: https://git.sakamoto.best/Lolz/Auto-Stars-Improved/issues
|
||||||
|
- Тема на форуме Lolz.team: https://lolz.team/threads/10187517/
|
||||||
|
|||||||
@@ -7,12 +7,26 @@ from dotenv import load_dotenv
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
"""Ошибка валидации конфигурации"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._config_data = self._load()
|
self._config_data = self._load()
|
||||||
logger.info("Конфигурация успешно загружена.")
|
logger.info("Конфигурация успешно загружена.")
|
||||||
|
|
||||||
def _load_replies(self, file_path: str = "storage/replies.txt"):
|
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:
|
try:
|
||||||
reply_lines = []
|
reply_lines = []
|
||||||
with open(file_path, "r", encoding="utf-8") as replies:
|
with open(file_path, "r", encoding="utf-8") as replies:
|
||||||
@@ -24,12 +38,35 @@ class Config:
|
|||||||
options = [o.strip() for o in line.split(" || ")]
|
options = [o.strip() for o in line.split(" || ")]
|
||||||
else:
|
else:
|
||||||
options = [line]
|
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:
|
except FileNotFoundError:
|
||||||
return [["✓"]]
|
return default_replies
|
||||||
|
|
||||||
|
def _load_captcha(self, file_path: str = "storage/captcha.txt"):
|
||||||
|
default_captcha = (
|
||||||
|
f"[CENTER][B]Напишите ответ:[/B]\n"
|
||||||
|
f"сколько будет 20 + 20? [I][COLOR=rgb(80, 80, 78)](Капча меняется раз в месяц)[/COLOR][/I][/CENTER]"
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
captcha = ""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as replies:
|
||||||
|
captcha = replies.read()
|
||||||
|
|
||||||
|
return captcha.strip() if captcha else default_captcha
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
return default_captcha
|
||||||
|
|
||||||
|
def _load_captcha_answer(self):
|
||||||
|
"""Load captcha answer from environment variable (supports live reload)"""
|
||||||
|
load_dotenv(override=True) # Reload .env file
|
||||||
|
return os.getenv(key="CAPTCHA_ANSWER", default=None)
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -43,24 +80,32 @@ class Config:
|
|||||||
|
|
||||||
# Notification bot (aiogram)
|
# Notification bot (aiogram)
|
||||||
bot_token = os.getenv(key="BOT_TOKEN", default=None)
|
bot_token = os.getenv(key="BOT_TOKEN", default=None)
|
||||||
admin_id = (
|
admin_id_str = os.getenv(key="ADMIN_ID", default=None)
|
||||||
os.getenv(key="ADMIN_ID", default=None)
|
admin_id = int(admin_id_str) if admin_id_str else None
|
||||||
if not os.getenv(key="ADMIN_ID", default=None)
|
|
||||||
else int(os.getenv(key="ADMIN_ID", default=None))
|
|
||||||
)
|
|
||||||
notify_admin = (
|
notify_admin = (
|
||||||
os.getenv(key="NOTIFY_ADMIN", default="true")
|
os.getenv(key="NOTIFY_ADMIN", default="true")
|
||||||
).lower() == "true"
|
).lower() == "true"
|
||||||
|
|
||||||
# Lolz
|
# Lolz - required fields
|
||||||
lolz_token = os.getenv(key="LOLZ_TOKEN", default=None)
|
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
|
||||||
stars_count = int(os.getenv(key="STARS_COUNT", default=1))
|
stars_count = int(os.getenv(key="STARS_COUNT", default=1))
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
default_choice = int(os.getenv(key="DEFAULT_CHOICE", default=2))
|
|
||||||
start_page = max(int(os.getenv(key="START_PAGE", default=1)), 1)
|
start_page = max(int(os.getenv(key="START_PAGE", default=1)), 1)
|
||||||
|
|
||||||
check_interval = max(
|
check_interval = max(
|
||||||
@@ -69,6 +114,7 @@ class Config:
|
|||||||
api_delay = max(
|
api_delay = max(
|
||||||
float(os.getenv(key="API_DELAY", default=5)), 2
|
float(os.getenv(key="API_DELAY", default=5)), 2
|
||||||
) # 30r/60s для non-GET запросов, 60/30 = минимум
|
) # 30r/60s для non-GET запросов, 60/30 = минимум
|
||||||
|
api_timeout = float(os.getenv(key="API_TIMEOUT", default=15))
|
||||||
max_retries = int(os.getenv(key="MAX_RETRIES", default=3))
|
max_retries = int(os.getenv(key="MAX_RETRIES", default=3))
|
||||||
|
|
||||||
enable_reply = (
|
enable_reply = (
|
||||||
@@ -78,6 +124,156 @@ class Config:
|
|||||||
os.getenv(key="SKIP_COMMENTED", default="true")
|
os.getenv(key="SKIP_COMMENTED", default="true")
|
||||||
).lower() == "true"
|
).lower() == "true"
|
||||||
|
|
||||||
|
enable_auto_bump = (
|
||||||
|
os.getenv(key="ENABLE_AUTO_BUMP", default="true")
|
||||||
|
).lower() == "true"
|
||||||
|
bump_check_interval = max(
|
||||||
|
float(os.getenv(key="BUMP_CHECK_INTERVAL", default=3600)), 3600
|
||||||
|
) # 1 поднятие/3600с (1ч) = минимум
|
||||||
|
|
||||||
|
enable_captcha = (
|
||||||
|
os.getenv(key="ENABLE_CAPTCHA", default="true")
|
||||||
|
).lower() == "true"
|
||||||
|
captcha_answer = os.getenv(key="CAPTCHA_ANSWER", default=None)
|
||||||
|
captcha_check_interval = max(
|
||||||
|
float(os.getenv(key="CAPTCHA_CHECK_INTERVAL", default=1800)), 300
|
||||||
|
) # проверка обновления капчи, минимум 300с (5 минут)
|
||||||
|
|
||||||
|
# Validate captcha configuration
|
||||||
|
if enable_captcha and not captcha_answer:
|
||||||
|
raise ConfigError(
|
||||||
|
"ENABLE_CAPTCHA установлен, но CAPTCHA_ANSWER не задан"
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy_telegram = (
|
||||||
|
os.getenv(key="PROXY_TELEGRAM", default="false")
|
||||||
|
).lower() == "true"
|
||||||
|
proxy_lolz = (
|
||||||
|
os.getenv(key="PROXY_LOLZ", default="false")
|
||||||
|
).lower() == "true"
|
||||||
|
|
||||||
|
proxy_protocol = None
|
||||||
|
proxy_host = None
|
||||||
|
proxy_port = None
|
||||||
|
proxy_port_str = None
|
||||||
|
proxy_username = None
|
||||||
|
proxy_password = None
|
||||||
|
|
||||||
|
if proxy_telegram or proxy_lolz:
|
||||||
|
proxy = os.getenv(key="PROXY", default=None)
|
||||||
|
|
||||||
|
proxy_protocol = os.getenv(key="PROXY_PROTOCOL", default="socks5")
|
||||||
|
proxy_host = os.getenv(key="PROXY_HOST", default=None)
|
||||||
|
proxy_port_str = os.getenv(key="PROXY_PORT", default=None)
|
||||||
|
proxy_username = os.getenv(key="PROXY_USERNAME", default=None)
|
||||||
|
proxy_password = os.getenv(key="PROXY_PASSWORD", default=None)
|
||||||
|
|
||||||
|
if proxy:
|
||||||
|
logger.info("Парсинг прокси из переменной PROXY...")
|
||||||
|
try:
|
||||||
|
parts = proxy.split("://")
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise ConfigError(
|
||||||
|
"Неверный формат PROXY. Ожидается: protocol://[username:password@]host:port"
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy_protocol = parts[0]
|
||||||
|
main = parts[1]
|
||||||
|
|
||||||
|
# Проверяем наличие авторизации (username:password@host:port)
|
||||||
|
if "@" in main:
|
||||||
|
main_split = main.split("@")
|
||||||
|
if len(main_split) != 2:
|
||||||
|
raise ConfigError("Неверный формат авторизации в PROXY")
|
||||||
|
|
||||||
|
authorization = main_split[0]
|
||||||
|
server = main_split[1]
|
||||||
|
|
||||||
|
# Парсим username:password (используем split с limit для поддержки : в пароле)
|
||||||
|
auth_parts = authorization.split(":", 1)
|
||||||
|
if len(auth_parts) != 2:
|
||||||
|
raise ConfigError(
|
||||||
|
"Неверный формат авторизации в PROXY. Ожидается: username:password"
|
||||||
|
)
|
||||||
|
proxy_username = auth_parts[0]
|
||||||
|
proxy_password = auth_parts[1]
|
||||||
|
else:
|
||||||
|
server = main
|
||||||
|
|
||||||
|
# Парсим host:port (используем rsplit для поддержки IPv6)
|
||||||
|
server_parts = server.rsplit(":", 1)
|
||||||
|
if len(server_parts) != 2:
|
||||||
|
raise ConfigError(
|
||||||
|
"Неверный формат сервера в PROXY. Ожидается: host:port"
|
||||||
|
)
|
||||||
|
proxy_host = server_parts[0]
|
||||||
|
try:
|
||||||
|
proxy_port = int(server_parts[1])
|
||||||
|
except ValueError as e:
|
||||||
|
raise ConfigError(
|
||||||
|
f"PROXY_PORT должен быть целым числом: {server_parts[1]}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Прокси успешно распарсен: protocol=%s, host=%s, port=%s, auth=%s",
|
||||||
|
proxy_protocol,
|
||||||
|
proxy_host,
|
||||||
|
proxy_port,
|
||||||
|
"да" if proxy_username else "нет",
|
||||||
|
)
|
||||||
|
|
||||||
|
except ConfigError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise ConfigError(
|
||||||
|
f"Произошла ошибка при парсе PROXY: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info("Загрузка прокси из отдельных переменных...")
|
||||||
|
# Используем отдельные поля
|
||||||
|
if not proxy_host or not proxy_port_str:
|
||||||
|
raise ConfigError(
|
||||||
|
"PROXY не указан, но PROXY_HOST или PROXY_PORT также отсутствуют"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy_port = int(proxy_port_str)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
raise ConfigError(
|
||||||
|
f"PROXY_PORT должен быть целым числом: {proxy_port_str}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Прокси загружен: protocol=%s, host=%s, port=%s, auth=%s",
|
||||||
|
proxy_protocol,
|
||||||
|
proxy_host,
|
||||||
|
proxy_port,
|
||||||
|
"да" if proxy_username else "нет",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Валидация
|
||||||
|
if proxy_protocol not in ["socks5", "https", "http"]:
|
||||||
|
raise ConfigError(
|
||||||
|
f"Формат прокси '{proxy_protocol}' не поддерживается. Укажите socks5 или https"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (proxy_username and not proxy_password) or (
|
||||||
|
proxy_password and not proxy_username
|
||||||
|
):
|
||||||
|
raise ConfigError(
|
||||||
|
"PROXY_USERNAME и PROXY_PASSWORD должны быть указаны оба или не указаны вовсе"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Финальная проверка
|
||||||
|
if not (proxy_protocol and proxy_host and proxy_port):
|
||||||
|
raise ConfigError("Прокси не полностью настроены")
|
||||||
|
|
||||||
|
if proxy_telegram:
|
||||||
|
logger.info("Прокси для Telegram включен")
|
||||||
|
if proxy_lolz:
|
||||||
|
logger.info("Прокси для Lolz API включен")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# Pyrogram
|
# Pyrogram
|
||||||
"api_id": api_id,
|
"api_id": api_id,
|
||||||
@@ -91,19 +287,43 @@ class Config:
|
|||||||
"lolz_thread_id": lolz_thread_id,
|
"lolz_thread_id": lolz_thread_id,
|
||||||
# Stars
|
# Stars
|
||||||
"stars_count": stars_count,
|
"stars_count": stars_count,
|
||||||
# Misc
|
# Posts
|
||||||
"default_choice": default_choice,
|
|
||||||
"start_page": start_page,
|
"start_page": start_page,
|
||||||
"check_interval": check_interval,
|
|
||||||
"api_delay": api_delay,
|
|
||||||
"max_retries": max_retries,
|
|
||||||
"enable_reply": enable_reply,
|
"enable_reply": enable_reply,
|
||||||
|
"check_interval": check_interval,
|
||||||
"skip_commented": skip_commented,
|
"skip_commented": skip_commented,
|
||||||
|
# Misc
|
||||||
|
"api_delay": api_delay,
|
||||||
|
"api_timeout": api_timeout,
|
||||||
|
"max_retries": max_retries,
|
||||||
|
# Bump
|
||||||
|
"enable_auto_bump": enable_auto_bump,
|
||||||
|
"bump_check_interval": bump_check_interval,
|
||||||
|
# Captcha
|
||||||
|
"enable_captcha": enable_captcha,
|
||||||
|
"captcha_answer": captcha_answer,
|
||||||
|
"captcha_check_interval": captcha_check_interval,
|
||||||
|
# Proxy
|
||||||
|
"proxy_telegram": proxy_telegram,
|
||||||
|
"proxy_lolz": proxy_lolz,
|
||||||
|
"proxy_protocol": proxy_protocol,
|
||||||
|
"proxy_host": proxy_host,
|
||||||
|
"proxy_port": proxy_port,
|
||||||
|
"proxy_username": proxy_username,
|
||||||
|
"proxy_password": proxy_password,
|
||||||
}
|
}
|
||||||
|
|
||||||
except:
|
except ConfigError:
|
||||||
raise
|
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:
|
def __getattr__(self, name: str) -> Any:
|
||||||
return self._config_data.get(
|
return self._config_data.get(
|
||||||
name, True if name == "skip_posts_with_comments" else None
|
name, True if name == "skip_posts_with_comments" else None
|
||||||
|
|||||||
+31
-5
@@ -1,6 +1,9 @@
|
|||||||
LOGS_PATH=./logs
|
LOGS_PATH=./logs
|
||||||
STORAGE_PATH=./storage
|
STORAGE_PATH=./storage
|
||||||
|
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
|
||||||
# Значения для клиента Telegram Desktop, ставятся по умолчанию
|
# Значения для клиента Telegram Desktop, ставятся по умолчанию
|
||||||
# Не рекомендуется оставлять, лучше использовать API_ID и API_HASH от
|
# Не рекомендуется оставлять, лучше использовать API_ID и API_HASH от
|
||||||
# аккаунта с которого будет воспроизведен вход
|
# аккаунта с которого будет воспроизведен вход
|
||||||
@@ -17,16 +20,39 @@ LOLZ_TOKEN=
|
|||||||
LOLZ_THREAD_ID=
|
LOLZ_THREAD_ID=
|
||||||
|
|
||||||
STARS_COUNT=1
|
STARS_COUNT=1
|
||||||
|
|
||||||
# 1 - Начать мониторинг
|
|
||||||
# 2 - Спарсить все существующие посты
|
|
||||||
DEFAULT_CHOICE=2
|
|
||||||
START_PAGE=1
|
START_PAGE=1
|
||||||
|
|
||||||
CHECK_INTERVAL=30
|
CHECK_INTERVAL=10
|
||||||
API_DELAY=5
|
API_DELAY=5
|
||||||
|
API_TIMEOUT=15
|
||||||
MAX_RETRIES=3
|
MAX_RETRIES=3
|
||||||
|
|
||||||
ENABLE_REPLY=true
|
ENABLE_REPLY=true
|
||||||
SKIP_COMMENTED=true
|
SKIP_COMMENTED=true
|
||||||
|
|
||||||
|
ENABLE_AUTO_BUMP=true
|
||||||
|
BUMP_CHECK_INTERVAL=3600
|
||||||
|
|
||||||
|
# По умолчанию капча выключена
|
||||||
|
# Можно использовать BBCode для оформления капчи
|
||||||
|
# Капча будет в самом нижнем спойлере: Капча (обязательно)
|
||||||
|
ENABLE_CAPTCHA=false
|
||||||
|
CAPTCHA_ANSWER=40
|
||||||
|
# Интервал проверки обновления капчи в секундах (минимум 300 секунд = 5 минут)
|
||||||
|
# Капча будет автоматически обновляться из файла storage/captcha.txt
|
||||||
|
CAPTCHA_CHECK_INTERVAL=1800
|
||||||
|
|
||||||
|
# Прокси: применяется в зависимости от выбранных опций PROXY_TELEGRAM и PROXY_LOLZ
|
||||||
|
PROXY_TELEGRAM=false
|
||||||
|
PROXY_LOLZ=false
|
||||||
|
# Прокси
|
||||||
|
# Формат protocol://username:password@host:port или protocol://host:port
|
||||||
|
PROXY=
|
||||||
|
|
||||||
|
# Можно задать данные для прокси отдельно
|
||||||
|
PROXY_PROTOCOL=socks5 # поддерживаются socks5 и https форматы
|
||||||
|
PROXY_HOST=
|
||||||
|
PROXY_PORT=
|
||||||
|
PROXY_USERNAME= # Можно не ставить, если для прокси авторизация не нужна
|
||||||
|
PROXY_PASSWORD= # Можно не ставить, если для прокси авторизация не нужна
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: auto-stars
|
|||||||
services:
|
services:
|
||||||
auto-stars:
|
auto-stars:
|
||||||
image: git.sakamoto.best/lolz/auto-stars:latest
|
image: git.sakamoto.best/lolz/auto-stars:latest
|
||||||
|
user: ${UID:-1000}:${GID:-1000}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${LOGS_PATH:-./logs}:/app/logs
|
- ${LOGS_PATH:-./logs}:/app/logs
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import os
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from config import Config
|
from config import Config, ConfigError
|
||||||
from modules.stars_bot import TelegramStarsBot
|
from modules.stars_bot import TelegramStarsBot
|
||||||
|
from modules.lolz import LolzAPIError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -26,6 +27,14 @@ async def main():
|
|||||||
|
|
||||||
await stars_bot.start()
|
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:
|
except (ValueError, TypeError) as e:
|
||||||
logger.critical(f"Ошибка в конфигурации или при запуске: {e}")
|
logger.critical(f"Ошибка в конфигурации или при запуске: {e}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
+250
-36
@@ -4,27 +4,195 @@ import asyncio
|
|||||||
from LOLZTEAM.Client import Forum
|
from LOLZTEAM.Client import Forum
|
||||||
from typing import Dict, Any, Optional, Union, List
|
from typing import Dict, Any, Optional, Union, List
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class Lolz:
|
||||||
def __init__(self, token: str):
|
def __init__(self, config: Config):
|
||||||
|
if not config.lolz_token:
|
||||||
|
raise ValueError("LOLZ_TOKEN обязателен")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.client = Forum(token=token, timeout=15)
|
if config.proxy_lolz:
|
||||||
|
if config.proxy_username and config.proxy_password:
|
||||||
|
proxy = f"{config.proxy_protocol}://{config.proxy_username}:{config.proxy_password}@{config.proxy_host}:{config.proxy_port}"
|
||||||
|
logger.info(
|
||||||
|
"Инициализация Lolz клиента с прокси: %s://%s:%s (с авторизацией)",
|
||||||
|
config.proxy_protocol,
|
||||||
|
config.proxy_host,
|
||||||
|
config.proxy_port
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
proxy = f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}"
|
||||||
|
logger.info(
|
||||||
|
"Инициализация Lolz клиента с прокси: %s://%s:%s (без авторизации)",
|
||||||
|
config.proxy_protocol,
|
||||||
|
config.proxy_host,
|
||||||
|
config.proxy_port
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client = Forum(
|
||||||
|
token=config.lolz_token, proxy=proxy, timeout=config.api_timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Инициализация Lolz клиента без прокси")
|
||||||
|
self.client = Forum(token=config.lolz_token, timeout=config.api_timeout)
|
||||||
|
|
||||||
|
# ! Не рекомендуется включать этот логгер,
|
||||||
|
# ! ибо логи занимают очень много места в экране с этим логгером
|
||||||
# self.client.settings.logger.enable()
|
# self.client.settings.logger.enable()
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
raise
|
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 edit_thread(
|
||||||
|
self, thread_id: Union[str, int], post_body: str = None, **kwargs
|
||||||
|
) -> bool:
|
||||||
|
logger.debug(f"Редактирую данные темы {thread_id}...")
|
||||||
|
try:
|
||||||
|
if post_body:
|
||||||
|
thread = await self.get_thread(thread_id=thread_id)
|
||||||
|
|
||||||
|
# Чтобы отредактировать соддержимое темы - надо отредактировать первый пост
|
||||||
|
post = thread.get("first_post", {})
|
||||||
|
post_id = post.get("post_id")
|
||||||
|
|
||||||
|
response = await self.client.posts.edit(
|
||||||
|
post_id=post_id, post_body=post_body
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(
|
||||||
|
f"Не удалось отредактировать пост {post_id}. Статус: {response.status_code}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
response = await self.client.threads.edit(thread_id=thread_id, **kwargs)
|
||||||
|
|
||||||
|
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 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]:
|
async def get_post(self, post_id: Union[str, int]) -> Dict[str, Any]:
|
||||||
logger.debug(f"Запрашиваю данные поста {post_id}...")
|
logger.debug(f"Запрашиваю данные поста {post_id}...")
|
||||||
response = await self.client.posts.get(post_id=post_id)
|
try:
|
||||||
post = (response.json()).get("post", {})
|
response = await self.client.posts.get(post_id=post_id)
|
||||||
|
post = (response.json()).get("post", {})
|
||||||
|
|
||||||
if post == {}:
|
if not post:
|
||||||
logger.warning(f"Пост {post_id} не найден или вернул пустой ответ.")
|
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(
|
async def get_thread_posts(
|
||||||
self, thread_id: Union[str, int], start_page: int = 1
|
self, thread_id: Union[str, int], start_page: int = 1
|
||||||
@@ -39,20 +207,36 @@ class Lolz:
|
|||||||
last_page = start_page
|
last_page = start_page
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
response = await self.client.posts.list(thread_id=thread_id, page=page)
|
try:
|
||||||
posts = (response.json()).get("posts", [])
|
response = await self.client.posts.list(thread_id=thread_id, page=page)
|
||||||
|
posts = (response.json()).get("posts", [])
|
||||||
|
|
||||||
if len(posts) == 0:
|
if len(posts) == 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
all_posts.extend(posts)
|
all_posts.extend(posts)
|
||||||
last_page = page
|
last_page = page
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Получено {len(posts)} постов из темы {thread_id} на странице {page}."
|
f"Получено {len(posts)} постов из темы {thread_id} на странице {page}."
|
||||||
)
|
)
|
||||||
|
|
||||||
page += 1
|
page += 1
|
||||||
await asyncio.sleep(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:
|
if all_posts:
|
||||||
logger.info(f"Всего получено {len(all_posts)} постов из темы {thread_id}.")
|
logger.info(f"Всего получено {len(all_posts)} постов из темы {thread_id}.")
|
||||||
@@ -69,31 +253,61 @@ class Lolz:
|
|||||||
|
|
||||||
async def get_post_comments(self, post_id: int) -> List[Dict[str, Any]]:
|
async def get_post_comments(self, post_id: int) -> List[Dict[str, Any]]:
|
||||||
logger.debug(f"Запрашиваю комментарии к посту {post_id}...")
|
logger.debug(f"Запрашиваю комментарии к посту {post_id}...")
|
||||||
response = await self.client.posts.comments.list(post_id=post_id)
|
try:
|
||||||
comments = (response.json()).get("comments", [])
|
response = await self.client.posts.comments.list(post_id=post_id)
|
||||||
logger.debug(f"Найдено {len(comments)} комментариев к посту {post_id}.")
|
comments = (response.json()).get("comments", [])
|
||||||
return 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(
|
async def has_comments(
|
||||||
self, post_id: Optional[int] = None, post: Dict[str, Any] = None
|
self, post_id: Optional[int] = None, post: Dict[str, Any] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not post:
|
if not post:
|
||||||
if not post_id:
|
if not post_id:
|
||||||
post = {}
|
raise ValueError("Необходимо указать post_id или post")
|
||||||
else:
|
post = await self.get_post(post_id=post_id)
|
||||||
post = await self.get_post(post_id=post_id)
|
|
||||||
|
|
||||||
return post.get("post_comment_count", 0) > 0
|
return post.get("post_comment_count", 0) > 0
|
||||||
|
|
||||||
async def create_comment(self, post_id: int, comment_body: str) -> bool:
|
async def create_comment(self, post_id: int, comment_body: str) -> bool:
|
||||||
logger.info(f"Публикую комментарий к посту {post_id}...")
|
logger.info(f"Публикую комментарий к посту {post_id}...")
|
||||||
response = await self.client.posts.comments.create(
|
try:
|
||||||
post_id=post_id, comment_body=comment_body
|
response = await self.client.posts.comments.create(
|
||||||
)
|
post_id=post_id, comment_body=comment_body
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
logger.info(f"Комментарий к посту {post_id} успешно опубликован.")
|
logger.info(f"Комментарий к посту {post_id} успешно опубликован.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"Не удалось опубликовать комментарий к посту {post_id}.")
|
logger.error(
|
||||||
return False
|
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
|
||||||
|
|||||||
+94
-27
@@ -1,11 +1,21 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
|
||||||
|
from aiohttp import BasicAuth
|
||||||
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
|
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
from aiogram.exceptions import (
|
||||||
|
TelegramAPIError,
|
||||||
|
TelegramBadRequest,
|
||||||
|
TelegramNetworkError,
|
||||||
|
)
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -15,23 +25,62 @@ class NotificationBot:
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.bot: Bot = Bot(
|
if config.proxy_telegram:
|
||||||
token=config.bot_token,
|
proxy = (
|
||||||
default=DefaultBotProperties(
|
f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}"
|
||||||
parse_mode="HTML",
|
)
|
||||||
disable_notification=True,
|
|
||||||
link_preview_is_disabled=True,
|
if config.proxy_username and config.proxy_password:
|
||||||
),
|
logger.info(
|
||||||
)
|
"Инициализация Telegram бота с прокси: %s://%s:%s (с авторизацией)",
|
||||||
|
config.proxy_protocol,
|
||||||
|
config.proxy_host,
|
||||||
|
config.proxy_port
|
||||||
|
)
|
||||||
|
auth = BasicAuth(
|
||||||
|
login=config.proxy_username, password=config.proxy_password
|
||||||
|
)
|
||||||
|
self.session = AiohttpSession(proxy=(proxy, auth))
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Инициализация Telegram бота с прокси: %s://%s:%s (без авторизации)",
|
||||||
|
config.proxy_protocol,
|
||||||
|
config.proxy_host,
|
||||||
|
config.proxy_port
|
||||||
|
)
|
||||||
|
self.session = AiohttpSession(proxy=proxy)
|
||||||
|
|
||||||
|
self.bot: Bot = Bot(
|
||||||
|
token=config.bot_token,
|
||||||
|
default=DefaultBotProperties(
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
disable_notification=True,
|
||||||
|
link_preview_is_disabled=True,
|
||||||
|
),
|
||||||
|
session=self.session,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Инициализация Telegram бота без прокси")
|
||||||
|
self.bot: Bot = Bot(
|
||||||
|
token=config.bot_token,
|
||||||
|
default=DefaultBotProperties(
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
disable_notification=True,
|
||||||
|
link_preview_is_disabled=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id
|
"Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
except:
|
except (TelegramAPIError, ValueError, TypeError) as e:
|
||||||
logger.exception("Не удалось инициализировать Бот для уведомлений")
|
logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}")
|
||||||
raise
|
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:
|
try:
|
||||||
logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link)
|
logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link)
|
||||||
keyboard = None
|
keyboard = None
|
||||||
@@ -57,10 +106,20 @@ class NotificationBot:
|
|||||||
)
|
)
|
||||||
return sent_message.message_id
|
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:
|
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:
|
try:
|
||||||
thread = post.get("thread")
|
thread = post.get("thread")
|
||||||
|
|
||||||
@@ -98,11 +157,14 @@ class NotificationBot:
|
|||||||
)
|
)
|
||||||
return post_message_id
|
return post_message_id
|
||||||
|
|
||||||
except:
|
except (KeyError, AttributeError, TypeError) as e:
|
||||||
logger.exception("Ошибка в new_post")
|
logger.error(f"Неверная структура поста в new_post: {e}")
|
||||||
raise
|
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:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Звезда успешно отправлена: link=%s, stars=%s",
|
"Звезда успешно отправлена: link=%s, stars=%s",
|
||||||
@@ -123,13 +185,15 @@ class NotificationBot:
|
|||||||
)
|
)
|
||||||
return success_message_id
|
return success_message_id
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
logger.exception("Ошибка в success")
|
logger.error(f"Неожиданная ошибка в success: {e}")
|
||||||
raise
|
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:
|
try:
|
||||||
permalink = post.get("links").get("permalink")
|
permalink = post.get("links", {}).get("permalink")
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Не удалось отправить звезду: reason=%r, link=%s", reason, permalink
|
"Не удалось отправить звезду: reason=%r, link=%s", reason, permalink
|
||||||
)
|
)
|
||||||
@@ -146,6 +210,9 @@ class NotificationBot:
|
|||||||
)
|
)
|
||||||
return failure_message_id
|
return failure_message_id
|
||||||
|
|
||||||
except:
|
except (KeyError, AttributeError, TypeError) as e:
|
||||||
logger.exception("Ошибка в failure")
|
logger.error(f"Неверная структура поста в failure: {e}")
|
||||||
raise
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неожиданная ошибка в failure: {e}")
|
||||||
|
return None
|
||||||
|
|||||||
+200
-31
@@ -53,7 +53,7 @@ class TelegramStarsBot:
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.start_page: int = 1
|
self.start_page: int = 1
|
||||||
self.lolz_api = Lolz(config.lolz_token)
|
self.lolz_api = Lolz(config=config)
|
||||||
self.processed_manager = ProcessedPostsManager()
|
self.processed_manager = ProcessedPostsManager()
|
||||||
|
|
||||||
self.client: Optional[Client] = None
|
self.client: Optional[Client] = None
|
||||||
@@ -141,6 +141,70 @@ class TelegramStarsBot:
|
|||||||
logger.error(f"Критическая ошибка при отправке звезд: {e}")
|
logger.error(f"Критическая ошибка при отправке звезд: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def update_captcha(self):
|
||||||
|
"""Update or add captcha text in the thread spoiler block"""
|
||||||
|
captcha_text = self.config._load_captcha()
|
||||||
|
captcha_answer = self.config._load_captcha_answer()
|
||||||
|
|
||||||
|
if not captcha_text or not self.config.enable_captcha or not captcha_answer:
|
||||||
|
if not self.config.enable_captcha:
|
||||||
|
logger.error("Капча выключена. Редактирование темы невозможно.")
|
||||||
|
elif not captcha_answer:
|
||||||
|
logger.error(
|
||||||
|
"Ответ для капчи не установлен. Редактирование темы невозможно."
|
||||||
|
)
|
||||||
|
elif not captcha_text:
|
||||||
|
logger.error(
|
||||||
|
"Вопрос (текст) капчи не установлен. Редактирование темы невозможно."
|
||||||
|
)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
thread = await self.lolz_api.get_thread(
|
||||||
|
thread_id=self.config.lolz_thread_id
|
||||||
|
)
|
||||||
|
thread_text = thread["first_post"].get("post_body", "")
|
||||||
|
|
||||||
|
# Pattern to match the captcha spoiler block
|
||||||
|
captcha_pattern = (
|
||||||
|
r"\[spoiler=Капча[^\]]*\]\[CENTER\].*?\[/CENTER\]\[/spoiler\]"
|
||||||
|
)
|
||||||
|
|
||||||
|
if re.search(captcha_pattern, thread_text, re.DOTALL):
|
||||||
|
# Replace existing captcha text
|
||||||
|
captcha_wrapper = f"[spoiler=Капча (обязательно);align=center][CENTER]{captcha_text}[/CENTER][/spoiler]"
|
||||||
|
updated_text = re.sub(
|
||||||
|
captcha_pattern, captcha_wrapper, thread_text, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if text actually changed
|
||||||
|
if updated_text == thread_text:
|
||||||
|
logger.info(
|
||||||
|
f"Капча в теме {self.config.lolz_thread_id} уже актуальна, изменений не требуется."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
thread_text = updated_text
|
||||||
|
logger.info(f"Обновление капчи в теме {self.config.lolz_thread_id}...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Add new captcha block
|
||||||
|
captcha_wrapper = f"\n\n[spoiler=Капча (обязательно);align=center][CENTER]{captcha_text}[/CENTER][/spoiler]"
|
||||||
|
thread_text += captcha_wrapper
|
||||||
|
logger.info(f"Добавление капчи в тему {self.config.lolz_thread_id}...")
|
||||||
|
|
||||||
|
await self.lolz_api.edit_thread(
|
||||||
|
thread_id=self.config.lolz_thread_id, post_body=thread_text
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Тема {self.config.lolz_thread_id} успешно отредактирована.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Критическая ошибка при редактировании темы: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def _process_single_post(self, post: Dict[str, Any]):
|
async def _process_single_post(self, post: Dict[str, Any]):
|
||||||
post_message_id = None
|
post_message_id = None
|
||||||
|
|
||||||
@@ -164,6 +228,12 @@ class TelegramStarsBot:
|
|||||||
f"Найден новый пост для обработки: ID {post_id} (от {poster_username})"
|
f"Найден новый пост для обработки: ID {post_id} (от {poster_username})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
if self.config.skip_commented:
|
if self.config.skip_commented:
|
||||||
if await self.lolz_api.has_comments(post=post):
|
if await self.lolz_api.has_comments(post=post):
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -172,11 +242,12 @@ class TelegramStarsBot:
|
|||||||
self.processed_manager.mark_processed(post_id)
|
self.processed_manager.mark_processed(post_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
post_content = post.get("post_body_html") or post.get("post_body")
|
if self.config.enable_captcha:
|
||||||
if not post_content:
|
captcha_answer = self.config._load_captcha_answer()
|
||||||
logger.warning(f"У поста {post_id} отсутствует содержимое. Пропускаем.")
|
if captcha_answer and str(captcha_answer) not in post_content:
|
||||||
self.processed_manager.mark_processed(post_id)
|
logger.info(f"Пост {post_id} не содержит капчу. Пропускаю обработку.")
|
||||||
return
|
self.processed_manager.mark_processed(post_id)
|
||||||
|
return
|
||||||
|
|
||||||
links = TelegramLinkExtractor.extract(post_content)
|
links = TelegramLinkExtractor.extract(post_content)
|
||||||
if not links:
|
if not links:
|
||||||
@@ -291,37 +362,111 @@ class TelegramStarsBot:
|
|||||||
logger.exception(f"Критическая ошибка в главном цикле: {e}")
|
logger.exception(f"Критическая ошибка в главном цикле: {e}")
|
||||||
await asyncio.sleep(self.config.check_interval)
|
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 _captcha_loop(self):
|
||||||
|
"""Loop to periodically check and update captcha text"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
f"Проверка обновления капчи для темы {self.config.lolz_thread_id}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.update_captcha()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Ожидание {self.config.captcha_check_interval} секунд до следующей проверки капчи..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(self.config.captcha_check_interval)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Получен сигнал прерывания (Ctrl+C).")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Критическая ошибка в цикле обновления капчи: {e}")
|
||||||
|
await asyncio.sleep(self.config.captcha_check_interval)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
|
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
|
||||||
|
|
||||||
if is_first_login:
|
if is_first_login:
|
||||||
logger.info("Сессия Telegram не найдена. Запускаю процесс входа...")
|
logger.info("Сессия Telegram не найдена. Запускаю процесс входа...")
|
||||||
else:
|
else:
|
||||||
if self.config.default_choice == 2:
|
|
||||||
logger.info("Выбран режим парсинга существующих постов.")
|
|
||||||
self.client = Client(
|
|
||||||
name=self.SESSION_NAME,
|
|
||||||
api_id=self.config.api_id,
|
|
||||||
api_hash=self.config.api_hash,
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.client.start()
|
|
||||||
await self.parse_existing_posts()
|
|
||||||
await self.client.stop()
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Парсинг завершен. Все существующие посты добавлены в обработанные."
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
self.start_page = self.config.start_page
|
self.start_page = self.config.start_page
|
||||||
|
|
||||||
self.client = Client(
|
if self.config.proxy_telegram:
|
||||||
name=self.SESSION_NAME,
|
proxy = {
|
||||||
api_id=self.config.api_id,
|
"scheme": self.config.proxy_protocol,
|
||||||
api_hash=self.config.api_hash,
|
"hostname": self.config.proxy_host,
|
||||||
)
|
"port": self.config.proxy_port,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.proxy_username and self.config.proxy_password:
|
||||||
|
logger.info(
|
||||||
|
"Инициализация Pyrogram с прокси: %s://%s:%s (с авторизацией)",
|
||||||
|
self.config.proxy_protocol,
|
||||||
|
self.config.proxy_host,
|
||||||
|
self.config.proxy_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy["username"] = self.config.proxy_username
|
||||||
|
proxy["password"] = self.config.proxy_password
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Инициализация Pyrogram с прокси: %s://%s:%s (без авторизации)",
|
||||||
|
self.config.proxy_protocol,
|
||||||
|
self.config.proxy_host,
|
||||||
|
self.config.proxy_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client = Client(
|
||||||
|
name=self.SESSION_NAME,
|
||||||
|
api_id=self.config.api_id,
|
||||||
|
api_hash=self.config.api_hash,
|
||||||
|
proxy=proxy,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.client = Client(
|
||||||
|
name=self.SESSION_NAME,
|
||||||
|
api_id=self.config.api_id,
|
||||||
|
api_hash=self.config.api_hash,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.client.start()
|
await self.client.start()
|
||||||
@@ -334,10 +479,34 @@ class TelegramStarsBot:
|
|||||||
|
|
||||||
logger.info(f"Проверка начинается со страницы: {self.start_page}")
|
logger.info(f"Проверка начинается со страницы: {self.start_page}")
|
||||||
logger.info("Бот в работе. Для остановки нажмите Ctrl+C.")
|
logger.info("Бот в работе. Для остановки нажмите Ctrl+C.")
|
||||||
await self._main_loop()
|
|
||||||
|
# _main_loop - Основной цикл для остлеживания темы
|
||||||
|
# _bump_loop - Цикл для авто-поднятия темы
|
||||||
|
# _captcha_loop - Цикл для обновления капчи
|
||||||
|
|
||||||
|
if self.config.enable_captcha:
|
||||||
|
await self.update_captcha()
|
||||||
|
|
||||||
|
# Gather all enabled loops
|
||||||
|
tasks = [self._main_loop()]
|
||||||
|
|
||||||
|
if self.config.enable_auto_bump:
|
||||||
|
tasks.append(self._bump_loop())
|
||||||
|
|
||||||
|
if self.config.enable_captcha:
|
||||||
|
tasks.append(self._captcha_loop())
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в главном процессе бота: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if self.client and self.client.is_connected:
|
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("Бот остановлен.")
|
logger.info("Бот остановлен.")
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
[B]Напишите ответ:[/B]
|
||||||
|
сколько будет 20 + 20? [I][COLOR=rgb(80, 80, 78)](Капча меняется раз в месяц)[/COLOR][/I]
|
||||||
Reference in New Issue
Block a user