@zhin.js/adapter-icqq 1.1.28 → 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 +32 -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/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';
|
|
@@ -53,6 +56,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
53
56
|
#retirementEpoch = 0;
|
|
54
57
|
#retirementArmed = false;
|
|
55
58
|
#disposeNativeEvents;
|
|
59
|
+
#disposeQrcodeError;
|
|
56
60
|
#resolveStartOnline;
|
|
57
61
|
#heldInbound = [];
|
|
58
62
|
#inflightInbound = new Set();
|
|
@@ -62,7 +66,15 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
62
66
|
loginControl;
|
|
63
67
|
#reloginTimer;
|
|
64
68
|
#reloginAttempts = 0;
|
|
69
|
+
/** Error code of the failure the automatic relogin is retrying (reported when it gives up). */
|
|
70
|
+
#lastAutoFailure = 'network';
|
|
65
71
|
#reloginInFlight;
|
|
72
|
+
/**
|
|
73
|
+
* Login flow of this Endpoint instance (ADR 0058). While a person answers a challenge no
|
|
74
|
+
* automatic relogin runs: a relogin starts a new SDK login, which issues a new challenge and
|
|
75
|
+
* voids the URL / ticket the operator is working on.
|
|
76
|
+
*/
|
|
77
|
+
#login;
|
|
66
78
|
#offlineNoticeAt = 0;
|
|
67
79
|
#challengeNoticeAt = 0;
|
|
68
80
|
#management = Object.freeze({
|
|
@@ -170,12 +182,28 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
170
182
|
get transportState() { return state(); },
|
|
171
183
|
management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
|
|
172
184
|
get content() { return content(); },
|
|
185
|
+
login: {
|
|
186
|
+
// The lease signal aborts on stop / retirement: never let a slow SDK call block it.
|
|
187
|
+
relogin: async ({ signal }) => { await (signal ? raceAbort(this.relogin(), signal) : this.relogin()); },
|
|
188
|
+
refreshChallenge: async ({ signal }) => {
|
|
189
|
+
await (signal ? raceAbort(this.refreshLoginChallenge(), signal) : this.refreshLoginChallenge());
|
|
190
|
+
},
|
|
191
|
+
},
|
|
173
192
|
});
|
|
193
|
+
// The SDK logs verification links (sid / sig) and the phone at `mark` level: redact them.
|
|
194
|
+
if (this.logger)
|
|
195
|
+
this.logger = redactingIcqqLogger(this.logger);
|
|
174
196
|
this.#contentResolver = new IcqqContentResolver(this);
|
|
175
197
|
this.#content = this.#contentResolver.port;
|
|
176
198
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
177
199
|
this.#options = options;
|
|
178
200
|
this.endpointName = options.config.id;
|
|
201
|
+
this.#login = new IcqqLoginDriver({
|
|
202
|
+
assist: options.loginAssist, client: this, endpointKey: this.endpointName, owner: this.#loginAssistOwner,
|
|
203
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
204
|
+
...(options.endpointIdentity ? { endpointIdentity: options.endpointIdentity } : {}),
|
|
205
|
+
logger: this.#logger, ...(options.locale ? { locale: options.locale } : {}),
|
|
206
|
+
});
|
|
179
207
|
this.loginControl = Object.freeze({
|
|
180
208
|
endpointId: this.endpointName,
|
|
181
209
|
state: () => ({
|
|
@@ -185,6 +213,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
185
213
|
pending: this.#pendingLoginTasks(),
|
|
186
214
|
}),
|
|
187
215
|
pending: () => this.#pendingLoginTasks(),
|
|
216
|
+
flow: () => this.#loginFlow(),
|
|
188
217
|
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
189
218
|
submit: (taskId, value) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
190
219
|
cancel: (taskId, reason) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
@@ -208,10 +237,21 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
208
237
|
// reconn_interval policy. No account-level logout or second retry timer.
|
|
209
238
|
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
210
239
|
}
|
|
211
|
-
/** Force a login attempt (
|
|
240
|
+
/** Force a login attempt (Console「重新登录」, recovery command): a new login flow. */
|
|
212
241
|
relogin() {
|
|
213
242
|
return this.#startRelogin(true);
|
|
214
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Console「刷新二维码」: a new QR code in place while a QR challenge is shown; any other
|
|
246
|
+
* challenge cannot be refreshed alone, so this logs in again.
|
|
247
|
+
*/
|
|
248
|
+
async refreshLoginChallenge() {
|
|
249
|
+
if (this.#stopped || this.#retiring || this.#options.signal?.aborted)
|
|
250
|
+
throw new Error('ICQQ endpoint 已停止');
|
|
251
|
+
if (await this.#login.refresh())
|
|
252
|
+
return;
|
|
253
|
+
await this.relogin();
|
|
254
|
+
}
|
|
215
255
|
#startRelogin(resetAttempts) {
|
|
216
256
|
if (this.#stopped || this.#retiring) {
|
|
217
257
|
return Promise.reject(new Error('ICQQ endpoint 已停止'));
|
|
@@ -220,7 +260,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
220
260
|
return this.#reloginInFlight;
|
|
221
261
|
if (resetAttempts)
|
|
222
262
|
this.#reloginAttempts = 0;
|
|
223
|
-
const task = this.#performRelogin();
|
|
263
|
+
const task = this.#performRelogin(resetAttempts);
|
|
224
264
|
this.#reloginInFlight = task;
|
|
225
265
|
void task.catch(() => undefined).finally(() => {
|
|
226
266
|
if (this.#reloginInFlight === task)
|
|
@@ -231,10 +271,15 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
231
271
|
});
|
|
232
272
|
return task;
|
|
233
273
|
}
|
|
234
|
-
async #performRelogin() {
|
|
274
|
+
async #performRelogin(manual) {
|
|
235
275
|
this.#clearReloginTimer();
|
|
236
276
|
this.cancelReconnect();
|
|
237
|
-
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
277
|
+
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName, manual }));
|
|
278
|
+
// A requested relogin is a new attempt; an automatic retry continues the current timeline.
|
|
279
|
+
if (manual)
|
|
280
|
+
this.#login.begin(t(this.#options.locale, 'loginReconnecting'));
|
|
281
|
+
else
|
|
282
|
+
this.#login.connect(t(this.#options.locale, 'loginReconnecting'));
|
|
238
283
|
try {
|
|
239
284
|
await this.login(this.#options.config.password);
|
|
240
285
|
}
|
|
@@ -247,15 +292,37 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
247
292
|
throw error;
|
|
248
293
|
}
|
|
249
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Automatic backoff relogin, only for failures that need no verification (network drop, kicked,
|
|
297
|
+
* a plain login that did not finish). Never while a person answers a challenge (ADR 0058 §6).
|
|
298
|
+
*/
|
|
250
299
|
#scheduleRelogin(reason) {
|
|
251
300
|
if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring)
|
|
252
301
|
return;
|
|
302
|
+
if (this.#login.busy)
|
|
303
|
+
return;
|
|
304
|
+
// A failed verification or an expired challenge waits for the person (next: relogin / refresh).
|
|
305
|
+
if (reason === 'relogin_failed' && this.#login.awaitingPerson)
|
|
306
|
+
return;
|
|
253
307
|
if (this.#reloginTimer || this.#reloginInFlight)
|
|
254
308
|
return;
|
|
255
|
-
if (this.#reloginAttempts >=
|
|
309
|
+
if (this.#reloginAttempts >= ICQQ_RELOGIN_ATTEMPTS) {
|
|
310
|
+
// Exhausted: the attempt ends and waits for the person.
|
|
311
|
+
if (!this.#login.awaitingPerson) {
|
|
312
|
+
const message = t(this.#options.locale, 'loginRetryExhausted', { attempts: ICQQ_RELOGIN_ATTEMPTS });
|
|
313
|
+
this.#login.fail(this.#lastAutoFailure, message, 'relogin');
|
|
314
|
+
this.#notifyOffline('relogin_exhausted', false, true);
|
|
315
|
+
}
|
|
256
316
|
return;
|
|
317
|
+
}
|
|
257
318
|
const attempt = this.#reloginAttempts++;
|
|
258
319
|
const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
|
|
320
|
+
const locale = this.#options.locale;
|
|
321
|
+
this.#login.info(t(locale, 'loginRetryScheduled', {
|
|
322
|
+
reason: t(locale, reason === 'kickoff' ? 'loginReasonKickoff' : reason === 'login_network' ? 'loginReasonNetwork' : 'loginReasonFailed'),
|
|
323
|
+
seconds: Math.round(delayMs / 1000),
|
|
324
|
+
attempt: attempt + 1,
|
|
325
|
+
}));
|
|
259
326
|
this.#logger.info(formatCompact({
|
|
260
327
|
op: 'icqq_relogin_scheduled',
|
|
261
328
|
endpoint: this.endpointName,
|
|
@@ -277,6 +344,18 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
277
344
|
clearTimeout(this.#reloginTimer);
|
|
278
345
|
this.#reloginTimer = undefined;
|
|
279
346
|
}
|
|
347
|
+
/** This incarnation's latest login flow (snapshot, including the active challenge's material). */
|
|
348
|
+
#loginFlow() {
|
|
349
|
+
if (this.#options.signal?.aborted)
|
|
350
|
+
return undefined;
|
|
351
|
+
const flow = this.#options.loginAssist.getFlow('icqq', this.endpointName);
|
|
352
|
+
const identity = this.#options.endpointIdentity;
|
|
353
|
+
if (!flow || !identity)
|
|
354
|
+
return flow;
|
|
355
|
+
const own = flow.endpointIdentity;
|
|
356
|
+
return own && own.canonicalId === identity.canonicalId && own.instanceId === identity.instanceId
|
|
357
|
+
&& own.epoch === identity.epoch && own.incarnationId === identity.incarnationId ? flow : undefined;
|
|
358
|
+
}
|
|
280
359
|
#pendingLoginTasks() {
|
|
281
360
|
if (this.#options.signal?.aborted)
|
|
282
361
|
return [];
|
|
@@ -288,12 +367,12 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
288
367
|
&& task.endpointIdentity.incarnationId === this.#options.endpointIdentity.incarnationId
|
|
289
368
|
: task.endpointKey === this.endpointName));
|
|
290
369
|
}
|
|
291
|
-
#notifyOffline(reason, willRetry) {
|
|
370
|
+
#notifyOffline(reason, willRetry, force = false) {
|
|
292
371
|
const notifier = this.#options.notifier;
|
|
293
372
|
if (!notifier?.configured)
|
|
294
373
|
return;
|
|
295
374
|
const now = Date.now();
|
|
296
|
-
if (now - this.#offlineNoticeAt < 60_000)
|
|
375
|
+
if (!force && now - this.#offlineNoticeAt < 60_000)
|
|
297
376
|
return;
|
|
298
377
|
this.#offlineNoticeAt = now;
|
|
299
378
|
const recovery = willRetry
|
|
@@ -323,18 +402,22 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
323
402
|
if (now - this.#challengeNoticeAt < 30_000)
|
|
324
403
|
return;
|
|
325
404
|
this.#challengeNoticeAt = now;
|
|
326
|
-
const
|
|
405
|
+
const id = this.endpointName;
|
|
406
|
+
const label = step === 'qrcode' ? '扫码登录'
|
|
327
407
|
: step === 'slider' ? '滑块验证'
|
|
328
|
-
: step === 'device' ? '
|
|
329
|
-
: '
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
408
|
+
: step === 'device' ? '设备锁验证'
|
|
409
|
+
: step === 'sms' ? '短信验证码'
|
|
410
|
+
: '身份验证';
|
|
411
|
+
const next = step === 'qrcode'
|
|
412
|
+
? `在 QQ 回复 \`icqq login qr ${id}\` 获取二维码,用手机 QQ 扫码并在手机上确认,登录会自动继续;没有继续时回复 \`icqq login confirm ${id}\`。`
|
|
413
|
+
: step === 'slider'
|
|
414
|
+
? `打开 Console 的 ICQQ 页面,用验证码辅助页完成滑块即可;也可以回复 \`icqq login status ${id}\` 查看滑块链接,再用 \`icqq login code ${id} <ticket>\` 提交。`
|
|
415
|
+
: step === 'device'
|
|
416
|
+
? `回复 \`icqq login status ${id}\` 查看验证链接,用手机完成验证后回复 \`icqq login confirm ${id}\`;改用短信验证码请回复 \`icqq login code ${id} sms\`。`
|
|
417
|
+
: step === 'sms'
|
|
418
|
+
? `收到短信后回复 \`icqq login code ${id} <6 位验证码>\`。`
|
|
419
|
+
: `需要复制设备信息到验证页,请打开 Console 的 ICQQ 页面按步骤完成;完成后点「已完成,继续登录」或回复 \`icqq login confirm ${id}\`。`;
|
|
420
|
+
void notifier.send(`[Zhin] ICQQ ${id} 需要${label}。\n${next}`).then((sent) => {
|
|
338
421
|
if (sent)
|
|
339
422
|
return;
|
|
340
423
|
this.#logger.warn(formatCompact({
|
|
@@ -397,6 +480,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
397
480
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
398
481
|
try {
|
|
399
482
|
this.#bindClientEvents();
|
|
483
|
+
this.#login.begin();
|
|
400
484
|
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
401
485
|
await onlineReady;
|
|
402
486
|
signal.throwIfAborted();
|
|
@@ -441,6 +525,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
441
525
|
this.#assertExternalLifecycle('stop');
|
|
442
526
|
this.#stopped = true;
|
|
443
527
|
this.#clearReloginTimer();
|
|
528
|
+
this.#login.invalidate();
|
|
444
529
|
this.#armRetirementFence();
|
|
445
530
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
446
531
|
this.#resolveStartOnline?.();
|
|
@@ -448,6 +533,8 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
448
533
|
this.#heldInbound.length = 0;
|
|
449
534
|
this.#disposeNativeEvents?.();
|
|
450
535
|
this.#disposeNativeEvents = undefined;
|
|
536
|
+
this.#disposeQrcodeError?.();
|
|
537
|
+
this.#disposeQrcodeError = undefined;
|
|
451
538
|
for (const event of BOUND_EVENTS) {
|
|
452
539
|
// The retirement guard owns this matcher until a late login handshake
|
|
453
540
|
// has finished. start()/open() replace it with the normal listener.
|
|
@@ -744,7 +831,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
744
831
|
this.#scheduleRelogin('kickoff');
|
|
745
832
|
});
|
|
746
833
|
this.on('system.login.qrcode', (event) => {
|
|
747
|
-
this.#logger.info('icqq
|
|
834
|
+
this.#logger.info('icqq 收到二维码登录事件,等待扫码(Console / 终端 / QQ 命令)');
|
|
748
835
|
this.#handleLoginChallenge('qrcode', event);
|
|
749
836
|
});
|
|
750
837
|
this.on('system.login.device', (event) => {
|
|
@@ -754,11 +841,24 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
754
841
|
this.#handleLoginChallenge('slider', event);
|
|
755
842
|
});
|
|
756
843
|
this.on('system.login.error', (event) => {
|
|
757
|
-
|
|
758
|
-
this.#logSystemEvent('login.error', event
|
|
759
|
-
|
|
760
|
-
|
|
844
|
+
const raw = event && typeof event === 'object' ? event : {};
|
|
845
|
+
this.#logSystemEvent('login.error', event, {
|
|
846
|
+
...(typeof raw.code === 'number' ? { code: raw.code } : {}),
|
|
847
|
+
message: sanitizeIcqqLoginMessage(raw.message),
|
|
848
|
+
});
|
|
849
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
|
|
850
|
+
return;
|
|
851
|
+
this.#handleLoginError(event);
|
|
852
|
+
});
|
|
853
|
+
// The SDK's second scan query (inside qrcodeLogin) failed: report it instead of verifying forever.
|
|
854
|
+
// Only our listener is removed on stop (the SDK has its own one on this internal event).
|
|
855
|
+
this.#disposeQrcodeError?.();
|
|
856
|
+
const disposeQrcodeError = this.on('internal.error.qrcode', (code, message) => {
|
|
857
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
|
|
858
|
+
return;
|
|
859
|
+
this.#login.qrcodeError(code, message);
|
|
761
860
|
});
|
|
861
|
+
this.#disposeQrcodeError = () => { disposeQrcodeError(); };
|
|
762
862
|
this.on('system.login.auth', (event) => {
|
|
763
863
|
this.#handleLoginChallenge('auth', event);
|
|
764
864
|
});
|
|
@@ -768,12 +868,12 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
768
868
|
if (this.#retiring)
|
|
769
869
|
return;
|
|
770
870
|
this.#everOnline = true;
|
|
871
|
+
this.#login.online();
|
|
771
872
|
this.#clearReloginTimer();
|
|
772
873
|
this.#reloginAttempts = 0;
|
|
773
874
|
this.#notifyRecovered();
|
|
774
875
|
try {
|
|
775
876
|
this.#resolveStartOnline?.();
|
|
776
|
-
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
777
877
|
try {
|
|
778
878
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
779
879
|
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
@@ -829,28 +929,50 @@ export class IcqqEndpoint extends ManagedIcqqClient {
|
|
|
829
929
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
830
930
|
? event
|
|
831
931
|
: null);
|
|
832
|
-
|
|
932
|
+
// Verification links carry a session id (sid / sig): logs get the challenge kind and host only.
|
|
933
|
+
this.#logSystemEvent(`login.${step}`, event, loginChallengeLogLine(step, payload));
|
|
833
934
|
this.#clearReloginTimer();
|
|
834
935
|
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
|
-
});
|
|
936
|
+
// The QR image is a Buffer, which serialization drops; the driver reads the raw event.
|
|
937
|
+
const raw = event && typeof event === 'object' ? event : {};
|
|
938
|
+
this.#login.challenge(step, { ...(payload ?? {}), ...raw });
|
|
847
939
|
}
|
|
848
|
-
|
|
849
|
-
|
|
940
|
+
/**
|
|
941
|
+
* `system.login.error` (ADR 0058 §5–6): a wrong code stays on its challenge; a verification
|
|
942
|
+
* failure ends the attempt and waits for the person (`next`). Only a plain login that failed on
|
|
943
|
+
* the network or for an unknown reason, without any challenge, keeps the automatic backoff.
|
|
944
|
+
*/
|
|
945
|
+
#handleLoginError(event) {
|
|
946
|
+
const { mapping, hadChallenge, retrying } = this.#login.loginError(event);
|
|
947
|
+
this.#logger.warn(formatCompact({
|
|
948
|
+
op: 'icqq_login_error', endpoint: this.endpointName, code: mapping.code,
|
|
949
|
+
...(mapping.platformCode !== undefined ? { platform_code: mapping.platformCode } : {}),
|
|
950
|
+
}));
|
|
951
|
+
if (retrying)
|
|
952
|
+
return;
|
|
953
|
+
const autoRetry = this.#options.config.autoReconnect
|
|
954
|
+
&& (mapping.code === 'network' || (mapping.code === 'unknown' && !hadChallenge));
|
|
955
|
+
this.#notifyOffline('login_error', autoRetry);
|
|
956
|
+
if (autoRetry) {
|
|
957
|
+
this.#login.outcome({ result: 'rejected', code: mapping.code, message: mapping.message, next: 'wait' });
|
|
958
|
+
this.#lastAutoFailure = mapping.code === 'network' ? 'network' : 'unknown';
|
|
959
|
+
this.#scheduleRelogin(mapping.code === 'network' ? 'login_network' : 'login_failed');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.#clearReloginTimer();
|
|
963
|
+
this.#login.fail(mapping.code, mapping.message, mapping.next);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* `safe` replaces the payload in the log line and the `system.receive` side event (login
|
|
967
|
+
* challenges keep only kind and host: links carry a sid, plus phone / device / QR image).
|
|
968
|
+
*/
|
|
969
|
+
#logSystemEvent(type, event, safe) {
|
|
970
|
+
const payload = safe === undefined ? serializeIcqqEvent(event) : safe;
|
|
971
|
+
const line = payload;
|
|
850
972
|
this.#logger.warn(formatCompact({
|
|
851
973
|
op: `system.${type}`,
|
|
852
974
|
endpoint: this.endpointName,
|
|
853
|
-
...(
|
|
975
|
+
...(line ? { event: JSON.stringify(line) } : {}),
|
|
854
976
|
}));
|
|
855
977
|
this.#dispatchIcqqSystemSideEvent(type, payload);
|
|
856
978
|
}
|
|
@@ -1262,6 +1384,11 @@ function resolveNativeClientConfig(config) {
|
|
|
1262
1384
|
reconn_interval: config.autoReconnect ? 5 : 0,
|
|
1263
1385
|
};
|
|
1264
1386
|
}
|
|
1387
|
+
/** Log line of a login challenge: its kind and the link's host, never the link, image or device. */
|
|
1388
|
+
export function loginChallengeLogLine(kind, payload) {
|
|
1389
|
+
const host = icqqLinkHost(payload?.url);
|
|
1390
|
+
return { kind, ...(host ? { host } : {}) };
|
|
1391
|
+
}
|
|
1265
1392
|
function serializeIcqqEvent(event) {
|
|
1266
1393
|
if (!event || typeof event !== 'object')
|
|
1267
1394
|
return null;
|
|
@@ -1286,4 +1413,6 @@ function serializeIcqqEvent(event) {
|
|
|
1286
1413
|
return Object.keys(out).length > 0 ? out : null;
|
|
1287
1414
|
}
|
|
1288
1415
|
const INBOUND_HOLD_LIMIT = 32;
|
|
1416
|
+
/** Automatic backoff relogins before the attempt fails and waits for the person. */
|
|
1417
|
+
const ICQQ_RELOGIN_ATTEMPTS = 8;
|
|
1289
1418
|
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;
|