Metadata-Version: 2.4
Name: hh-applicant-tool
Version: 2.2.2
Summary: HH-Applicant-Tool: An automation utility for HeadHunter (hh.ru) designed to streamline the job search process by auto-applying to relevant vacancies and periodically refreshing resumes to stay at the top of recruiter searches.
Author: Senior YAML Developer
Author-email: yamldeveloper@proton.me
Requires-Python: >=3.11,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Provides-Extra: ui
Requires-Dist: brotli (>=1.2.0,<2.0.0)
Requires-Dist: cryptography (>=50.0.2,<51.0.0)
Requires-Dist: pillow (>=12.3.0,<13.0.0)
Requires-Dist: pywebview (>=5.0) ; extra == "ui"
Requires-Dist: requests[socks] (>=2.32.3,<3.0.0)
Requires-Dist: tomli-w (>=1.2.0,<2.0.0)
Project-URL: Homepage, https://github.com/s3rgeym/hh-applicant-tool
Project-URL: Repository, https://github.com/s3rgeym/hh-applicant-tool
Description-Content-Type: text/markdown

# HH Applicant Tool

![Publish to PyPI](https://github.com/s3rgeym/hh-applicant-tool/actions/workflows/publish.yml/badge.svg)
[![PyPi Version](https://img.shields.io/pypi/v/hh-applicant-tool)](<>)
[![Python Versions](https://img.shields.io/pypi/pyversions/hh-applicant-tool.svg)](<>)
[![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/s3rgeym/hh-applicant-tool)](<>)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/hh-applicant-tool)](<>)
[![Total Downloads](https://static.pepy.tech/badge/hh-applicant-tool)](<>)

> [!IMPORTANT]
> После перехода на версию 2.0.0 часть функционала может не работать. Если
> что-то не работает, делаете fork, выполняете git clone, берете Claude/Chat GPT/Gemini просите
> его поправить код, в pyproject.toml повышаете версию, делаете push, потом
> создаете PR (Pull Request), и помогаете всем тысячам пользователей утилиты...
> Только реквест должен быть не большим. Не присылайте исправления на тысячи
> строк. Они часто ломают логику. Ну и, конечно, вы должны сами понимать, что
> сгенерированный код делает, я только недавно из проекта вычистил 5000 строк
> нейробреда.

> [!IMPORTANT]
> [Группа поддержки в Телеграме (для всех моих утилит и просто для общения)](https://t.me/s3rgeym_chat)

> [!IMPORTANT]
> Ищу почасовую или проектную [@feedback_s3rgeym_bot](https://t.me/feedback_s3rgeym_bot) (Python, Vue.js, DevOps). Не пишите туда с вопросами по утилите. Для них есть канал!!! Так же буду благодарен, если посоветуете меня в качестве сись-пись админа, погромиста и девжопса своим знакомым, начальству... Имею огромный опыт фриланса (это может и не нравится тупопездым идиотинам, мнящим себя дохуя специалистками по подбору), но наебизнес ценит тех, кто умеет решать проблемы быстро, не задавая лишние вопросы и без надзирателей... С овчарами общаться желания нет (не знаю как другим, но мне тошнит от них).

> [!IMPORTANT]
> Проект запрещено использовать в коммерческих проектах, а так же любых проектах
> с закрытым исходным кодом.
> Остерегайтесь мошенников! Существует мошеннический сайт HH Applicant Tool,
> который использует название проекта для привлечения клиентов. К проекту
> отношения он не имеет!!! Все барыги торгующие моей утилитой прокляты в веках!

<details>
<summary>Прочтите обращение <s>основателя Wikipedia</s> разработчика</summary>
> [!IMPORTANT]
> Прежде чем задавать долбоебские вопросы, прочитай весь текст ниже, особенно пункт [ответы на заебавшие вопросы](#ответы-на-заебавшие-вопросы).

> [!NOTE]
> Советую попробовать [HH AI Responder](https://github.com/s3rgeym/hh-ai-responder/tree/main) — это
> программа, которая не требует установки и зависимостей (просто скачал и
> запусти), написанная на Go под Linux, Mac, Windows и Android, которая
> автоматически рассылает отклики и общается с хрюшами и кабанычами через ХуИИ.
> Так же можете дописать [autoresponder](./src/hh_applicant_tool/operations/autoresponder.py), чтобы
> добавить автоответчик по чатам и в HH Applicant Tool, натравив на тот нейронку
> (я половину бреда чатагпт вычистил вручную).

> [!NOTE]
> Настоящие погромисты пишут только в <s>блокноте</s> <s>vim</s> neovim. ЗАЦЕНИТЕ МОЙ [КОНФИГ NEOVIM](https://github.com/s3rgeym/nvim-config) НА КОТОРОМ Я ФАКТИЧЕСКИ РЕДАКТИРУЮ ВЕСЬ КОД, УДАЛИВ ДАВНО VS CODE И ПР ЗАЛУПУ!!!

</details>

<div align="center">
  <img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
</div>

> Авторские права на изображение принадлежат Кандинскому... нет, не человеку, а
> боту от говносбера

### ☕ Поддержать проект

[![Donate BTC](https://img.shields.io/badge/Donate-BTC-orange?style=for-the-badge&logo=bitcoin&logoColor=white)](bitcoin:BC1QWQXZX6D5Q0J5QVGH2VYXTFXX9Y6EPPGCW3REHS?label=%D0%94%D0%BB%D1%8F%20%D0%BF%D0%BE%D0%B6%D0%B5%D1%80%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B9)

**BTC Address:** `BC1QWQXZX6D5Q0J5QVGH2VYXTFXX9Y6EPPGCW3REHS` (спасибо за донаты — это единственное, что двигает разработку, так что не стесняйтесь и шлите их чаще, вы ЭКОНОМИТЕ КУЧУ ДЕНЕГ с учетом жадности владельцев сервисов для откликов)

---

## Особенности и функционал

> [!IMPORTANT]
> Tool — утилита, это программа не имеющая GUI/TUI, те данное ПО задумывалось
> строго для работы в сосноли/терминале, а поэтому все ее гуевые фишки
> существуют постольку-поскольку. Её предназначение крутиться где-то в
> автозапуске, не отвлекая на себя внимание. Написал раз конфиг, запустил и
> забыл.

- 🐍 Утилита написана на Python, а значит может быть запущена на любой системе, куда его можно установить (Windows, Linux, Mac и даже на Android)
- 🤖 Поддержка ИИ (это сейчас нужно произносить как автомобиль в Поле чудес)
- 📤 Рассылка откликов
- ✍️ Автоматическая генерация сопроводительных
- 🧩 Решение тестов при отклике и распознание капчи
- 💬 Автоответчик в чате с работодателем, в тч для общения с ботом рекрутером
- 📧 Дополнительная рассылка писем на Email работодателям
- 👥 Поддержка нескольких аккаунтов через профили
- 📇 Сохранение контактов работодателей и информации о вакансиях
- 🛡️ Запуск прямо с вашего устройства с эмуляцией работы через браузер (в headless-режиме он не запускается, используется лишь "стандартная" библиотека `requests` для всех запросов) и официальное Android-приложение. Для HH вы выглядите как обычный пользователь, что сводит к минимуму риск блокировки (в отличие от сторонних веб-сервисов, чьи аккаунты быстро банят)
- 🔒 Данные хранятся локально, никаких скрытых зондов и отправки данных на сторонние сервера, а значит ваши данные не утекут в результате взлома или не будут переданы различным мошенникам
- 📖 Открытый исходный код
- 🖥️ Может запускаться на сервере. В отличии от других реализаций не использует Headless Chromium, поэтому потребляет минимальное количество ресурсов, что критично для бюджетных серверов. Хромиум сильно грузит процессор, потребляет сотни мегабайт оперативной памяти при открытии hh и требует 1-2 гигабайт свободного места. Так что утилите хватит и VDS за пару сотен.
- 🎨 Наличие простого UI
- 🎁 И другой функционал абсолютно бесплатно!

---

## Содержание

- [HH Applicant Tool](#hh-applicant-tool)
  - [☕ Поддержать проект](#-поддержать-проект)
  - [Особенности и функционал](#особенности-и-функционал)
  - [Содержание](#содержание)
  - [Установка](#установка)
  - [Авторизация](#авторизация)
  - [Описание команд](#описание-команд)
  - [Капча](#капча)
  - [Запуск через Docker](#запуск-через-docker)
  - [Настройки и данные приложения](#настройки-и-данные-приложения)
    - [Конфигурационный файл](#конфигурационный-файл)
    - [auth.json](#authjson)
    - [cookies.txt](#cookiestxt)
    - [Логи](#логи)
    - [База данных](#база-данных)
    - [Дополнительные настройки](#дополнительные-настройки)
    - [Переменные окружения](#переменные-окружения)
  - [Шаблоны сообщений](#шаблоны-сообщений)
  - [Для разработчиков и продвинутых
    пользователей](#для-разработчиков-и-продвинутых-пользователей)
    - [Мои заметки](#мои-заметки)
    - [Запросы к API через CLI](#запросы-к-api-через-cli)
    - [Разработка](#разработка)
    - [Интеграция и использование в скриптах](#интеграция-и-использование-в-скриптах)
  - [Ответы на заебавшие вопросы](#ответы-на-заебавшие-вопросы)
  - [Связанные проекты](#связанные-проекты)
  - [Лицензионное соглашение (Limited Non-Commercial License)](#лицензионное-соглашение-limited-non-commercial-license)

---

## Установка

Утилиту можно установить из `PyPi` или скачать из репозитория. Из остальных мест ее скачивать не рекомендуется.

Установка:

```bash
# Стандартный pip (в Linux только в виртуальное окружение получится установить)
$ pip install hh-applicant-tool

# Чтобы утилита поддерживала команду ui
$ pip install 'hh-applicant-tool[ui]'

# Но лучше использовать pipx
$ pipx install hh-applicant-tool

# Последнюю версию можно установить через git
$ pipx install "git+https://github.com/s3rgeym/hh-applicant-tool"

# Для обновления до новой версии
$ pipx upgrade hh-applicant-tool
```

pipx добавляет исполняемый файл `hh-applicant-tool` в `~/.local/bin`, делая эту команду доступной. Путь до `~/.local/bin` должен быть в `$PATH` (в большинстве дистрибутивов он добавлен).

Использование виртуальных окружений в Linux/Mac:

```sh
mkdir -p ~/.venvs
python -m venv ~/.venvs/hh-applicant-tool
# Это придется делать постоянно, чтобы команда hh-applicant-tool стала доступна
.  ~/.venvs/hh-applicant-tool/bin/activate
pip install hh-applicant-tool
```

Отдельно я распишу процесс установки в **Windows** в подробностях:

- Для начала поставьте последнюю версию **Python 3** любым удобным способом.
- Запустите **PowerShell** (не `CMD.EXE`, блять, а именно поверщель) от Администратора и выполните:

  ```ps
  Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
  ```

  Данная политика разрешает текущему пользователю (от которого зашли) запускать скрипты. Без нее не будут работать виртуальные окружения.

Далее можно поставить `pipx` и вернуться к инструкции вверху раздела:

- Всё так же от администратора выполните:

  ```ps
  python -m pip install --user pipx
  ```

  А затем:

  ```ps
  python -m pipx ensurepath
  ```

- Перезапускаем Terminal/Powershell и проверяем:

  ```ps
  pipx -h
  ```

С использованием вирт. окружений:

- Создайте и активируйте виртуальное окружение:

  ```ps
  PS> python -m venv hh-applicant-venv
  PS> .\hh-applicant-venv\Scripts\activate
  ```

- Поставьте все пакеты в виртуальное окружение `hh-applicant-venv`:

  ```ps
  (hh-applicant-venv) PS> pip install hh-applicant-tool
  ```

- Проверьте, работает ли оно:

  ```ps
  (hh-applicant-venv) PS> hh-applicant-tool -h
  ```

- В случае неудачи вернитесь к первому шагу.
- Для последующих запусков сначала активируйте виртуальное окружение.

---

## Авторизация

Авторизация по коду:

```bash
$ hh-applicant-tool authorize 79012345678
📱 Введите полученный код из SMS: 1488
🎉 Авторизация прошла успешно!
```

> Если у вас есть еще профиль работодятла или овчар-менеХера, то тип авторизации
> принудительно будет сменен на соискателя (applicant)

Проверка авторизации:

```bash
$ hh-applicant-tool whoami
🆔 27405918 Кузнецов Андрей Владимирович [ 📄 1 | 👁️ +115 | ✉️ +28 ]
```

В случае успешной авторизации токены будут сохранены в [auth.json](#authjson):

Токен доступа выдается на две недели. <s>Для его обновления нужно выполнить:

```bash
hh-applicant-tool refresh-token
```

</s> Он обновляется автоматически.

Помните, что у `refresh_token` тоже есть время жизни, поэтому может потребоваться полная авторизация, если долго не запускать утилиту + время жизни cookies может быть меньше времени жизни токенов, поэтому часть функционала, требующая авторизации на сайте может отваливаться, поэтому в любой непонятной ситуации следует попробовать авторизоваться по-новой.

---

## Описание команд

> [!IMPORTANT]
> ФЛАГИ `--use-ai`/`--ai` ЯВЛЯЮТСЯ ГЛОБАЛЬНЫМИ!!!

Примеры команд:

```bash
# Общий вид команды
$ hh-applicant-tool [глобальные флаги] [операция] [флаги самой операции]


# Справка по глобальным флагам и список операций
$ hh-applicant-tool -h

# Справка по операции
$ hh-applicant-tool authorize -h

# Авторизуемся
$ hh-applicant-tool authorize <phoneOrEmail>

# Если опустить первый аргумент, то будет использован ранее введенный телефон или email
$ hh-applicant-tool authorize

# Авторизация с использованием другого профиля
# В дальнейшем для вызова всех операций надо профиль передавать,
# либо установить временную окружения
$ hh-applicant-tool --profile profile123 authorize <phoneOrEmail>

# Рассылаем отклики
$ hh-applicant-tool apply-vacancies

# Рассылаем отклики с помощью нейронки
# Внимательно прочитайте раздел про конфигарационный файл перед этим
$ hh-applicant-tool --use-ai apply-vacancies

# Для тестирования поисковой строки и других параметров, используйте --dry-run.
# С ним отклики не отправляются, а лишь выводятся сообщения
$ hh-applicant-tool -vv apply-vacancies --search "Python программист" --per-page 3 --total-pages 1 --dry-run

# Фильтруем вакансии по исключаемым словам (fullstack, junior, php и т.п.)
$ hh-applicant-tool apply-vacancies --search "Python backend" --excluded-filter "fullstack,junior,php,java" --dry-run

# Поднимаем резюме
$ hh-applicant-tool update-resumes

# Запустить автоответчик через AI в чатах с перерывом каждые 15 минут между проверкой чатов
$ hh-applicant-tool --ai reply-chats -l 900

# Просмотр лога в реальном времени
$ hh-applicant-tool log -f

# Посмотреть содержимое конфига
$ hh-applicant-tool config

# Редактировать конфиг в стандартном редакторе
$ hh-applicant-tool config -e

# Установить значение в конфиге, например, socks-прокси
# Учтите, что при таком способе изменения конфига, исчезнут все комментарии в нем!!!
$ hh-applicant-tool config -s general.proxy_url socks5h://localhost:1080

# Вывести значение из конфига
$ hh-applicant-tool config -k general.proxy_url

# Удалить значение из конфига
# Эта операция тоже форматирует конфиг, удаляя из него комментарии
$ hh-applicant-tool config -u general.proxy_url

# Утилита все данные об откликах хранит в SQLite
$ hh-applicant-tool query 'select count(*) from vacancy_contacts;'

# Экспорт контактов в csv
$ hh-applicant-tool query 'select * from vacancy_contacts' --csv -o
contacts.csv

# Выполнение запросов в интерактивном режиме
$ hh-applicant-tool query

# Чистим отказы
$ hh-applicant-tool clear-negotiations

# При обновлении может сломаться схема БД, для ее починки нужно выполнить
# поочерёдно все миграции, добавленные после выхода последней версии
$ hh-applicant-tool migrate
List of migrations:

  [1]: 2026-01-07

Choose migration [1] (Keep empty to exit): 1
✅ Success!

# Вывести все настройки
$ hh-applicant-tool settings

# Получить значение по ключу
$ hh-applicant-tool settings auth.username

# Установить email, используемый для автологина
$ hh-applicant-tool settings auth.username 'user@example.com'
```

Глобальные флаги:

- `-v`: используется для вывода отладочной информации. Два таких флага, например, выводят запросы к **API**.
- `-c <path>`: путь до каталога, где хранятся конфигурации.
- `--profile <profile-id>`: можно указать профиль, данные которого будут храниться в подкаталоге.
- `--use-kitty`/`--use-sixel`: использовать опр протокол для отображения капчи
  в терминале. Если не указывать, то капча будет отображена через `tkinter`, если тот установлен....
- `--use-ai`/`--ai`: использовать автоматизацию распознания капч и прочих
  действий через ИИ.

| Операция                           | Описание                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **authorize**, **auth**            | Авторизация на hh.ru. Введенный логин "запоминается" и будет использован при следующем вызове команды. Раньше, когда авторизация запускалась через браузер, была авторизация и по паролю, но когда я переписал авторизацию без использования браузеров мне тупо лень делать еще и по паролю. Там просто капча другим способом обрабатывается, поэтому авторизация будет только по коду, пока кто-нибудь другой не подсуетится |
| **logout**                         | Выйти из профиля на сайте и из приложения                                                                                                                                                                                                                                                                                                                                                                                     |
| **ui**                             | Запуск UI для рассылки и управления откликами (сломан, удалять не буду, но кто-нибудь должен починить)                                                                                                                                                                                                                                                                                                                        |
| **whoami**, **id**                 | Выводит информацию об авторизованном пользователе. Используется для проверки не слетела ли авторизация                                                                                                                                                                                                                                                                                                                        |
| **list-resumes**, **list**, **ls** | Список резюме                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **update-resumes**, **update**     | Обновить все резюме. Аналогично нажатию кнопки «Обновить дату».                                                                                                                                                                                                                                                                                                                                                               |
| **apply-vacancies**, **apply**     | Откликнуться на подходящие вакансии. Если указана строка для поиска (`--search`), то поиск будет произведен по вакансиям, иначе по списку рекомендованных вакансий. С флагами `--ai`/`--use-ai` у hh-applicant-tool будет генерировать сопроводительные, решать тесты с помощью хуИИ. Лимит = 200 в день. На HH есть спам-фильтры, так что лучше не рассылайте отклики со ссылками, иначе рискуете попасть в теневой бан.     |
| **reply-chats**, **respond-chats** | Рассылка ответов в чаты... Если `--use-ai` передан для самой `hh-applicant-tool`, то будет от вашего имени общаться с работодятлами и их потаскухами как автоотетчик. Что он будет ответчать зависит лишь от того какие ему промпты в конифге пропишите. Без флага для написания ответа вручную будет запускаться `vim`. Не знаю кому нужен этот флаг ai, я бы вручную со всеми общался.                                      |
| **clear-negotiations**             | Отмена откликов                                                                                                                                                                                                                                                                                                                                                                                                               |
| **clear-skipped**                  | Очистка базы пропущенных вакансий. С флагом `--reason` можно указать конкретную причину (`ai_rejected`, `excluded_filter`). Надеюсь, хоть кто-то смотрит, что там в базе лежит...                                                                                                                                                                                                                                             |
| **call-api**, **api**              | Вызов произвольного метода API с выводом результата.                                                                                                                                                                                                                                                                                                                                                                          |
| **refresh-token**, **refresh**     | Обновляет access_token. Существует лишь как артефакт древних времен, когда токен нужно было обновлять вручную.                                                                                                                                                                                                                                                                                                                |
| **config**                         | Показывает содержимое конфига. С флагом -e открывает его для редактирования в редакторе по умолчанию.                                                                                                                                                                                                                                                                                                                         |
| **settings**                       | Просмотр и управление настройками в базе данных. Простое key-value хранилище. Предполагалось, что будет использоваться для хранения различных состояний.                                                                                                                                                                                                                                                                      |
| **check-proxy**                    | Проверяет используемые прокси                                                                                                                                                                                                                                                                                                                                                                                                 |
| **migrate**                        | Починить базу (к удалению, как-то никто не написал ни одной миграции)                                                                                                                                                                                                                                                                                                                                                         |
| **query**                          | Выполнение SQL-запросов к базе. Схема БД находится в файле [schema.sql](./src//hh_applicant_tool/storage/queries/schema.sql). Если скормить ее [DeepSeek](https://chat.deepseek.com), то он поможет написать любой запрос.                                                                                                                                                                                                    |
| **log**                            | Просмотр файла-лога. С флагом -f будет следить за изменениями. В логах частично скрыты идентификаторы в целях безопасности.                                                                                                                                                                                                                                                                                                   |

> [!IMPORTANT]
> Настройки промптов задаются ТОЛЬКО в [конфиге](#конфигурационный-файл). Если
> разрешить передавать километровые флаги, то они сломают вашу историю команд в терминале...

> [!NOTE]
> Почитайте про [язык для поисковых запросов](https://hh.ru/article/1175). Он позволяет отсеивать мусор при поиске подходящих вакансий, например, `(Go OR Golang) NOT PHP NOT JavaScript`.

---

## Капча

Если вызов какого-то метода API генерирует ошибку капчи, то она перехватывается, а далее происходит следующее:

- при флаге `--use-ai` капча будет распознаваться нейронкой
- иначе:
  - капча отображается в терминале, если есть флаги `--use-kitty`/`--use-sixel` с именами используемых протоколов для отображения изображений (первый поддерживает даже Windows Terminal)
  - если не поддерживается графика, то капча принудительно выводится в терминал через `sixel`
  - в противном случае капча будет отображена через всплывающее окно `tkinter`

Особенность API хх.сру является то, что при достижении некоторых лимитов, причем, у каждого эндпоинта они свои, или же, например, если ему не нравится юзер-агент (поэтому не меняйте дефолтный), он может на все попытки решить капчу отдавать 403, те не засчитывать правильные решения. Это сделано чтобы боты бесконечно сливали токены. Экспериментально установить лимиты сложно, потому как они постоянно меняются.

Некоторые модели плохо распознают русский язык, английский для можно установить с помощью передачи `--captcha-lang EN`.

<p align="center">
  <img
    width="406"
    height="345"
    src="https://github.com/user-attachments/assets/3eb291ef-e8b4-40f2-abc0-eca7dd85be46"
    alt="Окно ввода капчи"
  >
  <br>
  <em>Homo homini lupus est, ёпта&hellip;</em>
</p>

---

## Запуск через Docker

Это рекомендованный разработчиком способ. Если не работает стандартная установка, то используйте его. Также это самый простой способ запуска и использования утилиты, требующий скопипастить 5 команд.

Для начала нужно установить `docker` и `docker-compose`:

```sh
sudo apt install docker.io docker-compose-v2
```

Выкачиваем репозиторий и переходим в каталог:

```sh
git clone https://github.com/s3rgeym/hh-applicant-tool
cd hh-applicant-tool
```

> [!IMPORTANT]
> Команды с docker compose нужно запускать строго, находясь в данном каталоге!

Теперь авторизуемся:

```sh
# Чтобы ввести капчу в терминале укажите флаги --use-sixel/-s или --use-kitty/-k
# Это названия протоколов, которые исп-ся терминалом для отображения изображений
# Sixel поддерживается большинством терминалов
# А вот Windows Terminal поддерживает только Kitty
docker compose run -u docker -it hh_applicant_tool \
  hh-applicant-tool -s -vv auth <username>
```

Если использовать флаг `--ai`, то капча будет разгадана нейронкой и останется только ввести код подтверждения из SMS/Email.

Подробно про авторизацию можно почитать [здесь](#авторизация).

В случае успешной авторизации можно запускать рассылку откликов по крону:

```sh
docker compose up -d
```

Что будет делать?

- Рассылать отклики со всех опубликованных резюме.
- Поднимать резюме.

Просмотр логов `cron`:

```sh
docker compose logs -f
```

Чтобы прекратить просмотр логов, нажмите `Ctrl-C`.

Запущенные сервисы докер стартуют автоматически после перезагрузки. Остановить их можно выполнив:

```sh
docker compose down
```

Чтобы обновить утилиту, в большинстве случаев достаточно в каталоге выполнить:

```sh
git pull
```

В редких случаях нужно пересобрать все:

```sh
docker compose up -d --build
```

Чтобы рассылать отклики с нескольких аккаунтов, нужно переписать `docker-compose.yml`:

```yaml
services:
  # Не меняем ничего тут
  hh_applicant_tool:
    # ...

  # Добавляем новые строки

  # Просто копипастим, меняя имя сервиса, container_name и значение HH_PROFILE_ID
  hh_second:
    extends: hh_applicant_tool
    container_name: hh_second
    environment:
      - HH_PROFILE_ID=second

  hh_third:
    extends: hh_applicant_tool
    container_name: hh_third
    environment:
      - HH_PROFILE_ID=third

  # Общий шаблон для новых профилей
  уникальное_имя_сервиса:
    extends: hh_applicant_tool
    # может совпадать с именем сервиса
    container_name: уникальное_имя_контейнера
    environment:
      - HH_PROFILE_ID=название_профиля
```

> [!IMPORTANT]
> В этом файле важны отступы!

Обратите внимание на `HH_PROFILE_ID` — его значение указывается при авторизации, если профиль отличен от дефолтного. Далее нужно авторизоваться в каждом профиле:

```sh
# Авторизуемся со второго профиля
docker-compose exec -u docker -it hh_applicant_tool \
  hh-applicant-tool --profile-id second -k auth

# Авторизуемся с третьего профиля
docker-compose exec -u docker -it hh_applicant_tool \
  hh-applicant-tool --profile-id third -k auth

# И так далее
```

Ну и выполнить `docker-compose up -d`, чтобы запустить новые сервисы.

[Команды](#описание-команд) можно потестировать в запущенном контейнере:

```sh
$ docker-compose exec -u docker -it hh_applicant_tool bash
docker@1897bdd7c80b:/app$ hh-applicant-tool config -p
/app/config/config.toml
docker@1897bdd7c80b:/app$ hh-applicant-tool refresh-token
ℹ️ Токен не истек, обновление не требуется.
docker@1897bdd7c80b:/app$
```

> [!IMPORTANT]
> Обратите внимание, что `docker-compose exec`/`docker-compose run` запускаются с аргументами `-u docker`. Только для пользователя `docker` установлен `chromium`, необходимый для авторизации, а также это избавляет от проблем с правами, когда созданные файлы для изменения требуют root-права.

Если хотите команду `apply-vacancies` вызывать с какими-то аргументами, то создайте в корне файл `apply-vacancies.sh`:

```sh
#!/bin/bash

# Пример с фильтрацией по исключаемым словам
/usr/local/bin/python -m hh_applicant_tool apply-vacancies \
  --excluded-filter "fullstack|junior|php" # укажите любые аргументы
```

В файлах `startup.sh` и `crontab` замените `/usr/local/bin/python -m hh_applicant_tool apply-vacancies` на `/bin/sh /app/apply-vacancies.sh`.

---

## Настройки и данные приложения

Настройки и данные приложения хранятся по следующим путям:

| OS          | Путь                                                     |
| ----------- | -------------------------------------------------------- |
| **Windows** | `C:\Users\%username%\AppData\Roaming\hh-applicant-tool\` |
| **macOS**   | `~/Library/Application Support/hh-applicant-tool/`       |
| **Linux**   | `~/.config/hh-applicant-tool/`                           |

<p align="center">
<img width="477" height="146" alt="image" src="https://github.com/user-attachments/assets/b5a99657-4908-4b07-be3c-213dfd9efd77" />
<br><em>Содержимое директории</em>
</p>

### Конфигурационный файл

Конфигурационный называется `config.toml`. Он содержит основные настройки.

[TOML](https://ru.wikipedia.org/wiki/TOML) идеально подходит для хранения конфигураций, так как поддерживает мультистроки и комментарии в отличии от старого формата JSON, который разве что для роботов подходит.

Самый быстрый способ открыть конфиг в стандартном редакторе:

```bash
hh-applicant-tool config -e
```

Путь до него можно найти так:

```bash
hh-applicant-tool config -p

# Можно сразу открыть в любимом редакторе
code $(hh-applicant-tool config -p)
```

> Учтите, что настройки принадлежат той секции, чьи квадратные скобки выше
> ближе, те какой-то порядок надо соблюдать

Формат файла:

```toml
[general]
# Прокси для запросов к hh.ru
# Приоритет: флаг --proxy-url, затем этот параметр, затем переменные окружения HTTP_PROXY / HTTPS_PROXY
proxy_url = "http://user:pass@127.0.0.1:8080"

# Вместо дефолтного https://hh.ru впишите свой
base_url = "https://rabota.by"
# Можете вписать сюда ваш браузерный юзер-агент, если так спокойнее
# По умочанию там Yandex Browser
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:157.0) Gecko/20100101 Firefox/157.0"
captcha_lang = "RU"

# Шаблон сопроводительного письма
cover_letter = """
{Здравствуйте|Добрый день}, меня зовут %(first_name)s.

{Прошу|Предлагаю} рассмотреть {мою кандидатуру|мое резюме «%(resume_title)s»} на вакансию «%(vacancy_name)s».

У меня есть релевантный опыт работы и интерес к задачам вашей компании.

С уважением,
%(first_name)s
"""

# Промпт для генерации сопроводительного письма
cover_letter_prompt = """
Напиши сопроводительное письмо для отклика на вакансию %(vacancy_name)s.
"""

# Тема и текст письма, отправляемого на email компании
apply_mail_subject = "{Отклик|Резюме} на вакансию %(vacancy_name)s"

# Поддерживается HTML
apply_mail_body = """
<p>{Здравствуйте|Добрый день}, {прошу рассмотреть|пожалуйста рассмотрите} мое резюме %(resume_url)s на вакансию %(vacancy_name)s.</p>

<br>С уважением, %(first_name)s.
"""

# Промпты для команды reply-chats
# Эти параметры должны находиться в корне конфигурации, до первой секции TOML.
#
# Доступные плейсхолдеры:
# first_name, last_name, email, phone,
# vacancy_name, vacancy_url, vacancy_compensation,
# employer_name, contact_name,
# resume_title, resume_salary, resume_skills, resume_experience,
# reply_to_message, options, history, message_count

reply_chat_user_prompt = """
Ты помогаешь отвечать на сообщения работодателей на hh.ru.

Информация о вакансии:
- Название: %(vacancy_name)s
- Зарплата: %(vacancy_compensation)s
- Компания: %(employer_name)s
- Контакт: %(contact_name)s

Количество сообщений в переписке: %(message_count)s

Сообщение, на которое нужно ответить:
%(reply_to_message)s

История переписки:
%(history)s

Составь подходящий ответ работодателю с учетом контекста переписки.
"""

reply_chat_options_prompt = """
Сообщение, на которое нужно ответить:
%(reply_to_message)s

Варианты ответов:
%(options)s

Выбери наиболее подходящий вариант ответа с учетом контекста переписки.
"""

# Клиент для работы с API HH
# delay может быть переопределен флагом --api-delay
[api_client]
base_url = "https://api.hh.ru/"  # адрес API, должен заканчиваться на /
delay = 0.345                    # пауза между запросами, сек
captcha_cooldown = 5.0           # пауза после решения капчи и перед повторным запросом, сек
max_captcha_attempts = 5         # максимальное количество попыток решить капчу; -1 — без ограничений
max_retries = 3                  # число попыток при ошибках соединения

# Отправка писем на email при откликах (--send-email)
[smtp]
host = "smtp.example.com"
port = 465
user = "me@example.com"
password = "secret"
from = "Иван Иванов <me@example.com>"  # отправитель; по умолчанию = user
ssl = true                             # SSL-соединение, обычно порт 465
starttls = true                        # STARTTLS, обычно порт 587

# Настройки по умолчанию для работы с OpenAI API
[openai.default]
base_url = "https://api.openai.com/v1/chat/completions"
api_key = "sk-..."
model = "gpt-4o-mini"
proxy_url = "http://127.0.0.1:8080"  # отдельный прокси для AI
delay = 0.5                          # минимальный интервал между запросами, сек
timeout = 60.0                       # таймаут запроса, сек
connect_timeout = 10.0               # таймаут соединения, сек
max_retries = 3                      # число повторов при сетевых ошибках и лимитах
temperature = 0.0
max_completion_tokens = 1000

# Настройки для распознавания капчи
# Переопределяют соответствующие значения из openai.default
[openai.captcha]
base_url = "..."
api_key = "..."
model = "..."

# Настройки для команды reply-chats
# Переопределяют соответствующие значения из openai.default
[openai.reply_chat]
# system_prompt можно не указывать: будет использован DEFAULT_SYSTEM_PROMPT из кода.
# При необходимости раскомментируйте и настройте:
# system_prompt = """
# Ты помогаешь отвечать на сообщения работодателей.
# Учитывай опыт, навыки и ожидания соискателя.
# """

# temperature = 0.7
# max_completion_tokens = 1000

# Настройки для генерации сопроводительных писем
# Переопределяют соответствующие значения из openai.default
[openai.cover_letter]
system_prompt = "Ты помогаешь писать сопроводительные письма."
temperature = 0.8

# Пример передачи истории чата, которая будет передаваться с каждым вызовом complete
[[openai.cover_letter.history]]
role = "user"
content = "Меня зовут Иван, я Python-разработчик."

[[openai.cover_letter.history]]
role = "assistant"
content = "Приятно познакомиться, Иван. Чем помочь?"
```

Приоритет передачи параметров: значения, переданные через флаги перезаписывают найстройки, а настройки переопределяют дефолтные значения, скрытые в коде.

Часть настроек скрыта.

Некоторые настройки передаются как аргумент конструктора, поэтому если увидите сообщение типа:

```sh
TypeError: ApiClient.__init__() got an unexpected keyword argument 'timeoutt'
```

Параметр `delay` в секциях `[api_client]` и `[openai.*]` работает не так как все думают:

- Допустим, у нас `delay = 0.5`
- Например, со времени предыдущего запроса к API прошло 0.3 секунды, тожда мы ждем 0.2 секунды (0.5 - 0.3)
- Если же прошло больше этого времени, перед запросом ожидание не делается

Это универсальнее rate limit'ов, так как работать будет везде, а не только там, где можно друг-другом пять запросов выполнить, а на 6 лимит сработает. Много где 5 запросов в секунду - это 5 запросов с задержкой до 200мс. НЕ ЛЕЗЬТЕ СО СВОИМИ, БЛЯТЬ, ИДИОТСКИМИ rate limit'ами в код!!!

### auth.json

Хранит токены авторизации.

### cookies.txt

В `cookies.txt` хранятся куки, установленные при авторизации на сайте. Они нужны
для решения тестов при отклике на вакансию. Не редактируйте этот файл вручную.

Операция `logout` не только затирает токены, но очищает `cookies.txt`.

### Логи

В `log.txt` всегда можно посмотреть подробную информацию об ошибках. В целях безопасности токены, идентификаторы резюме и т. п. затираются. Для просмотра логов используйте операцию `log`.

### База данных

Данные о работодателях и их вакансиях хранятся в файле `data`. Это обычный с данными базы `sqlite3`, вы его можете просматривать через `DB Browser` и т.п., а так же выполнять запросы с помощью операции `query`.

Что хранится в базе:

- все просмотренные вакансии;
- профили работодателей;
- контакты работодателей;
- данные об откликах;
- пропущенные вакансии (отклонённые фильтром или AI);
- иные данные, важные для работы.

Если этот файл удалить, то сессия не слетит, вы потеряете лишь данные...

### Дополнительные настройки

Отключение проверки версии с выводом предупреждения:

```sh
hh-applicant-tool settings disable_version_check true
```

### Переменные окружения

Утилита читает следующие переменные окружения:

| Переменная                    | Описание                                                                                                                               |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `CONFIG_DIR`                  | Каталог, в котором хранятся данные приложения, вместо пути по умолчанию (см. «Данные приложения»). В Docker задаётся как `/app/config` |
| `HH_PROFILE_ID`               | Имя подкаталога-профиля внутри `CONFIG_DIR` (по умолчанию `.`). Позволяет держать несколько изолированных профилей                     |
| `HTTP_PROXY` / `http_proxy`   | Прокси для HTTP-запросов к API HH, если не задан `proxy_url` в конфиге                                                                 |
| `HTTPS_PROXY` / `https_proxy` | Прокси для HTTPS-запросов к API HH; если не задан — используется HTTP-прокси                                                           |

Первые нужны для удобства в `Docker`. Последние — де факто стандарт.

---

## Шаблоны сообщений

При откликах доступны сл плейсхолдеры (как это на русском?) шаблонов (актуальный список см в `apply-vacancies -h`):

- **`%(vacancy_name)s`**: Название вакансии.
- **`%(vacancy_url)s`**: Ссылка на вакансию.
- **`%(employer_name)s`**: Название работодателя.
- **`%(first_name)s`**: Имя пользователя.
- **`%(last_name)s`**: Фамилия пользователя.
- **`%(email)s`**: Email пользователя.
- **`%(phone)s`**: Телефон пользователя.
- **`%(resume_title)s`**: Название резюме.
- **`%(resume_hash)s`**: Идентификатор резюме.
- **`%(resume_url)s`**: Ссылка на резюме.
- ...

Например:

```
Меня заинтересовала ваша вакансия %(vacancy_name)s. Прошу рассмотреть мою кандидатуру. С уважением, %(first_name)s %(last_name)s.
```

Плейсхолдеры можно использовать в промптах, но не в системных, заданных в секциях `openai.*`.

Также можно делать текст уникальным с помощью `{}`. Внутри них через `|` перечисляются варианты, один из которых будет случайно выбран:

```
Увольте того {идиота|придурка}, который придумал этот {тупой|сраный|дурацкий} вопрос.
```

В итоге получится что-то вроде:

```
Увольте того идиота, который придумал этот сраный вопрос.
```

`{}` могут быть вложенными.

---

## Для разработчиков и продвинутых пользователей

### Мои заметки

> [!IMPORTANT]
> Данной утилите похуй на "запрет" доступа к API HH сторонним приложениям, так как она прикидывается официальным приложением под Android

Долгое время я делал массовые заявки с помощью консоли браузера:

```js
$$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click())
```

Оно работало, хоть и не идеально: например, при отклике на некоторые вакансии перебрасывало на другую страницу. Поэтому я пробовал автоматизировать рассылки через `p[yu]ppeteer`, пока не прочитал [документацию](https://github.com/hhru/api) и не обнаружил, что **API** (интерфейс) содержит все необходимые мне методы. Headhunter позволял создавать свое приложение, но там была ручная модерация, а палиться в нарушении правил пользования сайтом (автоматические отклики запрещены) не хотелось. И тогда я [декомпилировал](https://gist.github.com/s3rgeym/eee96bbf91b04f7eb46b7449f8884a00) официальное приложение для **Android**, получил **CLIENT_ID** и **CLIENT_SECRET**, необходимые для работы через **API**.

Позже оказалось, что работы только через **API** недостаточно. Например, в нем нет методов для решения тех же тестов. Поэтому для их решения приходится эмулировать работу через сайт. Точно так же обстоят дела с решением капчи. **API** буквально нам предлагает открыть страницу с капчей в браузере, решить ее, а потом вызвать нужный метод **API** снова.

Сам сайт ХХ использует React. На каждой странице есть тег `<template id="HH-Lux-InitialState"></template>`. Внутри него заключен JSON `{"redirectConfig":....}`, в котором разделители заменены на HTML-сущности. В нем содержатся данные, необходимые для рендеринга страницы. Если нужно спарсить какие-то данные с веб-версии (у них она называется десктопной), то искать их стоит там. Там, кстати, много лишнего хранится: от ссылки на бота в Telegram для сотрудников поддержки сайт до страниц каких-то инфоцыган (этот уебанский сайт ставку сделал на зарабатывание денег на соискателях, я это осуждаю в отличии от доения тамошних работодятлов с их тупорылыми херками).

Сами эндпоинты сайта работают довольно странно. Параметры могут передавать через тело, Query String, через куки и заголовки. Но не всегда, на отдельных эндпоинтах обязательна передача в теле запроса.

Так же при авторизации на сайте вам выдается `xsrf` токен, он используется во всех запросах, отправляемых через `XMLHttpRequest`...

Для решения капчи нужен `captchaState`. Его возвращают некоторые эндпоинты сайта к которым обращаются через `XMLHttpRequest`. Либо он содержится в ссылке. Далее нужно получить `captchaKey`, послав POST-запрос без тела на эндпоинт сайта `/captcha?lang=RU` (можно указать еще `EN`). После получения `captchaKey` мы грузим изображение `/captcha/picture?key=<captchaKey>`. Ну и далее решение капчи, `captchaState` и `captchaKey` отправляются на POST-запросом на `/account/captcha`. При авторизации `captchaState` и пр могут отправляться прямо в самом запросе к эндпоинту...

Касаемо ошибки капчи в API. Гарантированно ее вызвать может мобильный юзер-агент. А вот пустой(!!!) или браузерный реже. Сам процесс общий за одним но, перед переходом по ссылке, которая возвращается в поле `.errors[0].captcha_url`, приложением к ней добавляется `&backurl=hh://applicant/captchaSuccessBackUrl`. <s>Без добавления этого параметра к ссылке у меня не удавалось пройти капчу, она показывалась вновь и вновь, хоть я и скопировал все запросы, отправляемые браузером... Либо помогла чистая сессия, которая используется для решения капчи — я не знаю... Либо мобильный юзерагент (тот который используется в Android WebView)...</s> Оказалось, что некоторые эндпоинты всегда возвращают капчу, если им не понравится юзер-агент, я же чтобы вызвать капчу намеренно его поставил подозрительный...

Фрагмент кода из декомпилированного приложения:

`jadx_out/sources/ru/hh/shared/feature/captcha/presentation/view/CaptchaViewModel.java`

```java
/* JADX WARN: Type inference incomplete: some casts might be missing */
public final void C5() {
    boolean zB = this.A.b();
    BehaviorSubject<UiState> behaviorSubject = this.u;
    if (zB) {
        behaviorSubject.onNext((UiState) new ru.hh.shared.feature.captcha.presentation.model.a.b(f2g.h(this.y.m, "&backurl=hh://applicant/captchaSuccessBackUrl")));
    } else {
        behaviorSubject.onNext((UiState) this.z.a(CaptchaErrorAction.NETWORK_ERROR));
    }
}

public final boolean D5(String str) {
    str.getClass();
    if (!wjf.w(str, "hh://applicant/captchaSuccessBackUrl", false)) {
        return false;
    }
    ny1.a aVar = ny1.a.a;
    CaptchaInteractor captchaInteractor = this.x;
    captchaInteractor.getClass();
    aVar.getClass();
    captchaInteractor.b.onNext(aVar);
    captchaInteractor.c.set(false);
    this.B = true;
    z5(py1.a.a);
    return true;
}
```

Редирект на URL из `backurl` на страницуе происходит через JavaScript, если POST-запрос на `/account/captccha` возвращает 203. Когда вводишь неверную капчу 403 (403 у них для капчи используется в тч).

Изучая исходники, я так же наткнулся на волшебный заголовок `X-Force-App-Access: true`, который дает доступ к скрытым методам, но мне лень было выяснять каким... Скачать приложение в виде apk можно через combobox, а через jadx декомпилировать. Там не все исходники восстанавливаются...

<p align="center">
  <img width="1016" height="999" alt="image" src="https://github.com/user-attachments/assets/2f25282b-aecd-4e0b-95f5-07c703f6287b" />
  <br><em>Поиски функции, которая создает подпись запроса авторизации</em>
</p>

Самым сложным был "взлом" процесса авторизации. Мне пришлось два часа лазать искать как `loginTrustFlags` генерируются для "подписи" данных отправляемых при авторизации. Для этого мне пришлось во вкладке отладчика браузера `Sources` отрелактировать скрипты на сайте, вставив в нужные места `console.log`/`console.dir`/`console.table` перед `debugger;` (для этого нужно выбрать директорию для синхронизации, тогда станет доступна возможность редактировать скрипты "прямо на сайте"). Там на странице входа из объекта с `redirectConfig` берется `loginTrustFlagsSecret`. В нем содержится публичный сертификат, которым шифруется такой JSON:

```js
{"emailOrPhone":{"paste":false,"suggest":true},"ts":Date.now()}
```

По названиям полей несложно догадаться, что там указывается был ли логин вставлен в поле или выбран из выпадающего списка при вводе. Боты, которые работают через Headless Chromium обычно ничего не вставляют и не выбирают из выпадающего списка, а "печатают"...

> При авторизацию по паролю такое шифруется: {"password":{"suggest": false,"paste": false},"ts":...}

Далее этот объект пропускается через `JSON.stringify` и шифруется с помощью публичного ключа. Используется ассимитричное шифрование `RSA-OAEP` с хешированием `SHA-256` и кодированием в base64. Без этой подписи запроса авторизоваться не получится.

Без расстановки контрольных меток быстро данную функцию шифрования можно найти так: жмем `Ctrl-Shift-F` во вкладке `Sources` и вводим `await crypto[`, ниже будет что-то типа:

```js
// С каждой версией фронтенда имена переменных меняются
const _0x3cd982 = {
  ..._0x334204,
  // 'ts': Date['now']()
  '\x74\x73': Date[_0x50c3ac(0x13f, '\x51\x47\x76\x23')](),
};

// Добавляем и смотрим что шифруется в trustLoginFlags...
console.log('trust login: %o', _0x3cd982);
```

Большинство эндопоинтов шифрует лишь JSON с таймстермп наподобии такого: `{"ts":1791234567890}`. Я думаю, что кроме `ts` там обязательных полей нет.

В принципе больше нечего рассказать...

Таким образом моя утилита работает в гибридном режиме: через API и веб-версию. При этом она не использует Headless Chromium ни для авторизации, ни для решения капчи...

### Запросы к API через CLI

Для тестирования запросов к API используйте команду `call-api`. Если запросы к сайту можно прямо консоли из браузера выполнять, то для работы с API пришлось придумать эту операцию.

Эта команда вместе с `jq` для обработки JSON — основа для Bash-скриптинга, когда не хватает основного функционала и нет желания писать на питоне, а так же изучения самого API сайта.

Пример поиска работодателей:

```bash
$ hh-applicant-tool call-api /employers text="IT" only_with_vacancies=true | jq -r '.items[].alternate_url'
https://hh.ru/employer/1966364
https://hh.ru/employer/4679771
...
```

Синтаксис `call-api` немного похож на `httpie` или `curlie`:

```sh
hh-applicant-tool call-api [{GET|POST|PUT|DELETE}] <endpoint> [<key=value> ...]
```

- Если используется метод `GET` (исп-ся когда метод опущен) или `DELETE`, то параметры будут переданы как query string. Во всех остальных случаях параметры передаются как `application/x-www-form-urlencoded` в теле запроса.
- Если первым аргументом после endpoint передать JSON внутри `{}`, то на сервер отправится запрос с Content-Type `application/json` и телом в JSON. Эндпоинтов, работающих с JSON — считанные единицы. Но они есть... Я, правда, не могу вспомнить хотя бы один как называется, но помню, что был.
- Флаг `-j` принудительно выполняет запрос с телом JSON:

  ```sh
  # Отправит POST-запрос с `{"foo":"bar","baz":42}` в теле и с `Content-Type: application/json` в заголовках
  hh-applicant-tool api post -j /endpoint foo=bar baz=42
  ```

  > api — это алиас call-api

При ошибках API в `$?` будет 1.

Те такое работает:

```sh
$ hh-applicant-tool api post /test foo=bar &>/dev/null || echo "Ошибка запроса\!"
Ошибка запроса!
```

Для поиска вакансий по региону в `apply-vacancies` нужно указать параметр `area`. Можно его указывать как для страны (каз - 40, бел - 2238), так и отдельного города (мск - 1).

Пример поиска `area id` по стране и названию:

```sh
$ hh-applicant-tool api /areas | jq -r '.[] | select(.name == "Беларусь") | .. | objects | select(.name == "Барановичи")'
{
  "id": "2239",
  "parent_id": "2233",
  "name": "Барановичи",
  "areas": [],
  "utc_offset": "+03:00",
  "lat": 53.16592,
  "lng": 29.00806
}
```

Документация для работы с API соискателей была удалена с ха-ха.сру и его корпоративного репозитория. Можете не искать, они затерли даже историю репозитория. Но я через веб-архив выкачал документацию. Чтобы ее посмотреть, клонируйте этот репозиторий и откройте файл `./docs/hhapi/openapi.yml`, например, с помощью [Swagger Viewer](https://marketplace.visualstudio.com/items?itemName=Arjun.swagger-viewer).

<p align="center">
<img width="740" height="768" alt="image" src="https://github.com/user-attachments/assets/597fa31e-8bab-48c8-8601-ab9dfc9075b1" />
</p>

Также существуют CLI-утилиты:

```sh
sudo pacman -S openapi-tui

openapi-tui --input docs/hhapi/openapi.yml
```

Для тех, кто любит просматривать документацию в браузере:

```sh
npx @redocly/cli preview -d docs/hhapi
```

Потом нужно открыть в браузере [http://localhost:4000](http://localhost:4000).

Не все эндпоинты задокументированы, кстати.

Один из скрытых возвращает статистику ваших откликов:

```sh
$ hh-applicant-tool api /negotiations_statistic/mine
{"applicant_statistic": {"responses_streak": {"responses_count": 228, "responses_required": 10}}}
```

> [!NOTE]
> Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвращаться созданный объект. Также в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать, есть ли тело у ответа сервера, нужно его пробовать прочитать. Я так понял, там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бэкенд на Java падает либо долго отвечает (таймаут).
> По сути, никакие дополнительные команды, кроме имеющихся, не нужны. Вы можете сделать что угодно с помощью `call-api`, но если хочется чего-то особенного, можно добавить свои команды.

### Разработка

- `git clone ...`
- `poetry install .`
- **poetry** можно установить через **pipx**
- в дальнейшем можно запускать утилиту так `poetry run hh-applicant-tool authorize`
- либо можно активировать виртуальное окружение, которое создается в каталоге проекта (`source .venv/bin/activate`)
- далее вносим изменения, после них ничего переустанавливать не надо (`poetry install` устанавливает проект в режиме редактирования, те все изменения в каталоге проекта "сразу применяются"), запускает утилиту и смотрим, что получилось. Запуск с двумя флагами v (`-vv`) выведет все сообщения логера. В лог-фале по умолчанию абсолютно все сообщения присутствуют.

Утилита использует систему плагинов. Все они лежат в [operations](https://github.com/s3rgeym/hh-applicant-tool/tree/main/src/hh_applicant_tool/operations). Модули, расположенные там, автоматически добавляются как доступные команды. За основу для своего плагина можно взять [whoami.py](https://github.com/s3rgeym/hh-applicant-tool/tree/main/src/hh_applicant_tool/operations/whoami.py).

Для публикации новой версиии утилиты на `PyPi` нужно указывать версию. Она указывается в `pyproject.toml`. Когда делаете `Pull Request`, то увеличивайте эту версию на единичку (при малозначительных изменениях — последнее число).

Так же у меня замечание к принимаемым правкам. Например, есть клиенты HH API и OpenAI API, у них должны быть похожие "интерфейсы", те и там и там использоваться `delay`, `timeout`, `connect_timeout`. И тот и другой ничего не должны знать о классах, которых их используют, как те работают... Они должны быть максимально изолированы, но при этом подчиняться принципу единообразия какого-то... И не надо дергать методы экземпляров классов, начинающиеся с подчеркивания, они скрыты чтобы ничего не сломать, так же не надо модифицировать экземпляры на лету, устанавливая им атрибуты через `setattr`, а потом еще лезть в код, гадить, ставя проверку на `gettatr` с попыткой дернуть атрибут неизвестно кем и зачем установленный... Такую нейдродрисню я принимать не буду, я заебался ее чистить. Тут код и так "мягко" говоря не идеален, но написать хуже этого — это "талант". Нужно стремиться к простоте, чем проще код, тем меньше в нем багов. Так же не надо перехватывать все возможные ошибки: любая заглушенная ошибка — это потенциальный баг. Например, если пользователь передал какую-то хуйню вместо валидных данных, то пусть приложение падает ровно в том месте, где эти данные требуются. Идиоты могут ввести что угодно в приступах безумия, все не предусмотреть.

Идиотию типа фильтров `heavy`, `light` и прочую поеботу я выбросил. Не надо генерировать промпты внутри. Если хочется сравнивать свое охуенное срИзюме на 10 листов с требованиями, сгенерированными тупой пиздой какой-то нейронкой (а потому к реальности не имеющее отношение, потому как вместо Vue на фронте какой Angular.js, а пизде нейронка высрала что-то релевантное, что делает все эти ебанутые идеи с хуии-фильтрациями какой-то бессмыслицей), то заводите плейсхолдеры типа `%(vacancy_full_description)s`, и спользуйте их у себя в шаблонах, которые задаются в конфигах, НЕ ПЛОДИТЕ ЛИШНИЕ СУЩНОСТИ, не вводите лишние переменные, флаги и настройки, если хватает существующих, и грузите это полное описание ТОЛЬКО ЕСЛИ ЕСТЬ ДАННЫЙ ПЛЕЙСХОЛДЕР, другие пользователи не должны страдать из-за чужих "гениальных" идей, вводя ебаную капчу в ручном режиме, потому как эндпоинт загрузки полного описания вакансии кидает ее гарантированно. Не надо так же дефолтными делать промпты подходящие только для вас (можно только для меня, но я таким не страдаю).

### Интеграция и использование в скриптах

Если хотите использовать `hh-applicant-tool` в своих скриптах, то можно это сделать так:

```python
from hh_applicant_tool import HHApplicantTool

tool = HHApplicantTool()
print(tool.api_client.get('/me'))

# Какую-то вспомогательную информацию можно сохранять в настройках
import datetime as dt

tool.storage.settings.set_value("_last_script_run", dt.datetime.now())

# Учтите, что в таком случае токены, которые могут обновиться при выполнении запросов,
# нужно сохранять вручную
tool.save_token()
```

Команды тоже можно вызывать:

```python
>>> from hh_applicant_tool import HHApplicantTool
>>> HHApplicantTool().run(["auth", "v.pupkin@mail.ru"])
```

Если же утилита интегрируется в графические приложения, то как правило должна быть возможность отменить текущую операцию:

```python
import multiprocessing as mp

from hh_applicant_tool import HHApplicantTool

def run_command(argv: list[str]):
    HHApplicantTool().run(argv)

p = mp.Process(target=run_command, args=(['apply-vacancies', ...],))
p.start()

# Убили процесс
p.terminate()

# Ждем пока не помрет
p.join()
```

---

## Ответы на идиотские вопросы

> Утилита перестала откликаться на тесты ряяяяяяяяяяяяяя

- Обычно это происходит, когда истекают куки и слетает авторизация на самом сайте (вы долго не запускали отклики). При этом команды типа `whoami` могут продолжать работать. Нужно по новой авторизоваться.

> Ряяяя поиск возвращает не то, что я и ищу, утилита не работает

- Сайт хуй-хуй, созданный для овчарок, и поэтому имеющий красный логотип цвета менстуральных выделений, зарабатывает в тч на услугах по продвижению вакансий, когда вакансию показать некому, то он ее начинает подсовывать калдидатам, которых это "может" заинтересовать (хоть кому-то показать надо), так программистам предлагают вакансии учителей и наставников в онлайн-школах и прочую дичь. Ничего с этим не сделать кроме применения дополнительных фильтров для отсеивания поискового мусора...

<img width="792" height="198" alt="image" src="https://github.com/user-attachments/assets/c532afaf-8cde-478b-8bc2-63955133f502" />

- Как работало, так и работает. Меньше читайте всяких пиздаболов

---

## Связанные проекты

- [Приложение под Android](https://github.com/s3rgeym/hh-resume-automate)
- [Прога, не требующая установки (скачал и запустил), которая рассылает отклики и отвечает в чаты через хуИИ](https://github.com/s3rgeym/hh-ai-responder)

---

## Лицензионное соглашение (Limited Non-Commercial License)

Данное программное обеспечение распространяется на условиях **ограниченной некоммерческой лицензии**.

- **Разрешённое использование**
  - **Личное использование и изучение:** Разрешается копирование и модификация кода исключительно для личного ознакомления, обучения и отладки **без права публикации или распространения изменений**.
  - **Интеграция в программное обеспечение:** Разрешается включение данного кода в состав сторонних бесплатных некоммерческих продуктов **только при полном сохранении исходного кода в неизменном виде**.
  - **Атрибуция:** Любое распространение, демонстрация или использование кода (в том числе в составе других продуктов) допускается **только при обязательном указании автора и ссылки на оригинальный репозиторий**.

- **Ограничения на использование**
  **Строго запрещается** использование данного кода в любых коммерческих целях без предварительного письменного согласия автора, включая, но не ограничиваясь:
  - Интеграцией в платные сервисы, приложения, веб-сайты либо использованием в рамках оказания платных услуг.
  - Публикацией, распространением или передачей третьим лицам **модифицированных (изменённых) версий** кода.
  - Перепродажей, сублицензированием либо использованием кода в интересах коммерческих организаций.

- **Отказ от ответственности**
  Программное обеспечение предоставляется по принципу «как есть» (**AS IS**). Автор не несёт ответственности за любые прямые или косвенные убытки, включая, но не ограничиваясь, блокировкой аккаунтов, потерей данных или иными негативными последствиями, возникшими в результате использования данного программного обеспечения.

**© 2023-2026 Sergey M. Все права защищены.**

