hh-applicant-tool 0.3.6__py3-none-any.whl → 0.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hh-applicant-tool might be problematic. Click here for more details.

@@ -12,7 +12,7 @@ from ..main import Namespace as BaseNamespace, get_api
12
12
  from ..telemetry_client import TelemetryError
13
13
  from ..telemetry_client import get_client as get_telemetry_client
14
14
  from ..types import ApiListResponse, VacancyItem
15
- from ..utils import fix_datetime, print_err, truncate_string
15
+ from ..utils import fix_datetime, print_err, truncate_string, random_text
16
16
 
17
17
  logger = logging.getLogger(__package__)
18
18
 
@@ -23,8 +23,13 @@ class Namespace(BaseNamespace):
23
23
  force_message: bool
24
24
  apply_interval: Tuple[float, float]
25
25
  page_interval: Tuple[float, float]
26
+ message_interval: Tuple[float, float]
27
+ order_by: str
28
+ search: str
29
+ reply_message: str
26
30
 
27
31
 
32
+ # gx для открытия (никак не запомню в виме)
28
33
  # https://api.hh.ru/openapi/redoc
29
34
  class Operation(BaseOperation):
30
35
  """Откликнуться на все подходящие вакансии. По умолчанию применяются значения, которые были отмечены галочками в форме для поиска на сайте"""
@@ -33,7 +38,7 @@ class Operation(BaseOperation):
33
38
  parser.add_argument("--resume-id", help="Идентефикатор резюме")
34
39
  parser.add_argument(
35
40
  "--message-list",
36
- help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки. В сообщения можно использовать плейсхолдеры типа %%(name)s",
41
+ help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки. В сообщения можно использовать плейсхолдеры типа %%(vacabcy_name)s",
37
42
  type=argparse.FileType(),
38
43
  )
39
44
  parser.add_argument(
@@ -44,16 +49,22 @@ class Operation(BaseOperation):
44
49
  )
45
50
  parser.add_argument(
46
51
  "--apply-interval",
47
- help="Интервал между отправкой откликов в секундах (X, X-Y)",
52
+ help="Интервал перед отправкой откликов в секундах (X, X-Y)",
48
53
  default="1-5",
49
54
  type=self._parse_interval,
50
55
  )
51
56
  parser.add_argument(
52
57
  "--page-interval",
53
- help="Интервал между получением следующей страницы рекомендованных вакансий в секундах (X, X-Y)",
58
+ help="Интервал перед получением следующей страницы рекомендованных вакансий в секундах (X, X-Y)",
54
59
  default="1-3",
55
60
  type=self._parse_interval,
56
61
  )
62
+ parser.add_argument(
63
+ "--message-interval",
64
+ help="Интервал перед отправкой сообщения в секундах (X, X-Y)",
65
+ default="5-10",
66
+ type=self._parse_interval,
67
+ )
57
68
  parser.add_argument(
58
69
  "--order-by",
59
70
  help="Сортировка вакансий",
@@ -68,10 +79,15 @@ class Operation(BaseOperation):
68
79
  )
69
80
  parser.add_argument(
70
81
  "--search",
71
- help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500', те можно и город указать, и ожидаемую зряплату",
82
+ help="Строка поиска для фильтрации вакансий, например, 'москва бухгалтер 100500', те можно и город указать, и ожидаемую зарплату",
72
83
  type=str,
73
84
  default=None,
74
85
  )
86
+ parser.add_argument(
87
+ "--reply-message",
88
+ "--reply",
89
+ help="Отправить сообщение во все чаты, где ожидают ответа либо не прочитали ответ",
90
+ )
75
91
 
76
92
  @staticmethod
77
93
  def _parse_interval(interval: str) -> Tuple[float, float]:
@@ -89,6 +105,7 @@ class Operation(BaseOperation):
89
105
 
90
106
  apply_min_interval, apply_max_interval = args.apply_interval
91
107
  page_min_interval, page_max_interval = args.page_interval
108
+ message_min_interval, message_max_interval = args.message_interval
92
109
 
93
110
  self._apply_similar(
94
111
  api,
@@ -99,8 +116,11 @@ class Operation(BaseOperation):
99
116
  apply_max_interval,
100
117
  page_min_interval,
101
118
  page_max_interval,
119
+ message_min_interval,
120
+ message_max_interval,
102
121
  args.order_by,
103
122
  args.search,
123
+ args.reply_message or args.config["reply_message"],
104
124
  )
105
125
 
106
126
  def _get_resume_id(self, args: Namespace, api: ApiClient) -> str:
@@ -118,11 +138,8 @@ class Operation(BaseOperation):
118
138
  )
119
139
  else:
120
140
  application_messages = [
121
- "Меня заинтересовала ваша вакансия %(name)s",
122
- "Прошу рассмотреть мою жалкую кандидатуру на вакансию %(name)s",
123
- "Ваша вакансия %(name)s соответствует моим навыкам и опыту",
124
- "Хочу присоединиться к вашей успешной команде лидеров рынка в качестве %(name)s",
125
- "Мое резюме содержит все баззворды, указанные в вашей вакансии %(name)s",
141
+ "{Меня заинтересовала|Мне понравилась} ваша вакансия %(vacancy_name)s",
142
+ "{Прошу рассмотреть|Предлагаю рассмотреть} {мою кандидатуру|мое резюме} на вакансию %(vacancy_name)s",
126
143
  ]
127
144
  return application_messages
128
145
 
@@ -136,8 +153,11 @@ class Operation(BaseOperation):
136
153
  apply_max_interval: float,
137
154
  page_min_interval: float,
138
155
  page_max_interval: float,
156
+ message_min_interval: float,
157
+ message_max_interval: float,
139
158
  order_by: str,
140
159
  search: str | None = None,
160
+ reply_message: str | None = None,
141
161
  ) -> None:
142
162
  telemetry_client = get_telemetry_client()
143
163
  telemetry_data = defaultdict(dict)
@@ -154,33 +174,124 @@ class Operation(BaseOperation):
154
174
 
155
175
  self._collect_vacancy_telemetry(telemetry_data, vacancies)
156
176
 
177
+ me = api.get("/me")
178
+
179
+ basic_message_placeholders = {
180
+ "first_name": me.get("first_name", ""),
181
+ "last_name": me.get("last_name", ""),
182
+ "email": me.get("email", ""),
183
+ "phone": me.get("phone", ""),
184
+ }
185
+
186
+ do_apply = True
187
+
157
188
  for vacancy in vacancies:
158
189
  try:
159
190
  if getenv("TEST_TELEMETRY"):
160
191
  break
161
192
 
193
+ message_placeholders = {
194
+ "vacancy_name": vacancy.get("name", ""),
195
+ "employer_name": vacancy.get("employer", {}).get(
196
+ "name", ""
197
+ ),
198
+ **basic_message_placeholders,
199
+ }
200
+
201
+ logger.debug(
202
+ "Вакансия %(vacancy_name)s от %(employer_name)s"
203
+ % message_placeholders
204
+ )
205
+
162
206
  if vacancy.get("has_test"):
163
207
  print("🚫 Пропускаем тест", vacancy["alternate_url"])
164
208
  continue
165
209
 
210
+ if vacancy.get("archived"):
211
+ print(
212
+ "🚫 Пропускаем вакансию в архиве",
213
+ vacancy["alternate_url"],
214
+ )
215
+
216
+ continue
217
+
166
218
  relations = vacancy.get("relations", [])
167
219
 
168
220
  if relations:
