@zhin.js/adapter-icqq 1.1.18 → 1.1.19
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 +37 -0
- package/README.md +12 -8
- package/adapters/icqq/index.js +4 -28
- package/adapters/icqq/index.ts +3 -34
- package/commands/icqq/endpoint/definition.js +3 -25
- package/commands/icqq/endpoint/definition.ts +3 -24
- package/commands/icqq/login/code/index.js +3 -3
- package/commands/icqq/login/code/index.ts +3 -3
- package/commands/icqq/login/confirm/index.js +3 -3
- package/commands/icqq/login/confirm/index.ts +3 -3
- package/commands/icqq/login/definition.js +2 -2
- package/commands/icqq/login/definition.ts +2 -2
- package/commands/icqq/login/qr/index.js +3 -3
- package/commands/icqq/login/qr/index.ts +3 -3
- package/commands/icqq/login/retry/index.js +3 -3
- package/commands/icqq/login/retry/index.ts +3 -3
- package/commands/icqq/login/status/index.js +3 -3
- package/commands/icqq/login/status/index.ts +3 -3
- package/lib/adapter.d.ts +13 -0
- package/lib/adapter.js +27 -0
- package/lib/client.d.ts +6 -9
- package/lib/client.js +1 -3
- package/lib/console-spec.js +11 -1
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +6 -16
- package/lib/endpoint.js +195 -143
- package/lib/group-moderation.d.ts +6 -3
- package/lib/group-moderation.js +9 -8
- package/lib/group-scene.d.ts +3 -3
- package/lib/group-scene.js +8 -7
- package/lib/icqq-login-assist.d.ts +3 -1
- package/lib/icqq-login-assist.js +3 -1
- package/lib/icqq-login-control.d.ts +8 -2
- package/lib/icqq-login-control.js +22 -5
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +3 -0
- package/package.json +18 -18
- package/plugin.js +1 -8
- package/src/adapter.ts +29 -0
- package/src/client.ts +6 -11
- package/src/console-spec.ts +9 -0
- package/src/console.ts +2 -6
- package/src/endpoint.ts +205 -153
- package/src/group-moderation.ts +10 -10
- package/src/group-scene.ts +8 -8
- package/src/icqq-login-assist.ts +6 -2
- package/src/icqq-login-control.ts +20 -8
- package/src/index.ts +2 -1
- package/src/protocol.ts +3 -0
- package/lib/icqq-runtime-state.d.ts +0 -1
- package/lib/icqq-runtime-state.js +0 -6
- package/src/icqq-runtime-state.ts +0 -7
package/src/endpoint.ts
CHANGED
|
@@ -11,16 +11,29 @@ import {
|
|
|
11
11
|
import { inspect } from 'node:util';
|
|
12
12
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
createEndpointSpecification,
|
|
15
|
+
endpointSpec,
|
|
16
|
+
type Endpoint,
|
|
17
|
+
type EndpointSpecification,
|
|
15
18
|
type EndpointControl,
|
|
16
19
|
type EndpointManagement,
|
|
17
20
|
type EndpointPendingRequest,
|
|
18
21
|
type EndpointSendRequest,
|
|
19
22
|
type EndpointTransportState,
|
|
20
23
|
type AdapterOutageNotifier,
|
|
24
|
+
createEndpointOperation,
|
|
25
|
+
type EndpointManagementRequest,
|
|
26
|
+
type EndpointGroupRequest,
|
|
27
|
+
type EndpointApproveRequest,
|
|
28
|
+
type EndpointRejectRequest,
|
|
29
|
+
type EndpointMemberRequest,
|
|
30
|
+
type EndpointMuteRequest,
|
|
31
|
+
type EndpointAdminRequest,
|
|
32
|
+
type EndpointUserRequest,
|
|
33
|
+
type EndpointGroupNoticeRequest,
|
|
21
34
|
} from 'zhin.js/adapter';
|
|
22
35
|
import type { EndpointContentPort } from '@zhin.js/adapter';
|
|
23
|
-
import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments, type LoginAssist, type PendingLoginTask } from '@zhin.js/core';
|
|
36
|
+
import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type PendingLoginTask } from '@zhin.js/core';
|
|
24
37
|
import { EndpointDeliveryError, type EndpointSendPart, type EndpointSendResult, type MessageRef } from '@zhin.js/im-contract';
|
|
25
38
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
26
39
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -70,6 +83,8 @@ import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
|
|
|
70
83
|
export interface IcqqEndpointOptions {
|
|
71
84
|
readonly id: CapabilityId;
|
|
72
85
|
readonly config: ResolvedIcqqConfig;
|
|
86
|
+
readonly signal?: AbortSignal;
|
|
87
|
+
readonly endpointIdentity?: LoginAssistEndpointIdentity;
|
|
73
88
|
readonly loginAssist: LoginAssist;
|
|
74
89
|
readonly locale?: import('zhin.js/i18n').Locale;
|
|
75
90
|
/** Out-of-band outage notifier; absent in tests/hosts without outbound. */
|
|
@@ -95,8 +110,8 @@ const BOUND_EVENTS = [
|
|
|
95
110
|
'system.login.auth',
|
|
96
111
|
] as const;
|
|
97
112
|
|
|
98
|
-
export class IcqqEndpoint extends Endpoint
|
|
99
|
-
readonly
|
|
113
|
+
export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
114
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
100
115
|
readonly #logger: ReturnType<typeof getAdapterLogger>;
|
|
101
116
|
readonly #options: IcqqEndpointOptions;
|
|
102
117
|
readonly endpointName: string;
|
|
@@ -117,7 +132,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
117
132
|
readonly #inflightInbound = new Set<Promise<void>>();
|
|
118
133
|
readonly #inboundOwner = new AsyncLocalStorage<symbol>();
|
|
119
134
|
readonly #contentResolver: IcqqContentResolver;
|
|
120
|
-
readonly content: EndpointContentPort;
|
|
135
|
+
readonly #content: EndpointContentPort;
|
|
121
136
|
readonly loginControl: IcqqLoginControl;
|
|
122
137
|
#reloginTimer?: ReturnType<typeof setTimeout>;
|
|
123
138
|
#reloginAttempts = 0;
|
|
@@ -125,67 +140,71 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
125
140
|
#offlineNoticeAt = 0;
|
|
126
141
|
#challengeNoticeAt = 0;
|
|
127
142
|
|
|
128
|
-
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
129
|
-
inspectGroupMember:
|
|
130
|
-
inspectGroupScene:
|
|
131
|
-
inspectGroupMemberProfile:
|
|
132
|
-
moderateGroupMember: (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
listFriends: async () =>
|
|
136
|
-
Array.from(this.client.fl.values()).map((f) => ({
|
|
143
|
+
readonly #management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
144
|
+
inspectGroupMember: request => inspectGroupMember(this, request),
|
|
145
|
+
inspectGroupScene: request => inspectGroupScene(this, request),
|
|
146
|
+
inspectGroupMemberProfile: request => inspectGroupMemberProfile(this, request),
|
|
147
|
+
moderateGroupMember: request => moderateGroupMember({ client: this, isActive: () => this.#open && !this.#retiring && !this.#stopped }, request),
|
|
148
|
+
listFriends: async (request: EndpointManagementRequest = {}) =>
|
|
149
|
+
createEndpointOperation(request).run(() => Array.from(this.fl.values()).map((f) => ({
|
|
137
150
|
user_id: f.user_id,
|
|
138
151
|
nickname: f.nickname,
|
|
139
152
|
remark: f.remark ?? '',
|
|
140
|
-
})),
|
|
141
|
-
listGroups: async () =>
|
|
142
|
-
Array.from(this.
|
|
153
|
+
}))),
|
|
154
|
+
listGroups: async (request: EndpointManagementRequest = {}) =>
|
|
155
|
+
createEndpointOperation(request).run(() => Array.from(this.gl.values()).map((g) => ({
|
|
143
156
|
group_id: g.group_id,
|
|
144
157
|
name: g.group_name,
|
|
145
|
-
})),
|
|
146
|
-
listChannels: async () => {
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
}))),
|
|
159
|
+
listChannels: async (request: EndpointManagementRequest = {}) => {
|
|
160
|
+
const operation = createEndpointOperation(request);
|
|
161
|
+
await operation.run(() => this.#guildCatalog.syncAll(this));
|
|
162
|
+
return operation.run(() => this.#guildCatalog.getGuildChannelList());
|
|
149
163
|
},
|
|
150
|
-
listGroupMembers: async (groupId) =>
|
|
151
|
-
Array.from((await this.
|
|
152
|
-
listRequests: () => this.#listPendingRequests(),
|
|
153
|
-
approveRequest:
|
|
154
|
-
rejectRequest:
|
|
155
|
-
kickGroupMember: async (groupId, userId) => {
|
|
156
|
-
|
|
164
|
+
listGroupMembers: async ({ groupId, signal }: EndpointGroupRequest) =>
|
|
165
|
+
Array.from((await createEndpointOperation({ signal }).run(() => this.getGroupMemberList(Number(groupId)))).values()),
|
|
166
|
+
listRequests: (request: EndpointManagementRequest = {}) => createEndpointOperation(request).run(() => this.#listPendingRequests()),
|
|
167
|
+
approveRequest: request => this.#approveRequest(request),
|
|
168
|
+
rejectRequest: request => this.#rejectRequest(request),
|
|
169
|
+
kickGroupMember: async ({ groupId, userId, signal }: EndpointMemberRequest) => {
|
|
170
|
+
const operation = createEndpointOperation({ signal });
|
|
171
|
+
await operation.run(() => this.setGroupKick(Number(groupId), Number(userId)));
|
|
157
172
|
},
|
|
158
|
-
muteGroupMember: async (groupId, userId, duration) => {
|
|
159
|
-
|
|
173
|
+
muteGroupMember: async ({ groupId, userId, durationSeconds: duration, signal }: EndpointMuteRequest) => {
|
|
174
|
+
const operation = createEndpointOperation({ signal });
|
|
175
|
+
await operation.run(() => this.setGroupBan(Number(groupId), Number(userId), duration));
|
|
160
176
|
},
|
|
161
|
-
setGroupAdmin: async (groupId, userId, enabled) => {
|
|
162
|
-
|
|
177
|
+
setGroupAdmin: async ({ groupId, userId, enabled, signal }: EndpointAdminRequest) => {
|
|
178
|
+
const operation = createEndpointOperation({ signal });
|
|
179
|
+
await operation.run(() => this.setGroupAdmin(Number(groupId), Number(userId), enabled));
|
|
163
180
|
},
|
|
164
|
-
deleteFriend: async (userId) => {
|
|
165
|
-
|
|
181
|
+
deleteFriend: async ({ userId, signal }: EndpointUserRequest) => {
|
|
182
|
+
const operation = createEndpointOperation({ signal });
|
|
183
|
+
await operation.run(() => this.deleteFriend(Number(userId)));
|
|
166
184
|
},
|
|
167
|
-
sendGroupNotice: async (groupId, content) => {
|
|
168
|
-
|
|
185
|
+
sendGroupNotice: async ({ groupId, content, signal }: EndpointGroupNoticeRequest) => {
|
|
186
|
+
const operation = createEndpointOperation({ signal });
|
|
187
|
+
if (await operation.run(() => this.sendGroupNotice(Number(groupId), content)) === false) {
|
|
169
188
|
throw new Error('QQ did not confirm the group notice');
|
|
170
189
|
}
|
|
171
190
|
},
|
|
172
191
|
});
|
|
173
192
|
|
|
174
|
-
readonly control: EndpointControl = Object.freeze({
|
|
175
|
-
recall: async (message
|
|
193
|
+
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
194
|
+
recall: async ({ message, signal }) => {
|
|
176
195
|
if (!message.id || message.id.startsWith('outbound:')) return;
|
|
177
|
-
if (!await this.
|
|
196
|
+
if (!await createEndpointOperation({ signal }).run(() => this.deleteMsg(message.id))) throw new Error('QQ did not confirm message recall');
|
|
178
197
|
},
|
|
179
|
-
addReaction: async (message
|
|
198
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
180
199
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
181
200
|
if (!target) return null;
|
|
182
201
|
try {
|
|
183
202
|
// Do not await protocol ACK — packet timeout must not stall the AI reply.
|
|
184
|
-
|
|
185
|
-
'add',
|
|
186
|
-
|
|
187
|
-
);
|
|
203
|
+
await createEndpointOperation({ signal }).run(() => {
|
|
204
|
+
this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
|
|
205
|
+
});
|
|
188
206
|
} catch (error) {
|
|
207
|
+
if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive') throw error;
|
|
189
208
|
this.#logger.debug(formatCompact({
|
|
190
209
|
op: 'reaction_add_failed',
|
|
191
210
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -193,15 +212,15 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
193
212
|
}
|
|
194
213
|
return emoji;
|
|
195
214
|
},
|
|
196
|
-
removeReaction: async (message
|
|
215
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
197
216
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
198
217
|
if (!target) return;
|
|
199
218
|
try {
|
|
200
|
-
|
|
201
|
-
'remove',
|
|
202
|
-
|
|
203
|
-
);
|
|
219
|
+
await createEndpointOperation({ signal }).run(() => {
|
|
220
|
+
this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
|
|
221
|
+
});
|
|
204
222
|
} catch (error) {
|
|
223
|
+
if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive') throw error;
|
|
205
224
|
this.#logger.debug(formatCompact({
|
|
206
225
|
op: 'reaction_remove_failed',
|
|
207
226
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -211,12 +230,20 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
211
230
|
});
|
|
212
231
|
|
|
213
232
|
constructor(options: IcqqEndpointOptions) {
|
|
214
|
-
super()
|
|
215
|
-
const nativeConfig = resolveNativeClientConfig(options.config);
|
|
216
|
-
this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig,
|
|
233
|
+
super(Number(options.config.id), resolveNativeClientConfig(options.config),
|
|
217
234
|
(operation, error) => this.#handleBackgroundFailure(operation, error));
|
|
218
|
-
|
|
219
|
-
|
|
235
|
+
const name = () => this.endpointName;
|
|
236
|
+
const state = () => this.#transportState;
|
|
237
|
+
const content = () => this.#content;
|
|
238
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
239
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
|
|
240
|
+
send: request => this.#sendFramework(request), get name() { return name(); },
|
|
241
|
+
get transportState() { return state(); },
|
|
242
|
+
management: this.#management, control: this.#control,
|
|
243
|
+
get content() { return content(); },
|
|
244
|
+
});
|
|
245
|
+
this.#contentResolver = new IcqqContentResolver(this);
|
|
246
|
+
this.#content = this.#contentResolver.port;
|
|
220
247
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
221
248
|
this.#options = options;
|
|
222
249
|
this.endpointName = options.config.id;
|
|
@@ -224,14 +251,14 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
224
251
|
endpointId: this.endpointName,
|
|
225
252
|
state: () => ({
|
|
226
253
|
endpointId: this.endpointName,
|
|
227
|
-
transportState: this
|
|
228
|
-
online: this.
|
|
254
|
+
transportState: this.#transportState,
|
|
255
|
+
online: this.isOnline(),
|
|
229
256
|
pending: this.#pendingLoginTasks(),
|
|
230
257
|
}),
|
|
231
258
|
pending: () => this.#pendingLoginTasks(),
|
|
232
|
-
relogin: () => this.relogin(),
|
|
233
|
-
submit: (taskId: string, value: string | Record<string, unknown>) => this.#options.loginAssist.submit(taskId, value),
|
|
234
|
-
cancel: (taskId: string, reason?: string) => this.#options.loginAssist.cancel(taskId, reason),
|
|
259
|
+
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
260
|
+
submit: (taskId: string, value: string | Record<string, unknown>) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
261
|
+
cancel: (taskId: string, reason?: string) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
235
262
|
});
|
|
236
263
|
}
|
|
237
264
|
|
|
@@ -244,11 +271,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
244
271
|
: typeof error === 'string' ? error : 'Unknown ICQQ SDK background failure',
|
|
245
272
|
...(error && typeof error === 'object' && 'code' in error && typeof error.code === 'number' ? { code: error.code } : {}),
|
|
246
273
|
}));
|
|
247
|
-
if (operation === 'sso-heartbeat' && !this.
|
|
274
|
+
if (operation === 'sso-heartbeat' && !this.isOnline()) return;
|
|
248
275
|
if (operation === 'login' && !this.#everOnline) return;
|
|
249
276
|
// Delegate to the SDK's existing transport termination, offline event and
|
|
250
277
|
// reconn_interval policy. No account-level logout or second retry timer.
|
|
251
|
-
this.
|
|
278
|
+
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
252
279
|
}
|
|
253
280
|
|
|
254
281
|
/** Force a login attempt (manual recovery command). */
|
|
@@ -266,7 +293,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
266
293
|
this.#reloginInFlight = task;
|
|
267
294
|
void task.catch(() => undefined).finally(() => {
|
|
268
295
|
if (this.#reloginInFlight === task) this.#reloginInFlight = undefined;
|
|
269
|
-
if (this.#started && !this.#stopped && !this.#retiring && !this.
|
|
296
|
+
if (this.#started && !this.#stopped && !this.#retiring && !this.isOnline()) {
|
|
270
297
|
this.#scheduleRelogin('relogin_failed');
|
|
271
298
|
}
|
|
272
299
|
});
|
|
@@ -275,10 +302,10 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
275
302
|
|
|
276
303
|
async #performRelogin(): Promise<void> {
|
|
277
304
|
this.#clearReloginTimer();
|
|
278
|
-
this.
|
|
305
|
+
this.cancelReconnect();
|
|
279
306
|
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
280
307
|
try {
|
|
281
|
-
await this.
|
|
308
|
+
await this.login(this.#options.config.password);
|
|
282
309
|
} catch (error) {
|
|
283
310
|
this.#logger.warn(formatCompact({
|
|
284
311
|
op: 'icqq_relogin_failed',
|
|
@@ -318,8 +345,14 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
318
345
|
}
|
|
319
346
|
|
|
320
347
|
#pendingLoginTasks(): readonly PendingLoginTask[] {
|
|
348
|
+
if (this.#options.signal?.aborted) return [];
|
|
321
349
|
return this.#options.loginAssist.listPending()
|
|
322
|
-
.filter((task) => task.adapter === 'icqq' &&
|
|
350
|
+
.filter((task) => task.adapter === 'icqq' && (this.#options.endpointIdentity
|
|
351
|
+
? task.endpointIdentity?.canonicalId === this.#options.endpointIdentity.canonicalId
|
|
352
|
+
&& task.endpointIdentity.instanceId === this.#options.endpointIdentity.instanceId
|
|
353
|
+
&& task.endpointIdentity.epoch === this.#options.endpointIdentity.epoch
|
|
354
|
+
&& task.endpointIdentity.incarnationId === this.#options.endpointIdentity.incarnationId
|
|
355
|
+
: task.endpointKey === this.endpointName));
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
#notifyOffline(reason: string, willRetry: boolean): void {
|
|
@@ -395,15 +428,15 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
395
428
|
});
|
|
396
429
|
}
|
|
397
430
|
|
|
398
|
-
get transportState(): EndpointTransportState {
|
|
431
|
+
get #transportState(): EndpointTransportState {
|
|
399
432
|
if (this.#stopped || this.#retiring) return 'stopped';
|
|
400
433
|
if (!this.#started) return 'idle';
|
|
401
|
-
if (this.
|
|
434
|
+
if (this.isOnline()) return 'open';
|
|
402
435
|
if (!this.#everOnline) return 'connecting';
|
|
403
436
|
return this.#options.config.autoReconnect ? 'reconnecting' : 'closed';
|
|
404
437
|
}
|
|
405
438
|
|
|
406
|
-
async
|
|
439
|
+
async #startFramework(signal: AbortSignal): Promise<void> {
|
|
407
440
|
if (this.#started) return;
|
|
408
441
|
signal.throwIfAborted();
|
|
409
442
|
this.#stopped = false;
|
|
@@ -412,7 +445,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
412
445
|
this.#retirementEpoch += 1;
|
|
413
446
|
this.#retirementArmed = false;
|
|
414
447
|
this.#retiring = false;
|
|
415
|
-
this.
|
|
448
|
+
this.off('system.online');
|
|
416
449
|
this.#started = true;
|
|
417
450
|
let resolveOnline!: () => void;
|
|
418
451
|
const onlineReady = new Promise<void>((resolve) => {
|
|
@@ -423,17 +456,17 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
423
456
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
424
457
|
try {
|
|
425
458
|
this.#bindClientEvents();
|
|
426
|
-
await raceAbort(Promise.resolve(this.
|
|
459
|
+
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
427
460
|
await onlineReady;
|
|
428
461
|
signal.throwIfAborted();
|
|
429
462
|
await this.#pullPendingSystemMessages(signal);
|
|
430
463
|
signal.throwIfAborted();
|
|
431
464
|
this.#logger.info(
|
|
432
|
-
`connected (direct) | friends: ${this.
|
|
465
|
+
`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`,
|
|
433
466
|
);
|
|
434
467
|
} catch (error) {
|
|
435
468
|
this.#notifyOffline('start_failed', false);
|
|
436
|
-
await this
|
|
469
|
+
await this.#stopFramework();
|
|
437
470
|
this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
438
471
|
throw error;
|
|
439
472
|
} finally {
|
|
@@ -442,11 +475,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
442
475
|
}
|
|
443
476
|
}
|
|
444
477
|
|
|
445
|
-
|
|
478
|
+
#openFramework(): void {
|
|
446
479
|
if (this.#retirementArmed) {
|
|
447
480
|
this.#retirementEpoch += 1;
|
|
448
481
|
this.#retirementArmed = false;
|
|
449
|
-
this.
|
|
482
|
+
this.off('system.online');
|
|
450
483
|
this.#bindSystemOnlineEvent();
|
|
451
484
|
}
|
|
452
485
|
this.#retiring = false;
|
|
@@ -459,16 +492,16 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
459
492
|
}
|
|
460
493
|
}
|
|
461
494
|
|
|
462
|
-
async
|
|
495
|
+
async #closeFramework(): Promise<void> {
|
|
463
496
|
this.#assertExternalLifecycle('close');
|
|
464
497
|
this.#armRetirementFence();
|
|
465
498
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
466
499
|
}
|
|
467
500
|
|
|
468
|
-
async
|
|
501
|
+
async #stopFramework(): Promise<void> {
|
|
502
|
+
this.#assertExternalLifecycle('stop');
|
|
469
503
|
this.#stopped = true;
|
|
470
504
|
this.#clearReloginTimer();
|
|
471
|
-
this.#assertExternalLifecycle('stop');
|
|
472
505
|
this.#armRetirementFence();
|
|
473
506
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
474
507
|
this.#resolveStartOnline?.();
|
|
@@ -479,7 +512,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
479
512
|
for (const event of BOUND_EVENTS) {
|
|
480
513
|
// The retirement guard owns this matcher until a late login handshake
|
|
481
514
|
// has finished. start()/open() replace it with the normal listener.
|
|
482
|
-
if (event !== 'system.online') this.
|
|
515
|
+
if (event !== 'system.online') this.off(event);
|
|
483
516
|
}
|
|
484
517
|
// `Client.logout()` sends an account-level unregister packet before it
|
|
485
518
|
// closes this transport. During config HMR the replacement endpoint for
|
|
@@ -487,7 +520,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
487
520
|
// also closes the replacement session. Endpoint ownership is transport
|
|
488
521
|
// scoped: retire only this Client's TCP connection.
|
|
489
522
|
try {
|
|
490
|
-
this.
|
|
523
|
+
this.terminate();
|
|
491
524
|
} catch { /* ignore */ }
|
|
492
525
|
this.#inboundDeduper.clear();
|
|
493
526
|
this.#sideEventDeduper.clear();
|
|
@@ -504,26 +537,37 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
504
537
|
const retirementEpoch = ++this.#retirementEpoch;
|
|
505
538
|
// ICQQ owns reconnect through this per-Client timer. Clear it before any
|
|
506
539
|
// asynchronous drain so a queued callback cannot enter while close waits.
|
|
507
|
-
this.
|
|
540
|
+
this.cancelReconnect();
|
|
508
541
|
// A login callback may already be inside the SDK handshake, which has no
|
|
509
542
|
// abort API. Replace the normal online listener with an epoch-owned guard
|
|
510
543
|
// before draining inbound work. Transport termination is account-local;
|
|
511
544
|
// logout would also unregister an HMR replacement using the same UIN.
|
|
512
|
-
this.
|
|
513
|
-
this.
|
|
545
|
+
this.off('system.online');
|
|
546
|
+
this.on('system.online', () => {
|
|
514
547
|
if (this.#retirementEpoch !== retirementEpoch || !this.#retiring) return;
|
|
515
548
|
try {
|
|
516
|
-
this.
|
|
549
|
+
this.terminate();
|
|
517
550
|
} catch { /* already closed */ }
|
|
518
551
|
});
|
|
519
552
|
}
|
|
520
553
|
|
|
521
|
-
async
|
|
554
|
+
async #sendFramework({ conversation, payload, signal }: EndpointSendRequest): Promise<EndpointSendResult> {
|
|
522
555
|
if (!this.#started) throw new EndpointDeliveryError('endpoint_disconnected', 'icqq endpoint 未连接', 'not_sent');
|
|
556
|
+
const operation = createEndpointOperation({ signal });
|
|
523
557
|
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
524
|
-
const prepared = isIcqqSegmentPayload(payload)
|
|
558
|
+
const prepared = await operation.prepare(() => isIcqqSegmentPayload(payload)
|
|
525
559
|
? prepareIcqqOutboundMedia(normalizeIcqqImageMedia(payload), mediaMode)
|
|
526
|
-
: { content: payload, dispose() {} };
|
|
560
|
+
: { content: payload, dispose() {} });
|
|
561
|
+
// Cancellation ends our wait, not the SDK's use of temporary media. The actual native
|
|
562
|
+
// promises own that resource until they settle, including a late rejection after cancellation.
|
|
563
|
+
const pendingSends = new Set<Promise<{ message_id?: unknown }>>();
|
|
564
|
+
const sendNative = (target: ReturnType<typeof icqqOutboundTarget>, message: Sendable) => {
|
|
565
|
+
const task = this.#sendNative(target, message);
|
|
566
|
+
pendingSends.add(task);
|
|
567
|
+
const settled = () => { pendingSends.delete(task); };
|
|
568
|
+
void task.then(settled, settled);
|
|
569
|
+
return task;
|
|
570
|
+
};
|
|
527
571
|
try {
|
|
528
572
|
// Standalone elements (share / file / …) are split into ordered messages.
|
|
529
573
|
const messages = formatOutboundMessageParts(prepared.content);
|
|
@@ -533,7 +577,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
533
577
|
const preview = formatIcqqSendablePreview(message);
|
|
534
578
|
let result: { message_id?: unknown };
|
|
535
579
|
try {
|
|
536
|
-
result = await
|
|
580
|
+
result = await operation.run(() => sendNative(parsed, message));
|
|
537
581
|
} catch (error) {
|
|
538
582
|
this.#logger.warn(formatCompact({
|
|
539
583
|
op: 'icqq_send_failed',
|
|
@@ -560,12 +604,14 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
560
604
|
}
|
|
561
605
|
return { parts };
|
|
562
606
|
} finally {
|
|
563
|
-
|
|
564
|
-
prepared.dispose();
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
607
|
+
const disposeMedia = () => {
|
|
608
|
+
try { prepared.dispose(); } catch (error) {
|
|
609
|
+
// Temporary-file cleanup cannot change an observed send result.
|
|
610
|
+
this.#logger.warn(formatCompact({ op: 'icqq_media_cleanup_failed', error: describeUnknownError(error) }));
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
if (pendingSends.size) void Promise.allSettled([...pendingSends]).then(disposeMedia);
|
|
614
|
+
else disposeMedia();
|
|
569
615
|
}
|
|
570
616
|
}
|
|
571
617
|
|
|
@@ -576,20 +622,20 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
576
622
|
if (isIcqqShareElement(message)) {
|
|
577
623
|
switch (target.kind) {
|
|
578
624
|
case 'private': {
|
|
579
|
-
const friend = this.
|
|
625
|
+
const friend = this.pickFriend(target.userId);
|
|
580
626
|
await friend.share(message);
|
|
581
627
|
return {};
|
|
582
628
|
}
|
|
583
629
|
case 'group': {
|
|
584
|
-
const group = this.
|
|
630
|
+
const group = this.pickGroup(target.groupId);
|
|
585
631
|
await group.share(message);
|
|
586
632
|
return {};
|
|
587
633
|
}
|
|
588
634
|
case 'temp':
|
|
589
|
-
await this.
|
|
635
|
+
await this.pickMember(target.groupId, target.userId).share(message);
|
|
590
636
|
return {};
|
|
591
637
|
case 'channel': {
|
|
592
|
-
const channel = this.
|
|
638
|
+
const channel = this.pickGuild(target.guildId).channels.get(target.channelId);
|
|
593
639
|
if (!channel) {
|
|
594
640
|
throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
|
|
595
641
|
}
|
|
@@ -601,11 +647,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
601
647
|
if (isIcqqFileElement(message)) {
|
|
602
648
|
switch (target.kind) {
|
|
603
649
|
case 'private': {
|
|
604
|
-
await this.
|
|
650
|
+
await this.pickFriend(target.userId).sendFile(message.file, message.name);
|
|
605
651
|
return {};
|
|
606
652
|
}
|
|
607
653
|
case 'group': {
|
|
608
|
-
await this.
|
|
654
|
+
await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
|
|
609
655
|
return {};
|
|
610
656
|
}
|
|
611
657
|
case 'temp':
|
|
@@ -615,13 +661,13 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
615
661
|
}
|
|
616
662
|
switch (target.kind) {
|
|
617
663
|
case 'private':
|
|
618
|
-
return this.
|
|
664
|
+
return this.sendPrivateMsg(target.userId, message);
|
|
619
665
|
case 'group':
|
|
620
|
-
return this.
|
|
666
|
+
return this.sendGroupMsg(target.groupId, message);
|
|
621
667
|
case 'temp':
|
|
622
|
-
return this.
|
|
668
|
+
return this.sendTempMsg(target.groupId, target.userId, message);
|
|
623
669
|
case 'channel':
|
|
624
|
-
return this.
|
|
670
|
+
return this.sendGuildMsg(target.guildId, target.channelId, message) as unknown as { message_id?: unknown };
|
|
625
671
|
}
|
|
626
672
|
}
|
|
627
673
|
|
|
@@ -636,7 +682,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
636
682
|
this.#inflightInbound.add(settlement);
|
|
637
683
|
let operation: Promise<void>;
|
|
638
684
|
try {
|
|
639
|
-
operation = this.#inboundOwner.run(owner, run);
|
|
685
|
+
operation = this.#inboundOwner.run(owner, () => this[endpointSpec].events.run(run));
|
|
640
686
|
} catch (error) {
|
|
641
687
|
this.#inflightInbound.delete(settlement);
|
|
642
688
|
settle();
|
|
@@ -690,7 +736,9 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
690
736
|
+ (mentioned ? ' (mentioned)' : '')
|
|
691
737
|
+ ` | ${truncatePreview(msg.content, 80)}`,
|
|
692
738
|
);
|
|
693
|
-
await this.emit('message.receive', {
|
|
739
|
+
await this[endpointSpec].events.emit('message.receive', {
|
|
740
|
+
event: msg.event,
|
|
741
|
+
platformSentAt: Number.isFinite(msg.platformSentAt) ? msg.platformSentAt : undefined,
|
|
694
742
|
conversation,
|
|
695
743
|
message: { conversation, id: msg.id },
|
|
696
744
|
content: msg.content,
|
|
@@ -717,77 +765,70 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
717
765
|
#bindClientEvents(): void {
|
|
718
766
|
this.#bindNativeEvents();
|
|
719
767
|
const safe = (label: string, fn: () => void): void => {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
this.#logger.warn(formatCompact({
|
|
724
|
-
op: 'icqq_event_handler',
|
|
725
|
-
endpoint: this.endpointName,
|
|
726
|
-
event: label,
|
|
727
|
-
error: error instanceof Error ? error.message : String(error),
|
|
728
|
-
}));
|
|
729
|
-
}
|
|
768
|
+
void this[endpointSpec].events.run(fn).catch(() => {
|
|
769
|
+
this.#logger.debug(formatCompact({ op: 'icqq_event_not_admitted', endpoint: this.endpointName, event: label }));
|
|
770
|
+
});
|
|
730
771
|
};
|
|
731
|
-
this.
|
|
772
|
+
this.on('message.private', (event) => {
|
|
732
773
|
this.#startMessageEvent('message.private', event);
|
|
733
774
|
});
|
|
734
|
-
this.
|
|
775
|
+
this.on('message.group', (event) => {
|
|
735
776
|
this.#startMessageEvent('message.group', event);
|
|
736
777
|
});
|
|
737
|
-
this.
|
|
738
|
-
void this.#handleGuildEvent(event).catch((error) => {
|
|
778
|
+
this.on('message.guild', (event) => {
|
|
779
|
+
void this.#trackInbound(() => this.#handleGuildEvent(event)).catch((error) => {
|
|
739
780
|
this.#logInboundHandlerFailure('message.guild', error);
|
|
740
781
|
});
|
|
741
782
|
});
|
|
742
|
-
this.
|
|
783
|
+
this.on('request.friend', (event) => safe('request.friend', () => {
|
|
743
784
|
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
744
785
|
}));
|
|
745
|
-
this.
|
|
786
|
+
this.on('request.group', (event) => safe('request.group', () => {
|
|
746
787
|
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
747
788
|
}));
|
|
748
|
-
this.
|
|
789
|
+
this.on('notice.friend', (event) => safe('notice.friend', () => {
|
|
749
790
|
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
750
791
|
}));
|
|
751
|
-
this.
|
|
792
|
+
this.on('notice.group', (event) => safe('notice.group', () => {
|
|
752
793
|
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
753
794
|
}));
|
|
754
795
|
this.#bindSystemOnlineEvent();
|
|
755
|
-
this.
|
|
796
|
+
this.on('system.offline', (event) => {
|
|
756
797
|
this.#logSystemEvent('offline', event);
|
|
757
798
|
this.#notifyOffline('offline', this.#options.config.autoReconnect);
|
|
758
799
|
});
|
|
759
|
-
this.
|
|
800
|
+
this.on('system.offline.network', (event) => {
|
|
760
801
|
this.#logSystemEvent('offline.network', event);
|
|
761
802
|
this.#notifyOffline('network', this.#options.config.autoReconnect);
|
|
762
803
|
});
|
|
763
|
-
this.
|
|
804
|
+
this.on('system.offline.kickoff', (event) => {
|
|
764
805
|
this.#logSystemEvent('offline.kickoff', event);
|
|
765
806
|
this.#notifyOffline('kickoff', this.#options.config.autoReconnect);
|
|
766
807
|
this.#scheduleRelogin('kickoff');
|
|
767
808
|
});
|
|
768
|
-
this.
|
|
809
|
+
this.on('system.login.qrcode', (event) => {
|
|
769
810
|
this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
|
|
770
811
|
this.#handleLoginChallenge('qrcode', event);
|
|
771
812
|
});
|
|
772
|
-
this.
|
|
813
|
+
this.on('system.login.device', (event) => {
|
|
773
814
|
this.#handleLoginChallenge('device', event);
|
|
774
815
|
});
|
|
775
|
-
this.
|
|
816
|
+
this.on('system.login.slider', (event) => {
|
|
776
817
|
this.#handleLoginChallenge('slider', event);
|
|
777
818
|
});
|
|
778
|
-
this.
|
|
819
|
+
this.on('system.login.error', (event) => {
|
|
779
820
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
|
|
780
821
|
this.#logSystemEvent('login.error', event);
|
|
781
822
|
this.#notifyOffline('login_error', this.#options.config.autoReconnect);
|
|
782
823
|
this.#scheduleRelogin('login_error');
|
|
783
824
|
});
|
|
784
|
-
this.
|
|
825
|
+
this.on('system.login.auth', (event) => {
|
|
785
826
|
this.#handleLoginChallenge('auth', event);
|
|
786
827
|
});
|
|
787
828
|
}
|
|
788
829
|
|
|
789
830
|
#bindSystemOnlineEvent(): void {
|
|
790
|
-
this.
|
|
831
|
+
this.on('system.online', (event) => {
|
|
791
832
|
if (this.#retiring) return;
|
|
792
833
|
this.#everOnline = true;
|
|
793
834
|
this.#clearReloginTimer();
|
|
@@ -798,7 +839,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
798
839
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
799
840
|
try {
|
|
800
841
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
801
|
-
this.reportSelfUserId(resolveIcqqSelfUserId(this
|
|
842
|
+
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
802
843
|
} catch (error) {
|
|
803
844
|
this.#logger.warn(formatCompact({
|
|
804
845
|
op: 'icqq_report_self_user_id_failed',
|
|
@@ -822,7 +863,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
822
863
|
#bindNativeEvents(): void {
|
|
823
864
|
this.#disposeNativeEvents?.();
|
|
824
865
|
let matchedName: string | undefined;
|
|
825
|
-
const registration = this.
|
|
866
|
+
const registration = this.on(
|
|
826
867
|
(eventName: unknown) => {
|
|
827
868
|
matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
|
|
828
869
|
? eventName
|
|
@@ -838,7 +879,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
838
879
|
}
|
|
839
880
|
|
|
840
881
|
#emitNativeEvent(name: string, event: unknown): void {
|
|
841
|
-
void this.
|
|
882
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
842
883
|
this.#logger.warn(formatCompact({
|
|
843
884
|
op: 'icqq_platform_event_failed',
|
|
844
885
|
endpoint: this.endpointName,
|
|
@@ -852,6 +893,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
852
893
|
step: 'qrcode' | 'slider' | 'device' | 'auth',
|
|
853
894
|
event: unknown,
|
|
854
895
|
): void {
|
|
896
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
855
897
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
856
898
|
? event as Record<string, unknown>
|
|
857
899
|
: null);
|
|
@@ -861,10 +903,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
861
903
|
void runIcqqLoginAssistStep({
|
|
862
904
|
assist: this.#options.loginAssist,
|
|
863
905
|
locale: this.#options.locale,
|
|
864
|
-
client: this
|
|
906
|
+
client: this,
|
|
865
907
|
adapter: 'icqq',
|
|
866
908
|
endpointKey: this.endpointName,
|
|
867
909
|
owner: this.#loginAssistOwner,
|
|
910
|
+
signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
|
|
868
911
|
logger: this.#logger,
|
|
869
912
|
step,
|
|
870
913
|
event: payload ?? event,
|
|
@@ -885,14 +928,14 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
885
928
|
const postType = String(payload.post_type ?? '');
|
|
886
929
|
const isRequest = postType === 'request' || postType.startsWith('request.');
|
|
887
930
|
const flag = payload.flag != null ? String(payload.flag) : '';
|
|
888
|
-
void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
|
|
931
|
+
void receiveOneBotLikeSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), {
|
|
889
932
|
adapter: 'icqq',
|
|
890
933
|
endpointKey: this.endpointName,
|
|
891
934
|
platform: 'icqq',
|
|
892
935
|
raw: payload,
|
|
893
936
|
...(isRequest ? {
|
|
894
|
-
approve: (id, remark) => this.#approveRequest(flag || id, remark),
|
|
895
|
-
reject: (id, reason) => this.#rejectRequest(flag || id, reason),
|
|
937
|
+
approve: (id, remark) => this.#approveRequest({ requestId: flag || id, remark }),
|
|
938
|
+
reject: (id, reason) => this.#rejectRequest({ requestId: flag || id, reason }),
|
|
896
939
|
} : {}),
|
|
897
940
|
}).catch((err) => {
|
|
898
941
|
this.#logger.warn(formatCompact({
|
|
@@ -905,7 +948,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
905
948
|
}
|
|
906
949
|
|
|
907
950
|
#dispatchIcqqSystemSideEvent(type: string, payload: Record<string, unknown> | null): void {
|
|
908
|
-
void this.emit('system.receive', buildSystem(payload ?? {}, {
|
|
951
|
+
void this[endpointSpec].events.emit('system.receive', buildSystem(payload ?? {}, {
|
|
909
952
|
id: `system:${this.endpointName}:${type}:${Date.now()}`,
|
|
910
953
|
clientAdapter: 'icqq',
|
|
911
954
|
endpointId: this.endpointName,
|
|
@@ -998,6 +1041,8 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
998
1041
|
}));
|
|
999
1042
|
}
|
|
1000
1043
|
await this.#deliverInbound({
|
|
1044
|
+
event,
|
|
1045
|
+
platformSentAt: normalized.timestampMs,
|
|
1001
1046
|
id: normalized.messageId,
|
|
1002
1047
|
conversation,
|
|
1003
1048
|
content: formatInboundContent(normalized.rawMessage),
|
|
@@ -1029,13 +1074,13 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1029
1074
|
if (!Number.isFinite(userId) || userId <= 0) return null;
|
|
1030
1075
|
if (data.message_type === 'group' || data.group_id != null) {
|
|
1031
1076
|
const groupId = Number(data.group_id);
|
|
1032
|
-
return Number.isFinite(groupId) && groupId > 0 ? this.
|
|
1077
|
+
return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
|
|
1033
1078
|
}
|
|
1034
1079
|
const sourceGroupId = Number(data.sender?.group_id);
|
|
1035
1080
|
if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
|
|
1036
|
-
return this.
|
|
1081
|
+
return this.pickMember(sourceGroupId, userId);
|
|
1037
1082
|
}
|
|
1038
|
-
return this.
|
|
1083
|
+
return this.pickFriend(userId);
|
|
1039
1084
|
}
|
|
1040
1085
|
|
|
1041
1086
|
#logInboundHandlerFailure(event: string, error: unknown): void {
|
|
@@ -1072,6 +1117,9 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1072
1117
|
return;
|
|
1073
1118
|
}
|
|
1074
1119
|
await this.admit({
|
|
1120
|
+
event,
|
|
1121
|
+
platformSentAt: typeof normalized.raw.time === 'number' && Number.isFinite(normalized.raw.time)
|
|
1122
|
+
? normalized.timestampMs : undefined,
|
|
1075
1123
|
id: normalized.messageId,
|
|
1076
1124
|
conversation: icqqInboundConversation(String(this.#options.id), {
|
|
1077
1125
|
channelType: 'channel',
|
|
@@ -1103,7 +1151,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1103
1151
|
}
|
|
1104
1152
|
|
|
1105
1153
|
async #listPendingRequests(): Promise<readonly EndpointPendingRequest[]> {
|
|
1106
|
-
const messages = await this.
|
|
1154
|
+
const messages = await this.getSystemMsg();
|
|
1107
1155
|
const rows: EndpointPendingRequest[] = [];
|
|
1108
1156
|
for (const raw of messages) {
|
|
1109
1157
|
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
@@ -1131,7 +1179,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1131
1179
|
|
|
1132
1180
|
async #pullPendingSystemMessages(signal: AbortSignal): Promise<void> {
|
|
1133
1181
|
try {
|
|
1134
|
-
const messages = await raceAbort(Promise.resolve(this.
|
|
1182
|
+
const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
|
|
1135
1183
|
for (const raw of messages) {
|
|
1136
1184
|
signal.throwIfAborted();
|
|
1137
1185
|
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
@@ -1159,8 +1207,9 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1159
1207
|
}
|
|
1160
1208
|
}
|
|
1161
1209
|
|
|
1162
|
-
async #approveRequest(
|
|
1163
|
-
const
|
|
1210
|
+
async #approveRequest({ requestId: id, remark, signal }: EndpointApproveRequest): Promise<void> {
|
|
1211
|
+
const operation = createEndpointOperation({ signal });
|
|
1212
|
+
const messages = await operation.run(() => this.getSystemMsg());
|
|
1164
1213
|
const target = messages.find((m) => {
|
|
1165
1214
|
const msg = m as unknown as IcqqSystemMessage;
|
|
1166
1215
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -1168,15 +1217,17 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1168
1217
|
if (!target?.flag) {
|
|
1169
1218
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
1170
1219
|
}
|
|
1220
|
+
const flag = target.flag;
|
|
1171
1221
|
if ((target as { request_type?: string }).request_type === 'friend') {
|
|
1172
|
-
await this.
|
|
1222
|
+
await operation.run(() => this.setFriendAddRequest(flag, true, remark));
|
|
1173
1223
|
} else {
|
|
1174
|
-
await this.
|
|
1224
|
+
await operation.run(() => this.setGroupAddRequest(flag, true));
|
|
1175
1225
|
}
|
|
1176
1226
|
}
|
|
1177
1227
|
|
|
1178
|
-
async #rejectRequest(
|
|
1179
|
-
const
|
|
1228
|
+
async #rejectRequest({ requestId: id, reason, signal }: EndpointRejectRequest): Promise<void> {
|
|
1229
|
+
const operation = createEndpointOperation({ signal });
|
|
1230
|
+
const messages = await operation.run(() => this.getSystemMsg());
|
|
1180
1231
|
const target = messages.find((m) => {
|
|
1181
1232
|
const msg = m as unknown as IcqqSystemMessage;
|
|
1182
1233
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -1184,10 +1235,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
1184
1235
|
if (!target?.flag) {
|
|
1185
1236
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
1186
1237
|
}
|
|
1238
|
+
const flag = target.flag;
|
|
1187
1239
|
if ((target as { request_type?: string }).request_type === 'friend') {
|
|
1188
|
-
await this.
|
|
1240
|
+
await operation.run(() => this.setFriendAddRequest(flag, false));
|
|
1189
1241
|
} else {
|
|
1190
|
-
await this.
|
|
1242
|
+
await operation.run(() => this.setGroupAddRequest(flag, false, reason));
|
|
1191
1243
|
}
|
|
1192
1244
|
}
|
|
1193
1245
|
}
|