Merge branch 'dev'
Build and Push (main) / build (push) Successful in 45s
Build and Push (dev) / build (push) Successful in 12m19s

This commit is contained in:
2026-08-07 05:53:02 +05:00
7 changed files with 380 additions and 204 deletions
+1
View File
@@ -27,3 +27,4 @@ MAX_RETRIES=3
ENABLE_REPLY=true
SKIP_COMMENTED=true
BUMP_CHECK_INTERVAL=3600
+87 -129
View File
@@ -1,136 +1,94 @@
# ⭐ Auto-Stars
# ⭐ Auto-Stars-Improved
Бот автоматически отправляет Telegram Stars пользователям, которые оставляют посты в указанной теме на Lolz.team.
Бот для автоматической отправки Telegram Stars пользователям, которые оставляют посты в указанной теме на форуме Lolz.team.
## Как это работает
## Возможности
Бот следит за выбранной темой на форуме. Когда кто-то публикует новый пост — бот отправляет ему звёзды через Telegram. Опционально может оставлять комментарий под постом.
## Установка
### Вариант 1 — Docker (рекомендуется)
**1. Клонируйте репозиторий**
```bash
git clone https://github.com/ваш-ник/auto-stars
cd auto-stars
```
**2. Создайте `.env`** на основе примера и заполните его:
```bash
cp examples/.env.example .env
```
**3. Авторизуйтесь в Telegram**
Pyrogram требует интерактивного входа при первом запуске. Выполните его локально:
```bash
pip install -r requirements.txt
python main.py
```
Введите номер телефона и код подтверждения. После успешного входа бот создаст файл `storage/stars_bot.session` и завершится с сообщением о необходимости перезапуска. После этого Docker будет использовать готовую сессию.
**4. Запустите через Docker Compose**
```bash
docker compose -f examples/docker-compose.yml up -d
```
Данные сессии и логи хранятся в `./storage` и `./logs` на хосте (или по путям `LOGS_PATH` / `STORAGE_PATH` из `.env`).
**5. Просмотр логов**
```bash
docker compose -f examples/docker-compose.yml logs -f
```
---
### Вариант 2 — Запуск напрямую
**1. Клонируйте репозиторий**
```bash
git clone https://github.com/ваш-ник/auto-stars
cd auto-stars
```
**2. Установите зависимости**
```bash
pip install -r requirements.txt
```
**3. Создайте файл `.env`** на основе `.env.example` и заполните его (см. раздел ниже).
**4. Запустите бота**
```bash
python main.py
```
При первом запуске Pyrogram попросит войти в Telegram-аккаунт — введите номер телефона и код подтверждения.
## Настройка `.env`
```env
# Telegram API — получить на https://my.telegram.org
API_ID=ваш_api_id
API_HASH=ваш_api_hash
# Lolz.team — токен API из настроек профиля
LOLZ_TOKEN=ваш_токен
# ID темы на форуме, за которой следит бот
LOLZ_THREAD_ID=123456
# Сколько звёзд отправлять за каждый пост
STARS_COUNT=1
# Режим запуска:
# 1 — мониторинг новых постов в реальном времени
# 2 — обработать все существующие посты в теме
DEFAULT_CHOICE=2
# С какой страницы начинать (обычно 1)
START_PAGE=1
# Как часто проверять новые посты (в секундах, минимум ~0.2)
CHECK_INTERVAL=30
# Задержка между запросами к API (в секундах, минимум 2)
API_DELAY=5
# Количество повторных попыток при ошибке
MAX_RETRIES=3
# Оставлять ли комментарий под постом
ENABLE_REPLY=true
# Пропускать посты, под которыми уже есть комментарий
SKIP_COMMENTED=true
```
## Шаблоны ответов
Если `ENABLE_REPLY=true`, бот будет комментировать посты. Тексты берутся из файла `storage/replies.txt`.
Каждая строка — отдельный шаблон. Если в строке несколько вариантов через `||`, бот выберет один из них случайно:
```
:catdance: Отправил звёзды || :animeyes2: Отправил
:sadge: Тут может быть реклама || ​Реклама казика :animeyessir:
:sigma: Это третье сообщение
```
Если файл отсутствует или пустой — бот использует `✓` по умолчанию.
- Автоматический мониторинг темы на Lolz.team
- Отправка Telegram Stars за каждый новый пост
- Автоматические ответы с поддержкой шаблонов (включая случайный выбор)
- Полная поддержка Docker (рекомендуется)
- CI/CD через Gitea Actions (сборка и публикация образов)
- Уведомления администратору через Telegram
- Гибкая настройка через `.env`
- Логирование и сохранение сессии
## Требования
- Python 3.10+
- Аккаунт Telegram с доступом к [my.telegram.org](https://my.telegram.org)
- API-токен [Lolz.team](https://lolz.team)
- Аккаунт Telegram (для получения `API_ID` и `API_HASH`)
- API-токен Lolz.team
- Docker (рекомендуется) или Python окружение
## Быстрый старт (Docker — рекомендуется)
1. Клонируйте репозиторий:
```bash
git clone https://git.sakamoto.best/Lolz/Auto-Stars-Improved.git
cd Auto-Stars-Improved
```
2. Скопируйте пример конфигурации:
```bash
cp examples/.env.example .env
```
3. Отредактируйте `.env` и заполните необходимые значения (`API_ID`, `API_HASH`, `LOLZ_TOKEN`, `LOLZ_THREAD_ID` и т.д.).
4. Запустите:
```bash
docker compose -f examples/docker-compose.yml up -d
```
5. Посмотрите логи:
```bash
docker compose -f examples/docker-compose.yml logs -f
```
## Запуск без Docker
```bash
pip install -r requirements.txt
cp examples/.env.example .env
# отредактируйте .env
python main.py
```
> При первом запуске (если используете Pyrogram напрямую) потребуется интерактивная авторизация в Telegram.
## Конфигурация
Все настройки находятся в файле `.env`.
Пример конфигурации лежит здесь:
- `examples/.env.example`
**Основные параметры:**
| Переменная | Описание | По умолчанию |
|-----------------------|-----------------------------------------------|--------------|
| `API_ID` / `API_HASH` | Данные Telegram API | - |
| `LOLZ_TOKEN` | Токен API Lolz.team | - |
| `LOLZ_THREAD_ID` | ID темы на форуме | - |
| `STARS_COUNT` | Сколько звёзд отправлять за пост | 1 |
| `DEFAULT_CHOICE` | 1 — реал-тайм, 2 — обработать все посты | 2 |
| `ENABLE_REPLY` | Отправлять ли комментарии | true |
| `SKIP_COMMENTED` | Пропускать посты, на которые уже отвечали | true |
## Шаблоны ответов
Если `ENABLE_REPLY=true`, бот будет отвечать под постами.
Шаблоны хранятся в файле `storage/replies.txt`.
**Формат:**
- Каждая строка — отдельный шаблон
- Можно использовать `||` для случайного выбора
**Пример:**
```
:catdance: Отправил звёзды || :animeyes2: Отправил
:sadge: Тут может быть реклама
```
Если файл пустой — используется `✓`.
+50 -15
View File
@@ -7,12 +7,25 @@ from dotenv import load_dotenv
logger = logging.getLogger(__name__)
class ConfigError(Exception):
"""Ошибка валидации конфигурации"""
pass
class Config:
def __init__(self):
self._config_data = self._load()
logger.info("Конфигурация успешно загружена.")
def _load_replies(self, file_path: str = "storage/replies.txt"):
script_ad_reply = [
o.strip()
for o in str(
"Скрипт для авто-раздачи :da: [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad] || Сиськи, сливы 18+ [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad] || Твой помощник для авто-раздачи :animeyessir: [ad][UNFURL]https://lolz.team/threads/10187517/[/UNFURL][/ad]"
).split(" || ")
]
default_replies = [[""], script_ad_reply]
try:
reply_lines = []
with open(file_path, "r", encoding="utf-8") as replies:
@@ -24,12 +37,14 @@ class Config:
options = [o.strip() for o in line.split(" || ")]
else:
options = [line]
reply_lines.append(options)
return reply_lines if reply_lines else [[""]]
reply_lines.append(options)
reply_lines.append(script_ad_reply)
return reply_lines if reply_lines else default_replies
except FileNotFoundError:
return [[""]]
return default_replies
def _load(self):
load_dotenv()
@@ -43,18 +58,25 @@ class Config:
# Notification bot (aiogram)
bot_token = os.getenv(key="BOT_TOKEN", default=None)
admin_id = (
os.getenv(key="ADMIN_ID", default=None)
if not os.getenv(key="ADMIN_ID", default=None)
else int(os.getenv(key="ADMIN_ID", default=None))
)
admin_id_str = os.getenv(key="ADMIN_ID", default=None)
admin_id = int(admin_id_str) if admin_id_str else None
notify_admin = (
os.getenv(key="NOTIFY_ADMIN", default="true")
).lower() == "true"
# Lolz
# Lolz - required fields
lolz_token = os.getenv(key="LOLZ_TOKEN", default=None)
lolz_thread_id = int(os.getenv(key="LOLZ_THREAD_ID", default=None))
if not lolz_token:
raise ConfigError("LOLZ_TOKEN обязателен, но не установлен")
lolz_thread_id_str = os.getenv(key="LOLZ_THREAD_ID", default=None)
if not lolz_thread_id_str:
raise ConfigError("LOLZ_THREAD_ID обязателен, но не установлен")
try:
lolz_thread_id = int(lolz_thread_id_str)
except (ValueError, TypeError) as e:
raise ConfigError(f"LOLZ_THREAD_ID должен быть целым числом: {lolz_thread_id_str}") from e
# Stars
stars_count = int(os.getenv(key="STARS_COUNT", default=1))
@@ -78,6 +100,10 @@ class Config:
os.getenv(key="SKIP_COMMENTED", default="true")
).lower() == "true"
bump_check_interval = max(
float(os.getenv(key="BUMP_CHECK_INTERVAL", default=3600)), 3600
) # 1 поднятие/3600с (1ч) = минимум
return {
# Pyrogram
"api_id": api_id,
@@ -91,18 +117,27 @@ class Config:
"lolz_thread_id": lolz_thread_id,
# Stars
"stars_count": stars_count,
# Misc
"default_choice": default_choice,
# Posts
"start_page": start_page,
"enable_reply": enable_reply,
"check_interval": check_interval,
"skip_commented": skip_commented,
# Misc
"api_delay": api_delay,
"max_retries": max_retries,
"enable_reply": enable_reply,
"skip_commented": skip_commented,
"default_choice": default_choice,
# Bump
"bump_check_interval": bump_check_interval,
}
except:
except ConfigError:
raise
except (ValueError, TypeError) as e:
logger.error(f"Invalid configuration value: {e}")
raise ConfigError(f"Invalid configuration value: {e}") from e
except Exception as e:
logger.error(f"Unexpected error loading configuration: {e}")
raise ConfigError(f"Failed to load configuration: {e}") from e
def __getattr__(self, name: str) -> Any:
return self._config_data.get(
+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
+138 -34
View File
@@ -7,24 +7,105 @@ from typing import Dict, Any, Optional, Union, List
logger = logging.getLogger(__name__)
class LolzAPIError(Exception):
"""Base exception for Lolz API errors"""
pass
class LolzNetworkError(LolzAPIError):
"""Network-related errors"""
pass
class LolzResponseError(LolzAPIError):
"""API returned invalid or empty response"""
pass
class Lolz:
def __init__(self, token: str):
if not token:
raise ValueError("LOLZ_TOKEN обязателен")
try:
self.client = Forum(token=token, timeout=15)
# ! Не рекомендуется включить этот логгер,
# ! ибо логи занимают очень много места в экране при этом логгере
# self.client.settings.logger.enable()
except:
raise
except Exception as e:
logger.error(f"Не удалось инициализировать Lolz клиент: {e}")
raise LolzAPIError(f"Не удалось инициализировать Lolz клиент: {e}") from e
async def get_thread(self, thread_id: Union[str, int]) -> Dict[str, Any]:
logger.debug(f"Запрашиваю данные темы {thread_id}...")
try:
response = await self.client.threads.get(thread_id=thread_id)
thread = (response.json()).get("thread", {})
if not thread:
raise LolzResponseError(f"Тема {thread_id} не найдена или вернула пустой ответ")
return thread
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении темы {thread_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении темы {thread_id}") from e
except Exception as e:
logger.error(f"Ошибка при получении темы {thread_id}: {e}")
raise LolzAPIError(f"Не удалось получить тему {thread_id}") from e
async def can_bump(
self, thread_id: Optional[int] = None, thread: Dict[str, Any] = None
) -> bool:
if not thread:
if not thread_id:
raise ValueError("Необходимо указать thread_id или thread")
thread = await self.get_thread(thread_id=thread_id)
try:
return thread.get("permissions", {}).get("bump", {}).get("can", False)
except (KeyError, AttributeError, TypeError) as e:
logger.error(f"Неверная структура темы для проверки поднятия: {e}")
return False
async def bump_thread(self, thread_id: Union[str, int]) -> bool:
logger.debug(f"Поднимаю тему {thread_id}...")
try:
response = await self.client.threads.bump(thread_id=thread_id)
if response.status_code == 200:
logger.info(f"Тема {thread_id} успешно поднята.")
return True
else:
logger.error(f"Не удалось поднять тему {thread_id}. Статус: {response.status_code}")
return False
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при поднятии темы {thread_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при поднятии темы {thread_id}") from e
except Exception as e:
logger.error(f"Ошибка при поднятии темы {thread_id}: {e}")
raise LolzAPIError(f"Не удалось поднять тему {thread_id}") from e
async def get_post(self, post_id: Union[str, int]) -> Dict[str, Any]:
logger.debug(f"Запрашиваю данные поста {post_id}...")
response = await self.client.posts.get(post_id=post_id)
post = (response.json()).get("post", {})
try:
response = await self.client.posts.get(post_id=post_id)
post = (response.json()).get("post", {})
if post == {}:
logger.warning(f"Пост {post_id} не найден или вернул пустой ответ.")
if not post:
raise LolzResponseError(f"Пост {post_id} не найден или вернул пустой ответ")
return post
return post
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении поста {post_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении поста {post_id}") from e
except Exception as e:
logger.error(f"Ошибка при получении поста {post_id}: {e}")
raise LolzAPIError(f"Не удалось получить пост {post_id}") from e
async def get_thread_posts(
self, thread_id: Union[str, int], start_page: int = 1
@@ -39,20 +120,28 @@ class Lolz:
last_page = start_page
while True:
response = await self.client.posts.list(thread_id=thread_id, page=page)
posts = (response.json()).get("posts", [])
try:
response = await self.client.posts.list(thread_id=thread_id, page=page)
posts = (response.json()).get("posts", [])
if len(posts) == 0:
break
if len(posts) == 0:
break
all_posts.extend(posts)
last_page = page
logger.debug(
f"Получено {len(posts)} постов из темы {thread_id} на странице {page}."
)
all_posts.extend(posts)
last_page = page
logger.debug(
f"Получено {len(posts)} постов из темы {thread_id} на странице {page}."
)
page += 1
await asyncio.sleep(1)
page += 1
await asyncio.sleep(1)
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении постов из темы {thread_id}, страница {page}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении постов из темы {thread_id}") from e
except Exception as e:
logger.error(f"Ошибка при получении постов из темы {thread_id}, страница {page}: {e}")
raise LolzAPIError(f"Не удалось получить посты из темы {thread_id}") from e
if all_posts:
logger.info(f"Всего получено {len(all_posts)} постов из темы {thread_id}.")
@@ -69,31 +158,46 @@ class Lolz:
async def get_post_comments(self, post_id: int) -> List[Dict[str, Any]]:
logger.debug(f"Запрашиваю комментарии к посту {post_id}...")
response = await self.client.posts.comments.list(post_id=post_id)
comments = (response.json()).get("comments", [])
logger.debug(f"Найдено {len(comments)} комментариев к посту {post_id}.")
return comments
try:
response = await self.client.posts.comments.list(post_id=post_id)
comments = (response.json()).get("comments", [])
logger.debug(f"Найдено {len(comments)} комментариев к посту {post_id}.")
return comments
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении комментариев к посту {post_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении комментариев к посту {post_id}") from e
except Exception as e:
logger.error(f"Ошибка при получении комментариев к посту {post_id}: {e}")
raise LolzAPIError(f"Не удалось получить комментарии к посту {post_id}") from e
async def has_comments(
self, post_id: Optional[int] = None, post: Dict[str, Any] = None
) -> bool:
if not post:
if not post_id:
post = {}
else:
post = await self.get_post(post_id=post_id)
raise ValueError("Необходимо указать post_id или post")
post = await self.get_post(post_id=post_id)
return post.get("post_comment_count", 0) > 0
async def create_comment(self, post_id: int, comment_body: str) -> bool:
logger.info(f"Публикую комментарий к посту {post_id}...")
response = await self.client.posts.comments.create(
post_id=post_id, comment_body=comment_body
)
try:
response = await self.client.posts.comments.create(
post_id=post_id, comment_body=comment_body
)
if response.status_code == 200:
logger.info(f"Комментарий к посту {post_id} успешно опубликован.")
return True
else:
logger.error(f"Не удалось опубликовать комментарий к посту {post_id}.")
return False
if response.status_code == 200:
logger.info(f"Комментарий к посту {post_id} успешно опубликован.")
return True
else:
logger.error(f"Не удалось опубликовать комментарий к посту {post_id}. Статус: {response.status_code}")
return False
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при создании комментария к посту {post_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при создании комментария к посту {post_id}") from e
except Exception as e:
logger.error(f"Ошибка при создании комментария к посту {post_id}: {e}")
raise LolzAPIError(f"Не удалось создать комментарий к посту {post_id}") from e
+38 -20
View File
@@ -1,11 +1,13 @@
import logging
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.enums import ParseMode
from aiogram.client.default import DefaultBotProperties
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramNetworkError
from config import Config
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
logger = logging.getLogger(__name__)
@@ -18,7 +20,7 @@ class NotificationBot:
self.bot: Bot = Bot(
token=config.bot_token,
default=DefaultBotProperties(
parse_mode="HTML",
parse_mode=ParseMode.HTML,
disable_notification=True,
link_preview_is_disabled=True,
),
@@ -27,11 +29,11 @@ class NotificationBot:
"Бот для уведомлений инициализирован (admin_id=%s)", config.admin_id
)
except:
logger.exception("Не удалось инициализировать Бот для уведомлений")
except (TelegramAPIError, ValueError, TypeError) as e:
logger.error(f"Не удалось инициализировать Бот для уведомлений: {e}")
raise
async def _notify(self, message: str, reply_to: int = None, link: str = None):
async def _notify(self, message: str, reply_to: int = None, link: str = None) -> Optional[int]:
try:
logger.debug("Отправка уведомления (reply_to=%s, link=%s)", reply_to, link)
keyboard = None
@@ -57,10 +59,20 @@ class NotificationBot:
)
return sent_message.message_id
except TelegramBadRequest as e:
logger.error("Bad request sending notification: %s", e)
return None
except TelegramNetworkError as e:
logger.error("Network error sending notification: %s", e)
return None
except TelegramAPIError as e:
logger.error("Telegram API error sending notification: %s", e)
return None
except Exception as e:
logger.error("Ошибка отправки уведомления: %s", e)
logger.error("Unexpected error sending notification: %s", e)
return None
async def new_post(self, post: Dict[str, Any], links: List[str]):
async def new_post(self, post: Dict[str, Any], links: List[str]) -> Optional[int]:
try:
thread = post.get("thread")
@@ -98,11 +110,14 @@ class NotificationBot:
)
return post_message_id
except:
logger.exception("Ошибка в new_post")
raise
except (KeyError, AttributeError, TypeError) as e:
logger.error(f"Неверная структура поста в new_post: {e}")
return None
except Exception as e:
logger.error(f"Неожиданная ошибка в new_post: {e}")
return None
async def success(self, link: str, post_message_id: int):
async def success(self, link: str, post_message_id: int) -> Optional[int]:
try:
logger.info(
"Звезда успешно отправлена: link=%s, stars=%s",
@@ -123,13 +138,13 @@ class NotificationBot:
)
return success_message_id
except:
logger.exception("Ошибка в success")
raise
except Exception as e:
logger.error(f"Неожиданная ошибка в success: {e}")
return None
async def failure(self, post: Dict[str, Any], reason: str, post_message_id: int):
async def failure(self, post: Dict[str, Any], reason: str, post_message_id: int) -> Optional[int]:
try:
permalink = post.get("links").get("permalink")
permalink = post.get("links", {}).get("permalink")
logger.warning(
"Не удалось отправить звезду: reason=%r, link=%s", reason, permalink
)
@@ -146,6 +161,9 @@ class NotificationBot:
)
return failure_message_id
except:
logger.exception("Ошибка в failure")
raise
except (KeyError, AttributeError, TypeError) as e:
logger.error(f"Неверная структура поста в failure: {e}")
return None
except Exception as e:
logger.error(f"Неожиданная ошибка в failure: {e}")
return None
+56 -5
View File
@@ -291,6 +291,42 @@ class TelegramStarsBot:
logger.exception(f"Критическая ошибка в главном цикле: {e}")
await asyncio.sleep(self.config.check_interval)
async def _bump_loop(self):
while True:
try:
logger.info(
f"Проверка статуса поднятия для темы {self.config.lolz_thread_id}..."
)
thread = await self.lolz_api.get_thread(
thread_id=self.config.lolz_thread_id
)
can_bump = await self.lolz_api.can_bump(thread=thread)
if can_bump:
try:
await self.lolz_api.bump_thread(
thread_id=self.config.lolz_thread_id
)
except Exception as e:
logger.error(
f"Ошибка поднятия темы {self.config.lolz_thread_id}, пропускаю: {e}"
)
else:
logger.info("Тему нельзя поднять.")
logger.info(f"Ожидание {self.config.bump_check_interval} секунд...")
await asyncio.sleep(self.config.bump_check_interval)
except KeyboardInterrupt:
logger.info("Получен сигнал прерывания (Ctrl+C).")
break
except Exception as e:
logger.exception(f"Критическая ошибка в цикле поднятия темы: {e}")
await asyncio.sleep(self.config.bump_check_interval)
async def start(self):
is_first_login = not os.path.exists(f"{self.SESSION_NAME}.session")
@@ -305,9 +341,15 @@ class TelegramStarsBot:
api_hash=self.config.api_hash,
)
await self.client.start()
await self.parse_existing_posts()
await self.client.stop()
try:
await self.client.start()
await self.parse_existing_posts()
except Exception as e:
logger.error(f"Ошибка при парсинге существующих постов: {e}")
raise
finally:
if self.client and self.client.is_connected:
await self.client.stop()
logger.info(
"Парсинг завершен. Все существующие посты добавлены в обработанные."
@@ -334,10 +376,19 @@ class TelegramStarsBot:
logger.info(f"Проверка начинается со страницы: {self.start_page}")
logger.info("Бот в работе. Для остановки нажмите Ctrl+C.")
await self._main_loop()
# _main_loop - Основной цикл для остлеживания темы
# _bump_loop - Цикл для авто-поднятия темы
await asyncio.gather(self._main_loop(), self._bump_loop())
except Exception as e:
logger.error(f"Ошибка в главном процессе бота: {e}")
raise
finally:
if self.client and self.client.is_connected:
await self.client.stop()
try:
await self.client.stop()
except Exception as e:
logger.error(f"Ошибка при остановке клиента: {e}")
logger.info("Бот остановлен.")