@imessaging/headhunter 0.7.0
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.
- package/package.json +35 -0
- package/src/headhunter-transport.ts +130 -0
- package/src/index.ts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imessaging/headhunter",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "HeadHunter employer-dialog transport for imessaging",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"headhunter",
|
|
7
|
+
"hh.ru",
|
|
8
|
+
"messaging",
|
|
9
|
+
"transport"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/iconicompany/imessaging.git",
|
|
15
|
+
"directory": "packages/headhunter"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"type-check": "bunx tsgo --noEmit -p tsconfig.json",
|
|
30
|
+
"test": "bun test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@imessaging/core": "0.7.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessageTransport,
|
|
3
|
+
OutboundMessage,
|
|
4
|
+
SendResult,
|
|
5
|
+
TransportStatus,
|
|
6
|
+
} from "@imessaging/core";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://api.hh.ru";
|
|
9
|
+
|
|
10
|
+
export type HeadhunterTransportOptions = {
|
|
11
|
+
accountId: string;
|
|
12
|
+
accessToken: string;
|
|
13
|
+
/** Базовый URL API — позволяет проверить транспорт без сети. */
|
|
14
|
+
apiBaseUrl?: string;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class HeadhunterApiError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
readonly status: number,
|
|
21
|
+
readonly payload: unknown,
|
|
22
|
+
) {
|
|
23
|
+
super(`HeadHunter API returned HTTP ${status}: ${payloadText(payload)}`);
|
|
24
|
+
this.name = "HeadhunterApiError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Отправляет в уже существующий работодательский диалог HH по его negotiation ID. */
|
|
29
|
+
export class HeadhunterTransport implements MessageTransport {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
private readonly apiBaseUrl: string;
|
|
32
|
+
private connected = false;
|
|
33
|
+
private lastActivityAt?: Date;
|
|
34
|
+
private error?: string;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly options: HeadhunterTransportOptions) {
|
|
37
|
+
if (!options.accountId.trim()) throw new Error("accountId must not be empty");
|
|
38
|
+
if (!options.accessToken.trim()) throw new Error("accessToken must not be empty");
|
|
39
|
+
this.id = `headhunter:${options.accountId}`;
|
|
40
|
+
this.apiBaseUrl = (options.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/u, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async connect(): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
await this.request("/me");
|
|
46
|
+
this.connected = true;
|
|
47
|
+
this.error = undefined;
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
this.connected = false;
|
|
50
|
+
this.error = messageOf(cause);
|
|
51
|
+
throw cause;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async disconnect(): Promise<void> {
|
|
56
|
+
this.connected = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async send(message: OutboundMessage): Promise<SendResult> {
|
|
60
|
+
if (!this.connected) throw new Error(`${this.id} is not connected`);
|
|
61
|
+
if (message.recipient.type !== "headhunter") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${this.id} sends HeadHunter messages and cannot address ${message.recipient.type}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const negotiationId = message.recipient.negotiationId.trim();
|
|
67
|
+
if (!negotiationId) throw new Error("HeadHunter negotiationId must not be empty");
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.request<{ id?: string | number }>(
|
|
70
|
+
`/negotiations/${encodeURIComponent(negotiationId)}/messages`,
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
74
|
+
body: new URLSearchParams({ message: message.text }).toString(),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
this.lastActivityAt = new Date();
|
|
78
|
+
return {
|
|
79
|
+
transportId: this.id,
|
|
80
|
+
messageId: String(result.id ?? ""),
|
|
81
|
+
recipientId: negotiationId,
|
|
82
|
+
};
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
this.error = messageOf(cause);
|
|
85
|
+
throw cause;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getStatus(): Promise<TransportStatus> {
|
|
90
|
+
return {
|
|
91
|
+
connected: this.connected,
|
|
92
|
+
transportId: this.id,
|
|
93
|
+
accountId: this.options.accountId,
|
|
94
|
+
lastActivityAt: this.lastActivityAt,
|
|
95
|
+
error: this.error,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async request<T = Record<string, never>>(path: string, init?: RequestInit): Promise<T> {
|
|
100
|
+
const response = await fetch(`${this.apiBaseUrl}${path}`, {
|
|
101
|
+
...init,
|
|
102
|
+
headers: {
|
|
103
|
+
"User-Agent": this.options.userAgent ?? "imessaging/0.6",
|
|
104
|
+
Authorization: `Bearer ${this.options.accessToken}`,
|
|
105
|
+
...init?.headers,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const payload = await readPayload(response);
|
|
109
|
+
if (!response.ok) throw new HeadhunterApiError(response.status, payload);
|
|
110
|
+
return payload as T;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readPayload(response: Response): Promise<unknown> {
|
|
115
|
+
const text = await response.text();
|
|
116
|
+
if (!text) return {};
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(text) as unknown;
|
|
119
|
+
} catch {
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function payloadText(value: unknown): string {
|
|
125
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function messageOf(cause: unknown): string {
|
|
129
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
130
|
+
}
|