diff --git a/README.md b/README.md index 1fcb0b1..37baeb7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ### Описание -Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Так же утилита возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter), создавая ее при необходимости. Это делается анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя под которыми, вы авторизуетесь никуда не отправляются — только работовладельцев и их овчарок. Отправку этих данных можно отключить, но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или работовладелец его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**. +Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Обсуждения без отзывов удаляются. Данные на сервер передаются анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя утилиты под которыми, вы авторизуетесь никуда не отправляются — только работодателей и рекрутеров. Отправку этих данных можно отключить (флаг `--disable-telemetry`), но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или мудак его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**. Работает с Python >= 3.10. Нужную версию Python можно поставить через asdf/pyenv/conda и что-то еще. @@ -225,7 +225,7 @@ https://hh.ru/employer/1918903 | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят | | **call-api** | Вызов произвольного метода API с выводом результата. | | **refresh-token** | Обновляет access_token. | -| **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это премиальный функционал. Бот-пробивщик с аналогичным функционалом есть в группе. | +| **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это функционал для избранных, но в группе есть бесплатный бот с тем же функционалом. | ### Формат текста сообщений diff --git a/hh_applicant_tool/operations/apply_similar.py b/hh_applicant_tool/operations/apply_similar.py index 19cc30a..7d36e2a 100644 --- a/hh_applicant_tool/operations/apply_similar.py +++ b/hh_applicant_tool/operations/apply_similar.py @@ -3,7 +3,7 @@ import random import time from collections import defaultdict -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import TextIO, Tuple from ..api import ApiError, BadRequest @@ -216,7 +216,7 @@ def _apply_similar(self) -> None: not relations or parse_invalid_datetime(vacancy["created_at"]) + timedelta(days=7) - > datetime.now() + > datetime.now(tz=timezone.utc) ) ): employer = self.api.get(f"/employers/{employer_id}") @@ -228,23 +228,29 @@ def _apply_similar(self) -> None: "site_url": employer.get("site_url"), "area": employer.get("area", {}).get("name"), # город } - if "got_rejected" in relations: + if "got_rejection" in relations: try: + print( + "🚨 Вы получили отказ от https://hh.ru/employer/%s" + % employer_id + ) response = telemetry_client.send_telemetry( f"/employers/{employer_id}/complaint", employer_data, ) - print( - f"🚨 Вы получили отказ от {employer.get('name', 'какого-то ноунейма')}." - ) - print( - "Ссылка для отзыва:", - response["topic_url"], - ) + if "topic_url" in response: + print( + "Ссылка на обсуждение работодателя:", + response["topic_url"], + ) + else: + print( + "Создание темы для обсуждения работодателя добавлено в очередь..." + ) complained_employers.add(employer_id) except TelemetryError as ex: logger.error(ex) - else: + elif do_apply: telemetry_data["employers"][employer_id] = employer_data if not do_apply: diff --git a/hh_applicant_tool/telemetry_client.py b/hh_applicant_tool/telemetry_client.py index 8c436f5..36e121b 100644 --- a/hh_applicant_tool/telemetry_client.py +++ b/hh_applicant_tool/telemetry_client.py @@ -1,13 +1,15 @@ -import os import json -from urllib.parse import urljoin -import requests -from typing import Optional, Dict, Any import logging -from functools import partialmethod +import os +import time import warnings +from functools import partialmethod +from typing import Any, Dict, Optional +from urllib.parse import urljoin + +import requests -warnings.filterwarnings('ignore', message='Unverified HTTPS request') +warnings.filterwarnings("ignore", message="Unverified HTTPS request") logger = logging.getLogger(__package__) @@ -22,6 +24,7 @@ class TelemetryClient: """Клиент для отправки телеметрии на сервер.""" server_address: str = "https://hh-applicant-tool.mooo.com:54157/" + default_delay: float = 0.334 # Задержка по умолчанию в секундах def __init__( self, @@ -30,6 +33,7 @@ def __init__( session: Optional[requests.Session] = None, user_agent: str = "Mozilla/5.0 (HHApplicantTelemetry/1.0)", proxies: dict | None = None, + delay: Optional[float] = None, ) -> None: self.server_address = os.getenv( "TELEMETRY_SERVER", server_address or self.server_address @@ -37,17 +41,28 @@ def __init__( self.session = session or requests.Session() self.user_agent = user_agent self.proxies = proxies + self.delay = delay if delay is not None else self.default_delay + self.last_request_time = time.monotonic() # Время последнего запроса def request( self, method: str, endpoint: str, data: Dict[str, Any] | None = None, - **kwargs: Any + **kwargs: Any, ) -> Dict[str, Any]: method = method.upper() url = urljoin(self.server_address, endpoint) has_body = method in ["POST", "PUT", "PATCH"] + + # Вычисляем время, прошедшее с последнего запроса + current_time = time.monotonic() + time_since_last_request = current_time - self.last_request_time + + # Если прошло меньше времени, чем задержка, ждем оставшееся время + if time_since_last_request < self.delay: + time.sleep(self.delay - time_since_last_request) + try: response = self.session.request( method, @@ -58,7 +73,7 @@ def request( json=data if has_body else None, verify=False, # Игнорирование истекшего сертификата **kwargs, - ) + ) # response.raise_for_status() result = response.json() if "error" in result: @@ -70,6 +85,9 @@ def request( json.JSONDecodeError, ) as ex: raise TelemetryError(str(ex)) from ex + finally: + # Обновляем время последнего запроса + self.last_request_time = time.monotonic() get_telemetry = partialmethod(request, "GET") send_telemetry = partialmethod(request, "POST")