diff --git a/config.py b/config.py index 2f27473..7779420 100644 --- a/config.py +++ b/config.py @@ -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, diff --git a/modules/lolz.py b/modules/lolz.py index 6b54477..f364541 100644 --- a/modules/lolz.py +++ b/modules/lolz.py @@ -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 diff --git a/modules/stars_bot.py b/modules/stars_bot.py index 44295cf..33a078e 100644 --- a/modules/stars_bot.py +++ b/modules/stars_bot.py @@ -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: