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

This commit is contained in:
2026-08-07 10:51:54 +05:00
parent 9ee9e7b731
commit 0cc2440736
3 changed files with 162 additions and 18 deletions
+4
View File
@@ -131,6 +131,10 @@ class Config:
).lower() == "true"
captcha_answer = os.getenv(key="CAPTCHA_ANSWER", default=None)
# Validate captcha configuration
if enable_captcha and not captcha_answer:
raise ConfigError("ENABLE_CAPTCHA установлен, но CAPTCHA_ANSWER не задан")
return {
# Pyrogram
"api_id": api_id,
+103 -18
View File
@@ -9,16 +9,19 @@ 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
@@ -45,17 +48,68 @@ class Lolz:
thread = (response.json()).get("thread", {})
if not thread:
raise LolzResponseError(f"Тема {thread_id} не найдена или вернула пустой ответ")
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
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.client.threads.get(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:
@@ -79,12 +133,16 @@ class Lolz:
logger.info(f"Тема {thread_id} успешно поднята.")
return True
else:
logger.error(f"Не удалось поднять тему {thread_id}. Статус: {response.status_code}")
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
raise LolzNetworkError(
f"Превышено время ожидания при поднятии темы {thread_id}"
) from e
except Exception as e:
logger.error(f"Ошибка при поднятии темы {thread_id}: {e}")
raise LolzAPIError(f"Не удалось поднять тему {thread_id}") from e
@@ -96,13 +154,17 @@ class Lolz:
post = (response.json()).get("post", {})
if not post:
raise LolzResponseError(f"Пост {post_id} не найден или вернул пустой ответ")
raise LolzResponseError(
f"Пост {post_id} не найден или вернул пустой ответ"
)
return post
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении поста {post_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении поста {post_id}") from 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
@@ -137,11 +199,19 @@ class Lolz:
await asyncio.sleep(1)
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении постов из темы {thread_id}, страница {page}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении постов из темы {thread_id}") from 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
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}.")
@@ -165,11 +235,17 @@ class Lolz:
return comments
except asyncio.TimeoutError as e:
logger.error(f"Превышено время ожидания при получении комментариев к посту {post_id}: {e}")
raise LolzNetworkError(f"Превышено время ожидания при получении комментариев к посту {post_id}") from 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
raise LolzAPIError(
f"Не удалось получить комментарии к посту {post_id}"
) from e
async def has_comments(
self, post_id: Optional[int] = None, post: Dict[str, Any] = None
@@ -192,12 +268,21 @@ class Lolz:
logger.info(f"Комментарий к посту {post_id} успешно опубликован.")
return True
else:
logger.error(f"Не удалось опубликовать комментарий к посту {post_id}. Статус: {response.status_code}")
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
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
raise LolzAPIError(
f"Не удалось создать комментарий к посту {post_id}"
) from e
+55
View File
@@ -141,6 +141,51 @@ class TelegramStarsBot:
logger.error(f"Критическая ошибка при отправке звезд: {e}")
return False
async def add_captcha(self):
captcha_text = self.config._load_captcha()
if (
not captcha_text
or not self.config.enable_captcha
or not self.config.captcha_answer
):
if not self.config.enable_captcha:
logger.error("Капча выключена. Редактирование темы невозможно.")
elif not self.config.captcha_answer:
logger.error(
"Ответ для капчи не установлен. Редактирование темы невозможно."
)
elif not captcha_text:
logger.error(
"Вопрос (текст) капчи не установлен. Редактирование темы невозможно."
)
return False
try:
thread = await self.lolz_api.get_thread(thread_id=self.config.thread_id)
thread_text = thread.get("post_body", "")
# Check if captcha already exists in thread
if "[spoiler=Капча" in thread_text:
logger.info(f"Капча уже существует в теме {self.config.thread_id}, пропускаю добавление.")
return True
captcha_wrapper = f"\n\n[spoiler=Капча (обязательно);align=center][CENTER]{captcha_text}[/CENTER][/spoiler]"
thread_text += captcha_wrapper
await self.lolz_api.edit_thread(
thread_id=self.config.thread_id, post_body=thread_text
)
logger.info(
f"Тема {self.config.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
@@ -178,6 +223,12 @@ class TelegramStarsBot:
self.processed_manager.mark_processed(post_id)
return
if self.config.enable_captcha and self.config.captcha_answer:
if not str(self.config.captcha_answer) in post_content:
logger.info(f"Пост {post_id} не содержит капчу. Пропускаю обработку.")
self.processed_manager.mark_processed(post_id)
return
links = TelegramLinkExtractor.extract(post_content)
if not links:
logger.info(f"В посте {post_id} не найдено ссылок Telegram.")
@@ -380,6 +431,9 @@ class TelegramStarsBot:
# _main_loop - Основной цикл для остлеживания темы
# _bump_loop - Цикл для авто-поднятия темы
if self.config.enable_captcha:
await self.add_captcha()
if self.config.enable_auto_bump:
await asyncio.gather(self._main_loop(), self._bump_loop())
else:
@@ -388,6 +442,7 @@ class TelegramStarsBot:
except Exception as e:
logger.error(f"Ошибка в главном процессе бота: {e}")
raise
finally:
if self.client and self.client.is_connected:
try: