hh-applicant-tool 0.4.1__py3-none-any.whl → 0.5.0__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.

hh_applicant_tool/main.py CHANGED
@@ -4,23 +4,25 @@ import argparse
4
4
  import logging
5
5
  import sys
6
6
  from importlib import import_module
7
+ from os import getenv
7
8
  from pathlib import Path
8
9
  from pkgutil import iter_modules
9
- from typing import Sequence, Literal
10
+ from typing import Literal, Sequence
11
+
10
12
  from .api import ApiClient
11
13
  from .color_log import ColorHandler
12
14
  from .utils import Config, get_config_path
13
- from os import getenv
14
15
 
15
16
  DEFAULT_CONFIG_PATH = (
16
- get_config_path() / (__package__ or '').replace("_", "-") / "config.json"
17
+ get_config_path() / (__package__ or "").replace("_", "-") / "config.json"
17
18
  )
18
19
 
19
20
  logger = logging.getLogger(__package__)
20
21
 
21
22
 
22
23
  class BaseOperation:
23
- def setup_parser(self, parser: argparse.ArgumentParser) -> None: ...
24
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
25
+ ...
24
26
 
25
27
  def run(self, args: argparse.Namespace) -> None | int:
26
28
  raise NotImplementedError()
@@ -62,7 +64,7 @@ class HHApplicantTool:
62
64
 
63
65
  Исходники и предложения: <https://github.com/s3rgeym/hh-applicant-tool>
64
66
 
