@zhin.js/adapter-icqq 1.1.28 → 1.1.30
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 +70 -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 +14 -1
- package/lib/endpoint.js +178 -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 +45 -38
- package/plugin.js +0 -3
- package/src/endpoint.ts +176 -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/lib/endpoint.js
CHANGED
|
@@ -6,7 +6,10 @@ import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
|
|
|
6
6
|
import { buildSystem, toCanonicalSegments } from '@zhin.js/core';
|
|
7
7
|
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
8
8
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
9
|
-
import {
|
|
9
|
+
import { IcqqLoginDriver, icqqLinkHost } from './icqq-login-assist.js';
|
|
10
|
+
import { messages as t } from './i18n.js';
|
|
11
|
+
import { redactingIcqqLogger } from './sdk-logger.js';
|
|
12
|
+
import { sanitizeIcqqLoginMessage } from './icqq-login-errors.js';
|
|
10
13
|
import { IcqqContentResolver } from './content-resolver.js';
|
|
11
14
|
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, resolveIcqqSelfUserId, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
|
|
12
15
|
import { icqqNoticeDedupeKey, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
@@ -36,6 +39,14 @@ const BOUND_EVENTS = [
|
|
|
36
39
|
'system.login.error',
|
|
37
40
|
'system.login.auth',
|
|
38
41
|
];
|
|
42
|
+
/**
|
|
43
|
+
* Log level of an ICQQ `system.*` event by meaning: coming online and login challenges waiting for
|
|
44
|
+
* the person (QR code, device lock, slider, identity check) are normal progress (`info`); only
|
|
45
|
+
* failures (login errors, going offline) are `warn`, so a routine login stays out of the warnings.
|
|
46
|
+
*/
|
|
47
|
+
export function icqqSystemEventLogLevel(type) {
|
|
48
|
+
return type === 'online' || type.startsWith('login.') && type !== 'login.error' ? 'info' : 'warn';
|
|
49
|
+
}
|
|
39
50
|
export class IcqqEndpoint extends ManagedIcqqClient {
|
|
40
51
|
[endpointSpec];
|
|
41
52
|
#logger;
|
|
@@ -53,6 +64,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
53
64
|
#retirementEpoch = 0;
|
|
54
65
|
#retirementArmed = false;
|
|
55
66
|
#disposeNativeEvents;
|
|
67
|
+
#disposeQrcodeError;
|
|
56
68
|
#resolveStartOnline;
|
|
57
69
|
#heldInbound = [];
|
|
58
70
|
#inflightInbound = new Set();
|
|
@@ -62,7 +74,15 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
62
74
|
loginControl;
|
|
63
75
|
#reloginTimer;
|
|
64
76
|
#reloginAttempts = 0;
|
|
77
|
+
/** Error code of the failure the automatic relogin is retrying (reported when it gives up). */
|
|
78
|
+
#lastAutoFailure = 'network';
|
|
65
79
|
#reloginInFlight;
|
|
80
|
+
/**
|
|
81
|
+
* Login flow of this Endpoint instance (ADR 0058). While a person answers a challenge no
|
|
82
|
+
* automatic relogin runs: a relogin starts a new SDK login, which issues a new challenge and
|
|
83
|
+
* voids the URL / ticket the operator is working on.
|
|
84
|
+
*/
|
|
85
|
+
#login;
|
|
66
86
|
#offlineNoticeAt = 0;
|
|
67
87
|
#challengeNoticeAt = 0;
|
|
68
88
|
#management = Object.freeze({
|
|
@@ -170,12 +190,28 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
170
190
|
get transportState() { return state(); },
|
|
171
191
|
management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
|
|
172
192
|
get content() { return content(); },
|
|
193
|
+
login: {
|
|
194
|
+
// The lease signal aborts on stop / retirement: never let a slow SDK call block it.
|
|
195
|
+
relogin: async ({ signal }) => { await (signal ? raceAbort(this.relogin(), signal) : this.relogin()); },
|
|
196
|
+
refreshChallenge: async ({ signal }) => {
|
|
197
|
+
await (signal ? raceAbort(this.refreshLoginChallenge(), signal) : this.refreshLoginChallenge());
|
|
198
|
+
},
|
|
199
|
+
},
|
|
173
200
|
});
|
|
201
|
+
// The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
|
|
202
|
+
if (this.logger)
|
|
203
|
+
this.logger = redactingIcqqLogger(this.logger);
|
|
174
204
|
this.#contentResolver = new IcqqContentResolver(this);
|
|
175
205
|
this.#content = this.#contentResolver.port;
|
|
176
206
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
177
207
|
this.#options = options;
|
|
178
208
|
this.endpointName = options.config.id;
|
|
209
|
+
this.#login = new IcqqLoginDriver({
|
|
210
|
+
assist: options.loginAssist, client: this, endpointKey: this.endpointName, owner: this.#loginAssistOwner,
|
|
211
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
212
|
+
...(options.endpointIdentity ? { endpointIdentity: options.endpointIdentity } : {}),
|
|
213
|
+
logger: this.#logger, ...(options.locale ? { locale: options.locale } : {}),
|
|
214
|
+
});
|
|
179
215
|
this.loginControl = Object.freeze({
|
|
180
216
|
endpointId: this.endpointName,
|
|
181
217
|
state: () => ({
|
|
@@ -185,6 +221,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
185
221
|
pending: this.#pendingLoginTasks(),
|
|
186
222
|
}),
|
|
187
223
|
pending: () => this.#pendingLoginTasks(),
|
|
224
|
+
flow: () => this.#loginFlow(),
|
|
188
225
|
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
189
226
|
submit: (taskId, value) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
190
227
|
cancel: (taskId, reason) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
@@ -208,10 +245,21 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
208
245
|
// reconn_interval policy. No account-level logout or second retry timer.
|
|
209
246
|
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
210
247
|
}
|
|
211
|
-
/** Force a login attempt (
|
|
248
|
+
/** Force a login attempt (Console「重新登录」, recovery command): a new login flow. */
|
|
212
249
|
relogin() {
|
|
213
250
|
return this.#startRelogin(true);
|
|
214
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Console「刷新二维码」: a new QR code in place while a QR challenge is shown; any other
|
|
254
|
+
* challenge cannot be refreshed alone, so this logs in again.
|
|
255
|
+
*/
|
|
256
|
+
async refreshLoginChallenge() {
|
|
257
|
+
if (this.#stopped || this.#retiring || this.#options.signal?.aborted)
|
|
258
|
+
throw new Error('ICQQ endpoint 已停止');
|
|
259
|
+
if (await this.#login.refresh())
|
|
260
|
+
return;
|
|
261
|
+
await this.relogin();
|
|
262
|
+
}
|
|
215
263
|
#startRelogin(resetAttempts) {
|
|
216
264
|
if (this.#stopped || this.#retiring) {
|
|
217
265
|
return Promise.reject(new Error('ICQQ endpoint 已停止'));
|
|
@@ -220,7 +268,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
220
268
|
return this.#reloginInFlight;
|
|
221
269
|
if (resetAttempts)
|
|
222
270
|
this.#reloginAttempts = 0;
|
|
223
|
-
const task = this.#performRelogin();
|
|
271
|
+
const task = this.#performRelogin(resetAttempts);
|
|
224
272
|
this.#reloginInFlight = task;
|
|
225
273
|
void task.catch(() => undefined).finally(() => {
|
|
226
274
|
if (this.#reloginInFlight === task)
|
|
@@ -231,10 +279,15 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
231
279
|
});
|
|
232
280
|
return task;
|
|
233
281
|
}
|
|
234
|
-
async #performRelogin() {
|
|
282
|
+
async #performRelogin(manual) {
|
|
235
283
|
this.#clearReloginTimer();
|
|
236
284
|
this.cancelReconnect();
|
|
237
|
-
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
285
|
+
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName, manual }));
|
|
286
|
+
// A requested relogin is a new attempt; an automatic retry continues the current timeline.
|
|
287
|
+
if (manual)
|
|
288
|
+
this.#login.begin(t(this.#options.locale, 'loginReconnecting'));
|
|
289
|
+
else
|
|
290
|
+
this.#login.connect(t(this.#options.locale, 'loginReconnecting'));
|
|
238
291
|
try {
|
|
239
292
|
await this.login(this.#options.config.password);
|
|
240
293
|
}
|
|
@@ -247,15 +300,37 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
247
300
|
throw error;
|
|
248
301
|
}
|
|
249
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Automatic backoff relogin, only for failures that need no verification (network drop, kicked,
|
|
305
|
+
* a plain login that did not finish). Never while a person answers a challenge (ADR 0058 §6).
|
|
306
|
+
*/
|
|
250
307
|
#scheduleRelogin(reason) {
|
|
251
308
|
if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring)
|
|
252
309
|
return;
|
|
310
|
+
if (this.#login.busy)
|
|
311
|
+
return;
|
|
312
|
+
// A failed verification or an expired challenge waits for the person (next: relogin / refresh).
|
|
313
|
+
if (reason === 'relogin_failed' && this.#login.awaitingPerson)
|
|
314
|
+
return;
|
|
253
315
|
if (this.#reloginTimer || this.#reloginInFlight)
|
|
254
316
|
return;
|
|
255
|
-
if (this.#reloginAttempts >=
|
|
317
|
+
if (this.#reloginAttempts >= ICQQ_RELOGIN_ATTEMPTS) {
|
|
318
|
+
// Exhausted: the attempt ends and waits for the person.
|
|
319
|
+
if (!this.#login.awaitingPerson) {
|
|
320
|
+
const message = t(this.#options.locale, 'loginRetryExhausted', { attempts: ICQQ_RELOGIN_ATTEMPTS });
|
|
321
|
+
this.#login.fail(this.#lastAutoFailure, message, 'relogin');
|
|
322
|
+
this.#notifyOffline('relogin_exhausted', false, true);
|
|
323
|
+
}
|
|
256
324
|
return;
|
|
325
|
+
}
|
|
257
326
|
const attempt = this.#reloginAttempts++;
|
|
258
327
|
const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
|
|
328
|
+
const locale = this.#options.locale;
|
|
329
|
+
this.#login.info(t(locale, 'loginRetryScheduled', {
|
|
330
|
+
reason: t(locale, reason === 'kickoff' ? 'loginReasonKickoff' : reason === 'login_network' ? 'loginReasonNetwork' : 'loginReasonFailed'),
|
|
331
|
+
seconds: Math.round(delayMs / 1000),
|
|
332
|
+
attempt: attempt + 1,
|
|
333
|
+
}));
|
|
259
334
|
this.#logger.info(formatCompact({
|
|
260
335
|
op: 'icqq_relogin_scheduled',
|
|
261
336
|
endpoint: this.endpointName,
|
|
@@ -277,6 +352,18 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
277
352
|
clearTimeout(this.#reloginTimer);
|
|
278
353
|
this.#reloginTimer = undefined;
|
|
279
354
|
}
|
|
355
|
+
/** This incarnation's latest login flow (snapshot, including the active challenge's material). */
|
|
356
|
+
#loginFlow() {
|
|
357
|
+
if (this.#options.signal?.aborted)
|
|
358
|
+
return undefined;
|
|
359
|
+
const flow = this.#options.loginAssist.getFlow('icqq', this.endpointName);
|
|
360
|
+
const identity = this.#options.endpointIdentity;
|
|
361
|
+
if (!flow || !identity)
|
|
362
|
+
return flow;
|
|
363
|
+
const own = flow.endpointIdentity;
|
|
364
|
+
return own && own.canonicalId === identity.canonicalId && own.instanceId === identity.instanceId
|
|
365
|
+
&& own.epoch === identity.epoch && own.incarnationId === identity.incarnationId ? flow : undefined;
|
|
366
|
+
}
|
|
280
367
|
#pendingLoginTasks() {
|
|
281
368
|
if (this.#options.signal?.aborted)
|
|
282
369
|
return [];
|
|
@@ -288,12 +375,12 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
288
375
|
&& task.endpointIdentity.incarnationId === this.#options.endpointIdentity.incarnationId
|
|
289
376
|
: task.endpointKey === this.endpointName));
|
|
290
377
|
}
|
|
291
|
-
#notifyOffline(reason, willRetry) {
|
|
378
|
+
#notifyOffline(reason, willRetry, force = false) {
|
|
292
379
|
const notifier = this.#options.notifier;
|
|
293
380
|
if (!notifier?.configured)
|
|
294
381
|
return;
|
|
295
382
|
const now = Date.now();
|
|
296
|
-
if (now - this.#offlineNoticeAt < 60_000)
|
|
383
|
+
if (!force && now - this.#offlineNoticeAt < 60_000)
|
|
297
384
|
return;
|
|
298
385
|
this.#offlineNoticeAt = now;
|
|
299
386
|
const recovery = willRetry
|
|
@@ -323,18 +410,22 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
323
410
|
if (now - this.#challengeNoticeAt < 30_000)
|
|
324
411
|
return;
|
|
325
412
|
this.#challengeNoticeAt = now;
|
|
326
|
-
const
|
|
413
|
+
const id = this.endpointName;
|
|
414
|
+
const label = step === 'qrcode' ? '扫码登录'
|
|
327
415
|
: step === 'slider' ? '滑块验证'
|
|
328
|
-
: step === 'device' ? '
|
|
329
|
-
: '
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
416
|
+
: step === 'device' ? '设备锁验证'
|
|
417
|
+
: step === 'sms' ? '短信验证码'
|
|
418
|
+
: '身份验证';
|
|
419
|
+
const next = step === 'qrcode'
|
|
420
|
+
? `在 QQ 回复 \`icqq login qr ${id}\` 获取二维码,用手机 QQ 扫码并在手机上确认,登录会自动继续;没有继续时回复 \`icqq login confirm ${id}\`。`
|
|
421
|
+
: step === 'slider'
|
|
422
|
+
? `打开 Console 的 ICQQ 页面,用验证码辅助页完成滑块即可;也可以回复 \`icqq login status ${id}\` 查看滑块链接,再用 \`icqq login code ${id} <ticket>\` 提交。`
|
|
423
|
+
: step === 'device'
|
|
424
|
+
? `回复 \`icqq login status ${id}\` 查看验证链接,用手机完成验证后回复 \`icqq login confirm ${id}\`;改用短信验证码请回复 \`icqq login code ${id} sms\`。`
|
|
425
|
+
: step === 'sms'
|
|
426
|
+
? `收到短信后回复 \`icqq login code ${id} <6 位验证码>\`。`
|
|
427
|
+
: `需要复制设备信息到验证页,请打开 Console 的 ICQQ 页面按步骤完成;完成后点「已完成,继续登录」或回复 \`icqq login confirm ${id}\`。`;
|
|
428
|
+
void notifier.send(`[Zhin] ICQQ ${id} 需要${label}。\n${next}`).then((sent) => {
|
|
338
429
|
if (sent)
|
|
339
430
|
return;
|
|
340
431
|
this.#logger.warn(formatCompact({
|
|
@@ -397,6 +488,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
397
488
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
398
489
|
try {
|
|
399
490
|
this.#bindClientEvents();
|
|
491
|
+
this.#login.begin();
|
|
400
492
|
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
401
493
|
await onlineReady;
|
|
402
494
|
signal.throwIfAborted();
|
|
@@ -441,6 +533,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
441
533
|
this.#assertExternalLifecycle('stop');
|
|
442
534
|
this.#stopped = true;
|
|
443
535
|
this.#clearReloginTimer();
|
|
536
|
+
this.#login.invalidate();
|
|
444
537
|
this.#armRetirementFence();
|
|
445
538
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
446
539
|
this.#resolveStartOnline?.();
|
|
@@ -448,6 +541,8 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
448
541
|
this.#heldInbound.length = 0;
|
|
449
542
|
this.#disposeNativeEvents?.();
|
|
450
543
|
this.#disposeNativeEvents = undefined;
|
|
544
|
+
this.#disposeQrcodeError?.();
|
|
545
|
+
this.#disposeQrcodeError = undefined;
|
|
451
546
|
for (const event of BOUND_EVENTS) {
|
|
452
547
|
// The retirement guard owns this matcher until a late login handshake
|
|
453
548
|
// has finished. start()/open() replace it with the normal listener.
|
|
@@ -744,7 +839,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
744
839
|
this.#scheduleRelogin('kickoff');
|
|
745
840
|
});
|
|
746
841
|
this.on('system.login.qrcode', (event) => {
|
|
747
|
-
this.#logger.info('icqq
|
|
842
|
+
this.#logger.info('icqq 收到二维码登录事件,等待扫码(Console / 终端 / QQ 命令)');
|
|
748
843
|
this.#handleLoginChallenge('qrcode', event);
|
|
749
844
|
});
|
|
750
845
|
this.on('system.login.device', (event) => {
|
|
@@ -754,11 +849,24 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
754
849
|
this.#handleLoginChallenge('slider', event);
|
|
755
850
|
});
|
|
756
851
|
this.on('system.login.error', (event) => {
|
|
757
|
-
|
|
758
|
-
this.#logSystemEvent('login.error', event
|
|
759
|
-
|
|
760
|
-
|
|
852
|
+
const raw = event && typeof event === 'object' ? event : {};
|
|
853
|
+
this.#logSystemEvent('login.error', event, {
|
|
854
|
+
...(typeof raw.code === 'number' ? { code: raw.code } : {}),
|
|
855
|
+
message: sanitizeIcqqLoginMessage(raw.message),
|
|
856
|
+
});
|
|
857
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
|
|
858
|
+
return;
|
|
859
|
+
this.#handleLoginError(event);
|
|
860
|
+
});
|
|
861
|
+
// The SDK's second scan query (inside qrcodeLogin) failed: report it instead of verifying forever.
|
|
862
|
+
// Only our listener is removed on stop (the SDK has its own one on this internal event).
|
|
863
|
+
this.#disposeQrcodeError?.();
|
|
864
|
+
const disposeQrcodeError = this.on('internal.error.qrcode', (code, message) => {
|
|
865
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
|
|
866
|
+
return;
|
|
867
|
+
this.#login.qrcodeError(code, message);
|
|
761
868
|
});
|
|
869
|
+
this.#disposeQrcodeError = () => { disposeQrcodeError(); };
|
|
762
870
|
this.on('system.login.auth', (event) => {
|
|
763
871
|
this.#handleLoginChallenge('auth', event);
|
|
764
872
|
});
|
|
@@ -768,12 +876,12 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
768
876
|
if (this.#retiring)
|
|
769
877
|
return;
|
|
770
878
|
this.#everOnline = true;
|
|
879
|
+
this.#login.online();
|
|
771
880
|
this.#clearReloginTimer();
|
|
772
881
|
this.#reloginAttempts = 0;
|
|
773
882
|
this.#notifyRecovered();
|
|
774
883
|
try {
|
|
775
884
|
this.#resolveStartOnline?.();
|
|
776
|
-
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
777
885
|
try {
|
|
778
886
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
779
887
|
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
@@ -829,28 +937,50 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
829
937
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
830
938
|
? event
|
|
831
939
|
: null);
|
|
832
|
-
|
|
940
|
+
// Verification links carry a session id (sid / sig): logs get the challenge kind and host only.
|
|
941
|
+
this.#logSystemEvent(`login.${step}`, event, loginChallengeLogLine(step, payload));
|
|
833
942
|
this.#clearReloginTimer();
|
|
834
943
|
this.#notifyChallenge(step);
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
client: this,
|
|
839
|
-
adapter: 'icqq',
|
|
840
|
-
endpointKey: this.endpointName,
|
|
841
|
-
owner: this.#loginAssistOwner,
|
|
842
|
-
signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
|
|
843
|
-
logger: this.#logger,
|
|
844
|
-
step,
|
|
845
|
-
event: payload ?? event,
|
|
846
|
-
});
|
|
944
|
+
// The QR image is a Buffer, which serialization drops; the driver reads the raw event.
|
|
945
|
+
const raw = event && typeof event === 'object' ? event : {};
|
|
946
|
+
this.#login.challenge(step, { ...(payload ?? {}), ...raw });
|
|
847
947
|
}
|
|
848
|
-
|
|
849
|
-
|
|
948
|
+
/**
|
|
949
|
+
* `system.login.error` (ADR 0058 §5–6): a wrong code stays on its challenge; a verification
|
|
950
|
+
* failure ends the attempt and waits for the person (`next`). Only a plain login that failed on
|
|
951
|
+
* the network or for an unknown reason, without any challenge, keeps the automatic backoff.
|
|
952
|
+
*/
|
|
953
|
+
#handleLoginError(event) {
|
|
954
|
+
const { mapping, hadChallenge, retrying } = this.#login.loginError(event);
|
|
850
955
|
this.#logger.warn(formatCompact({
|
|
956
|
+
op: 'icqq_login_error', endpoint: this.endpointName, code: mapping.code,
|
|
957
|
+
...(mapping.platformCode !== undefined ? { platform_code: mapping.platformCode } : {}),
|
|
958
|
+
}));
|
|
959
|
+
if (retrying)
|
|
960
|
+
return;
|
|
961
|
+
const autoRetry = this.#options.config.autoReconnect
|
|
962
|
+
&& (mapping.code === 'network' || (mapping.code === 'unknown' && !hadChallenge));
|
|
963
|
+
this.#notifyOffline('login_error', autoRetry);
|
|
964
|
+
if (autoRetry) {
|
|
965
|
+
this.#login.outcome({ result: 'rejected', code: mapping.code, message: mapping.message, next: 'wait' });
|
|
966
|
+
this.#lastAutoFailure = mapping.code === 'network' ? 'network' : 'unknown';
|
|
967
|
+
this.#scheduleRelogin(mapping.code === 'network' ? 'login_network' : 'login_failed');
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this.#clearReloginTimer();
|
|
971
|
+
this.#login.fail(mapping.code, mapping.message, mapping.next);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* `safe` replaces the payload in the log line and the `system.receive` side event (login
|
|
975
|
+
* challenges keep only kind and host: links carry a sid, plus phone / device / QR image).
|
|
976
|
+
*/
|
|
977
|
+
#logSystemEvent(type, event, safe) {
|
|
978
|
+
const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
|
|
979
|
+
const line = payload;
|
|
980
|
+
this.#logger[icqqSystemEventLogLevel(type)](formatCompact({
|
|
851
981
|
op: `system.${type}`,
|
|
852
982
|
endpoint: this.endpointName,
|
|
853
|
-
...(
|
|
983
|
+
...(line ? { event: JSON.stringify(line) } : {}),
|
|
854
984
|
}));
|
|
855
985
|
this.#dispatchIcqqSystemSideEvent(type, payload);
|
|
856
986
|
}
|
|
@@ -1262,6 +1392,11 @@ function resolveNativeClientConfig(config) {
|
|
|
1262
1392
|
reconn_interval: config.autoReconnect ? 5 : 0,
|
|
1263
1393
|
};
|
|
1264
1394
|
}
|
|
1395
|
+
/** Log line of a login challenge: its kind and the link's host, never the link, image or device. */
|
|
1396
|
+
export function loginChallengeLogLine(kind, payload) {
|
|
1397
|
+
const host = icqqLinkHost(payload?.url);
|
|
1398
|
+
return { kind, ...(host ? { host } : {}) };
|
|
1399
|
+
}
|
|
1265
1400
|
function serializeIcqqEvent(event) {
|
|
1266
1401
|
if (!event || typeof event !== 'object')
|
|
1267
1402
|
return null;
|
|
@@ -1286,4 +1421,6 @@ function serializeIcqqEvent(event) {
|
|
|
1286
1421
|
return Object.keys(out).length > 0 ? out : null;
|
|
1287
1422
|
}
|
|
1288
1423
|
const INBOUND_HOLD_LIMIT = 32;
|
|
1424
|
+
/** Automatic backoff relogins before the attempt fails and waits for the person. */
|
|
1425
|
+
const ICQQ_RELOGIN_ATTEMPTS = 8;
|
|
1289
1426
|
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'];
|
package/lib/i18n.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const messages: (locale: import("zhin.js").Locale | undefined, key: "direct" | "addUsage" | "uinRequired" | "uinInvalid" | "endpointAdded" | "endpointAddFailed" | "
|
|
1
|
+
export declare const messages: (locale: import("zhin.js").Locale | undefined, key: "direct" | "addUsage" | "uinRequired" | "uinInvalid" | "endpointAdded" | "endpointAddFailed" | "loginConnecting" | "loginReconnecting" | "loginRetryScheduled" | "loginReasonKickoff" | "loginReasonNetwork" | "loginReasonFailed" | "loginSubmit" | "loginVerifying" | "loginVerifyingSlider" | "loginVerifyingDevice" | "loginVerifyingSms" | "loginVerifyingAuth" | "loginSdkCallFailed" | "qrTitle" | "qrShort" | "qrStepScan" | "qrStepConfirm" | "qrStepContinue" | "qrConfirm" | "qrScanned" | "qrNotScanned" | "qrExpired" | "qrRefreshing" | "qrExpiredRefreshing" | "sliderTitle" | "sliderShort" | "sliderStepOpen" | "sliderStepDevtools" | "sliderStepCopy" | "sliderLink" | "sliderInput" | "sliderRandstrMissing" | "deviceTitle" | "deviceShort" | "deviceStepScan" | "deviceStepNoLink" | "deviceStepVerify" | "deviceStepContinue" | "deviceStepSms" | "deviceLink" | "deviceContinue" | "deviceSms" | "smsSendFailed" | "smsTitle" | "smsShort" | "smsPhoneFallback" | "smsInput" | "smsHint" | "smsResend" | "smsResent" | "authTitle" | "authShort" | "authStepCopy" | "authStepOpen" | "authStepScript" | "authStepVerify" | "authStepContinue" | "authWarning" | "authJson" | "authScript" | "authScriptCollapsed" | "authLink" | "authContinue" | "loginErrorCodeInvalid" | "loginErrorRateLimited" | "loginErrorExpired" | "loginErrorRisk" | "loginErrorCredential" | "loginErrorNetwork" | "loginErrorUnknown" | "loginErrorTokenIncompatible" | "loginRetryExhausted", values?: import("zhin.js").MessageValues) => string;
|