221
+ if "got_rejection" in relations:
222
+ print(
223
+ "🚫 Пропускаем отказ на вакансию",
224
+ vacancy["alternate_url"],
225
+ )
226
+ continue
227
+
228
+ if reply_message:
229
+ r = api.get("/negotiations", vacancy_id=vacancy["id"])
230
+
231
+ if len(r["items"]) == 1:
232
+ neg = r["items"][0]
233
+ nid = neg["id"]
234
+
235
+ page: int = 0
236
+ last_message: dict | None = None
237
+ while True:
238
+ r2 = api.get(
239
+ f"/negotiations/{nid}/messages", page=page
240
+ )
241
+ last_message = r2["items"][-1]
242
+ if page + 1 >= r2["pages"]:
243
+ break
244
+
245
+ page = r2["pages"] - 1
246
+
247
+ logger.debug(last_message["text"])
248
+
249
+ if last_message["author"][
250
+ "participant_type"
251
+ ] == "employer" or not neg.get(
252
+ "viewed_by_opponent"
253
+ ):
254
+ message = (
255
+ random_text(reply_message)
256
+ % message_placeholders
257
+ )
258
+ logger.debug(message)
259
+
260
+ time.sleep(
261
+ random.uniform(
262
+ message_min_interval,
263
+ message_max_interval,
264
+ )
265
+ )
266
+ api.post(
267
+ f"/negotiations/{nid}/messages",
268
+ message=message,
269
+ )
270
+ print(
271
+ "📨 Отправили сообщение для привлечения внимания",
272
+ vacancy["alternate_url"],
273
+ )
274
+ continue
275
+ else:
276
+ logger.warning(
277
+ "Приглашение без чата для вакансии: %s",
278
+ vacancy["alternate_url"],
279
+ )
280
+
169
281
  print(
170
- "🚫 Пропускаем ответ на заявку",
282
+ "🚫 Пропускаем вакансию с откликом",
171
283
  vacancy["alternate_url"],
172
284
  )
173
285
  continue
174
286
 
175
- try:
176
- employer_id = vacancy["employer"]["id"]
177
- except KeyError:
178
- logger.warning(
179
- f"Вакансия без работодателя: {vacancy['alternate_url']}"
180
- )
181
- else:
182
- employer = api.get(f"/employers/{employer_id}")
287
+ employer_id = vacancy.get("employer", {}).get("id")
183
288
 
289
+ if (
290
+ employer_id
291
+ and employer_id not in telemetry_data["employers"]
292
+ and 200 > len(telemetry_data["employers"])
293
+ ):
294
+ employer = api.get(f"/employers/{employer_id}")
184
295
  telemetry_data["employers"][employer_id] = {
185
296
  "name": employer.get("name"),
186
297
  "type": employer.get("type"),
@@ -189,11 +300,9 @@ class Operation(BaseOperation):
189
300
  "area": employer.get("area", {}).get("name"), # город
190
301
  }
191
302
 
192
- # Задержка перед отправкой отклика
193
- interval = random.uniform(
194
- apply_min_interval, apply_max_interval
195
- )
196
- time.sleep(interval)
303
+ if not do_apply:
304
+ logger.debug("skip apply similar")
305
+ continue
197
306
 
198
307
  params = {
199
308
  "resume_id": resume_id,
@@ -201,19 +310,23 @@ class Operation(BaseOperation):
201
310
  "message": "",
202
311
  }
203
312
 
204
- if vacancy.get("response_letter_required"):
205
- message_template = random.choice(application_messages)
206
-
207
- try:
208
- params["message"] = message_template % vacancy
209
- except TypeError as ex:
210
- # TypeError: not enough arguments for format string
211
- # API HH все кривое, иногда нет идентификатора работодателя, иногда у вакансии нет названия.
212
- # И это типа рашн хайлоад, где из-за дрочки на аджайл слепили кривую говнину.
213
- logger.error(
214
- f"Ошибка форматирования шаблона сообщения {template_message!r} для {vacancy = }"
215
- )
216
- continue
313
+ if force_message or vacancy.get("response_letter_required"):
314
+ msg = params["message"] = (
315
+ random_text(random.choice(application_messages))
316
+ % message_placeholders
317
+ )
318
+ logger.debug(msg)
319
+
320
+ # Задержка перед отправкой отклика
321
+ interval = random.uniform(
322
+ max(apply_min_interval, message_min_interval)
323
+ if params["message"]
324
+ else apply_min_interval,
325
+ max(apply_max_interval, message_max_interval)
326
+ if params["message"]
327
+ else apply_max_interval,
328
+ )
329
+ time.sleep(interval)
217
330
 
218
331
  res = api.post("/negotiations", params)
219
332
  assert res == {}
@@ -225,12 +338,16 @@ class Operation(BaseOperation):
225
338
  ")",
