38 Commits

Author SHA1 Message Date
root 31bf50aaca feat(proxy): add proxy support to Telegram client
Build and Push (main) / build (push) Successful in 39s
Build and Push (dev) / build (push) Successful in 35s
2026-08-16 06:36:49 +05:00
root 8423912dbb chore(examples): add UID/GID environment defaults 2026-08-16 06:23:03 +05:00
root 64508438e7 docs(readme): reformat tables and reorg sections
Build and Push (main) / build (push) Successful in 2m33s
2026-08-16 06:02:13 +05:00
root f5cf63bac0 docs: update README with new features and settings 2026-08-16 05:41:19 +05:00
root 37d4400513 refactor(config): remove DEFAULT_CHOICE option
Build and Push (dev) / build (push) Successful in 1m56s
2026-08-16 05:20:24 +05:00
root 5dece14c8b build: update requirements.txt 2026-08-16 05:18:16 +05:00
root 5c325bd8b9 feat(config): add proxy support and API timeout
Build and Push (dev) / build (push) Successful in 3m31s
2026-08-16 05:15:24 +05:00
root 62873c7f8c Delete .gitea/FUNDING.yml 2026-08-12 21:28:27 +00:00
root 4883320aaa Update .gitea/FUNDING.yml 2026-08-12 21:27:51 +00:00
root 0f9d6470b8 Add .gitea/FUNDING.md 2026-08-12 21:27:19 +00:00
root 76f37673d4 refactor(stars_bot): tidy captcha handling
Build and Push (dev) / build (push) Successful in 53s
Build and Push (main) / build (push) Successful in 50s
2026-08-08 08:51:42 +05:00
root 5772df644f feat(examples): add auto bump and captcha env vars 2026-08-08 08:41:22 +05:00
root f5fd0187ec refactor(captcha): load answer from env dynamically
Build and Push (main) / build (push) Successful in 54s
Build and Push (dev) / build (push) Successful in 50s
2026-08-08 06:26:56 +05:00
root cb83fc1608 Merge branch 'dev'
Build and Push (main) / build (push) Successful in 52s
2026-08-08 06:16:35 +05:00
root f82f9ef752 feat(captcha): add periodic captcha update
Build and Push (dev) / build (push) Successful in 48s
2026-08-08 06:16:09 +05:00
root 96179c5715 Merge branch 'dev'
Build and Push (main) / build (push) Successful in 41s
2026-08-08 05:11:16 +05:00
root 39defecb2f revert 8a1e467a15
revert Update storage/captcha.txt
2026-08-07 23:22:44 +00:00
root 8a1e467a15 Update storage/captcha.txt 2026-08-07 23:22:21 +00:00
root 7e85a1af4c refactor: streamline captcha and thread access
Build and Push (dev) / build (push) Successful in 1m29s
2026-08-08 04:03:32 +05:00
root 0cc2440736 feat(captcha): add validation and thread editing
Build and Push (dev) / build (push) Successful in 38m3s
Build and Push (main) / build (push) Canceled after 21m44s
2026-08-07 10:51:54 +05:00
root 9ee9e7b731 chore(deps): update Python dependencies 2026-08-07 10:33:43 +05:00
root 62c44e885b chore(gitignore): remove duplicate *.txt entry 2026-08-07 09:26:48 +05:00
root 2919e95c4c chore(gitignore): add txt files to .gitignore 2026-08-07 09:26:32 +05:00
root 620115d89a docs(captcha): simplify captcha prompt formatting 2026-08-07 09:26:02 +05:00
root d6d2752341 feat(config): add auto bump and captcha options 2026-08-07 09:04:16 +05:00
root f76c74ce3f feat(captcha): add captcha config and template 2026-08-07 09:02:32 +05:00
root 4abb5d6acc Merge branch 'dev'
Build and Push (main) / build (push) Successful in 45s
Build and Push (dev) / build (push) Successful in 12m19s
2026-08-07 05:53:02 +05:00
open-webui adf963a1a7 Optimize dev workflow with separate cache
Build and Push (main) / build (push) Canceled after 0s
2026-08-07 00:48:41 +00:00
open-webui 6bcb89ffd0 Optimize build workflow for speed
Build and Push (main) / build (push) Canceled after 0s
2026-08-07 00:48:40 +00:00
open-webui 74b8dddd9d Optimize Dockerfile with multi-stage build
Build and Push (main) / build (push) Canceled after 1m34s
2026-08-07 00:48:39 +00:00
root 76e6659c00 refactor: add ConfigError and Lolz error handling
Build and Push (dev) / build (push) Successful in 10m53s
2026-08-07 05:08:46 +05:00
root b7a7508654 fix(stars_bot): correct async gathering and interval config 2026-08-07 04:08:25 +05:00
root b557ff0403 feat(bump): add auto thread bumping 2026-08-07 04:00:20 +05:00
root 03c463b87c refactor(notification_bot): use ParseMode enum for bot default 2026-08-07 03:57:06 +05:00
root c59a8b9a47 feat(config): include default ad reply
Build and Push (dev) / build (push) Failing after 42s
2026-07-30 08:20:37 +05:00
root 958b00a8f3 Update README.md 2026-07-30 01:05:25 +00:00
root 61527aa4c5 Update README.md 2026-07-30 01:04:49 +00:00
open-webui f37977e88f feat: Rewrite README.md with improved structure, Docker-first instructions, and correct repo URLs 2026-07-30 01:02:41 +00:00
14 changed files with 1070 additions and 257 deletions
+27 -5
View File
@@ -14,16 +14,38 @@ LOLZ_TOKEN=
LOLZ_THREAD_ID=
STARS_COUNT=1
# 1 - Начать мониторинг
# 2 - Спарсить все существующие посты
DEFAULT_CHOICE=2
START_PAGE=1
CHECK_INTERVAL=30
CHECK_INTERVAL=10
API_DELAY=5
API_TIMEOUT=15
MAX_RETRIES=3
ENABLE_REPLY=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= # Можно не ставить, если для прокси авторизация не нужна
+6 -2
View File
@@ -16,6 +16,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -40,7 +42,9 @@ jobs:
tags: |
git.sakamoto.best/lolz/auto-stars:${{ steps.vars.outputs.SHORT_SHA }}
git.sakamoto.best/lolz/auto-stars:dev
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-from: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache-dev
cache-to: type=registry,ref=git.sakamoto.best/lolz/auto-stars:buildcache-dev,mode=max
provenance: false
sbom: false
build-args: |
BUILDKIT_INLINE_CACHE=1
+4
View File
@@ -16,6 +16,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -42,5 +44,7 @@ jobs:
git.sakamoto.best/lolz/auto-stars:latest
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
provenance: false
sbom: false
build-args: |
BUILDKIT_INLINE_CACHE=1
+17 -4
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim
FROM python:3.12-slim AS builder
# Prevent .pyc files and enable unbuffered stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -6,14 +6,27 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
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 && \
apt-get install -y --no-install-recommends gcc libc6-dev && \
rm -rf /var/lib/apt/lists/*
# Install deps first — layer cache reuse on code-only changes
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 main.py config.py ./
@@ -29,4 +42,4 @@ USER botuser
# Persistent volume for Pyrogram session and logs
VOLUME ["/app/storage", "/app/logs"]
CMD ["python", "main.py"]
CMD ["python", "main.py"]
+191 -129
View File
@@ -1,136 +1,198 @@
# ⭐ 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 за каждый новый пост
- Автоматические ответы с поддержкой шаблонов (включая случайный выбор)
- Автоматическое поднятие темы (bump) с настраиваемым интервалом
- Система капчи для фильтрации пользователей
- Поддержка прокси для Telegram и Lolz API (SOCKS5/HTTPS)
- Полная поддержка 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 |
| `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/
+237 -17
View File
@@ -7,12 +7,26 @@ 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 +38,35 @@ 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_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):
load_dotenv()
@@ -43,24 +80,32 @@ 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))
# Misc
default_choice = int(os.getenv(key="DEFAULT_CHOICE", default=2))
start_page = max(int(os.getenv(key="START_PAGE", default=1)), 1)
check_interval = max(
@@ -69,6 +114,7 @@ class Config:
api_delay = max(
float(os.getenv(key="API_DELAY", default=5)), 2
) # 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))
enable_reply = (
@@ -78,6 +124,156 @@ class Config:
os.getenv(key="SKIP_COMMENTED", default="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"]:
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 {
# Pyrogram
"api_id": api_id,
@@ -91,19 +287,43 @@ class Config:
"lolz_thread_id": lolz_thread_id,
# Stars
"stars_count": stars_count,
# Misc
"default_choice": default_choice,
# Posts
"start_page": start_page,
"check_interval": check_interval,
"api_delay": api_delay,
"max_retries": max_retries,
"enable_reply": enable_reply,
"check_interval": check_interval,
"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
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(
name, True if name == "skip_posts_with_comments" else None
+31 -5
View File
@@ -1,6 +1,9 @@
LOGS_PATH=./logs
STORAGE_PATH=./storage
UID=1000
GID=1000
# Значения для клиента Telegram Desktop, ставятся по умолчанию
# Не рекомендуется оставлять, лучше использовать API_ID и API_HASH от
# аккаунта с которого будет воспроизведен вход
@@ -17,16 +20,39 @@ LOLZ_TOKEN=
LOLZ_THREAD_ID=
STARS_COUNT=1
# 1 - Начать мониторинг
# 2 - Спарсить все существующие посты
DEFAULT_CHOICE=2
START_PAGE=1
CHECK_INTERVAL=30
CHECK_INTERVAL=10
API_DELAY=5
API_TIMEOUT=15
MAX_RETRIES=3
ENABLE_REPLY=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= # Можно не ставить, если для прокси авторизация не нужна
+1
View File
@@ -3,6 +3,7 @@ name: auto-stars
services:
auto-stars:
image: git.sakamoto.best/lolz/auto-stars:latest
user: ${UID:-1000}:${GID:-1000}
volumes:
- ${LOGS_PATH:-./logs}:/app/logs
+10 -1
View File
@@ -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
+250 -36
View File
@@ -4,27 +4,195 @@ import asyncio
from LOLZTEAM.Client import Forum
from typing import Dict, Any, Optional, Union, List
from config import Config
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):
def __init__(self, config: Config):
if not config.lolz_token:
raise ValueError("LOLZ_TOKEN обязателен")
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()
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 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]:
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 +207,36 @@ 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 +253,61 @@ 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
+94 -27
View File
@@ -1,11 +1,21 @@
import logging
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.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__)
@@ -15,23 +25,62 @@ class NotificationBot:
self.config = config
try:
self.bot: Bot = Bot(
token=config.bot_token,
default=DefaultBotProperties(
parse_mode="HTML",
disable_notification=True,
link_preview_is_disabled=True,
),
)
if config.proxy_telegram:
proxy = (
f"{config.proxy_protocol}://{config.proxy_host}:{config.proxy_port}"
)
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(
"Бот для уведомлений инициализирован (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 +106,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 +157,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 +185,15 @@ 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 +210,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
+200 -31
View File
@@ -53,7 +53,7 @@ class TelegramStarsBot:
self.config = config
self.start_page: int = 1
self.lolz_api = Lolz(config.lolz_token)
self.lolz_api = Lolz(config=config)
self.processed_manager = ProcessedPostsManager()
self.client: Optional[Client] = None
@@ -141,6 +141,70 @@ class TelegramStarsBot:
logger.error(f"Критическая ошибка при отправке звезд: {e}")
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]):
post_message_id = None
@@ -164,6 +228,12 @@ class TelegramStarsBot:
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 await self.lolz_api.has_comments(post=post):
logger.info(
@@ -172,11 +242,12 @@ class TelegramStarsBot:
self.processed_manager.mark_processed(post_id)
return
post_content = post.get("post_body_html") or post.get("post_body")
if not post_content:
logger.warning(f"У поста {post_id} отсутствует содержимое. Пропускаем.")
self.processed_manager.mark_processed(post_id)
return
if self.config.enable_captcha:
captcha_answer = self.config._load_captcha_answer()
if captcha_answer and str(captcha_answer) not in post_content:
logger.info(f"Пост {post_id} не содержит капчу. Пропускаю обработку.")
self.processed_manager.mark_processed(post_id)
return
links = TelegramLinkExtractor.extract(post_content)
if not links:
@@ -291,37 +362,111 @@ 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 _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):
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
if is_first_login:
logger.info("Сессия Telegram не найдена. Запускаю процесс входа...")
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.client = Client(
name=self.SESSION_NAME,
api_id=self.config.api_id,
api_hash=self.config.api_hash,
)
if self.config.proxy_telegram:
proxy = {
"scheme": self.config.proxy_protocol,
"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:
await self.client.start()
@@ -334,10 +479,34 @@ class TelegramStarsBot:
logger.info(f"Проверка начинается со страницы: {self.start_page}")
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:
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("Бот остановлен.")
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
[B]Напишите ответ:[/B]
сколько будет 20 + 20? [I][COLOR=rgb(80, 80, 78)](Капча меняется раз в месяц)[/COLOR][/I]