65
- Группа поддержки: <https://t.me/+aSjr8qM_AP85ZDBi>
67
+ Группа поддержки: <https://t.me/otzyvy_headhunter>
66
68
  """
67
69
 
68
70
  class ArgumentFormatter(
@@ -3,15 +3,18 @@ import logging
3
3
  import random
4
4
  import time
5
5
  from collections import defaultdict
6
+ from datetime import datetime, timedelta, timezone
6
7
  from typing import TextIO, Tuple
7
8
 
8
9
  from ..api import ApiError, BadRequest
9
10
  from ..main import BaseOperation
10
- from ..main import Namespace as BaseNamespace, get_api
11
+ from ..main import Namespace as BaseNamespace
12
+ from ..main import get_api
13
+ from ..mixins import GetResumeIdMixin
11
14
  from ..telemetry_client import TelemetryClient, TelemetryError
12
15
  from ..types import ApiListResponse, VacancyItem
13
- from ..utils import fix_datetime, truncate_string, random_text, parse_interval
14
- from ..mixins import GetResumeIdMixin
16
+ from ..utils import (fix_datetime, parse_interval, parse_invalid_datetime,
17
+ random_text, truncate_string)
15
18
 
16
19
  logger = logging.getLogger(__package__)
17
20
 
@@ -95,10 +98,12 @@ class Operation(BaseOperation, GetResumeIdMixin):
95
98
  logger.info("Телеметрия отключена.")
96
99
  else:
97
100
  logger.info("Спасибо за то что оставили телеметрию включенной!")
98
-
101
+
99
102
  self.api = get_api(args)
100
103
  self.resume_id = args.resume_id or self._get_resume_id()
101
- self.application_messages = self._get_application_messages(args.message_list)
104
+ self.application_messages = self._get_application_messages(
105
+ args.message_list
106
+ )
102
107
 
103
108
  self.apply_min_interval, self.apply_max_interval = args.apply_interval
104
109
  self.page_min_interval, self.page_max_interval = args.page_interval
@@ -109,7 +114,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
109
114
  self.dry_run = args.dry_run
110
115
  self._apply_similar()
111
116
 
112
- def _get_application_messages(self, message_list: TextIO | None) -> list[str]:
117
+ def _get_application_messages(
118
+ self, message_list: TextIO | None
119
+ ) -> list[str]:
113
120
  if message_list:
114
121
  application_messages = list(
115
122
  filter(None, map(str.strip, message_list))
@@ -134,7 +141,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
134
141
  "name": vacancy.get("name"),
135
142
  "type": vacancy.get("type", {}).get("id"), # open/closed
136
143
  "area": vacancy.get("area", {}).get("name"), # город
137
- "salary": vacancy.get("salary"), # from, to, currency, gross
144
+ "salary": vacancy.get(
145
+ "salary"
146
+ ), # from, to, currency, gross
138
147
  "direct_url": vacancy.get(
139
148
  "alternate_url"
140
149
  ), # ссылка на вакансию
@@ -150,6 +159,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
150
159
  "employer_id": int(vacancy["employer"]["id"])
151
160
  if "employer" in vacancy and "id" in vacancy["employer"]
152
161
  else None,
162
+ # "relations": vacancy.get("relations", []),
153
163
  # Остальное неинтересно
154
164
  }
155
165
 
@@ -162,6 +172,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
162
172
  "phone": me.get("phone", ""),
163
173
  }
164
174
 
175
+ do_apply = True
176
+ complained_employers = set()
177
+
165
178
  for vacancy in vacancies:
166
179
  try:
167
180
  message_placeholders = {
@@ -178,41 +191,81 @@ class Operation(BaseOperation, GetResumeIdMixin):
178
191
  )
179
192
 
180
193
  if vacancy.get("has_test"):
181
- print("🚫 Пропускаем тест", vacancy["alternate_url"])
182
- continue
183
-
184
- if vacancy.get("archived"):
185
- print(
186
- "🚫 Пропускаем вакансию в архиве",
194
+ logger.debug(
195
+ "Пропускаем вакансию с тестом: %s",
187
196
  vacancy["alternate_url"],
188
197
  )
189
198
  continue
190
199
 
191
- relations = vacancy.get("relations", [])
192
-
193
- if relations:
194
- print(
195
- "🚫 Пропускаем вакансию с",
196
- ["откликом или приглашением", "отказом"]["got_rejection" in relations],
200
+ if vacancy.get("archived"):
201
+ logger.warning(
202
+ "Пропускаем вакансию в архиве: %s",
197
203
  vacancy["alternate_url"],
198
204
  )
199
205
  continue
200
206
 
207
+ relations = vacancy.get("relations", [])
201
208
  employer_id = vacancy.get("employer", {}).get("id")
202
209
 
203
210
  if (
204
211
  self.enable_telemetry
205
212
  and employer_id
206
213
  and employer_id not in telemetry_data["employers"]
214
+ and employer_id not in complained_employers
215
+ and (
216
+ not relations
217
+ or parse_invalid_datetime(vacancy["created_at"])
218
+ + timedelta(days=7)
219
+ > datetime.now(tz=timezone.utc)
220
+ )
207
221
  ):
208
222
  employer = self.api.get(f"/employers/{employer_id}")
209
- telemetry_data["employers"][employer_id] = {
223
+
224
+ employer_data = {
210
225
  "name": employer.get("name"),
211
226
  "type": employer.get("type"),
212
227
  "description": employer.get("description"),
213
228
  "site_url": employer.get("site_url"),
214
229
  "area": employer.get("area", {}).get("name"), # город
215
230
  }
231
+ if "got_rejection" in relations:
232
+ try:
233
+ print(
234
+ "🚨 Вы получили отказ от https://hh.ru/employer/%s"
235
+ % employer_id
236
+ )
237
+ response = telemetry_client.send_telemetry(
238
+ f"/employers/{employer_id}/complaint",
239
+ employer_data,
240
+ )
241
+ if "topic_url" in response:
242
+ print(
243
+ "Ссылка на обсуждение работодателя:",
244
+ response["topic_url"],
245
+ )
246
+ else:
247
+ print(
248
+ "Создание темы для обсуждения работодателя добавлено в очередь..."
249
+ )
250
+ complained_employers.add(employer_id)
251
+ except TelemetryError as ex:
252
+ logger.error(ex)
253
+ elif do_apply:
254
+ telemetry_data["employers"][employer_id] = employer_data
255
+
256
+ if not do_apply:
257
+ logger.debug(
258
+ "Проопускаем вакансию так как достигла лимита заявок: %s",
259
+ vacancy["alternate_url"],
260
+ )
261
+ continue
262
+
263
+ if relations:
264
+ logger.debug(
265
+ "Пропускаем вакансию с откликом: %s",
266
+ vacancy["alternate_url"],
267
+ )
268
+ continue
216
269
 
217
270
  params = {
218
271
  "resume_id": self.resume_id,
@@ -220,7 +273,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
220
273
  "message": "",
221
274
  }
222
275
 
223
- if self.force_message or vacancy.get("response_letter_required"):
276
+ if self.force_message or vacancy.get(
277
+ "response_letter_required"
278
+ ):
224
279
  msg = params["message"] = (
225
280
  random_text(random.choice(self.application_messages))
226
281
  % message_placeholders
@@ -229,7 +284,7 @@ class Operation(BaseOperation, GetResumeIdMixin):
229
284
 
230
285
  if self.dry_run:
231
286
  logger.info(
232
- "Dry Run: Отправка отклика на вакансию %s с параметрами: %s",
287
+ "Dry Run: Отправка отклика на вакансию %s с параметрами: %s",
233
288
  vacancy["alternate_url"],
234
289
  params,
235
290
  )
@@ -253,24 +308,28 @@ class Operation(BaseOperation, GetResumeIdMixin):
253
308
  except ApiError as ex:
254
309
  logger.error(ex)
255
310
  if isinstance(ex, BadRequest) and ex.limit_exceeded:
256
- break
311
+ do_apply = False
257
312
 
258
313
  print("📝 Отклики на вакансии разосланы!")
259
314
 
260
315
  if self.enable_telemetry:
261
316
  if self.dry_run:
262
317
  # С --dry-run можно посмотреть что отправляется
263
- logger.info('Dry Run: Данные телеметрии для отправки на сервер: %r', telemetry_data)
318
+ logger.info(
319
+ "Dry Run: Данные телеметрии для отправки на сервер: %r",
320
+ telemetry_data,
321
+ )
264
322
  return
265
323
 
266
324
  try:
267
- telemetry_client.send_telemetry("/collect", dict(telemetry_data))
325
+ response = telemetry_client.send_telemetry(
326
+ "/collect", dict(telemetry_data)
327
+ )
328
+ logger.debug(response)
268
329
  except TelemetryError as ex:
269
330
  logger.error(ex)
270
-
271
- def _get_vacancies(
272
- self, per_page: int = 100
273
- ) -> list[VacancyItem]:
331
+
332
+ def _get_vacancies(self, per_page: int = 100) -> list[VacancyItem]:
274
333
  rv = []
275
334
  for page in range(20):
276
335
  params = {
@@ -289,8 +348,9 @@ class Operation(BaseOperation, GetResumeIdMixin):
289
348
 
290
349
  # Задержка перед получением следующей страницы
291
350
  if page > 0:
292
- interval = random.uniform(self.page_min_interval, self.page_max_interval)
351
+ interval = random.uniform(
352
+ self.page_min_interval, self.page_max_interval
353
+ )
293
354
  time.sleep(interval)
294
355
 
295
356
  return rv
296
-
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ import logging
3
+ from os import getenv
4
+
5
+ from ..main import BaseOperation
6
+ from ..main import Namespace as BaseNamespace
7
+ from ..main import get_proxies
8
+ from ..telemetry_client import TelemetryClient
9
+
10
+ logger = logging.getLogger(__package__)
11
+
12
+
13
+ class Namespace(BaseNamespace):
14
+ username: str | None = None
15
+ password: str | None = None
16
+ search: str | None = None
17
+
18
+
19
+ class Operation(BaseOperation):
20
+ """Выведет контакты работодателя по заданной строке поиска"""
21
+
22
+ def setup_parser(self, parser: argparse.ArgumentParser) -> None:
23
+ parser.add_argument(
24
+ "-u",
25
+ "--username",
26
+ type=str,
27
+ help="Имя пользователя для аутентификации",
28
+ default=getenv("AUTH_USERNAME"),
29
+ )
30
+ parser.add_argument(
31
+ "-P",
32
+ "--password",
33
+ type=str,
34
+ help="Пароль для аутентификации",
35
+ default=getenv("AUTH_PASSWORD"),
36
+ )
37
+ parser.add_argument(
38
+ "-s",
39
+ "--search",
40
+ type=str,
41
+ default="",
42
+ help="Строка поиска для контактов работодателя",
43
+ )
44
+ parser.add_argument(
45
+ "-p",
46
+ "--page",
47
+ default=1,
48
+ help="Номер страницы в выдаче",
49
+ )
50
+
51
+ def run(self, args: Namespace) -> None:
52
+ proxies = get_proxies(args)
53
+ client = TelemetryClient(proxies=proxies)
54
+ auth = (
55
+ (args.username, args.password)
56
+ if args.username and args.password
57
+ else None
58
+ )
59
+ # Аутентификация пользователя
60
+ results = client.get_telemetry(
61
+ "/contact/persons",
62
+ {"search": args.search, "per_page": 10, "page": args.page},
63
+ auth=auth,
64
+ )
65
+ self._print_contacts(results)
66
+
67
+ def _print_contacts(self, data: dict) -> None:
68
+ """Вывод всех контактов в древовидной структуре."""
69
+ page = data["page"]
70
+ pages = (data["total"] // data["per_page"]) + 1
71
+ print(f"📋 Контакты ({page}/{pages}):")
72
+ contacts = data.get("contact_persons", [])
73
+ for idx, contact in enumerate(contacts):
74
+ is_last_contact = idx == len(contacts) - 1
75
+ self._print_contact(contact, is_last_contact)
76
+ print()
77
+
78
+ def _print_contact(self, contact: dict, is_last_contact: bool) -> None:
79
+ """Вывод информации о конкретном контакте."""
80
+ prefix = "└──" if is_last_contact else "├──"
81
+ print(f" {prefix} 🧑 {contact.get('name', 'н/д')}")
82
+ prefix2 = " " if is_last_contact else " │ "
83
+ print(f"{prefix2}├── 📧 Email: {contact.get('email', 'н/д')}")
84
+ employer = contact.get("employer") or {}
85
+ print(f"{prefix2}├── 🏢 Работодатель: {employer.get('name', 'н/д')}")
86
+ print(f"{prefix2}├── 🏠 Город: {employer.get('area', 'н/д')}")
87
+ print(f"{prefix2}├── 🌐 Сайт: {employer.get('site_url', 'н/д')}")
88
+
89
+ phones = contact["phone_numbers"] or [{"phone_number": "(нет номеров)"}]
90
+ print(f"{prefix2}├── 📞 Телефоны:")
91
+ last_phone = len(phones) - 1
92
+ for i, phone in enumerate(phones):
93
+ sub_prefix = "└──" if i == last_phone else "├──"
94
+ print(f"{prefix2}│ {sub_prefix} {phone['phone_number']}")
95
+
96
+ telegrams = contact["telegram_usernames"] or [
97
+ {"username": "(нет аккаунтов)"}
98
+ ]
99
+ print(f"{prefix2}└── 📱 Telegram:")
100
+ last_telegram = len(telegrams) - 1
101
+ for i, telegram in enumerate(telegrams):
102
+ sub_prefix = "└──" if i == last_telegram else "├──"
103
+ print(f"{prefix2} {sub_prefix} {telegram['username']}")
@@ -1,13 +1,15 @@
1
- import os
2
1
  import json
3
- from urllib.parse import urljoin
4
- import requests
5
- from typing import Optional, Dict, Any
6
2
  import logging
7
- from functools import partialmethod
3
+ import os
4
+ import time
8
5
  import warnings
6
+ from functools import partialmethod
7
+ from typing import Any, Dict, Optional
8
+ from urllib.parse import urljoin
9
+
10
+ import requests
9
11
 
10
- warnings.filterwarnings('ignore', message='Unverified HTTPS request')
12
+ warnings.filterwarnings("ignore", message="Unverified HTTPS request")
11
13
 
12
14
  logger = logging.getLogger(__package__)
13
15
 
@@ -22,6 +24,7 @@ class TelemetryClient:
22
24
  """Клиент для отправки телеметрии на сервер."""
23
25
 
24
26
  server_address: str = "https://hh-applicant-tool.mooo.com:54157/"
27
+ default_delay: float = 0.334 # Задержка по умолчанию в секундах
25
28
 
26
29
  def __init__(
27
30
  self,
@@ -30,6 +33,7 @@ class TelemetryClient:
30
33
  session: Optional[requests.Session] = None,
31
34
  user_agent: str = "Mozilla/5.0 (HHApplicantTelemetry/1.0)",
32
35
  proxies: dict | None = None,
36
+ delay: Optional[float] = None,
33
37
  ) -> None:
34
38
  self.server_address = os.getenv(
35
39
  "TELEMETRY_SERVER", server_address or self.server_address
@@ -37,16 +41,28 @@ class TelemetryClient:
37
41
  self.session = session or requests.Session()
38
42
  self.user_agent = user_agent
39
43
  self.proxies = proxies
44
+ self.delay = delay if delay is not None else self.default_delay
45
+ self.last_request_time = time.monotonic() # Время последнего запроса
40
46
 
41
47
  def request(
42
48
  self,
43
49
  method: str,
44
50
  endpoint: str,
45
51
  data: Dict[str, Any] | None = None,
52
+ **kwargs: Any,
46
53
  ) -> Dict[str, Any]:
47
54
  method = method.upper()
48
55
  url = urljoin(self.server_address, endpoint)
49
56
  has_body = method in ["POST", "PUT", "PATCH"]
57
+
58
+ # Вычисляем время, прошедшее с последнего запроса
59
+ current_time = time.monotonic()
60
+ time_since_last_request = current_time - self.last_request_time
61
+
62
+ # Если прошло меньше времени, чем задержка, ждем оставшееся время
63
+ if time_since_last_request < self.delay:
64
+ time.sleep(self.delay - time_since_last_request)
65
+
50
66
  try:
51
67
  response = self.session.request(
52
68
  method,
@@ -56,6 +72,7 @@ class TelemetryClient:
56
72
  params=data if not has_body else None,
57
73
  json=data if has_body else None,
58
74
  verify=False, # Игнорирование истекшего сертификата
75
+ **kwargs,
59
76
  )
60
77
  # response.raise_for_status()
61
78
  result = response.json()
@@ -68,5 +85,9 @@ class TelemetryClient:
68
85
  json.JSONDecodeError,
69
86
  ) as ex:
70
87
  raise TelemetryError(str(ex)) from ex
88
+ finally:
89
+ # Обновляем время последнего запроса
90
+ self.last_request_time = time.monotonic()
71
91
 
92
+ get_telemetry = partialmethod(request, "GET")
72
93
  send_telemetry = partialmethod(request, "POST")
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
- from datetime import datetime
2
+
3
3
  import hashlib
4
4
  import json
5
5
  import platform
6
+ import random
7
+ import re
6
8
  import sys
9
+ from datetime import datetime
7
10
  from functools import partial
11
+ from os import getenv
8
12
  from pathlib import Path
9
13
  from threading import Lock
10
14
  from typing import Any
11
- from os import getenv
15
+
12
16
  from .constants import INVALID_ISO8601_FORMAT
13
- import re
14
- import random
15
17
 
16
18
  print_err = partial(print, file=sys.stderr, flush=True)
17
19
 
@@ -53,7 +55,13 @@ class Config(dict):
53
55
  self._config_path.parent.mkdir(exist_ok=True, parents=True)
54
56
  with self._lock:
55
57
  with self._config_path.open("w+") as fp:
56
- json.dump(self, fp, ensure_ascii=True, indent=2, sort_keys=True)
58
+ json.dump(
59
+ self,
60
+ fp,
61
+ ensure_ascii=True,
62
+ indent=2,
63
+ sort_keys=True,
64
+ )
57
65
 
58
66
  __getitem__ = dict.get
59
67
 
@@ -62,18 +70,17 @@ def truncate_string(s: str, limit: int = 75, ellipsis: str = "…") -> str:
62
70
  return s[:limit] + bool(s[limit:]) * ellipsis
63
71
 
64
72
 
65
- def hash_with_salt(data: str, salt: str = "HorsePenis") -> str:
66
- # Объединяем данные и соль
67
- salted_data = data + salt
73
+ def make_hash(data: str) -> str:
68
74
  # Вычисляем хеш SHA-256
69
- hashed_data = hashlib.sha256(salted_data.encode()).hexdigest()
70
- return hashed_data
75
+ return hashlib.sha256(data.encode()).hexdigest()
76
+
77
+
78
+ def parse_invalid_datetime(dt: str) -> datetime:
79
+ return datetime.strptime(dt, INVALID_ISO8601_FORMAT)
71
80
 
72
81
 
73
82
  def fix_datetime(dt: str | None) -> str | None:
74
- if dt is None:
75
- return None
76
- return datetime.strptime(dt, INVALID_ISO8601_FORMAT).isoformat()
83
+ return parse_invalid_datetime(dt).isoformat() if dt is not None else None
77
84
 
78
85
 
79
86
  def random_text(s: str) -> str:
@@ -89,6 +96,7 @@ def random_text(s: str) -> str:
89
96
  s = s1
90
97
  return s
91
98
 
99
+
92
100
  def parse_interval(interval: str) -> tuple[float, float]:
93
101
  """Парсит строку интервала и возвращает кортеж с минимальным и максимальным значениями."""
94
102
  if "-" in interval:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hh-applicant-tool
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary:
5
5
  Author: Senior YAML Developer
6
6
  Author-email: yamldeveloper@proton.me
@@ -32,10 +32,10 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  ### Описание
34
34
 
35
- Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Поддержка осуществляется строго в группе https://t.me/+aSjr8qM_AP85ZDBi (в ней разрешены мат, п*рнография, оскорбления всех участников кроме админа, а так же слив любой информации про хуевых работодателей и нерадивых херок).
35
+ Утилита для успешных волчат, служащая для автоматизации действий на HH.RU таких как рассылка откликов на подходящие вакансии и обновление всех резюме. Но данная утилита больше чем просто спамилка отзывами, вы так же выступаете в роли тайного агента, и если в списке подходящих вакансий встречается отказ, она возвращает ссылку на обсуждение работодателя в группе [Отзывы о работодателях с HH.RU](https://t.me/otzyvy_headhunter). Там вы можете написать отзыв о работодателе и почитать чужие. Обсуждения без отзывов удаляются. Данные на сервер передаются анонимно. Для этого собираются данные о работодателях и их вакансиях. Никакие персональные данные пользователя утилиты под которыми, вы авторизуетесь никуда не отправляются — только работодателей и рекрутеров. Отправку этих данных можно отключить (флаг `--disable-telemetry`), но тогда вы не получите ссылку на обсуждение, а так же не сможете пожаловаться на неадекватного мудака, выкатившего отказ после "небольшого" тестового задания на недельку. Через сайты на таких жаловаться бесполезно: владелец сайта за деньги отзывы удаляет, или мудак его запугает и жалоб в РКН накидает, а последний всегда найдет за что сайт заблокировать. Единственное место где можно написать отзыв — это **Telegram**.
36
36
 
37
37
  Работает с Python >= 3.10. Нужную версию Python можно поставить через
38
- asdf/pyenv/conda и что-то еще...
38
+ asdf/pyenv/conda и что-то еще.
39
39
 
40
40
  Данная утилита написана для Linux, но будет работать и на Ga..Mac OS, и в Windows, но с WSL не будет, так как для авторизации требуются оконный сервер X11 либо Wayland — только прямая установка пакета через pip в Windows. После авторизации вы можете перенести конфиг на сервер и запускать утилиту через systemd или cron. Столь странный процесс связан с тем, что на странице авторизации запускается море скриптов, которые шифруют данные на клиенте перед отправкой на сервер, а так же выполняется куча запросов чтобы проверить не бот ли ты. Хорошо, что после авторизации никаких проверок по факту нет, даже айпи не проверяется на соответсвие тому с какого была авторизация. В этой лапше мне лень разбираться. Так же при наличии рутованного телефона можно вытащить `access` и `refresh` токены из официального приложения и добавить их в конфиг.
41
41
 
@@ -244,7 +244,7 @@ https://hh.ru/employer/1918903
244
244
  | **clear-negotiations** | Удаляет отказы и отменяет заявки, которые долго висят |
245
245
  | **call-api** | Вызов произвольного метода API с выводом результата. |
246
246
  | **refresh-token** | Обновляет access_token. |
247
- | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Контакты получаются строго из публичного доступа, например, с сайта фирмы и могут быть удалены только по просьбе уполнамоченного лица. Данная функция готова и будет доступна после 100 ⭐ |
247
+ | **get-employer-contacts** | Получить список контактов работодателя, даже если тот не высылал приглашения. Это функционал для избранных, но в группе есть бесплатный бот с тем же функционалом. |
248
248
 
249
249
  ### Формат текста сообщений
250
250
 
@@ -289,24 +289,3 @@ https://hh.ru/employer/1918903
289
289
 
290
290
  Для тестирования запросов к API используйте команду `call-api` и `jq` для вывода JSON в удобочитаемом формате.
291
291
 
292
- ### Сбор данных
293
-
294
- Сбор и передачу данных о вакансиях и работодателях можно отключить, но так никакие персональные данные пользователя не передаются, то можно ничего не менять. Так как утилита бесплатна, то передачу вышеуказанной телеметрии можете рассматривать как плату за пользование.
295
-
296
- Утилита по умолчанию собирает и передает на сервер разработчика следующие данные:
297
-
298
- 1. Название вакансии.
299
- 1. Тип вакансии (открытая/закрытая).
300
- 1. Город, в котором размещена вакансия.
301
- 1. Информация о зарплате (минимальная, максимальная, валюта, указана ли зарплата до вычета налогов).
302
- 1. Прямая ссылка на вакансию.
303
- 1. Дата создания вакансии.
304
- 1. Дата публикации вакансии.
305
- 1. Контактная информация работодателя, указанная в вакансии с целью сохранения email, телефона и юзернейма аккаунта Telegram. Все эти данные вместе и каждое в частности не являются персональными данными на что есть [определение Верховного Суда](https://base.garant.ru/407421338/), так что я не вижу препятствий для того чтобы в дальнейшем утилита любому желающему давала возможность получить контакты любого работодателя, например, email и телегу. Тем более желающих послать нахуй тупую пизду, выкатившую немотивированный отказ, всегда будет предостаточно.
306
- 1. Название компании.
307
- 1. Тип компании.
308
- 1. Описание компании.
309
- 1. Ссылка на сайт компании.
310
- 1. Город, в котором находится компания.
311
-
312
-
@@ -5,22 +5,23 @@ hh_applicant_tool/api/client.py,sha256=um9NX22hNOtSuPCobCKf1anIFp-jiZlIXm4BuqN-L
5
5
  hh_applicant_tool/api/errors.py,sha256=0SoWKnsSUA0K2YgQA8GwGhe-pRMwtfK94MR6_MgbORQ,1722
6
6
  hh_applicant_tool/color_log.py,sha256=gN6j1Ayy1G7qOMI_e3WvfYw_ublzeQbKgsVLhqGg_3s,823
7
7
  hh_applicant_tool/constants.py,sha256=lpgKkP2chWgTnBXvzxbSPWpKcfzp8VxMTlkssFcQhH4,469
8
- hh_applicant_tool/main.py,sha256=DhnyINELRlp4i9ENlwDmzgU-C23ngy-hYlKXScivPIg,4797
8
+ hh_applicant_tool/main.py,sha256=z_SAW7cV83P5mVEkuddSzETUGLqocsNyEKlA6HBHjQ0,4806
9
9
  hh_applicant_tool/mixins.py,sha256=66LmyYSsDfhrpUwoAONjzrd5aoXqaZVoQ-zXhyYbYMk,418
10
10
  hh_applicant_tool/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- hh_applicant_tool/operations/apply_similar.py,sha256=X3OLYzMnRXI7_v6w2i3RxpDDHUj-Yf5DAJB7KCSAmWA,12348
11
+ hh_applicant_tool/operations/apply_similar.py,sha256=iMHbuqNYogL-cfP_RYxKTkWnWQDRWgiY0YACWw3Glzw,14811
12
12
  hh_applicant_tool/operations/authorize.py,sha256=TyUTCSOGwSYVJMEd5vSI981LRRI-RZf8hnlVYhtRVwA,3184
13
13
  hh_applicant_tool/operations/call_api.py,sha256=qwPDrWP9tiqHkaYYWYBZtymj9AaxObB86Eny-Bf5q_c,1314
14
14
  hh_applicant_tool/operations/clear_negotiations.py,sha256=98Yuw9xP4dN5sUnUKlZxqfhU40TQ-aCjK4vj4LRTeYo,3894
15
+ hh_applicant_tool/operations/get_employer_contacts.py,sha256=7BEzEnNAp87RlOP6HX0LR6cbtud2FuKCKK5sq6fINq8,4066
15
16
  hh_applicant_tool/operations/list_resumes.py,sha256=XBrVFTnl45BUtuvjVm70h_CXZrOvAULnLkLkyUh7gxw,1134
16
17
  hh_applicant_tool/operations/refresh_token.py,sha256=hYTmzBzJFSsb-LDO2_w0Y30WVWsctIH7vTzirkLwzWo,1267
17
18
  hh_applicant_tool/operations/reply_employers.py,sha256=wwDcI9YeZGUwadWQYFBwNpXb8qSAejaJ4KAuQTfFIuk,5686
18
19
  hh_applicant_tool/operations/update_resumes.py,sha256=gGxMYMoT9GqJjwn4AgrOAEJCZu4sdhaV0VmPr3jG-q8,1049
19
20
  hh_applicant_tool/operations/whoami.py,sha256=sg0r7m6oKkpMEmGt4QdtYdS-1gf-1KKdnk32yblbRJs,714
20
- hh_applicant_tool/telemetry_client.py,sha256=nNNr1drXY9Z01u5tJX---BXxBg1y06nJpNbhU45DmE0,2239
21
+ hh_applicant_tool/telemetry_client.py,sha256=wYLbKnx3sOmESFHqjLt-0Gww1O3lJiXFYdWnsorIhK8,3261
21
22
  hh_applicant_tool/types.py,sha256=q3yaIcq-UOkPzjxws0OFa4w9fTty-yx79_dic70_dUM,843
22
- hh_applicant_tool/utils.py,sha256=XFdQUOUm1DHJhVLRDLbXabOXtwfQAuk8Mqd-TTqNdgc,3017
23
- hh_applicant_tool-0.4.1.dist-info/METADATA,sha256=XhxhXDFl5Q2Lp6TNfS9YY0oHwoZxt1PouUTOWcLGE00,20967
24
- hh_applicant_tool-0.4.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
25
- hh_applicant_tool-0.4.1.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
26
- hh_applicant_tool-0.4.1.dist-info/RECORD,,
23
+ hh_applicant_tool/utils.py,sha256=vjSRbwU8mduFgORcyO2sQj-2B6klzQCtg_CFVCsgCo4,3067
24
+ hh_applicant_tool-0.5.0.dist-info/METADATA,sha256=xoA2y1wx6s-oTOS8GMw-9o78NpUy34SuLaf85w8eGos,19969
25
+ hh_applicant_tool-0.5.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
26
+ hh_applicant_tool-0.5.0.dist-info/entry_points.txt,sha256=Vb7M2YaYLMtKYJZh8chIrXZApMzSRFT1-rQw-U9r10g,65
27
+ hh_applicant_tool-0.5.0.dist-info/RECORD,,