226
339
  )
227
340
  except ApiError as ex:
228
- print_err("❗ Ошибка:", ex)
341
+ logger.error(ex)
229
342
  if isinstance(ex, BadRequest) and ex.limit_exceeded:
230
- break
343
+ if not reply_message:
344
+ break
345
+ do_apply = False
231
346
 
232
347
  print("📝 Отклики на вакансии разосланы!")
233
348
 
349
+ # Я собираюсь задеанонить всех хрюш яндексов и прочей хуеты, которую
350
+ # считаю вселенским злом, так что телеметирию не трогайте
234
351
  self._send_telemetry(telemetry_client, telemetry_data)
235
352
 
236
353
  def _get_vacancies(
@@ -10,6 +10,7 @@ from threading import Lock
10
10
  from typing import Any
11
11
  from os import getenv
12
12
  from .constants import INVALID_ISO8601_FORMAT
13
+ import re, random
13
14
 
14
15
  print_err = partial(print, file=sys.stderr, flush=True)
15
16
 
@@ -72,3 +73,17 @@ def fix_datetime(dt: str | None) -> str | None:
72
73
  if dt is None:
73
74
  return None
74
75
  return datetime.strptime(dt, INVALID_ISO8601_FORMAT).isoformat()
76
+
77
+
78
+ def random_text(s: str) -> str:
79
+ while (
80
+ s1 := re.sub(
81
+ r"{([^{}]+)}",
82
+ lambda m: random.choice(
83
+ m.group(1).split("|"),
84
+ ),
85
+ s,
86
+ )
87
+ ) != s:
88
+ s = s1
89
+ return s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -30,19 +30,21 @@ Description-Content-Type: text/markdown
30
30
  <img src="https://github.com/user-attachments/assets/29d91490-2c83-4e3f-a573-c7a6182a4044" width="500">
31
31
  </div>
32
32
 
33
+ ### Описание
34
+
33
35
  Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
34
36
 
35
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
36
38
  asdf/pyenv/conda и что-то еще...
37
39
 
38
- Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести `~/.config/hh-applicant-tool/config.json` (`C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` — в Windows) на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
40
+ Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
39
41
 
40
42
  Пример работы:
41
43
 
42
44
  ![image](https://github.com/user-attachments/assets/55ab24ba-5325-40b4-9bd9-69ebcbc011c4)
43
45
 
44
46
 
45
- Предыстория.
47
+ ### Предыстория
46
48
 
47
49
  Был один знакомый знакомого, который работал хрюшей. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отличить Java от JavaScript, но я думаю, что <s>в значительном числе случаев, тут имеют место такие вот рассылки</s> они просто идиотки... И я тупо стал спамить как они. Мне уже было просто лень читать весь этот бред, что пишут долбоебы в описании вакансий. Там стандартное ООП, алгоритмы и прочая хуета... Вроде все подходят, а вроде хз — все не мое.
48
50
 
@@ -54,7 +56,7 @@ $$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click());
54
56
 
55
57
  Оно работает, хоть и не идеально. Я даже пробовал автоматизировать рассылки через `p[yu]ppeeter`, пока не прочитал [документацию](https://github.com/hhru/api). И не обнаружил, что **API** (интерфейс) содержит все необходимые мне методы. Headhunter позволяет создать свое приложение, но там ручная модерация, и наврядли кто-то разрешит мне создать приложение для спама заявками. Я [декомпилировал](https://gist.github.com/s3rgeym/eee96bbf91b04f7eb46b7449f8884a00) официальное приложение для **Android** и получил **CLIENT_ID** и **CLIENT_SECRET**, необходимые для работы через **API**.
56
58
 
57
- Установка:
59
+ ### Установка
58
60
 
59
61
  ```bash
60
62
  # Версия с поддержкой авторизации через запуск окна с браузером (эта версия очень много весит)
@@ -91,9 +93,90 @@ $ pipx upgrade hh-applicant-tool
91
93
  ```
92
94
  * В случае неудачи вернитесь к первому шагу.
93
95
  * Для последующих запусков сначала активируйте виртуальное окружение.
94
-
95
96
 
96
- Использование:
97
+ ### Авторизация
98
+
99
+ ```bash
100
+ $ hh-applicant-tool -vv authorize
101
+ ```
102
+
103
+ ![image](https://github.com/user-attachments/assets/88961e31-4ea3-478f-8c43-914d6785bc3b)
104
+
105
+ > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
106
+
107
+ Проверка авторизации:
108
+
109
+ ```bash
110
+ $ hh-applicant-tool whoami
111
+ {
112
+ "auth_type": "applicant",
113
+ "counters": {
114
+ "new_resume_views": 1488,
115
+ "resumes_count": 1,
116
+ "unread_negotiations": 228
117
+ },
118
+ "email": "vasya.pupkin@gmail.com",
119
+ "employer": null,
120
+ "first_name": "Вася",
121
+ "id": "1234567890",
122
+ "is_admin": false,
123
+ "is_anonymous": false,
124
+ "is_applicant": true,
125
+ "is_application": false,
126
+ "is_employer": false,
127
+ "is_in_search": true,
128
+ "last_name": "Пупкин",
129
+ "manager": null,
130
+ "mid_name": null,
131
+ "middle_name": null,
132
+ "negotiations_url": "https://api.hh.ru/negotiations",
133
+ "personal_manager": null,
134
+ "phone": "79012345678",
135
+ "profile_videos": {
136
+ "items": []
137
+ },
138
+ "resumes_url": "https://api.hh.ru/resumes/mine"
139
+ }
140
+ ```
141
+
142
+ В случае успешной авторизации токены будут сохранены в `config.json`:
143
+
144
+ ```json
145
+ {
146
+ "token": {
147
+ "access_token": "...",
148
+ "created_at": 1678151427,
149
+ "expires_in": 1209599,
150
+ "refresh_token": "...",
151
+ "token_type": "bearer"
152
+ }
153
+ }
154
+ ```
155
+
156
+ Токен доступа выдается на две недели. После его нужно обновить:
157
+
158
+ ```bash
159
+ $ hh-applicant-tool refresh-token
160
+ ```
161
+
162
+ ### Пути до файла config.json
163
+
164
+ | OS | Путь |
165
+ |----------------------------|---------------------------------------------------------------------|
166
+ | **Windows** | `C:\Users\%username%\AppData\Roaming\hh-applicant-tool\config.json` |
167
+ | **macOS** | `~/Library/Application Support/hh-applicant-tool/config.json` |
168
+ | **Linux** | `~/.config/hh-applicant-tool/config.json` |
169
+
170
+
171
+ Через этот файл, например, можно задать кастомный `user_agent`:
172
+
173
+ ```json
174
+ {
175
+ "user_agent": "Mozilla/5.0 YablanBrowser"
176
+ }
177
+ ```
178
+
179
+ ### Описание команд
97
180
 
98
181
  ```bash
99
182
  $ hh-applicant-tool [ GLOBAL_FLAGS ] [ OPERATION [ OPERATION_FLAGS ] ]
@@ -160,107 +243,47 @@ https://hh.ru/employer/1918903
160
243
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
161
244
  | **call-api** | Вызов произвольного метода API с выводом результата. |
162
245
  | **refresh-token** | Обновляет access_token. |
163
- | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, сайта фирмы и могут быть удалены по просьбе упал_намоченный лицо. Данная функция готова и будет доступна после 100 ⭐ |
246
+ | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, с сайта фирмы и могут быть удалены только по просьбе уполнамоченного лица. Данная функция готова и будет доступна после 100 ⭐ |
164
247
 
165
- Авторизуемся:
248
+ ### Формат текста сообщений
166
249
 
167
- ```bash
168
- $ hh-applicant-tool -vv authorize
169
- ```
250
+ Команда `apply-similar` поддерживает специальный формат сообщений.
170
251
 
171
- ![image](https://github.com/user-attachments/assets/88961e31-4ea3-478f-8c43-914d6785bc3b)
252
+ Так же в сообщении можно использовать плейсхолдеры:
172
253
 
173
- > В Windows не забудьте разрешить доступ к сети (Allow access) в всплывающем окне.
254
+ - **`%(vacancy_name)s`**: Название вакансии.
255
+ - **`%(employer_name)s`**: Название работодателя.
256
+ - **`%(first_name)s`**: Имя пользователя.
257
+ - **`%(last_name)s`**: Фамилия пользователя.
258
+ - **`%(email)s`**: Email пользователя.
259
+ - **`%(phone)s`**: Телефон пользователя.
174
260
 
261
+ Эти плейсхолдеры могут быть использованы в сообщениях для отклика на вакансии, чтобы динамически подставлять соответствующие данные в текст сообщения. Например:
175
262
 
176
-
177
- В случае успешной авторизации токены будут сохранены в `~/.config/hh-applicant-tool/config.json`:
178
-
179
- ```json
180
- {
181
- "token": {
182
- "access_token": "...",
183
- "created_at": 1678151427,
184
- "expires_in": 1209599,
185
- "refresh_token": "...",
186
- "token_type": "bearer"
187
- }
188
- }
189
263
  ```
190
-
191
- Через этот файл можно задать кастомный `user_agent`:
192
-
193
- ```json
194
- {
195
- "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0"
196
- }
264
+ "Меня заинтересовала ваша вакансия %(vacancy_name)s. Прошу рассмотреть мою кандидатуру. С уважением, %(first_name)s %(last_name)s."
197
265
  ```
198
266
 
199
- Проверка авторизации:
267
+ Так же можно делать текст уникальным с помощью `{}`. Внутри них через `|` перечисляются варианты, один из которых будет случайно выбран:
200
268
 
201
- ```bash
202
- $ hh-applicant-tool whoami
203
- {
204
- "auth_type": "applicant",
205
- "counters": {
206
- "new_resume_views": 1488,
207
- "resumes_count": 1,
208
- "unread_negotiations": 228
209
- },
210
- "email": "vasya.pupkin@gmail.com",
211
- "employer": null,
212
- "first_name": "Вася",
213
- "id": "1234567890",
214
- "is_admin": false,
215
- "is_anonymous": false,
216
- "is_applicant": true,
217
- "is_application": false,
218
- "is_employer": false,
219
- "is_in_search": true,
220
- "last_name": "Пупкин",
221
- "manager": null,
222
- "mid_name": null,
223
- "middle_name": null,
224
- "negotiations_url": "https://api.hh.ru/negotiations",
225
- "personal_manager": null,
226
- "phone": "79012345678",
227
- "profile_videos": {
228
- "items": []
229
- },
230
- "resumes_url": "https://api.hh.ru/resumes/mine"
231
- }
232
269
  ```
233
-
234
- Токен выдается на две недели:
235
-
236
- ```python
237
- Python 3.10.9 (main, Dec 19 2022, 17:35:49) [GCC 12.2.0] on linux
238
- Type "help", "copyright", "credits" or "license" for more information.
239
- >>> from datetime import datetime, timedelta
240
- >>> datetime.now() + timedelta(seconds=1209599)
241
- datetime.datetime(2023, 3, 23, 6, 36, 15, 596290)
242
- >>>
270
+ {Здоров|Привет}, {как {ты|сам}|что делаешь}?
243
271
  ```
244
272
 
245
- После нужно вызвать `refresh-token`:
273
+ В итоге получится что-то типа:
246
274
 
247
- ```bash
248
- $ hh-applicant-tool refresh-token
249
275
  ```
250
-
251
- Удаление хвостов:
252
-
253
- ```bash
254
- rm -rf ~/.config/hh-applicant-tool
255
-
256
- # В старых версиях добавлялся обработчик протокола через socat
257
- rm -f ~/.local/share/applications/hhandroid.desktop
276
+ Привет, как ты?
258
277
  ```
259
278
 
279
+ ### Написание плагинов
280
+
260
281
  Утилита использует систему плагинов. Все они лежат в [operations](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations). Модули расположенные там автоматически добавляются как доступные операции. За основу для своего плагина можно взять [whoami.py](https://github.com/s3rgeym/hh-applicant-tool/tree/main/hh_applicant_tool/operations/whoami.py).
261
282
 
262
283
  Отдельные замечания у меня к API HH. Оно пиздец какое кривое. Например, при создании заявки возвращается пустой ответ либо редирект, хотя по логике должен возвраться созданный объект. Так же в ответах сервера нет `Content-Length`. Из-за этого нельзя узнать есть тело у ответа сервера нужно его пробовать прочитать. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда `Transfer-Encoding: Chunked`. А еще он возвращает 502 ошибку, когда бекенд на Java падает либо долго отвечает (таймаут)? А вот [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии.
263
284
 
285
+ ### Сбор данных
286
+
264
287
  Утилита собирает и передает на сервер разработчика следующую информацию:
265
288
 
266
289
  1. Название вакансии.
@@ -270,7 +293,7 @@ rm -f ~/.local/share/applications/hhandroid.desktop
270
293
  1. Прямая ссылка на вакансию.
271
294
  1. Дата создания вакансии.
272
295
  1. Дата публикации вакансии.
273
- 1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, хранящаеся в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля (может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и росписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова).
296
+ 1. Контактная информация работодателя, которую он или его сотрудники сами выложили в общественный доступ, которая в дальнейшем _может_ храниться в строго обезличенной форме с соблюдением законов РФ, GDPR и американского экспортного контроля. Данная информация может быть удалена при письменном запросе в утвержденной Законом форме с оригинальными печатями фирмы и подписью генерального директора и/или по требованию РКН, прокуратуры или лично Адама Кадырова.
274
297
  1. Название компании.
275
298
  1. Тип компании.
276
299
  1. Описание компании.
@@ -7,7 +7,7 @@ hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3
7
7
  hh_applicant_tool/constants.py,sha256=lpgKkP2chWgTnBXvzxbSPWpKcfzp8VxMTlkssFcQhH4,469
8
8
  hh_applicant_tool/main.py,sha256=sL9eSWUkOz-NJbkq8PxRluvXUh5AqLVi7qmrdN1YAKY,3996
9
9
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- hh_applicant_tool/operations/apply_similar.py,sha256=fNiQlxjdRzaG2w-mWhZoaCo8OyAp71NM3PJamyTGyi8,12726
10
+ hh_applicant_tool/operations/apply_similar.py,sha256=SbSrZPbQkOcDqbuV8mqX9lMRsvBYLwq3xVnpQcbM3Q0,17166
11
11
  hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
12
12
  hh_applicant_tool/operations/call_api.py,sha256=qwPDrWP9tiqHkaYYWYBZtymj9AaxObB86Eny-Bf5q_c,1314
13
13
  hh_applicant_tool/operations/clear_negotiations.py,sha256=98Yuw9xP4dN5sUnUKlZxqfhU40TQ-aCjK4vj4LRTeYo,3894
@@ -17,8 +17,8 @@ hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu
17
17
  hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
18
18
  hh_applicant_tool/telemetry_client.py,sha256=1jgbc8oMfLhbEi2pTA2fF0pKlHSWekHY3oEJCDI8Uas,2268
19
19
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
20
- hh_applicant_tool/utils.py,sha256=wXcxs5IinK7PH6ipCCpngw7faUiboEpi9a8j1wDEPKw,2282
21
- hh_applicant_tool-0.3.6.dist-info/METADATA,sha256=gxwj6cYpX-LDDSgoEQeIGAkeBuxrsSelggU47MOg4hw,18509
22
- hh_applicant_tool-0.3.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
- hh_applicant_tool-0.3.6.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
- hh_applicant_tool-0.3.6.dist-info/RECORD,,
20
+ hh_applicant_tool/utils.py,sha256=DKD1b4mItuUugP6aV2vEoO59cIU2mJp5twc8WdvaSA4,2551
21
+ hh_applicant_tool-0.3.7.dist-info/METADATA,sha256=hYTuJoXxmRrCEnvcvdJz4AJEFKm6nHL6o2udu0Bi1bI,20109
22
+ hh_applicant_tool-0.3.7.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
+ hh_applicant_tool-0.3.7.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
24
+ hh_applicant_tool-0.3.7.dist-info/RECORD,,