From 5d0cdc421ad4b6a1dcc2d0d6e5c66e41b4e41218 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Mon, 9 Dec 2024 04:59:37 +0300 Subject: [PATCH] fix(core): handle MTPROTO_CLUSTER_INVALID --- packages/core/scripts/generate-client.cjs | 1 + packages/core/src/highlevel/base.ts | 5 ++ packages/core/src/highlevel/client.ts | 5 +- packages/core/src/highlevel/client.types.ts | 2 + .../methods/files/download-iterable.ts | 6 +++ packages/core/src/highlevel/worker/port.ts | 2 + packages/core/src/network/network-manager.ts | 53 +++++++++++++++++-- 7 files changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/core/scripts/generate-client.cjs b/packages/core/scripts/generate-client.cjs index d691ac05..d2d898df 100644 --- a/packages/core/scripts/generate-client.cjs +++ b/packages/core/scripts/generate-client.cjs @@ -733,6 +733,7 @@ withParams(params: RpcCallOptions): this\n`) 'computeNewPasswordHash', 'changePrimaryDc', 'getMtprotoMessageId', + 'recreateDc', ].forEach((name) => { output.write( `TelegramClient.prototype.${name} = function(...args) {\n` diff --git a/packages/core/src/highlevel/base.ts b/packages/core/src/highlevel/base.ts index d16f877f..7fb418d6 100644 --- a/packages/core/src/highlevel/base.ts +++ b/packages/core/src/highlevel/base.ts @@ -346,4 +346,9 @@ export class BaseTelegramClient implements ITelegramClient { async getMtprotoMessageId(): Promise { return this.mt.network.getMtprotoMessageId() } + + async recreateDc(dcId: number): Promise { + await this.mt.network.config.update(true) + await this.mt.network.recreateDc(dcId) + } } diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index 31ee3b18..ffbd52fb 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -3445,8 +3445,6 @@ export interface TelegramClient extends ITelegramClient { /** * Maximum message ID to return. * - * Unless {@link addOffset} is used, this will work the same as {@link offset}. - * * @default `0` (disabled). */ maxId?: number @@ -6946,3 +6944,6 @@ TelegramClient.prototype.changePrimaryDc = function (...args) { TelegramClient.prototype.getMtprotoMessageId = function (...args) { return this._client.getMtprotoMessageId(...args) } +TelegramClient.prototype.recreateDc = function (...args) { + return this._client.recreateDc(...args) +} diff --git a/packages/core/src/highlevel/client.types.ts b/packages/core/src/highlevel/client.types.ts index 221993d1..a621a703 100644 --- a/packages/core/src/highlevel/client.types.ts +++ b/packages/core/src/highlevel/client.types.ts @@ -72,4 +72,6 @@ export interface ITelegramClient { computeSrpParams(request: tl.account.RawPassword, password: string): Promise computeNewPasswordHash(algo: tl.TypePasswordKdfAlgo, password: string): Promise getMtprotoMessageId(): Promise + + recreateDc(dcId: number): Promise } diff --git a/packages/core/src/highlevel/methods/files/download-iterable.ts b/packages/core/src/highlevel/methods/files/download-iterable.ts index 7752fd9d..0f393111 100644 --- a/packages/core/src/highlevel/methods/files/download-iterable.ts +++ b/packages/core/src/highlevel/methods/files/download-iterable.ts @@ -145,6 +145,12 @@ export async function* downloadAsIterable( // todo: implement someday // see: https://github.com/LonamiWebs/Telethon/blob/0e8bd8248cc649637b7c392616887c50986427a0/telethon/client/downloads.py#L99 throw new MtUnsupportedError('File ref expired!') + } else if (e.is('MTPROTO_CLUSTER_INVALID') && dcId != null) { + // this is a weird error that happens when we are trying to download a file from a "wrong" media dc + // (e.g. this happens when we load media dc ip from session, but the current media dc ip is different) + client.log.debug('received cluster invalid error for dc %d, recreating dc', dcId) + await client.recreateDc(dcId) + return downloadChunk(chunk) } else { throw e } diff --git a/packages/core/src/highlevel/worker/port.ts b/packages/core/src/highlevel/worker/port.ts index bc9ec9b6..b01d2c46 100644 --- a/packages/core/src/highlevel/worker/port.ts +++ b/packages/core/src/highlevel/worker/port.ts @@ -56,6 +56,7 @@ export abstract class TelegramWorkerPort imp readonly startUpdatesLoop: ITelegramClient['startUpdatesLoop'] readonly stopUpdatesLoop: ITelegramClient['stopUpdatesLoop'] readonly getMtprotoMessageId: ITelegramClient['getMtprotoMessageId'] + readonly recreateDc: ITelegramClient['recreateDc'] private _abortController = new AbortController() readonly stopSignal: AbortSignal = this._abortController.signal @@ -92,6 +93,7 @@ export abstract class TelegramWorkerPort imp this.startUpdatesLoop = bind('startUpdatesLoop') this.stopUpdatesLoop = bind('stopUpdatesLoop') this.getMtprotoMessageId = bind('getMtprotoMessageId') + this.recreateDc = bind('recreateDc') this.timers = new TimersManager() this.timers.onError(err => this.onError.emit(unknownToError(err))) diff --git a/packages/core/src/network/network-manager.ts b/packages/core/src/network/network-manager.ts index b191d784..ab84fa37 100644 --- a/packages/core/src/network/network-manager.ts +++ b/packages/core/src/network/network-manager.ts @@ -487,6 +487,7 @@ export class NetworkManager { protected readonly _dcConnections: Map = new Map() protected _primaryDc?: DcConnectionManager + protected _primaryDcRecreationPromise?: Deferred private _updateHandler: (upd: tl.TypeUpdates, fromClient: boolean) => void @@ -804,7 +805,11 @@ export class NetworkManager { params?: RpcCallOptions, ): Promise => { if (!this._primaryDc) { - throw new MtcuteError('Not connected to any DC') + if (this._primaryDcRecreationPromise) { + await this._primaryDcRecreationPromise.promise + } else { + throw new MtcuteError('Not connected to any DC') + } } const kind = params?.kind ?? 'main' @@ -812,10 +817,10 @@ export class NetworkManager { if (params?.manager) { manager = params.manager - } else if (params?.dcId && params.dcId !== this._primaryDc.dcId) { + } else if (params?.dcId && params.dcId !== this._primaryDc!.dcId) { manager = await this._getOtherDc(params.dcId) } else { - manager = this._primaryDc + manager = this._primaryDc! } let multi = manager[kind] @@ -912,4 +917,46 @@ export class NetworkManager { getMtprotoMessageId(): Long { return this._primaryDc!.main._sessions[0].getMessageId() } + + async recreateDc(dcId: number): Promise { + this._log.debug('recreating dc %d', dcId) + const existing = this._dcConnections.get(dcId) + if (existing) { + await existing.destroy() + } + + this._dcConnections.delete(dcId) + + if (dcId === this._primaryDc?.dcId) { + const oldPrimaryDc = this._primaryDc + this._primaryDc = undefined + this._primaryDcRecreationPromise = new Deferred() + + try { + const newDefaultDcs: DcOptions = { + main: asNonNull(await this.config.findOption({ + dcId, + allowIpv6: this.params.useIpv6, + })), + media: asNonNull(await this.config.findOption({ + dcId, + allowIpv6: this.params.useIpv6, + allowMedia: true, + preferMedia: true, + })), + } + + await this._storage.dcs.store(newDefaultDcs) + await this.connect(newDefaultDcs) + this._primaryDcRecreationPromise.resolve() + this._primaryDcRecreationPromise = undefined + } catch (e) { + // restore old primary dc to avoid a deadlock + this._primaryDc = oldPrimaryDc + this._primaryDcRecreationPromise?.resolve() + this._primaryDcRecreationPromise = undefined + throw e + } + } + } }