@zhin.js/adapter-icqq 1.1.27 → 1.1.29
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 +45 -0
- package/README.md +59 -9
- package/adapters/icqq/index.js +3 -0
- package/adapters/icqq/index.ts +3 -0
- package/commands/icqq/login/code/index.js +41 -16
- package/commands/icqq/login/code/index.ts +37 -13
- package/commands/icqq/login/confirm/index.js +15 -9
- package/commands/icqq/login/confirm/index.ts +15 -6
- package/commands/icqq/login/definition.js +57 -3
- package/commands/icqq/login/definition.ts +62 -3
- package/commands/icqq/login/qr/index.js +14 -16
- package/commands/icqq/login/qr/index.ts +17 -15
- package/commands/icqq/login/status/index.js +32 -13
- package/commands/icqq/login/status/index.ts +30 -12
- package/lib/endpoint.d.ts +8 -1
- package/lib/endpoint.js +170 -41
- package/lib/i18n.d.ts +1 -1
- package/lib/i18n.js +283 -15
- package/lib/icqq-login-assist.d.ts +113 -20
- package/lib/icqq-login-assist.js +468 -97
- package/lib/icqq-login-control.d.ts +4 -1
- package/lib/icqq-login-descriptors.d.ts +48 -0
- package/lib/icqq-login-descriptors.js +187 -0
- package/lib/icqq-login-errors.d.ts +42 -0
- package/lib/icqq-login-errors.js +77 -0
- package/lib/sdk-logger.d.ts +14 -0
- package/lib/sdk-logger.js +44 -0
- package/package.json +19 -19
- package/src/endpoint.ts +167 -48
- package/src/i18n.ts +283 -15
- package/src/icqq-login-assist.ts +533 -114
- package/src/icqq-login-control.ts +4 -1
- package/src/icqq-login-descriptors.ts +200 -0
- package/src/icqq-login-errors.ts +112 -0
- package/src/sdk-logger.ts +57 -0
package/src/endpoint.ts
CHANGED
|
@@ -34,12 +34,15 @@ import {
|
|
|
34
34
|
} from 'zhin.js/adapter';
|
|
35
35
|
import type { EndpointContentPort } from '@zhin.js/adapter';
|
|
36
36
|
import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
|
|
37
|
-
import { buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type PendingLoginTask } from '@zhin.js/core';
|
|
37
|
+
import { buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type LoginFlowSnapshot, type PendingLoginTask } from '@zhin.js/core';
|
|
38
38
|
import { EndpointDeliveryError, type EndpointSendPart, type EndpointSendResult, type MessageRef } from '@zhin.js/im-contract';
|
|
39
39
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
40
40
|
import type { CapabilityId } from 'zhin.js';
|
|
41
41
|
import type { IcqqLoginControl } from './icqq-login-control.js';
|
|
42
|
-
import {
|
|
42
|
+
import { IcqqLoginDriver, icqqLinkHost, type IcqqChallengeKind } from './icqq-login-assist.js';
|
|
43
|
+
import { messages as t } from './i18n.js';
|
|
44
|
+
import { redactingIcqqLogger, type IcqqSdkLogger } from './sdk-logger.js';
|
|
45
|
+
import { sanitizeIcqqLoginMessage } from './icqq-login-errors.js';
|
|
43
46
|
import { IcqqContentResolver } from './content-resolver.js';
|
|
44
47
|
import {
|
|
45
48
|
InboundMessageDeduper,
|
|
@@ -130,6 +133,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
130
133
|
#retirementEpoch = 0;
|
|
131
134
|
#retirementArmed = false;
|
|
132
135
|
#disposeNativeEvents?: () => void;
|
|
136
|
+
#disposeQrcodeError?: () => void;
|
|
133
137
|
#resolveStartOnline?: () => void;
|
|
134
138
|
#heldInbound: IcqqInboundMessage[] = [];
|
|
135
139
|
readonly #inflightInbound = new Set<Promise<void>>();
|
|
@@ -139,7 +143,15 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
139
143
|
readonly loginControl: IcqqLoginControl;
|
|
140
144
|
#reloginTimer?: ReturnType<typeof setTimeout>;
|
|
141
145
|
#reloginAttempts = 0;
|
|
146
|
+
/** Error code of the failure the automatic relogin is retrying (reported when it gives up). */
|
|
147
|
+
#lastAutoFailure: 'network' | 'unknown' = 'network';
|
|
142
148
|
#reloginInFlight?: Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Login flow of this Endpoint instance (ADR 0058). While a person answers a challenge no
|
|
151
|
+
* automatic relogin runs: a relogin starts a new SDK login, which issues a new challenge and
|
|
152
|
+
* voids the URL / ticket the operator is working on.
|
|
153
|
+
*/
|
|
154
|
+
readonly #login: IcqqLoginDriver;
|
|
143
155
|
#offlineNoticeAt = 0;
|
|
144
156
|
#challengeNoticeAt = 0;
|
|
145
157
|
|
|
@@ -246,12 +258,27 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
246
258
|
get transportState() { return state(); },
|
|
247
259
|
management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
|
|
248
260
|
get content() { return content(); },
|
|
261
|
+
login: {
|
|
262
|
+
// The lease signal aborts on stop / retirement: never let a slow SDK call block it.
|
|
263
|
+
relogin: async ({ signal }) => { await (signal ? raceAbort(this.relogin(), signal) : this.relogin()); },
|
|
264
|
+
refreshChallenge: async ({ signal }) => {
|
|
265
|
+
await (signal ? raceAbort(this.refreshLoginChallenge(), signal) : this.refreshLoginChallenge());
|
|
266
|
+
},
|
|
267
|
+
},
|
|
249
268
|
});
|
|
269
|
+
// The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
|
|
270
|
+
if (this.logger) this.logger = redactingIcqqLogger(this.logger as IcqqSdkLogger) as typeof this.logger;
|
|
250
271
|
this.#contentResolver = new IcqqContentResolver(this);
|
|
251
272
|
this.#content = this.#contentResolver.port;
|
|
252
273
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
253
274
|
this.#options = options;
|
|
254
275
|
this.endpointName = options.config.id;
|
|
276
|
+
this.#login = new IcqqLoginDriver({
|
|
277
|
+
assist: options.loginAssist, client: this, endpointKey: this.endpointName, owner: this.#loginAssistOwner,
|
|
278
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
279
|
+
...(options.endpointIdentity ? { endpointIdentity: options.endpointIdentity } : {}),
|
|
280
|
+
logger: this.#logger, ...(options.locale ? { locale: options.locale } : {}),
|
|
281
|
+
});
|
|
255
282
|
this.loginControl = Object.freeze({
|
|
256
283
|
endpointId: this.endpointName,
|
|
257
284
|
state: () => ({
|
|
@@ -261,6 +288,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
261
288
|
pending: this.#pendingLoginTasks(),
|
|
262
289
|
}),
|
|
263
290
|
pending: () => this.#pendingLoginTasks(),
|
|
291
|
+
flow: () => this.#loginFlow(),
|
|
264
292
|
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
265
293
|
submit: (taskId: string, value: string | Record<string, unknown>) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
266
294
|
cancel: (taskId: string, reason?: string) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
@@ -283,18 +311,28 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
283
311
|
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
284
312
|
}
|
|
285
313
|
|
|
286
|
-
/** Force a login attempt (
|
|
314
|
+
/** Force a login attempt (Console「重新登录」, recovery command): a new login flow. */
|
|
287
315
|
relogin(): Promise<void> {
|
|
288
316
|
return this.#startRelogin(true);
|
|
289
317
|
}
|
|
290
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Console「刷新二维码」: a new QR code in place while a QR challenge is shown; any other
|
|
321
|
+
* challenge cannot be refreshed alone, so this logs in again.
|
|
322
|
+
*/
|
|
323
|
+
async refreshLoginChallenge(): Promise<void> {
|
|
324
|
+
if (this.#stopped || this.#retiring || this.#options.signal?.aborted) throw new Error('ICQQ endpoint 已停止');
|
|
325
|
+
if (await this.#login.refresh()) return;
|
|
326
|
+
await this.relogin();
|
|
327
|
+
}
|
|
328
|
+
|
|
291
329
|
#startRelogin(resetAttempts: boolean): Promise<void> {
|
|
292
330
|
if (this.#stopped || this.#retiring) {
|
|
293
331
|
return Promise.reject(new Error('ICQQ endpoint 已停止'));
|
|
294
332
|
}
|
|
295
333
|
if (this.#reloginInFlight) return this.#reloginInFlight;
|
|
296
334
|
if (resetAttempts) this.#reloginAttempts = 0;
|
|
297
|
-
const task = this.#performRelogin();
|
|
335
|
+
const task = this.#performRelogin(resetAttempts);
|
|
298
336
|
this.#reloginInFlight = task;
|
|
299
337
|
void task.catch(() => undefined).finally(() => {
|
|
300
338
|
if (this.#reloginInFlight === task) this.#reloginInFlight = undefined;
|
|
@@ -305,10 +343,13 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
305
343
|
return task;
|
|
306
344
|
}
|
|
307
345
|
|
|
308
|
-
async #performRelogin(): Promise<void> {
|
|
346
|
+
async #performRelogin(manual: boolean): Promise<void> {
|
|
309
347
|
this.#clearReloginTimer();
|
|
310
348
|
this.cancelReconnect();
|
|
311
|
-
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
349
|
+
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName, manual }));
|
|
350
|
+
// A requested relogin is a new attempt; an automatic retry continues the current timeline.
|
|
351
|
+
if (manual) this.#login.begin(t(this.#options.locale, 'loginReconnecting'));
|
|
352
|
+
else this.#login.connect(t(this.#options.locale, 'loginReconnecting'));
|
|
312
353
|
try {
|
|
313
354
|
await this.login(this.#options.config.password);
|
|
314
355
|
} catch (error) {
|
|
@@ -321,12 +362,33 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
321
362
|
}
|
|
322
363
|
}
|
|
323
364
|
|
|
324
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Automatic backoff relogin, only for failures that need no verification (network drop, kicked,
|
|
367
|
+
* a plain login that did not finish). Never while a person answers a challenge (ADR 0058 §6).
|
|
368
|
+
*/
|
|
369
|
+
#scheduleRelogin(reason: 'kickoff' | 'login_network' | 'login_failed' | 'relogin_failed'): void {
|
|
325
370
|
if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring) return;
|
|
371
|
+
if (this.#login.busy) return;
|
|
372
|
+
// A failed verification or an expired challenge waits for the person (next: relogin / refresh).
|
|
373
|
+
if (reason === 'relogin_failed' && this.#login.awaitingPerson) return;
|
|
326
374
|
if (this.#reloginTimer || this.#reloginInFlight) return;
|
|
327
|
-
if (this.#reloginAttempts >=
|
|
375
|
+
if (this.#reloginAttempts >= ICQQ_RELOGIN_ATTEMPTS) {
|
|
376
|
+
// Exhausted: the attempt ends and waits for the person.
|
|
377
|
+
if (!this.#login.awaitingPerson) {
|
|
378
|
+
const message = t(this.#options.locale, 'loginRetryExhausted', { attempts: ICQQ_RELOGIN_ATTEMPTS });
|
|
379
|
+
this.#login.fail(this.#lastAutoFailure, message, 'relogin');
|
|
380
|
+
this.#notifyOffline('relogin_exhausted', false, true);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
328
384
|
const attempt = this.#reloginAttempts++;
|
|
329
385
|
const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
|
|
386
|
+
const locale = this.#options.locale;
|
|
387
|
+
this.#login.info(t(locale, 'loginRetryScheduled', {
|
|
388
|
+
reason: t(locale, reason === 'kickoff' ? 'loginReasonKickoff' : reason === 'login_network' ? 'loginReasonNetwork' : 'loginReasonFailed'),
|
|
389
|
+
seconds: Math.round(delayMs / 1000),
|
|
390
|
+
attempt: attempt + 1,
|
|
391
|
+
}));
|
|
330
392
|
this.#logger.info(formatCompact({
|
|
331
393
|
op: 'icqq_relogin_scheduled',
|
|
332
394
|
endpoint: this.endpointName,
|
|
@@ -349,6 +411,17 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
349
411
|
this.#reloginTimer = undefined;
|
|
350
412
|
}
|
|
351
413
|
|
|
414
|
+
/** This incarnation's latest login flow (snapshot, including the active challenge's material). */
|
|
415
|
+
#loginFlow(): LoginFlowSnapshot | undefined {
|
|
416
|
+
if (this.#options.signal?.aborted) return undefined;
|
|
417
|
+
const flow = this.#options.loginAssist.getFlow('icqq', this.endpointName);
|
|
418
|
+
const identity = this.#options.endpointIdentity;
|
|
419
|
+
if (!flow || !identity) return flow;
|
|
420
|
+
const own = flow.endpointIdentity;
|
|
421
|
+
return own && own.canonicalId === identity.canonicalId && own.instanceId === identity.instanceId
|
|
422
|
+
&& own.epoch === identity.epoch && own.incarnationId === identity.incarnationId ? flow : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
352
425
|
#pendingLoginTasks(): readonly PendingLoginTask[] {
|
|
353
426
|
if (this.#options.signal?.aborted) return [];
|
|
354
427
|
return this.#options.loginAssist.listPending()
|
|
@@ -360,11 +433,11 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
360
433
|
: task.endpointKey === this.endpointName));
|
|
361
434
|
}
|
|
362
435
|
|
|
363
|
-
#notifyOffline(reason: string, willRetry: boolean): void {
|
|
436
|
+
#notifyOffline(reason: string, willRetry: boolean, force = false): void {
|
|
364
437
|
const notifier = this.#options.notifier;
|
|
365
438
|
if (!notifier?.configured) return;
|
|
366
439
|
const now = Date.now();
|
|
367
|
-
if (now - this.#offlineNoticeAt < 60_000) return;
|
|
440
|
+
if (!force && now - this.#offlineNoticeAt < 60_000) return;
|
|
368
441
|
this.#offlineNoticeAt = now;
|
|
369
442
|
const recovery = willRetry
|
|
370
443
|
? '已开始自动重新登录。'
|
|
@@ -385,24 +458,28 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
385
458
|
});
|
|
386
459
|
}
|
|
387
460
|
|
|
388
|
-
#notifyChallenge(step:
|
|
461
|
+
#notifyChallenge(step: IcqqChallengeKind): void {
|
|
389
462
|
const notifier = this.#options.notifier;
|
|
390
463
|
if (!notifier?.configured) return;
|
|
391
464
|
const now = Date.now();
|
|
392
465
|
if (now - this.#challengeNoticeAt < 30_000) return;
|
|
393
466
|
this.#challengeNoticeAt = now;
|
|
394
|
-
const
|
|
467
|
+
const id = this.endpointName;
|
|
468
|
+
const label = step === 'qrcode' ? '扫码登录'
|
|
395
469
|
: step === 'slider' ? '滑块验证'
|
|
396
|
-
: step === 'device' ? '
|
|
397
|
-
: '
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
470
|
+
: step === 'device' ? '设备锁验证'
|
|
471
|
+
: step === 'sms' ? '短信验证码'
|
|
472
|
+
: '身份验证';
|
|
473
|
+
const next = step === 'qrcode'
|
|
474
|
+
? `在 QQ 回复 \`icqq login qr ${id}\` 获取二维码,用手机 QQ 扫码并在手机上确认,登录会自动继续;没有继续时回复 \`icqq login confirm ${id}\`。`
|
|
475
|
+
: step === 'slider'
|
|
476
|
+
? `打开 Console 的 ICQQ 页面,用验证码辅助页完成滑块即可;也可以回复 \`icqq login status ${id}\` 查看滑块链接,再用 \`icqq login code ${id} <ticket>\` 提交。`
|
|
477
|
+
: step === 'device'
|
|
478
|
+
? `回复 \`icqq login status ${id}\` 查看验证链接,用手机完成验证后回复 \`icqq login confirm ${id}\`;改用短信验证码请回复 \`icqq login code ${id} sms\`。`
|
|
479
|
+
: step === 'sms'
|
|
480
|
+
? `收到短信后回复 \`icqq login code ${id} <6 位验证码>\`。`
|
|
481
|
+
: `需要复制设备信息到验证页,请打开 Console 的 ICQQ 页面按步骤完成;完成后点「已完成,继续登录」或回复 \`icqq login confirm ${id}\`。`;
|
|
482
|
+
void notifier.send(`[Zhin] ICQQ ${id} 需要${label}。\n${next}`).then((sent) => {
|
|
406
483
|
if (sent) return;
|
|
407
484
|
this.#logger.warn(formatCompact({
|
|
408
485
|
op: 'icqq_notify_failed',
|
|
@@ -461,6 +538,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
461
538
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
462
539
|
try {
|
|
463
540
|
this.#bindClientEvents();
|
|
541
|
+
this.#login.begin();
|
|
464
542
|
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
465
543
|
await onlineReady;
|
|
466
544
|
signal.throwIfAborted();
|
|
@@ -507,6 +585,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
507
585
|
this.#assertExternalLifecycle('stop');
|
|
508
586
|
this.#stopped = true;
|
|
509
587
|
this.#clearReloginTimer();
|
|
588
|
+
this.#login.invalidate();
|
|
510
589
|
this.#armRetirementFence();
|
|
511
590
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
512
591
|
this.#resolveStartOnline?.();
|
|
@@ -514,6 +593,8 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
514
593
|
this.#heldInbound.length = 0;
|
|
515
594
|
this.#disposeNativeEvents?.();
|
|
516
595
|
this.#disposeNativeEvents = undefined;
|
|
596
|
+
this.#disposeQrcodeError?.();
|
|
597
|
+
this.#disposeQrcodeError = undefined;
|
|
517
598
|
for (const event of BOUND_EVENTS) {
|
|
518
599
|
// The retirement guard owns this matcher until a late login handshake
|
|
519
600
|
// has finished. start()/open() replace it with the normal listener.
|
|
@@ -812,7 +893,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
812
893
|
this.#scheduleRelogin('kickoff');
|
|
813
894
|
});
|
|
814
895
|
this.on('system.login.qrcode', (event) => {
|
|
815
|
-
this.#logger.info('icqq
|
|
896
|
+
this.#logger.info('icqq 收到二维码登录事件,等待扫码(Console / 终端 / QQ 命令)');
|
|
816
897
|
this.#handleLoginChallenge('qrcode', event);
|
|
817
898
|
});
|
|
818
899
|
this.on('system.login.device', (event) => {
|
|
@@ -822,11 +903,22 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
822
903
|
this.#handleLoginChallenge('slider', event);
|
|
823
904
|
});
|
|
824
905
|
this.on('system.login.error', (event) => {
|
|
825
|
-
|
|
826
|
-
this.#logSystemEvent('login.error', event
|
|
827
|
-
|
|
828
|
-
|
|
906
|
+
const raw = event && typeof event === 'object' ? event as { code?: unknown; message?: unknown } : {};
|
|
907
|
+
this.#logSystemEvent('login.error', event, {
|
|
908
|
+
...(typeof raw.code === 'number' ? { code: raw.code } : {}),
|
|
909
|
+
message: sanitizeIcqqLoginMessage(raw.message),
|
|
910
|
+
});
|
|
911
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
912
|
+
this.#handleLoginError(event);
|
|
913
|
+
});
|
|
914
|
+
// The SDK's second scan query (inside qrcodeLogin) failed: report it instead of verifying forever.
|
|
915
|
+
// Only our listener is removed on stop (the SDK has its own one on this internal event).
|
|
916
|
+
this.#disposeQrcodeError?.();
|
|
917
|
+
const disposeQrcodeError = this.on('internal.error.qrcode', (code: unknown, message: unknown) => {
|
|
918
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
919
|
+
this.#login.qrcodeError(code, message);
|
|
829
920
|
});
|
|
921
|
+
this.#disposeQrcodeError = () => { disposeQrcodeError(); };
|
|
830
922
|
this.on('system.login.auth', (event) => {
|
|
831
923
|
this.#handleLoginChallenge('auth', event);
|
|
832
924
|
});
|
|
@@ -836,12 +928,12 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
836
928
|
this.on('system.online', (event) => {
|
|
837
929
|
if (this.#retiring) return;
|
|
838
930
|
this.#everOnline = true;
|
|
931
|
+
this.#login.online();
|
|
839
932
|
this.#clearReloginTimer();
|
|
840
933
|
this.#reloginAttempts = 0;
|
|
841
934
|
this.#notifyRecovered();
|
|
842
935
|
try {
|
|
843
936
|
this.#resolveStartOnline?.();
|
|
844
|
-
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
845
937
|
try {
|
|
846
938
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
847
939
|
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
@@ -894,37 +986,56 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
|
|
|
894
986
|
});
|
|
895
987
|
}
|
|
896
988
|
|
|
897
|
-
#handleLoginChallenge(
|
|
898
|
-
step: 'qrcode' | 'slider' | 'device' | 'auth',
|
|
899
|
-
event: unknown,
|
|
900
|
-
): void {
|
|
989
|
+
#handleLoginChallenge(step: Exclude<IcqqChallengeKind, 'sms'>, event: unknown): void {
|
|
901
990
|
if (this.#retiring || this.#stopped || this.#options.signal?.aborted) return;
|
|
902
991
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
903
992
|
? event as Record<string, unknown>
|
|
904
993
|
: null);
|
|
905
|
-
|
|
994
|
+
// Verification links carry a session id (sid / sig): logs get the challenge kind and host only.
|
|
995
|
+
this.#logSystemEvent(`login.${step}`, event, loginChallengeLogLine(step, payload));
|
|
906
996
|
this.#clearReloginTimer();
|
|
907
997
|
this.#notifyChallenge(step);
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
client: this,
|
|
912
|
-
adapter: 'icqq',
|
|
913
|
-
endpointKey: this.endpointName,
|
|
914
|
-
owner: this.#loginAssistOwner,
|
|
915
|
-
signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
|
|
916
|
-
logger: this.#logger,
|
|
917
|
-
step,
|
|
918
|
-
event: payload ?? event,
|
|
919
|
-
});
|
|
998
|
+
// The QR image is a Buffer, which serialization drops; the driver reads the raw event.
|
|
999
|
+
const raw = event && typeof event === 'object' ? event as Record<string, unknown> : {};
|
|
1000
|
+
this.#login.challenge(step, { ...(payload ?? {}), ...raw });
|
|
920
1001
|
}
|
|
921
1002
|
|
|
922
|
-
|
|
923
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* `system.login.error` (ADR 0058 §5–6): a wrong code stays on its challenge; a verification
|
|
1005
|
+
* failure ends the attempt and waits for the person (`next`). Only a plain login that failed on
|
|
1006
|
+
* the network or for an unknown reason, without any challenge, keeps the automatic backoff.
|
|
1007
|
+
*/
|
|
1008
|
+
#handleLoginError(event: unknown): void {
|
|
1009
|
+
const { mapping, hadChallenge, retrying } = this.#login.loginError(event);
|
|
1010
|
+
this.#logger.warn(formatCompact({
|
|
1011
|
+
op: 'icqq_login_error', endpoint: this.endpointName, code: mapping.code,
|
|
1012
|
+
...(mapping.platformCode !== undefined ? { platform_code: mapping.platformCode } : {}),
|
|
1013
|
+
}));
|
|
1014
|
+
if (retrying) return;
|
|
1015
|
+
const autoRetry = this.#options.config.autoReconnect
|
|
1016
|
+
&& (mapping.code === 'network' || (mapping.code === 'unknown' && !hadChallenge));
|
|
1017
|
+
this.#notifyOffline('login_error', autoRetry);
|
|
1018
|
+
if (autoRetry) {
|
|
1019
|
+
this.#login.outcome({ result: 'rejected', code: mapping.code, message: mapping.message, next: 'wait' });
|
|
1020
|
+
this.#lastAutoFailure = mapping.code === 'network' ? 'network' : 'unknown';
|
|
1021
|
+
this.#scheduleRelogin(mapping.code === 'network' ? 'login_network' : 'login_failed');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
this.#clearReloginTimer();
|
|
1025
|
+
this.#login.fail(mapping.code, mapping.message, mapping.next);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* `safe` replaces the payload in the log line and the `system.receive` side event (login
|
|
1030
|
+
* challenges keep only kind and host: links carry a sid, plus phone / device / QR image).
|
|
1031
|
+
*/
|
|
1032
|
+
#logSystemEvent(type: string, event: unknown, safe?: Record<string, unknown> | null): void {
|
|
1033
|
+
const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
|
|
1034
|
+
const line = payload;
|
|
924
1035
|
this.#logger.warn(formatCompact({
|
|
925
1036
|
op: `system.${type}`,
|
|
926
1037
|
endpoint: this.endpointName,
|
|
927
|
-
...(
|
|
1038
|
+
...(line ? { event: JSON.stringify(line) } : {}),
|
|
928
1039
|
}));
|
|
929
1040
|
this.#dispatchIcqqSystemSideEvent(type, payload);
|
|
930
1041
|
}
|
|
@@ -1338,6 +1449,12 @@ function resolveNativeClientConfig(config: ResolvedIcqqConfig): NativeIcqqClient
|
|
|
1338
1449
|
};
|
|
1339
1450
|
}
|
|
1340
1451
|
|
|
1452
|
+
/** Log line of a login challenge: its kind and the link's host, never the link, image or device. */
|
|
1453
|
+
export function loginChallengeLogLine(kind: string, payload: Record<string, unknown> | null): Record<string, unknown> {
|
|
1454
|
+
const host = icqqLinkHost(payload?.url);
|
|
1455
|
+
return { kind, ...(host ? { host } : {}) };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1341
1458
|
function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
1342
1459
|
if (!event || typeof event !== 'object') return null;
|
|
1343
1460
|
const nativeToJSON = (event as { toJSON?: (keys: string[]) => unknown }).toJSON;
|
|
@@ -1360,5 +1477,7 @@ function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
|
|
|
1360
1477
|
}
|
|
1361
1478
|
|
|
1362
1479
|
const INBOUND_HOLD_LIMIT = 32;
|
|
1480
|
+
/** Automatic backoff relogins before the attempt fails and waits for the person. */
|
|
1481
|
+
const ICQQ_RELOGIN_ATTEMPTS = 8;
|
|
1363
1482
|
|
|
1364
1483
|
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'] as const;
|