@zhin.js/adapter-icqq 8.0.13 → 8.0.14
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 +47 -0
- package/README.md +8 -4
- package/adapters/icqq.js +3 -55
- package/adapters/icqq.ts +12 -62
- package/lib/cq-message.d.ts +5 -13
- package/lib/cq-message.js +6 -72
- package/lib/endpoint.d.ts +9 -10
- package/lib/endpoint.js +429 -141
- package/lib/forward-msg.d.ts +3 -22
- package/lib/forward-msg.js +38 -307
- package/lib/icqq-inbound.d.ts +29 -3
- package/lib/icqq-inbound.js +95 -12
- package/lib/icqq-login-assist.d.ts +27 -0
- package/lib/icqq-login-assist.js +109 -0
- package/lib/index.d.ts +1 -1
- package/lib/outbound-media.d.ts +6 -2
- package/lib/outbound-media.js +15 -8
- package/lib/protocol.d.ts +10 -1
- package/lib/protocol.js +82 -20
- package/package.json +13 -13
- package/src/cq-message.ts +7 -74
- package/src/endpoint.ts +463 -153
- package/src/forward-msg.ts +35 -342
- package/src/icqq-inbound.ts +112 -13
- package/src/icqq-login-assist.ts +132 -0
- package/src/index.ts +0 -1
- package/src/outbound-media.ts +20 -5
- package/src/protocol.ts +82 -22
- package/tools/announce.js +1 -1
- package/tools/announce.ts +1 -1
- package/tools/essence.js +1 -1
- package/tools/essence.ts +1 -1
- package/tools/friend_list.js +5 -4
- package/tools/friend_list.ts +7 -5
- package/tools/group_list.js +6 -5
- package/tools/group_list.ts +8 -6
- package/tools/group_member_info.js +27 -0
- package/tools/group_member_info.ts +30 -0
- package/tools/set_mute.js +1 -1
- package/tools/set_mute.ts +1 -1
- package/tools/set_title.js +1 -1
- package/tools/set_title.ts +1 -1
package/src/endpoint.ts
CHANGED
|
@@ -2,35 +2,46 @@ import {
|
|
|
2
2
|
Client,
|
|
3
3
|
parseGroupMessageId,
|
|
4
4
|
type Config as NativeIcqqClientConfig,
|
|
5
|
+
type FileElem,
|
|
6
|
+
type PttElem,
|
|
5
7
|
type Sendable,
|
|
8
|
+
type VideoElem,
|
|
6
9
|
} from '@icqqjs/icqq';
|
|
7
10
|
import { inspect } from 'node:util';
|
|
8
11
|
import type {
|
|
9
12
|
EndpointControl,
|
|
10
13
|
EndpointInstance,
|
|
11
14
|
EndpointManagement,
|
|
15
|
+
EndpointPendingRequest,
|
|
12
16
|
EndpointSendRequest,
|
|
13
17
|
} from 'zhin.js/adapter';
|
|
14
|
-
import type {
|
|
15
|
-
import type {
|
|
18
|
+
import type { EndpointContentPort, EndpointContentResolveContext } from '@zhin.js/adapter';
|
|
19
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
20
|
+
import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
|
|
21
|
+
import type {
|
|
22
|
+
ConversationMessage,
|
|
23
|
+
ConversationReference,
|
|
24
|
+
ForwardEntry,
|
|
25
|
+
MessageRef,
|
|
26
|
+
Segment,
|
|
27
|
+
} from '@zhin.js/im-contract';
|
|
16
28
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
17
29
|
import type { CapabilityId } from 'zhin.js';
|
|
18
30
|
import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
|
|
31
|
+
import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
|
|
19
32
|
import {
|
|
20
33
|
InboundMessageDeduper,
|
|
21
34
|
findIcqqNestedMessageSource,
|
|
22
35
|
isIcqqBotMentioned,
|
|
23
36
|
isIcqqMessagePostType,
|
|
24
37
|
normalizeIcqqInboundMessage,
|
|
25
|
-
|
|
38
|
+
messageLookupFromIcqqSource,
|
|
26
39
|
resolveIcqqQuoteIdFromEvent,
|
|
40
|
+
resolveIcqqInboundMedia,
|
|
27
41
|
shouldSkipSelfInboundMessage,
|
|
28
42
|
type IcqqMessageEvent,
|
|
29
43
|
} from './icqq-inbound.js';
|
|
30
44
|
import {
|
|
31
|
-
buildIcqqInboxNoticeRow,
|
|
32
|
-
buildIcqqInboxRequestRow,
|
|
33
|
-
buildIcqqSystemRequestRow,
|
|
34
45
|
isIcqqNoticePayload,
|
|
35
46
|
isIcqqRequestPayload,
|
|
36
47
|
} from './icqq-inbox.js';
|
|
@@ -39,7 +50,7 @@ import {
|
|
|
39
50
|
normalizeIcqqGuildInboundMessage,
|
|
40
51
|
} from './icqq-guild.js';
|
|
41
52
|
import {
|
|
42
|
-
|
|
53
|
+
prepareIcqqOutboundMedia,
|
|
43
54
|
resolveIcqqOutboundMediaMode,
|
|
44
55
|
} from './outbound-media.js';
|
|
45
56
|
import {
|
|
@@ -51,18 +62,14 @@ import {
|
|
|
51
62
|
type ResolvedIcqqConfig,
|
|
52
63
|
} from './protocol.js';
|
|
53
64
|
import type { SystemMessage as IcqqSystemMessage } from './types.js';
|
|
54
|
-
|
|
55
|
-
export interface IcqqInboxHooks {
|
|
56
|
-
recordRequest(row: Record<string, unknown>): void | Promise<void>;
|
|
57
|
-
recordNotice(row: Record<string, unknown>): void | Promise<void>;
|
|
58
|
-
publish?(type: string, data: unknown): void;
|
|
59
|
-
}
|
|
65
|
+
import { normalizeForwardMsgResponse } from './forward-msg.js';
|
|
60
66
|
|
|
61
67
|
export interface IcqqEndpointOptions {
|
|
62
68
|
readonly id: CapabilityId;
|
|
63
69
|
readonly gateway: MessageGateway;
|
|
64
70
|
readonly config: ResolvedIcqqConfig;
|
|
65
|
-
readonly
|
|
71
|
+
readonly sideEvents: SideEventGateway;
|
|
72
|
+
readonly loginAssist: LoginAssist;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
const BOUND_EVENTS = [
|
|
@@ -90,11 +97,96 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
90
97
|
readonly endpointName: string;
|
|
91
98
|
readonly #guildCatalog = new IcqqGuildCatalog();
|
|
92
99
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
93
|
-
readonly #
|
|
100
|
+
readonly #sideEventDeduper = new InboundMessageDeduper();
|
|
101
|
+
readonly #loginAssistOwner = Object.freeze({});
|
|
94
102
|
#open = false;
|
|
103
|
+
#retiring = false;
|
|
95
104
|
#started = false;
|
|
105
|
+
#resolveStartOnline?: () => void;
|
|
96
106
|
#heldInbound: IcqqInboundMessage[] = [];
|
|
97
107
|
#unregisterAgent?: () => void;
|
|
108
|
+
readonly #inflightInbound = new Set<Promise<void>>();
|
|
109
|
+
readonly #messageContent = new Map<string, ConversationMessage>();
|
|
110
|
+
|
|
111
|
+
readonly content: EndpointContentPort = Object.freeze({
|
|
112
|
+
resolve: async (reference: ConversationReference, context: EndpointContentResolveContext) => {
|
|
113
|
+
context.signal.throwIfAborted();
|
|
114
|
+
if (reference.kind === 'message') {
|
|
115
|
+
const cached = this.#messageContent.get(reference.message.id);
|
|
116
|
+
return cached
|
|
117
|
+
? Object.freeze({ status: 'resolved' as const, reference, value: cached })
|
|
118
|
+
: Object.freeze({ status: 'not_found' as const, code: 'icqq_message_not_observed' });
|
|
119
|
+
}
|
|
120
|
+
if (reference.kind === 'forward') {
|
|
121
|
+
try {
|
|
122
|
+
const entries = await this.#resolveForwardEntries(
|
|
123
|
+
reference.forwardId,
|
|
124
|
+
context,
|
|
125
|
+
{ remainingEntries: context.maxEntries, path: new Set<string>() },
|
|
126
|
+
0,
|
|
127
|
+
);
|
|
128
|
+
if (entries.length === 0) return Object.freeze({ status: 'not_found' as const, code: 'icqq_forward_not_found' });
|
|
129
|
+
return Object.freeze({ status: 'resolved' as const, reference, value: Object.freeze(entries) });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (context.signal.aborted) throw error;
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
status: 'failed' as const,
|
|
134
|
+
code: 'icqq_forward_fetch_failed',
|
|
135
|
+
message: error instanceof Error ? error.message : String(error),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (reference.media.kind === 'url' || reference.media.kind === 'base64' || reference.media.kind === 'path') {
|
|
140
|
+
return Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
|
|
141
|
+
}
|
|
142
|
+
return Object.freeze({ status: 'unsupported' as const, code: 'icqq_media_reference_unsupported' });
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
async #resolveForwardEntries(
|
|
147
|
+
forwardId: string,
|
|
148
|
+
context: EndpointContentResolveContext,
|
|
149
|
+
state: { remainingEntries: number; readonly path: Set<string> },
|
|
150
|
+
depth: number,
|
|
151
|
+
): Promise<readonly ForwardEntry[]> {
|
|
152
|
+
context.signal.throwIfAborted();
|
|
153
|
+
if (state.remainingEntries <= 0 || state.path.has(forwardId)) return Object.freeze([]);
|
|
154
|
+
state.path.add(forwardId);
|
|
155
|
+
try {
|
|
156
|
+
// ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
|
|
157
|
+
// generation lease until the native operation actually settles, then honor
|
|
158
|
+
// cancellation before publishing any resolved content.
|
|
159
|
+
const raw = await this.getForwardMsg(forwardId);
|
|
160
|
+
context.signal.throwIfAborted();
|
|
161
|
+
const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
|
|
162
|
+
state.remainingEntries -= entries.length;
|
|
163
|
+
const expanded: ForwardEntry[] = [];
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
context.signal.throwIfAborted();
|
|
166
|
+
const segments: Segment[] = [];
|
|
167
|
+
for (const segment of entry.segments) {
|
|
168
|
+
if (segment.type !== 'forward' || depth >= context.maxDepth) {
|
|
169
|
+
segments.push(segment);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const nestedId = String((segment.data as { forward_id?: unknown }).forward_id ?? '').trim();
|
|
173
|
+
if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
|
|
174
|
+
segments.push(segment);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
|
|
178
|
+
segments.push(Object.freeze({
|
|
179
|
+
...segment,
|
|
180
|
+
data: Object.freeze({ ...segment.data, ...(nested.length > 0 ? { entries: nested } : {}) }),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze(expanded);
|
|
186
|
+
} finally {
|
|
187
|
+
state.path.delete(forwardId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
98
190
|
|
|
99
191
|
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
100
192
|
listFriends: async () =>
|
|
@@ -114,6 +206,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
114
206
|
},
|
|
115
207
|
listGroupMembers: async (groupId) =>
|
|
116
208
|
Array.from((await this.getGroupMemberList(Number(groupId))).values()),
|
|
209
|
+
listRequests: () => this.#listPendingRequests(),
|
|
117
210
|
approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
|
|
118
211
|
rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
|
|
119
212
|
kickGroupMember: async (groupId, userId) => {
|
|
@@ -186,17 +279,24 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
186
279
|
});
|
|
187
280
|
}
|
|
188
281
|
|
|
189
|
-
async start(): Promise<void> {
|
|
282
|
+
async start(signal: AbortSignal): Promise<void> {
|
|
190
283
|
if (this.#started) return;
|
|
284
|
+
signal.throwIfAborted();
|
|
191
285
|
this.#started = true;
|
|
286
|
+
let resolveOnline!: () => void;
|
|
287
|
+
const onlineReady = new Promise<void>((resolve) => {
|
|
288
|
+
resolveOnline = resolve;
|
|
289
|
+
});
|
|
290
|
+
const onAbort = () => resolveOnline();
|
|
291
|
+
this.#resolveStartOnline = resolveOnline;
|
|
292
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
192
293
|
try {
|
|
193
|
-
const onlineReady = new Promise<void>((resolve) => {
|
|
194
|
-
this.on('system.online', () => resolve());
|
|
195
|
-
});
|
|
196
294
|
this.#bindClientEvents();
|
|
197
|
-
await this.login(this.#options.config.password);
|
|
295
|
+
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
198
296
|
await onlineReady;
|
|
199
|
-
|
|
297
|
+
signal.throwIfAborted();
|
|
298
|
+
await this.#pullPendingSystemMessages(signal);
|
|
299
|
+
signal.throwIfAborted();
|
|
200
300
|
this.#unregisterAgent = registerIcqqAgentEndpoint(this.endpointName, this);
|
|
201
301
|
this.#logger.info(
|
|
202
302
|
`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`,
|
|
@@ -205,21 +305,31 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
205
305
|
await this.stop();
|
|
206
306
|
this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
207
307
|
throw error;
|
|
308
|
+
} finally {
|
|
309
|
+
signal.removeEventListener('abort', onAbort);
|
|
310
|
+
if (this.#resolveStartOnline === resolveOnline) this.#resolveStartOnline = undefined;
|
|
208
311
|
}
|
|
209
312
|
}
|
|
210
313
|
|
|
211
314
|
open(): void {
|
|
315
|
+
this.#retiring = false;
|
|
212
316
|
this.#open = true;
|
|
213
317
|
const held = this.#heldInbound.splice(0);
|
|
214
318
|
for (const msg of held) this.admit(msg);
|
|
215
319
|
}
|
|
216
320
|
|
|
217
|
-
close(): void {
|
|
321
|
+
async close(): Promise<void> {
|
|
322
|
+
this.#retiring = true;
|
|
218
323
|
this.#open = false;
|
|
324
|
+
await Promise.allSettled([...this.#inflightInbound]);
|
|
219
325
|
}
|
|
220
326
|
|
|
221
327
|
async stop(): Promise<void> {
|
|
328
|
+
this.#retiring = true;
|
|
222
329
|
this.#open = false;
|
|
330
|
+
await Promise.allSettled([...this.#inflightInbound]);
|
|
331
|
+
this.#resolveStartOnline?.();
|
|
332
|
+
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'endpoint_stopped');
|
|
223
333
|
this.#heldInbound.length = 0;
|
|
224
334
|
this.#unregisterAgent?.();
|
|
225
335
|
this.#unregisterAgent = undefined;
|
|
@@ -230,8 +340,8 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
230
340
|
try {
|
|
231
341
|
this.terminate();
|
|
232
342
|
} catch { /* ignore */ }
|
|
233
|
-
|
|
234
|
-
|
|
343
|
+
this.#inboundDeduper.clear();
|
|
344
|
+
this.#sideEventDeduper.clear();
|
|
235
345
|
this.#guildCatalog.clear();
|
|
236
346
|
this.#started = false;
|
|
237
347
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
@@ -240,46 +350,70 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
240
350
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
241
351
|
if (!this.#started) throw new Error('icqq endpoint 未连接');
|
|
242
352
|
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
243
|
-
const
|
|
244
|
-
?
|
|
245
|
-
: payload;
|
|
246
|
-
const message = formatOutboundBody(content);
|
|
247
|
-
const parsed = icqqOutboundTarget(conversation);
|
|
248
|
-
let result: { message_id?: unknown };
|
|
353
|
+
const prepared = isIcqqSegmentPayload(payload)
|
|
354
|
+
? prepareIcqqOutboundMedia(payload, mediaMode)
|
|
355
|
+
: { content: payload, dispose() {} };
|
|
249
356
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
357
|
+
const message = formatOutboundBody(prepared.content);
|
|
358
|
+
const preview = formatIcqqSendablePreview(message);
|
|
359
|
+
const parsed = icqqOutboundTarget(conversation);
|
|
360
|
+
let result: { message_id?: unknown };
|
|
361
|
+
try {
|
|
362
|
+
result = await this.#sendNative(parsed, message);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
this.#logger.warn(formatCompact({
|
|
365
|
+
op: 'icqq_send_failed',
|
|
366
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
367
|
+
error: describeUnknownError(error),
|
|
368
|
+
preview: truncatePreview(preview, 120),
|
|
369
|
+
}));
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
|
|
373
|
+
this.#logger.info(
|
|
374
|
+
`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`,
|
|
375
|
+
);
|
|
376
|
+
return messageId;
|
|
377
|
+
} finally {
|
|
378
|
+
prepared.dispose();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async #sendNative(
|
|
383
|
+
target: ReturnType<typeof icqqOutboundTarget>,
|
|
384
|
+
message: Sendable,
|
|
385
|
+
): Promise<{ message_id?: unknown }> {
|
|
386
|
+
if (isIcqqFileElement(message)) {
|
|
387
|
+
switch (target.kind) {
|
|
388
|
+
case 'private': {
|
|
389
|
+
const id = await this.pickFriend(target.userId).sendFile(message.file, message.name);
|
|
390
|
+
return { message_id: id };
|
|
391
|
+
}
|
|
392
|
+
case 'group': {
|
|
393
|
+
const stat = await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
|
|
394
|
+
return { message_id: stat.fid };
|
|
395
|
+
}
|
|
257
396
|
case 'temp':
|
|
258
|
-
result = await this.sendTempMsg(parsed.groupId, parsed.userId, message as Sendable);
|
|
259
|
-
break;
|
|
260
397
|
case 'channel':
|
|
261
|
-
|
|
262
|
-
break;
|
|
398
|
+
throw new TypeError(`ICQQ ${target.kind} conversation does not support file delivery`);
|
|
263
399
|
}
|
|
264
|
-
} catch (error) {
|
|
265
|
-
this.#logger.warn(formatCompact({
|
|
266
|
-
op: 'icqq_send_failed',
|
|
267
|
-
target: `${conversation.kind}:${conversation.id}`,
|
|
268
|
-
error: describeUnknownError(error),
|
|
269
|
-
preview: truncatePreview(typeof message === 'string' ? message : String(message), 120),
|
|
270
|
-
}));
|
|
271
|
-
throw error;
|
|
272
400
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
401
|
+
switch (target.kind) {
|
|
402
|
+
case 'private':
|
|
403
|
+
return this.sendPrivateMsg(target.userId, message);
|
|
404
|
+
case 'group':
|
|
405
|
+
return this.sendGroupMsg(target.groupId, message);
|
|
406
|
+
case 'temp':
|
|
407
|
+
return this.sendTempMsg(target.groupId, target.userId, message);
|
|
408
|
+
case 'channel':
|
|
409
|
+
return this.sendGuildMsg(target.guildId, target.channelId, message) as unknown as { message_id?: unknown };
|
|
410
|
+
}
|
|
278
411
|
}
|
|
279
412
|
|
|
280
|
-
admit(msg: IcqqInboundMessage): void {
|
|
413
|
+
admit(msg: IcqqInboundMessage, admittedBeforeRetire = false): void {
|
|
281
414
|
const conversation = msg.conversation;
|
|
282
|
-
if (!this.#open) {
|
|
415
|
+
if (!this.#open && !admittedBeforeRetire) {
|
|
416
|
+
if (this.#retiring) return;
|
|
283
417
|
if (this.#heldInbound.length >= INBOUND_HOLD_LIMIT) {
|
|
284
418
|
const dropped = this.#heldInbound.shift();
|
|
285
419
|
this.#logger.warn(formatCompact({
|
|
@@ -311,8 +445,6 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
311
445
|
+ (mentioned ? ' (mentioned)' : '')
|
|
312
446
|
+ ` | ${truncatePreview(msg.content, 80)}`,
|
|
313
447
|
);
|
|
314
|
-
const quoteId = msg.metadata?.quote_id;
|
|
315
|
-
const { quote_id: _dropQuoteId, ...restMetadata } = msg.metadata ?? {};
|
|
316
448
|
void this.#options.gateway.receive({
|
|
317
449
|
conversation,
|
|
318
450
|
message: { conversation, id: msg.id },
|
|
@@ -321,10 +453,10 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
321
453
|
sender: msg.sender,
|
|
322
454
|
endpointId: this.endpointName,
|
|
323
455
|
...(mentioned ? { mentioned: true } : {}),
|
|
324
|
-
...(
|
|
456
|
+
...(msg.replyTo ? { replyTo: msg.replyTo } : {}),
|
|
325
457
|
metadata: Object.freeze({
|
|
326
458
|
channelType: msg.channelType,
|
|
327
|
-
...
|
|
459
|
+
...(msg.metadata ?? {}),
|
|
328
460
|
}),
|
|
329
461
|
}).catch((err) => {
|
|
330
462
|
this.#logger.warn(formatCompact({
|
|
@@ -350,23 +482,52 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
350
482
|
}));
|
|
351
483
|
}
|
|
352
484
|
};
|
|
353
|
-
this.on('message.private', (event) =>
|
|
354
|
-
this.on('message.group', (event) =>
|
|
485
|
+
this.on('message.private', (event) => this.#startMessageEvent('message.private', event));
|
|
486
|
+
this.on('message.group', (event) => this.#startMessageEvent('message.group', event));
|
|
355
487
|
this.on('message.guild', (event) => safe('message.guild', () => this.#handleGuildEvent(event)));
|
|
356
|
-
this.on('request.friend', (event) => safe('request.friend', () => this.#
|
|
357
|
-
this.on('request.group', (event) => safe('request.group', () => this.#
|
|
358
|
-
this.on('notice.friend', (event) => safe('notice.friend', () => this.#
|
|
359
|
-
this.on('notice.group', (event) => safe('notice.group', () => this.#
|
|
488
|
+
this.on('request.friend', (event) => safe('request.friend', () => this.#onRequestEvent(serializeIcqqEvent(event))));
|
|
489
|
+
this.on('request.group', (event) => safe('request.group', () => this.#onRequestEvent(serializeIcqqEvent(event))));
|
|
490
|
+
this.on('notice.friend', (event) => safe('notice.friend', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
|
|
491
|
+
this.on('notice.group', (event) => safe('notice.group', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
|
|
492
|
+
this.on('system.online', (event) => safe('system.online', () => {
|
|
493
|
+
this.#resolveStartOnline?.();
|
|
494
|
+
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
495
|
+
this.#logSystemEvent('online', event);
|
|
496
|
+
}));
|
|
360
497
|
this.on('system.offline', (event) => this.#logSystemEvent('offline', event));
|
|
361
498
|
this.on('system.offline.network', (event) => this.#logSystemEvent('offline.network', event));
|
|
362
499
|
this.on('system.offline.kickoff', (event) => this.#logSystemEvent('offline.kickoff', event));
|
|
363
|
-
this.on('system.login.qrcode', () => {
|
|
364
|
-
this.#logger.info('icqq
|
|
500
|
+
this.on('system.login.qrcode', (event) => {
|
|
501
|
+
this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
|
|
502
|
+
this.#handleLoginChallenge('qrcode', event);
|
|
503
|
+
});
|
|
504
|
+
this.on('system.login.device', (event) => this.#handleLoginChallenge('device', event));
|
|
505
|
+
this.on('system.login.slider', (event) => this.#handleLoginChallenge('slider', event));
|
|
506
|
+
this.on('system.login.error', (event) => {
|
|
507
|
+
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
|
|
508
|
+
this.#logSystemEvent('login.error', event);
|
|
509
|
+
});
|
|
510
|
+
this.on('system.login.auth', (event) => this.#handleLoginChallenge('auth', event));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#handleLoginChallenge(
|
|
514
|
+
step: 'qrcode' | 'slider' | 'device' | 'auth',
|
|
515
|
+
event: unknown,
|
|
516
|
+
): void {
|
|
517
|
+
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
518
|
+
? event as Record<string, unknown>
|
|
519
|
+
: null);
|
|
520
|
+
this.#logSystemEvent(`login.${step}`, event);
|
|
521
|
+
void runIcqqLoginAssistStep({
|
|
522
|
+
assist: this.#options.loginAssist,
|
|
523
|
+
client: this,
|
|
524
|
+
adapter: 'icqq',
|
|
525
|
+
endpointKey: this.endpointName,
|
|
526
|
+
owner: this.#loginAssistOwner,
|
|
527
|
+
logger: this.#logger,
|
|
528
|
+
step,
|
|
529
|
+
event: payload ?? event,
|
|
365
530
|
});
|
|
366
|
-
this.on('system.login.device', (event) => this.#logSystemEvent('login.device', event));
|
|
367
|
-
this.on('system.login.slider', (event) => this.#logSystemEvent('login.slider', event));
|
|
368
|
-
this.on('system.login.error', (event) => this.#logSystemEvent('login.error', event));
|
|
369
|
-
this.on('system.login.auth', (event) => this.#logSystemEvent('login.auth', event));
|
|
370
531
|
}
|
|
371
532
|
|
|
372
533
|
#logSystemEvent(type: string, event: unknown): void {
|
|
@@ -376,9 +537,58 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
376
537
|
endpoint: this.endpointName,
|
|
377
538
|
...(payload ? { event: JSON.stringify(payload) } : {}),
|
|
378
539
|
}));
|
|
540
|
+
this.#dispatchIcqqSystemSideEvent(type, payload);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#dispatchIcqqSideEvent(payload: Record<string, unknown>): void {
|
|
544
|
+
const sideEvents = this.#options.sideEvents;
|
|
545
|
+
const postType = String(payload.post_type ?? '');
|
|
546
|
+
const isRequest = postType === 'request' || postType.startsWith('request.');
|
|
547
|
+
const flag = payload.flag != null ? String(payload.flag) : '';
|
|
548
|
+
void receiveOneBotLikeSideEvent(sideEvents, {
|
|
549
|
+
adapter: 'icqq',
|
|
550
|
+
endpointKey: this.endpointName,
|
|
551
|
+
platform: 'icqq',
|
|
552
|
+
raw: payload,
|
|
553
|
+
...(isRequest ? {
|
|
554
|
+
approve: (id, remark) => this.#approveRequest(flag || id, remark),
|
|
555
|
+
reject: (id, reason) => this.#rejectRequest(flag || id, reason),
|
|
556
|
+
} : {}),
|
|
557
|
+
}).catch((err) => {
|
|
558
|
+
this.#logger.warn(formatCompact({
|
|
559
|
+
op: 'icqq_side_event_failed',
|
|
560
|
+
endpoint: this.endpointName,
|
|
561
|
+
post_type: postType || undefined,
|
|
562
|
+
error: err instanceof Error ? err.message : String(err),
|
|
563
|
+
}));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#dispatchIcqqSystemSideEvent(type: string, payload: Record<string, unknown> | null): void {
|
|
568
|
+
const sideEvents = this.#options.sideEvents;
|
|
569
|
+
const { scene_type, sub_type } = parseIcqqSystemScene(type);
|
|
570
|
+
void sideEvents.receiveSystem(SystemEvent.from(payload ?? {}, {
|
|
571
|
+
$id: `system:${this.endpointName}:${type}:${Date.now()}`,
|
|
572
|
+
$adapter: 'icqq' as never,
|
|
573
|
+
$endpoint: this.endpointName,
|
|
574
|
+
$type: 'system',
|
|
575
|
+
$scene_id: this.endpointName,
|
|
576
|
+
$scene_type: scene_type,
|
|
577
|
+
$sub_type: sub_type,
|
|
578
|
+
$timestamp: Date.now(),
|
|
579
|
+
})).catch((err) => {
|
|
580
|
+
this.#logger.warn(formatCompact({
|
|
581
|
+
op: 'icqq_system_side_event_failed',
|
|
582
|
+
endpoint: this.endpointName,
|
|
583
|
+
type,
|
|
584
|
+
error: err instanceof Error ? err.message : String(err),
|
|
585
|
+
}));
|
|
586
|
+
});
|
|
379
587
|
}
|
|
380
588
|
|
|
381
|
-
#handleMessageEvent(eventName: 'message.private' | 'message.group', event: unknown): void {
|
|
589
|
+
async #handleMessageEvent(eventName: 'message.private' | 'message.group', event: unknown): Promise<void> {
|
|
590
|
+
if (this.#retiring) return;
|
|
591
|
+
const admittedBeforeRetire = this.#open;
|
|
382
592
|
const payload = serializeIcqqEvent(event);
|
|
383
593
|
if (!payload || !isIcqqMessagePostType(payload)) {
|
|
384
594
|
this.#logger.debug(formatCompact({
|
|
@@ -400,6 +610,14 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
400
610
|
}));
|
|
401
611
|
return;
|
|
402
612
|
}
|
|
613
|
+
const contact = this.#resolveInboundContact(data);
|
|
614
|
+
if (contact) {
|
|
615
|
+
await resolveIcqqInboundMedia(data.message, {
|
|
616
|
+
getPttUrl: (element) => contact.getPttUrl(element as PttElem),
|
|
617
|
+
getVideoUrl: (element) => contact.getVideoUrl(element as VideoElem),
|
|
618
|
+
});
|
|
619
|
+
if (!this.#started) return;
|
|
620
|
+
}
|
|
403
621
|
const normalized = normalizeIcqqInboundMessage(data);
|
|
404
622
|
if (!normalized) {
|
|
405
623
|
this.#logger.debug(formatCompact({
|
|
@@ -420,13 +638,31 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
420
638
|
}));
|
|
421
639
|
return;
|
|
422
640
|
}
|
|
641
|
+
const conversation = icqqInboundConversation(String(this.#options.id), {
|
|
642
|
+
channelType: normalized.channelType,
|
|
643
|
+
channelId: normalized.channelId,
|
|
644
|
+
channelParentGroupId: normalized.channelParentGroupId,
|
|
645
|
+
});
|
|
646
|
+
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
647
|
+
const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
648
|
+
if (quoteId && quoted) {
|
|
649
|
+
this.#messageContent.set(quoteId, Object.freeze({
|
|
650
|
+
ref: Object.freeze({ conversation, id: quoteId }),
|
|
651
|
+
...(quoted.sender?.id ? { actor: Object.freeze({
|
|
652
|
+
id: quoted.sender.id,
|
|
653
|
+
...(quoted.sender.name ? { displayName: quoted.sender.name } : {}),
|
|
654
|
+
}) } : {}),
|
|
655
|
+
segments: Object.freeze(toCanonicalSegments(
|
|
656
|
+
Array.isArray(quoted.content)
|
|
657
|
+
? quoted.content
|
|
658
|
+
: [{ type: 'text', data: { text: quoted.content } }],
|
|
659
|
+
)),
|
|
660
|
+
timestamp: quoted.time ? quoted.time * (quoted.time < 1e12 ? 1000 : 1) : Date.now(),
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
423
663
|
this.admit({
|
|
424
664
|
id: normalized.messageId,
|
|
425
|
-
conversation
|
|
426
|
-
channelType: normalized.channelType,
|
|
427
|
-
channelId: normalized.channelId,
|
|
428
|
-
channelParentGroupId: normalized.channelParentGroupId,
|
|
429
|
-
}),
|
|
665
|
+
conversation,
|
|
430
666
|
content: formatInboundContent(normalized.rawMessage),
|
|
431
667
|
segments: normalized.content,
|
|
432
668
|
sender: {
|
|
@@ -435,13 +671,47 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
435
671
|
...(normalized.senderRole ? { roles: [normalized.senderRole] } : {}),
|
|
436
672
|
},
|
|
437
673
|
channelType: normalized.channelType,
|
|
438
|
-
|
|
674
|
+
...(quoteId ? { replyTo: { id: quoteId } } : {}),
|
|
675
|
+
metadata: {
|
|
439
676
|
nickname: normalized.nickname,
|
|
440
677
|
senderRole: normalized.senderRole,
|
|
441
|
-
}
|
|
678
|
+
},
|
|
679
|
+
}, admittedBeforeRetire);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#startMessageEvent(eventName: 'message.private' | 'message.group', event: unknown): void {
|
|
683
|
+
const operation = this.#handleMessageEvent(eventName, event);
|
|
684
|
+
this.#inflightInbound.add(operation);
|
|
685
|
+
void operation.catch((error) => {
|
|
686
|
+
this.#logInboundHandlerFailure(eventName, error);
|
|
687
|
+
}).finally(() => {
|
|
688
|
+
this.#inflightInbound.delete(operation);
|
|
442
689
|
});
|
|
443
690
|
}
|
|
444
691
|
|
|
692
|
+
#resolveInboundContact(data: IcqqMessageEvent) {
|
|
693
|
+
const userId = Number(data.user_id ?? data.from_id);
|
|
694
|
+
if (!Number.isFinite(userId) || userId <= 0) return null;
|
|
695
|
+
if (data.message_type === 'group' || data.group_id != null) {
|
|
696
|
+
const groupId = Number(data.group_id);
|
|
697
|
+
return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
|
|
698
|
+
}
|
|
699
|
+
const sourceGroupId = Number(data.sender?.group_id);
|
|
700
|
+
if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
|
|
701
|
+
return this.pickMember(sourceGroupId, userId);
|
|
702
|
+
}
|
|
703
|
+
return this.pickFriend(userId);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#logInboundHandlerFailure(event: string, error: unknown): void {
|
|
707
|
+
this.#logger.warn(formatCompact({
|
|
708
|
+
op: 'icqq_inbound_handler_failed',
|
|
709
|
+
endpoint: this.endpointName,
|
|
710
|
+
event,
|
|
711
|
+
error: describeUnknownError(error),
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
|
|
445
715
|
#handleGuildEvent(event: unknown): void {
|
|
446
716
|
const payload = serializeIcqqEvent(event);
|
|
447
717
|
if (!payload || typeof payload !== 'object') return;
|
|
@@ -484,61 +754,76 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
484
754
|
});
|
|
485
755
|
}
|
|
486
756
|
|
|
487
|
-
#
|
|
757
|
+
#onRequestEvent(payload: Record<string, unknown> | null): void {
|
|
488
758
|
if (!payload || !isIcqqRequestPayload(payload)) return;
|
|
489
|
-
const
|
|
490
|
-
if (!
|
|
491
|
-
|
|
492
|
-
if (!row) return;
|
|
493
|
-
if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) return;
|
|
494
|
-
void hooks.recordRequest(row);
|
|
495
|
-
hooks.publish?.('endpoint:request', row);
|
|
759
|
+
const flag = payload.flag != null ? String(payload.flag) : (payload.seq != null ? String(payload.seq) : '');
|
|
760
|
+
if (flag && !this.#sideEventDeduper.shouldProcess(`request:${flag}`)) return;
|
|
761
|
+
this.#dispatchIcqqSideEvent(payload);
|
|
496
762
|
}
|
|
497
763
|
|
|
498
|
-
#
|
|
764
|
+
#onNoticeEvent(payload: Record<string, unknown> | null): void {
|
|
499
765
|
if (!payload || !isIcqqNoticePayload(payload)) return;
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
766
|
+
const key = [
|
|
767
|
+
payload.time,
|
|
768
|
+
payload.operator_id ?? payload.user_id,
|
|
769
|
+
payload.notice_type ?? payload.type,
|
|
770
|
+
payload.sub_type,
|
|
771
|
+
].join('_');
|
|
772
|
+
if (!this.#sideEventDeduper.shouldProcess(`notice:${key}`)) return;
|
|
773
|
+
this.#dispatchIcqqSideEvent(payload);
|
|
507
774
|
}
|
|
508
775
|
|
|
509
|
-
#
|
|
510
|
-
const
|
|
511
|
-
|
|
776
|
+
async #listPendingRequests(): Promise<readonly EndpointPendingRequest[]> {
|
|
777
|
+
const messages = await this.getSystemMsg();
|
|
778
|
+
const rows: EndpointPendingRequest[] = [];
|
|
779
|
+
for (const raw of messages) {
|
|
780
|
+
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
781
|
+
const kind = msg.request_type === 'friend' ? 'friend' : 'group';
|
|
782
|
+
const platformRequestId = (msg.flag && String(msg.flag).trim())
|
|
783
|
+
|| (msg.seq != null ? String(msg.seq) : '');
|
|
784
|
+
if (!platformRequestId || msg.user_id == null) continue;
|
|
785
|
+
const createdAt = Number(msg.time);
|
|
786
|
+
rows.push(Object.freeze({
|
|
787
|
+
platform_request_id: platformRequestId,
|
|
788
|
+
type: kind,
|
|
789
|
+
scene_type: kind === 'group' ? 'group' : null,
|
|
790
|
+
scene_id: String(kind === 'group' ? (msg.group_id ?? msg.user_id) : msg.user_id),
|
|
791
|
+
sub_type: kind === 'group' ? (msg.type != null ? String(msg.type) : null) : null,
|
|
792
|
+
actor_id: String(msg.user_id),
|
|
793
|
+
actor_name: msg.nickname != null ? String(msg.nickname) : null,
|
|
794
|
+
comment: msg.comment != null ? String(msg.comment) : null,
|
|
795
|
+
created_at: Number.isFinite(createdAt) && createdAt > 0
|
|
796
|
+
? (createdAt < 1e12 ? createdAt * 1000 : createdAt)
|
|
797
|
+
: Date.now(),
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
return Object.freeze(rows);
|
|
512
801
|
}
|
|
513
802
|
|
|
514
|
-
async #pullPendingSystemMessages(): Promise<void> {
|
|
515
|
-
const hooks = this.#options.inbox;
|
|
516
|
-
if (!hooks) return;
|
|
803
|
+
async #pullPendingSystemMessages(signal: AbortSignal): Promise<void> {
|
|
517
804
|
try {
|
|
518
|
-
const messages = await this.getSystemMsg();
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) continue;
|
|
536
|
-
void hooks.recordRequest(row);
|
|
537
|
-
hooks.publish?.('endpoint:request', row);
|
|
805
|
+
const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
|
|
806
|
+
for (const raw of messages) {
|
|
807
|
+
signal.throwIfAborted();
|
|
808
|
+
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
809
|
+
const kind = msg.request_type === 'friend' ? 'friend' : 'group';
|
|
810
|
+
const flag = msg.flag != null ? String(msg.flag) : (msg.seq != null ? String(msg.seq) : '');
|
|
811
|
+
if (!flag || !this.#sideEventDeduper.shouldProcess(`request:${flag}`)) continue;
|
|
812
|
+
this.#dispatchIcqqSideEvent({
|
|
813
|
+
post_type: 'request',
|
|
814
|
+
request_type: kind,
|
|
815
|
+
flag: msg.flag,
|
|
816
|
+
user_id: msg.user_id,
|
|
817
|
+
group_id: msg.group_id,
|
|
818
|
+
comment: msg.comment,
|
|
819
|
+
time: msg.time,
|
|
820
|
+
sub_type: msg.type,
|
|
821
|
+
});
|
|
538
822
|
}
|
|
539
823
|
} catch (error) {
|
|
824
|
+
if (signal.aborted) throw signal.reason ?? error;
|
|
540
825
|
this.#logger.debug(formatCompact({
|
|
541
|
-
op: '
|
|
826
|
+
op: 'side_event_pull_system_msg',
|
|
542
827
|
endpoint: this.endpointName,
|
|
543
828
|
error: error instanceof Error ? error.message : String(error),
|
|
544
829
|
}));
|
|
@@ -578,6 +863,41 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
578
863
|
}
|
|
579
864
|
}
|
|
580
865
|
|
|
866
|
+
function formatIcqqSendablePreview(message: Sendable): string {
|
|
867
|
+
if (typeof message === 'string') return message;
|
|
868
|
+
try {
|
|
869
|
+
return JSON.stringify(message, (_key, value) => Buffer.isBuffer(value) ? '<buffer>' : value);
|
|
870
|
+
} catch {
|
|
871
|
+
return '[ICQQ Sendable]';
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function isIcqqSegmentPayload(payload: unknown): payload is import('zhin.js').SendContent {
|
|
876
|
+
if (Array.isArray(payload)) return true;
|
|
877
|
+
return typeof payload === 'object'
|
|
878
|
+
&& payload !== null
|
|
879
|
+
&& typeof (payload as { type?: unknown }).type === 'string';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function isIcqqFileElement(message: Sendable): message is FileElem {
|
|
883
|
+
return !Array.isArray(message)
|
|
884
|
+
&& typeof message !== 'string'
|
|
885
|
+
&& message.type === 'file';
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function raceAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
889
|
+
signal.throwIfAborted();
|
|
890
|
+
let rejectAbort!: (reason: unknown) => void;
|
|
891
|
+
const aborted = new Promise<never>((_resolve, reject) => { rejectAbort = reject; });
|
|
892
|
+
const onAbort = () => rejectAbort(signal.reason ?? new Error('ICQQ operation aborted'));
|
|
893
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
894
|
+
try {
|
|
895
|
+
return await Promise.race([operation, aborted]);
|
|
896
|
+
} finally {
|
|
897
|
+
signal.removeEventListener('abort', onAbort);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
581
901
|
function describeUnknownError(error: unknown): string {
|
|
582
902
|
if (error instanceof Error) return error.stack ?? error.message;
|
|
583
903
|
if (error && typeof error === 'object') {
|
|
@@ -625,36 +945,6 @@ function resolveNativeClientConfig(config: ResolvedIcqqConfig): NativeIcqqClient
|
|
|
625
945
|
};
|
|
626
946
|
}
|
|
627
947
|
|
|
628
|
-
function buildIcqqQuoteMetadata(
|
|
629
|
-
data: IcqqMessageEvent,
|
|
630
|
-
base: Record<string, unknown>,
|
|
631
|
-
): Record<string, unknown> {
|
|
632
|
-
const metadata: Record<string, unknown> = { ...base };
|
|
633
|
-
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
634
|
-
if (quoteId) metadata.quote_id = quoteId;
|
|
635
|
-
const quoted = quotedPayloadFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
636
|
-
if (quoted?.sender?.id) metadata.quote_sender_id = quoted.sender.id;
|
|
637
|
-
if (quoted?.sender?.name) metadata.quote_sender_name = quoted.sender.name;
|
|
638
|
-
const quoteText = quoted
|
|
639
|
-
? (typeof quoted.content === 'string'
|
|
640
|
-
? quoted.content
|
|
641
|
-
: quoted.content
|
|
642
|
-
.map((seg) =>
|
|
643
|
-
seg && typeof seg === 'object' && seg.type === 'text'
|
|
644
|
-
? String((seg.data as { text?: unknown } | undefined)?.text ?? '')
|
|
645
|
-
: '',
|
|
646
|
-
)
|
|
647
|
-
.join('')
|
|
648
|
-
).trim() || quoted.raw || ''
|
|
649
|
-
: '';
|
|
650
|
-
if (quoteText) metadata.quote_content = quoteText;
|
|
651
|
-
return metadata;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const INBOUND_HOLD_LIMIT = 32;
|
|
655
|
-
|
|
656
|
-
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'] as const;
|
|
657
|
-
|
|
658
948
|
function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
659
949
|
if (!event || typeof event !== 'object') return null;
|
|
660
950
|
const nativeToJSON = (event as { toJSON?: (keys: string[]) => unknown }).toJSON;
|
|
@@ -675,3 +965,23 @@ function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
|
675
965
|
}
|
|
676
966
|
return Object.keys(out).length > 0 ? out : null;
|
|
677
967
|
}
|
|
968
|
+
|
|
969
|
+
function parseIcqqSystemScene(type: string): { scene_type: string; sub_type: string } {
|
|
970
|
+
if (type.startsWith('login.')) {
|
|
971
|
+
return { scene_type: 'login', sub_type: type.slice('login.'.length) || 'unknown' };
|
|
972
|
+
}
|
|
973
|
+
if (type.startsWith('offline.')) {
|
|
974
|
+
return { scene_type: 'offline', sub_type: type.slice('offline.'.length) || 'unknown' };
|
|
975
|
+
}
|
|
976
|
+
if (type === 'online') {
|
|
977
|
+
return { scene_type: 'online', sub_type: 'online' };
|
|
978
|
+
}
|
|
979
|
+
if (type === 'offline') {
|
|
980
|
+
return { scene_type: 'offline', sub_type: 'offline' };
|
|
981
|
+
}
|
|
982
|
+
return { scene_type: 'system', sub_type: type };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const INBOUND_HOLD_LIMIT = 32;
|
|
986
|
+
|
|
987
|
+
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'] as const;
|