@zhin.js/adapter-milky 3.0.2 → 4.0.1
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/CHANGELOG.md +443 -0
- package/README.md +35 -121
- package/adapters/milky.ts +68 -0
- package/lib/endpoint.d.ts +7 -0
- package/lib/endpoint.js +6 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/milky-agent-deps.d.ts +24 -0
- package/lib/milky-agent-deps.js +30 -0
- package/lib/milky-auth.d.ts +3 -0
- package/lib/milky-auth.js +30 -0
- package/lib/protocol.d.ts +162 -0
- package/lib/protocol.js +337 -0
- package/lib/sse-client.d.ts +20 -0
- package/lib/sse-client.js +85 -0
- package/lib/sse-endpoint.d.ts +50 -0
- package/lib/sse-endpoint.js +261 -0
- package/lib/webhook-endpoint.d.ts +40 -0
- package/lib/webhook-endpoint.js +212 -0
- package/lib/ws-endpoint.d.ts +42 -0
- package/lib/ws-endpoint.js +320 -0
- package/lib/ws-types.d.ts +11 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-endpoint.d.ts +40 -0
- package/lib/wss-endpoint.js +263 -0
- package/package.json +43 -20
- package/plugin.ts +8 -0
- package/schema.json +36 -0
- package/src/endpoint.ts +26 -0
- package/src/index.ts +59 -57
- package/src/milky-agent-deps.ts +44 -8
- package/src/milky-auth.ts +29 -0
- package/src/protocol.ts +491 -0
- package/src/sse-client.ts +106 -0
- package/src/sse-endpoint.ts +318 -0
- package/src/webhook-endpoint.ts +262 -0
- package/src/ws-endpoint.ts +373 -0
- package/src/ws-types.ts +12 -0
- package/src/wss-endpoint.ts +305 -0
- package/lib/src/adapter.js +0 -99
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/api.js +0 -48
- package/lib/src/api.js.map +0 -1
- package/lib/src/endpoint-sse.js +0 -197
- package/lib/src/endpoint-sse.js.map +0 -1
- package/lib/src/endpoint-webhook.js +0 -189
- package/lib/src/endpoint-webhook.js.map +0 -1
- package/lib/src/endpoint-ws.js +0 -264
- package/lib/src/endpoint-ws.js.map +0 -1
- package/lib/src/endpoint-wss.js +0 -227
- package/lib/src/endpoint-wss.js.map +0 -1
- package/lib/src/index.js +0 -45
- package/lib/src/index.js.map +0 -1
- package/lib/src/milky-agent-deps.js +0 -10
- package/lib/src/milky-agent-deps.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js +0 -5
- package/lib/src/types.js.map +0 -1
- package/lib/src/utils.js +0 -169
- package/lib/src/utils.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -111
- package/src/api.ts +0 -63
- package/src/endpoint-sse.ts +0 -232
- package/src/endpoint-webhook.ts +0 -225
- package/src/endpoint-ws.ts +0 -300
- package/src/endpoint-wss.ts +0 -272
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -73
- package/src/utils.ts +0 -182
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
|
+
*/
|
|
4
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
8
|
+
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
9
|
+
import {
|
|
10
|
+
buildSendAction,
|
|
11
|
+
buildSseConnectOptions,
|
|
12
|
+
callApi,
|
|
13
|
+
extractInboundAudioUrl,
|
|
14
|
+
formatInboundContent,
|
|
15
|
+
formatInboundMessageId,
|
|
16
|
+
formatInboundTarget,
|
|
17
|
+
formatOutboundMessageId,
|
|
18
|
+
formatOutboundSegments,
|
|
19
|
+
isMentioned,
|
|
20
|
+
parseMessageReceiveData,
|
|
21
|
+
parseMilkyMessageId,
|
|
22
|
+
senderNickname,
|
|
23
|
+
type MilkyEvent,
|
|
24
|
+
type MilkyIncomingMessage,
|
|
25
|
+
type MilkySseConfig,
|
|
26
|
+
} from './protocol.js';
|
|
27
|
+
import { openSseStream, type SseClientHandle } from './sse-client.js';
|
|
28
|
+
|
|
29
|
+
const logger = getLogger('milky');
|
|
30
|
+
|
|
31
|
+
export type CreateMilkySseStream = (options: {
|
|
32
|
+
readonly url: string;
|
|
33
|
+
readonly headers: Record<string, string>;
|
|
34
|
+
readonly onMessage: (data: string) => void;
|
|
35
|
+
readonly onError?: (error: Error) => void;
|
|
36
|
+
readonly onOpen?: () => void;
|
|
37
|
+
}) => SseClientHandle;
|
|
38
|
+
|
|
39
|
+
export interface MilkySseEndpointOptions {
|
|
40
|
+
readonly id: CapabilityId;
|
|
41
|
+
readonly gateway: MessageGateway;
|
|
42
|
+
readonly config: MilkySseConfig;
|
|
43
|
+
readonly createSseStream?: CreateMilkySseStream;
|
|
44
|
+
readonly callApi?: typeof callApi;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class MilkySseEndpoint implements EndpointInstance {
|
|
48
|
+
readonly #options: MilkySseEndpointOptions;
|
|
49
|
+
readonly #callApi: typeof callApi;
|
|
50
|
+
#stream?: SseClientHandle;
|
|
51
|
+
#reconnectTimer?: NodeJS.Timeout;
|
|
52
|
+
#open = false;
|
|
53
|
+
#started = false;
|
|
54
|
+
#stopping = false;
|
|
55
|
+
#unregisterAgent?: () => void;
|
|
56
|
+
|
|
57
|
+
constructor(options: MilkySseEndpointOptions) {
|
|
58
|
+
this.#options = options;
|
|
59
|
+
this.#callApi = options.callApi ?? callApi;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start(): Promise<void> {
|
|
63
|
+
if (this.#started) return;
|
|
64
|
+
this.#started = true;
|
|
65
|
+
this.#stopping = false;
|
|
66
|
+
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
|
|
67
|
+
await this.#connect();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
open(): void {
|
|
71
|
+
this.#open = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close(): void {
|
|
75
|
+
this.#open = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async stop(): Promise<void> {
|
|
79
|
+
this.#open = false;
|
|
80
|
+
this.#stopping = true;
|
|
81
|
+
this.#started = false;
|
|
82
|
+
this.#unregisterAgent?.();
|
|
83
|
+
this.#unregisterAgent = undefined;
|
|
84
|
+
if (this.#reconnectTimer) {
|
|
85
|
+
clearTimeout(this.#reconnectTimer);
|
|
86
|
+
this.#reconnectTimer = undefined;
|
|
87
|
+
}
|
|
88
|
+
this.#stream?.close();
|
|
89
|
+
this.#stream = undefined;
|
|
90
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name, mode: 'sse' }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
94
|
+
const message = formatOutboundSegments(payload);
|
|
95
|
+
const { action, params } = buildSendAction(target, message);
|
|
96
|
+
const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
|
|
97
|
+
const messageId = formatOutboundMessageId(target, data?.message_seq);
|
|
98
|
+
logger.debug(formatCompact({
|
|
99
|
+
op: 'milky_send',
|
|
100
|
+
endpoint: this.#options.config.name,
|
|
101
|
+
target,
|
|
102
|
+
messageId,
|
|
103
|
+
mode: 'sse',
|
|
104
|
+
}));
|
|
105
|
+
return messageId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
109
|
+
return this.#callApi(this.apiOptions(), action, params);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async recallMessage(id: string): Promise<void> {
|
|
113
|
+
const parsed = parseMilkyMessageId(id);
|
|
114
|
+
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
115
|
+
if (parsed.message_scene === 'group') {
|
|
116
|
+
await this.callApi('recall_group_message', {
|
|
117
|
+
group_id: parsed.peer_id,
|
|
118
|
+
message_seq: parsed.message_seq,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
await this.callApi('recall_private_message', {
|
|
122
|
+
user_id: parsed.peer_id,
|
|
123
|
+
message_seq: parsed.message_seq,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
|
|
129
|
+
await this.callApi('kick_group_member', {
|
|
130
|
+
group_id: groupId,
|
|
131
|
+
user_id: userId,
|
|
132
|
+
reject_add_request: rejectAddRequest,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
|
|
138
|
+
await this.callApi('set_group_member_mute', {
|
|
139
|
+
group_id: groupId,
|
|
140
|
+
user_id: userId,
|
|
141
|
+
duration,
|
|
142
|
+
});
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async muteAll(groupId: number, enable = true): Promise<boolean> {
|
|
147
|
+
await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
|
|
152
|
+
await this.callApi('set_group_member_admin', {
|
|
153
|
+
group_id: groupId,
|
|
154
|
+
user_id: userId,
|
|
155
|
+
is_set: enable,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
|
|
161
|
+
await this.callApi('set_group_member_card', {
|
|
162
|
+
group_id: groupId,
|
|
163
|
+
user_id: userId,
|
|
164
|
+
card,
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
|
|
170
|
+
await this.callApi('set_group_member_special_title', {
|
|
171
|
+
group_id: groupId,
|
|
172
|
+
user_id: userId,
|
|
173
|
+
special_title: title,
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async setGroupName(groupId: number, name: string): Promise<boolean> {
|
|
179
|
+
await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getMemberList(groupId: number): Promise<unknown[]> {
|
|
184
|
+
return this.callApi('get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getGroupInfo(groupId: number): Promise<unknown> {
|
|
188
|
+
return this.callApi('get_group_info', { group_id: groupId });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
admit(event: MilkyEvent): void {
|
|
192
|
+
const data = parseMessageReceiveData(event);
|
|
193
|
+
if (!this.#open || !data) return;
|
|
194
|
+
this.#admitMessage(data, event);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
apiOptions(): { baseUrl: string; access_token?: string } {
|
|
198
|
+
return {
|
|
199
|
+
baseUrl: this.#options.config.baseUrl,
|
|
200
|
+
access_token: this.#options.config.access_token,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
205
|
+
const target = formatInboundTarget(data);
|
|
206
|
+
const content = formatInboundContent(data);
|
|
207
|
+
const audioUrl = extractInboundAudioUrl(data);
|
|
208
|
+
const nickname = senderNickname(data);
|
|
209
|
+
const mentioned = isMentioned(data, event.self_id);
|
|
210
|
+
void this.#options.gateway.receive({
|
|
211
|
+
adapter: this.#options.id,
|
|
212
|
+
target,
|
|
213
|
+
content,
|
|
214
|
+
sender: String(data.sender_id),
|
|
215
|
+
id: formatInboundMessageId(data),
|
|
216
|
+
metadata: Object.freeze({
|
|
217
|
+
message_scene: data.message_scene,
|
|
218
|
+
peer_id: String(data.peer_id),
|
|
219
|
+
sender_id: String(data.sender_id),
|
|
220
|
+
message_seq: data.message_seq,
|
|
221
|
+
endpoint: this.#options.config.name,
|
|
222
|
+
time: data.time ?? event.time,
|
|
223
|
+
self_id: event.self_id != null ? String(event.self_id) : undefined,
|
|
224
|
+
...(nickname ? { nickname } : {}),
|
|
225
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
226
|
+
...(audioUrl ? { audio_url: audioUrl } : {}),
|
|
227
|
+
}),
|
|
228
|
+
}).catch((err) => {
|
|
229
|
+
logger.warn(formatCompact({
|
|
230
|
+
op: 'milky_gateway_receive_failed',
|
|
231
|
+
target,
|
|
232
|
+
error: err instanceof Error ? err.message : String(err),
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async #connect(): Promise<void> {
|
|
238
|
+
const { url, headers, safeUrl } = buildSseConnectOptions(this.#options.config);
|
|
239
|
+
const create = this.#options.createSseStream ?? ((opts) => openSseStream(opts));
|
|
240
|
+
|
|
241
|
+
await new Promise<void>((resolve, reject) => {
|
|
242
|
+
let settled = false;
|
|
243
|
+
const stream = create({
|
|
244
|
+
url,
|
|
245
|
+
headers,
|
|
246
|
+
onOpen: () => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
settled = true;
|
|
249
|
+
logger.debug(formatCompact({
|
|
250
|
+
endpoint: this.#options.config.name,
|
|
251
|
+
mode: 'sse',
|
|
252
|
+
url: safeUrl,
|
|
253
|
+
}));
|
|
254
|
+
resolve();
|
|
255
|
+
},
|
|
256
|
+
onMessage: (data) => this.#onMessage(data),
|
|
257
|
+
onError: (error) => {
|
|
258
|
+
logger.warn(formatCompact({
|
|
259
|
+
op: 'sse_error',
|
|
260
|
+
endpoint: this.#options.config.name,
|
|
261
|
+
ok: false,
|
|
262
|
+
error: error.message,
|
|
263
|
+
}));
|
|
264
|
+
if (!settled) {
|
|
265
|
+
settled = true;
|
|
266
|
+
reject(error);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
this.#stream = stream;
|
|
271
|
+
void stream.closed.then(() => {
|
|
272
|
+
if (this.#stopping) return;
|
|
273
|
+
logger.warn(formatCompact({
|
|
274
|
+
op: 'disconnect',
|
|
275
|
+
endpoint: this.#options.config.name,
|
|
276
|
+
mode: 'sse',
|
|
277
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
278
|
+
}));
|
|
279
|
+
if (!settled) {
|
|
280
|
+
settled = true;
|
|
281
|
+
reject(new Error('Milky SSE closed before open'));
|
|
282
|
+
}
|
|
283
|
+
this.#scheduleReconnect();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#onMessage(data: string): void {
|
|
289
|
+
try {
|
|
290
|
+
const event = JSON.parse(data) as MilkyEvent;
|
|
291
|
+
this.admit(event);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logger.warn(formatCompact({
|
|
294
|
+
op: 'milky_parse_failed',
|
|
295
|
+
endpoint: this.#options.config.name,
|
|
296
|
+
mode: 'sse',
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#scheduleReconnect(): void {
|
|
303
|
+
if (this.#stopping || !this.#started || this.#reconnectTimer) return;
|
|
304
|
+
const delay = this.#options.config.reconnect_interval;
|
|
305
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
306
|
+
this.#reconnectTimer = undefined;
|
|
307
|
+
void this.#connect().catch((err) => {
|
|
308
|
+
logger.warn(formatCompact({
|
|
309
|
+
op: 'reconnect',
|
|
310
|
+
endpoint: this.#options.config.name,
|
|
311
|
+
mode: 'sse',
|
|
312
|
+
ok: false,
|
|
313
|
+
error: err instanceof Error ? err.message : String(err),
|
|
314
|
+
}));
|
|
315
|
+
});
|
|
316
|
+
}, delay);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky webhook endpoint — httpHostToken POST inbound + baseUrl HTTP API outbound.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
|
|
11
|
+
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
12
|
+
import {
|
|
13
|
+
buildSendAction,
|
|
14
|
+
callApi,
|
|
15
|
+
extractInboundAudioUrl,
|
|
16
|
+
formatInboundContent,
|
|
17
|
+
formatInboundMessageId,
|
|
18
|
+
formatInboundTarget,
|
|
19
|
+
formatOutboundMessageId,
|
|
20
|
+
formatOutboundSegments,
|
|
21
|
+
isMentioned,
|
|
22
|
+
parseMessageReceiveData,
|
|
23
|
+
parseMilkyMessageId,
|
|
24
|
+
senderNickname,
|
|
25
|
+
type MilkyEvent,
|
|
26
|
+
type MilkyIncomingMessage,
|
|
27
|
+
type MilkyWebhookConfig,
|
|
28
|
+
} from './protocol.js';
|
|
29
|
+
|
|
30
|
+
const logger = getLogger('milky');
|
|
31
|
+
|
|
32
|
+
export interface MilkyWebhookEndpointOptions {
|
|
33
|
+
readonly id: CapabilityId;
|
|
34
|
+
readonly gateway: MessageGateway;
|
|
35
|
+
readonly http: HttpHost;
|
|
36
|
+
readonly config: MilkyWebhookConfig;
|
|
37
|
+
readonly callApi?: typeof callApi;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
41
|
+
readonly #options: MilkyWebhookEndpointOptions;
|
|
42
|
+
readonly #callApi: typeof callApi;
|
|
43
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
44
|
+
#open = false;
|
|
45
|
+
#started = false;
|
|
46
|
+
#unregisterAgent?: () => void;
|
|
47
|
+
|
|
48
|
+
constructor(options: MilkyWebhookEndpointOptions) {
|
|
49
|
+
this.#options = options;
|
|
50
|
+
this.#callApi = options.callApi ?? callApi;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async start(): Promise<void> {
|
|
54
|
+
if (this.#started) return;
|
|
55
|
+
this.#started = true;
|
|
56
|
+
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
|
|
57
|
+
this.#setupRoutes();
|
|
58
|
+
logger.info(formatCompact({
|
|
59
|
+
op: 'listen',
|
|
60
|
+
endpoint: this.#options.config.name,
|
|
61
|
+
mode: 'webhook',
|
|
62
|
+
path: this.#options.config.path,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
open(): void {
|
|
67
|
+
this.#open = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
close(): void {
|
|
71
|
+
this.#open = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async stop(): Promise<void> {
|
|
75
|
+
this.#open = false;
|
|
76
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
77
|
+
this.#unregisterAgent?.();
|
|
78
|
+
this.#unregisterAgent = undefined;
|
|
79
|
+
this.#started = false;
|
|
80
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
84
|
+
const message = formatOutboundSegments(payload);
|
|
85
|
+
const { action, params } = buildSendAction(target, message);
|
|
86
|
+
const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
|
|
87
|
+
const messageId = formatOutboundMessageId(target, data?.message_seq);
|
|
88
|
+
logger.debug(formatCompact({
|
|
89
|
+
op: 'milky_send',
|
|
90
|
+
endpoint: this.#options.config.name,
|
|
91
|
+
target,
|
|
92
|
+
messageId,
|
|
93
|
+
}));
|
|
94
|
+
return messageId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
98
|
+
return this.#callApi(this.apiOptions(), action, params);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async recallMessage(id: string): Promise<void> {
|
|
102
|
+
const parsed = parseMilkyMessageId(id);
|
|
103
|
+
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
104
|
+
if (parsed.message_scene === 'group') {
|
|
105
|
+
await this.callApi('recall_group_message', {
|
|
106
|
+
group_id: parsed.peer_id,
|
|
107
|
+
message_seq: parsed.message_seq,
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
await this.callApi('recall_private_message', {
|
|
111
|
+
user_id: parsed.peer_id,
|
|
112
|
+
message_seq: parsed.message_seq,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
|
|
118
|
+
await this.callApi('kick_group_member', {
|
|
119
|
+
group_id: groupId,
|
|
120
|
+
user_id: userId,
|
|
121
|
+
reject_add_request: rejectAddRequest,
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
|
|
127
|
+
await this.callApi('set_group_member_mute', {
|
|
128
|
+
group_id: groupId,
|
|
129
|
+
user_id: userId,
|
|
130
|
+
duration,
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async muteAll(groupId: number, enable = true): Promise<boolean> {
|
|
136
|
+
await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
|
|
141
|
+
await this.callApi('set_group_member_admin', {
|
|
142
|
+
group_id: groupId,
|
|
143
|
+
user_id: userId,
|
|
144
|
+
is_set: enable,
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
|
|
150
|
+
await this.callApi('set_group_member_card', {
|
|
151
|
+
group_id: groupId,
|
|
152
|
+
user_id: userId,
|
|
153
|
+
card,
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
|
|
159
|
+
await this.callApi('set_group_member_special_title', {
|
|
160
|
+
group_id: groupId,
|
|
161
|
+
user_id: userId,
|
|
162
|
+
special_title: title,
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async setGroupName(groupId: number, name: string): Promise<boolean> {
|
|
168
|
+
await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getMemberList(groupId: number): Promise<unknown[]> {
|
|
173
|
+
return this.callApi('get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getGroupInfo(groupId: number): Promise<unknown> {
|
|
177
|
+
return this.callApi('get_group_info', { group_id: groupId });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
admit(event: MilkyEvent): void {
|
|
181
|
+
const data = parseMessageReceiveData(event);
|
|
182
|
+
if (!this.#open || !data) return;
|
|
183
|
+
this.#admitMessage(data, event);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
apiOptions(): { baseUrl: string; access_token?: string } {
|
|
187
|
+
return {
|
|
188
|
+
baseUrl: this.#options.config.baseUrl,
|
|
189
|
+
access_token: this.#options.config.access_token,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
194
|
+
const target = formatInboundTarget(data);
|
|
195
|
+
const content = formatInboundContent(data);
|
|
196
|
+
const audioUrl = extractInboundAudioUrl(data);
|
|
197
|
+
const nickname = senderNickname(data);
|
|
198
|
+
const mentioned = isMentioned(data, event.self_id);
|
|
199
|
+
void this.#options.gateway.receive({
|
|
200
|
+
adapter: this.#options.id,
|
|
201
|
+
target,
|
|
202
|
+
content,
|
|
203
|
+
sender: String(data.sender_id),
|
|
204
|
+
id: formatInboundMessageId(data),
|
|
205
|
+
metadata: Object.freeze({
|
|
206
|
+
message_scene: data.message_scene,
|
|
207
|
+
peer_id: String(data.peer_id),
|
|
208
|
+
sender_id: String(data.sender_id),
|
|
209
|
+
message_seq: data.message_seq,
|
|
210
|
+
endpoint: this.#options.config.name,
|
|
211
|
+
time: data.time ?? event.time,
|
|
212
|
+
self_id: event.self_id != null ? String(event.self_id) : undefined,
|
|
213
|
+
...(nickname ? { nickname } : {}),
|
|
214
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
215
|
+
...(audioUrl ? { audio_url: audioUrl } : {}),
|
|
216
|
+
}),
|
|
217
|
+
}).catch((err) => {
|
|
218
|
+
logger.warn(formatCompact({
|
|
219
|
+
op: 'milky_gateway_receive_failed',
|
|
220
|
+
target,
|
|
221
|
+
error: err instanceof Error ? err.message : String(err),
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#setupRoutes(): void {
|
|
227
|
+
const path = this.#options.config.path;
|
|
228
|
+
this.#routeReleases.push(
|
|
229
|
+
this.#options.http.route('POST', path, async (request, response) => {
|
|
230
|
+
await this.#handleWebhook(request, response);
|
|
231
|
+
}, { summary: 'Milky webhook callback', tags: ['milky'] }),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async #handleWebhook(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
236
|
+
try {
|
|
237
|
+
if (!verifyMilkyAccessToken(this.#options.config.access_token, request)) {
|
|
238
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
239
|
+
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const raw = await readRequestBody(request);
|
|
243
|
+
let event: MilkyEvent;
|
|
244
|
+
try {
|
|
245
|
+
event = JSON.parse(raw) as MilkyEvent;
|
|
246
|
+
} catch {
|
|
247
|
+
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
248
|
+
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (this.#open) this.admit(event);
|
|
252
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
253
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.error('Milky webhook error:', error);
|
|
256
|
+
if (!response.headersSent) {
|
|
257
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
258
|
+
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|