2021-08-05 20:38:24 +03:00
|
|
|
import { tl } from '@mtcute/tl'
|
2021-04-08 12:19:38 +03:00
|
|
|
import {
|
|
|
|
CryptoProviderFactory,
|
|
|
|
ICryptoProvider,
|
|
|
|
defaultCryptoProviderFactory,
|
2021-11-23 00:03:59 +03:00
|
|
|
sleep,
|
|
|
|
getAllPeersFrom,
|
|
|
|
encodeUrlSafeBase64,
|
|
|
|
parseUrlSafeBase64,
|
|
|
|
LogManager,
|
|
|
|
toggleChannelIdMark,
|
|
|
|
} from './utils'
|
2021-04-08 12:19:38 +03:00
|
|
|
import {
|
|
|
|
TransportFactory,
|
|
|
|
defaultReconnectionStrategy,
|
|
|
|
ReconnectionStrategy,
|
|
|
|
defaultTransportFactory,
|
2021-11-23 00:03:59 +03:00
|
|
|
SessionConnection,
|
2021-04-08 12:19:38 +03:00
|
|
|
} from './network'
|
|
|
|
import { PersistentConnectionParams } from './network/persistent-connection'
|
|
|
|
import {
|
|
|
|
defaultProductionDc,
|
|
|
|
defaultProductionIpv6Dc,
|
2021-07-27 15:32:18 +03:00
|
|
|
defaultTestDc,
|
|
|
|
defaultTestIpv6Dc,
|
2021-04-08 12:19:38 +03:00
|
|
|
} from './utils/default-dcs'
|
|
|
|
import { addPublicKey } from './utils/crypto/keys'
|
|
|
|
import { ITelegramStorage, MemoryStorage } from './storage'
|
2021-07-17 17:26:31 +03:00
|
|
|
import EventEmitter from 'events'
|
2021-11-23 00:03:59 +03:00
|
|
|
import Long from 'long'
|
|
|
|
import {
|
|
|
|
ControllablePromise,
|
|
|
|
createControllablePromise,
|
|
|
|
} from './utils/controllable-promise'
|
|
|
|
import {
|
|
|
|
TlBinaryReader,
|
|
|
|
TlBinaryWriter,
|
|
|
|
TlReaderMap,
|
|
|
|
TlWriterMap,
|
|
|
|
} from '@mtcute/tl-runtime'
|
|
|
|
|
|
|
|
import defaultReaderMap from '@mtcute/tl/binary/reader'
|
|
|
|
import defaultWriterMap from '@mtcute/tl/binary/writer'
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
export namespace BaseTelegramClient {
|
|
|
|
export interface Options {
|
|
|
|
/**
|
|
|
|
* API ID from my.telegram.org
|
|
|
|
*/
|
|
|
|
apiId: number | string
|
|
|
|
/**
|
|
|
|
* API hash from my.telegram.org
|
|
|
|
*/
|
|
|
|
apiHash: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Telegram storage to use.
|
|
|
|
* If omitted, {@link MemoryStorage} is used
|
|
|
|
*/
|
|
|
|
storage?: ITelegramStorage
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cryptography provider factory to allow delegating
|
|
|
|
* crypto to native addon, worker, etc.
|
|
|
|
*/
|
|
|
|
crypto?: CryptoProviderFactory
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to use IPv6 datacenters
|
|
|
|
* (IPv6 will be preferred when choosing a DC by id)
|
|
|
|
* (default: false)
|
|
|
|
*/
|
|
|
|
useIpv6?: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Primary DC to use for initial connection.
|
|
|
|
* This does not mean this will be the only DC used,
|
|
|
|
* nor that this DC will actually be primary, this only
|
|
|
|
* determines the first DC the library will try to connect to.
|
|
|
|
* Can be used to connect to other networks (like test DCs).
|
|
|
|
*
|
|
|
|
* When session already contains primary DC, this parameter is ignored.
|
|
|
|
* Defaults to Production DC 2.
|
|
|
|
*/
|
|
|
|
primaryDc?: tl.RawDcOption
|
|
|
|
|
2021-07-27 15:32:18 +03:00
|
|
|
/**
|
|
|
|
* Whether to connect to test servers.
|
|
|
|
*
|
|
|
|
* If passed, {@link primaryDc} defaults to Test DC 2.
|
|
|
|
*
|
|
|
|
* **Must** be passed if using test servers, even if
|
|
|
|
* you passed custom {@link primaryDc}
|
|
|
|
*/
|
|
|
|
testMode?: boolean
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
|
|
|
* Additional options for initConnection call.
|
|
|
|
* `apiId` and `query` are not available and will be ignored.
|
|
|
|
* Omitted values will be filled with defaults
|
|
|
|
*/
|
|
|
|
initConnectionOptions?: Partial<
|
|
|
|
Omit<tl.RawInitConnectionRequest, 'apiId' | 'query'>
|
|
|
|
>
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transport factory to use in the client.
|
|
|
|
* Defaults to platform-specific transport: WebSocket on the web, TCP in node
|
|
|
|
*/
|
|
|
|
transport?: TransportFactory
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reconnection strategy.
|
|
|
|
* Defaults to simple reconnection strategy: first 0ms, then up to 5s (increasing by 1s)
|
|
|
|
*/
|
|
|
|
reconnectionStrategy?: ReconnectionStrategy<PersistentConnectionParams>
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum duration of a flood_wait that will be waited automatically.
|
|
|
|
* Flood waits above this threshold will throw a FloodWaitError.
|
|
|
|
* Set to 0 to disable. Can be overridden with `throwFlood` parameter in call() params
|
|
|
|
*
|
|
|
|
* @default 10000
|
|
|
|
*/
|
|
|
|
floodSleepThreshold?: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum number of retries when calling RPC methods.
|
|
|
|
* Call is retried when InternalError or FloodWaitError is encountered.
|
|
|
|
* Can be set to Infinity.
|
|
|
|
*
|
|
|
|
* @default 5
|
|
|
|
*/
|
|
|
|
rpcRetryCount?: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, every single API call will be wrapped with `tl.invokeWithoutUpdates`,
|
|
|
|
* effectively disabling the server-sent events for the clients.
|
|
|
|
* May be useful in some cases.
|
|
|
|
*
|
|
|
|
* Note that this only wraps calls made with `.call()` within the primary
|
|
|
|
* connection. Additional connections and direct `.sendForResult()` calls
|
|
|
|
* must be wrapped manually.
|
|
|
|
*
|
|
|
|
* @default false
|
|
|
|
*/
|
|
|
|
disableUpdates?: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, RPC errors will have a stack trace of the initial `.call()`
|
|
|
|
* or `.sendForResult()` call position, which drastically improves
|
|
|
|
* debugging experience.<br>
|
2021-08-05 20:38:24 +03:00
|
|
|
* If false, they will have a stack trace of MTCute internals.
|
2021-04-08 12:19:38 +03:00
|
|
|
*
|
|
|
|
* Internally this creates a stack capture before every RPC call
|
|
|
|
* and stores it until the result is received. This might
|
|
|
|
* use a lot more memory than normal, thus can be disabled here.
|
|
|
|
*
|
|
|
|
* @default true
|
|
|
|
*/
|
|
|
|
niceStacks?: boolean
|
2021-07-24 17:00:20 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* **EXPERT USE ONLY!**
|
|
|
|
*
|
|
|
|
* Override TL layer used for the connection. /;'
|
|
|
|
*
|
|
|
|
* **Does not** change the schema used.
|
|
|
|
*/
|
|
|
|
overrideLayer?: number
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* **EXPERT USE ONLY**
|
|
|
|
*
|
|
|
|
* Override reader map used for the connection.
|
|
|
|
*/
|
|
|
|
readerMap?: TlReaderMap
|
|
|
|
|
|
|
|
/**
|
|
|
|
* **EXPERT USE ONLY**
|
|
|
|
*
|
|
|
|
* Override writer map used for the connection.
|
|
|
|
*/
|
|
|
|
writerMap?: TlWriterMap
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-17 17:26:31 +03:00
|
|
|
export class BaseTelegramClient extends EventEmitter {
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
|
|
|
* `initConnection` params taken from {@link BaseTelegramClient.Options.initConnectionOptions}.
|
|
|
|
*/
|
|
|
|
protected readonly _initConnectionParams: tl.RawInitConnectionRequest
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Crypto provider taken from {@link BaseTelegramClient.Options.crypto}
|
|
|
|
*/
|
|
|
|
protected readonly _crypto: ICryptoProvider
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transport factory taken from {@link BaseTelegramClient.Options.transport}
|
|
|
|
*/
|
|
|
|
protected readonly _transportFactory: TransportFactory
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Telegram storage taken from {@link BaseTelegramClient.Options.storage}
|
|
|
|
*/
|
|
|
|
readonly storage: ITelegramStorage
|
|
|
|
|
|
|
|
/**
|
|
|
|
* API hash taken from {@link BaseTelegramClient.Options.apiHash}
|
|
|
|
*/
|
|
|
|
protected readonly _apiHash: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* "Use IPv6" taken from {@link BaseTelegramClient.Options.useIpv6}
|
|
|
|
*/
|
|
|
|
protected readonly _useIpv6: boolean
|
|
|
|
|
2021-07-27 15:32:18 +03:00
|
|
|
/**
|
|
|
|
* "Test mode" taken from {@link BaseTelegramClient.Options.testMode}
|
|
|
|
*/
|
|
|
|
protected readonly _testMode: boolean
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
|
|
|
* Reconnection strategy taken from {@link BaseTelegramClient.Options.reconnectionStrategy}
|
|
|
|
*/
|
|
|
|
protected readonly _reconnectionStrategy: ReconnectionStrategy<PersistentConnectionParams>
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flood sleep threshold taken from {@link BaseTelegramClient.Options.floodSleepThreshold}
|
|
|
|
*/
|
|
|
|
protected readonly _floodSleepThreshold: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* RPC retry count taken from {@link BaseTelegramClient.Options.rpcRetryCount}
|
|
|
|
*/
|
|
|
|
protected readonly _rpcRetryCount: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* "Disable updates" taken from {@link BaseTelegramClient.Options.disableUpdates}
|
|
|
|
*/
|
|
|
|
protected readonly _disableUpdates: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Primary DC taken from {@link BaseTelegramClient.Options.primaryDc},
|
|
|
|
* loaded from session or changed by other means (like redirecting).
|
|
|
|
*/
|
|
|
|
protected _primaryDc: tl.RawDcOption
|
|
|
|
|
|
|
|
private _niceStacks: boolean
|
2021-07-24 17:00:20 +03:00
|
|
|
readonly _layer: number
|
2021-11-23 00:03:59 +03:00
|
|
|
readonly _readerMap: TlReaderMap
|
|
|
|
readonly _writerMap: TlWriterMap
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-07-30 17:40:50 +03:00
|
|
|
private _keepAliveInterval?: NodeJS.Timeout
|
2021-08-05 20:14:19 +03:00
|
|
|
protected _lastUpdateTime = 0
|
2021-04-08 12:19:38 +03:00
|
|
|
private _floodWaitedRequests: Record<string, number> = {}
|
|
|
|
|
2021-04-18 16:23:25 +03:00
|
|
|
protected _config?: tl.RawConfig
|
|
|
|
protected _cdnConfig?: tl.RawCdnConfig
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
private _additionalConnections: SessionConnection[] = []
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
// not really connected, but rather "connect() was called"
|
2021-11-23 00:03:59 +03:00
|
|
|
private _connected: ControllablePromise<void> | boolean = false
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
private _onError?: (err: Error, connection?: SessionConnection) => void
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
/**
|
2021-11-23 00:03:59 +03:00
|
|
|
* The primary {@link SessionConnection} that is used for
|
2021-04-08 12:19:38 +03:00
|
|
|
* most of the communication with Telegram.
|
|
|
|
*
|
|
|
|
* Methods for downloading/uploading files may create additional connections as needed.
|
|
|
|
*/
|
2021-11-23 00:03:59 +03:00
|
|
|
primaryConnection!: SessionConnection
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-06-05 20:25:08 +03:00
|
|
|
private _importFrom?: string
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
|
|
|
* Method which is called every time the client receives a new update.
|
|
|
|
*
|
|
|
|
* User of the class is expected to override it and handle the given update
|
|
|
|
*
|
|
|
|
* @param update Raw update object sent by Telegram
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
protected _handleUpdate(update: tl.TypeUpdates): void {}
|
|
|
|
|
2021-08-14 12:57:26 +03:00
|
|
|
readonly log = new LogManager()
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
constructor(opts: BaseTelegramClient.Options) {
|
2021-07-17 17:26:31 +03:00
|
|
|
super()
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
const apiId =
|
|
|
|
typeof opts.apiId === 'string' ? parseInt(opts.apiId) : opts.apiId
|
|
|
|
if (isNaN(apiId))
|
|
|
|
throw new Error('apiId must be a number or a numeric string!')
|
|
|
|
|
|
|
|
this._transportFactory = opts.transport ?? defaultTransportFactory
|
|
|
|
this._crypto = (opts.crypto ?? defaultCryptoProviderFactory)()
|
|
|
|
this.storage = opts.storage ?? new MemoryStorage()
|
|
|
|
this._apiHash = opts.apiHash
|
|
|
|
this._useIpv6 = !!opts.useIpv6
|
2021-07-27 15:32:18 +03:00
|
|
|
this._testMode = !!opts.testMode
|
2021-04-08 12:19:38 +03:00
|
|
|
this._primaryDc =
|
|
|
|
opts.primaryDc ??
|
2021-07-27 15:32:18 +03:00
|
|
|
(this._testMode
|
|
|
|
? this._useIpv6
|
|
|
|
? defaultTestIpv6Dc
|
|
|
|
: defaultTestDc
|
|
|
|
: this._useIpv6
|
|
|
|
? defaultProductionIpv6Dc
|
|
|
|
: defaultProductionDc)
|
2021-04-08 12:19:38 +03:00
|
|
|
this._reconnectionStrategy =
|
|
|
|
opts.reconnectionStrategy ?? defaultReconnectionStrategy
|
|
|
|
this._floodSleepThreshold = opts.floodSleepThreshold ?? 10000
|
|
|
|
this._rpcRetryCount = opts.rpcRetryCount ?? 5
|
|
|
|
this._disableUpdates = opts.disableUpdates ?? false
|
|
|
|
this._niceStacks = opts.niceStacks ?? true
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
this._layer = opts.overrideLayer ?? tl.LAYER
|
|
|
|
this._readerMap = opts.readerMap ?? defaultReaderMap
|
|
|
|
this._writerMap = opts.writerMap ?? defaultWriterMap
|
2021-08-14 12:57:26 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
this.storage.setup?.(this.log, this._readerMap, this._writerMap)
|
2021-07-24 17:00:20 +03:00
|
|
|
|
2021-08-05 20:38:24 +03:00
|
|
|
let deviceModel = 'MTCute on '
|
2021-04-08 12:19:38 +03:00
|
|
|
if (typeof process !== 'undefined' && typeof require !== 'undefined') {
|
|
|
|
const os = require('os')
|
|
|
|
deviceModel += `${os.type()} ${os.arch()} ${os.release()}`
|
|
|
|
} else if (typeof navigator !== 'undefined') {
|
|
|
|
deviceModel += navigator.userAgent
|
|
|
|
} else deviceModel += 'unknown'
|
|
|
|
|
|
|
|
this._initConnectionParams = {
|
|
|
|
_: 'initConnection',
|
|
|
|
deviceModel,
|
|
|
|
systemVersion: '1.0',
|
|
|
|
appVersion: '1.0.0',
|
|
|
|
systemLangCode: 'en',
|
|
|
|
langPack: '', // "langPacks are for official apps only"
|
|
|
|
langCode: 'en',
|
|
|
|
...(opts.initConnectionOptions ?? {}),
|
|
|
|
apiId,
|
|
|
|
query: null,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-17 18:56:51 +03:00
|
|
|
protected async _loadStorage(): Promise<void> {
|
|
|
|
await this.storage.load?.()
|
|
|
|
}
|
|
|
|
|
2021-06-06 15:20:41 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2021-06-05 20:25:08 +03:00
|
|
|
protected async _saveStorage(afterImport = false): Promise<void> {
|
2021-04-17 18:56:51 +03:00
|
|
|
await this.storage.save?.()
|
|
|
|
}
|
|
|
|
|
2021-08-05 20:14:19 +03:00
|
|
|
protected _keepAliveAction(): void {
|
2021-11-23 00:03:59 +03:00
|
|
|
if (this._disableUpdates) return
|
|
|
|
|
2021-08-05 20:14:19 +03:00
|
|
|
// telegram asks to fetch pending updates
|
|
|
|
// if there are no updates for 15 minutes.
|
|
|
|
// core does not have update handling,
|
|
|
|
// so we just use getState so the server knows
|
|
|
|
// we still do need updates
|
|
|
|
this.call({ _: 'updates.getState' }).catch((e) => {
|
2022-04-01 22:17:10 +03:00
|
|
|
if (!(e instanceof tl.errors.RpcError)) {
|
2021-08-05 20:14:19 +03:00
|
|
|
this.primaryConnection.reconnect()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
private _cleanupPrimaryConnection(forever = false): void {
|
|
|
|
if (forever && this.primaryConnection) this.primaryConnection.destroy()
|
|
|
|
if (this._keepAliveInterval) clearInterval(this._keepAliveInterval)
|
|
|
|
}
|
|
|
|
|
|
|
|
private _setupPrimaryConnection(): void {
|
|
|
|
this._cleanupPrimaryConnection(true)
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
this.primaryConnection = new SessionConnection(
|
|
|
|
{
|
|
|
|
crypto: this._crypto,
|
|
|
|
initConnection: this._initConnectionParams,
|
|
|
|
transportFactory: this._transportFactory,
|
|
|
|
dc: this._primaryDc,
|
|
|
|
testMode: this._testMode,
|
|
|
|
reconnectionStrategy: this._reconnectionStrategy,
|
|
|
|
layer: this._layer,
|
|
|
|
disableUpdates: this._disableUpdates,
|
|
|
|
readerMap: this._readerMap,
|
|
|
|
writerMap: this._writerMap,
|
|
|
|
},
|
|
|
|
this.log.create('connection')
|
|
|
|
)
|
|
|
|
|
|
|
|
this.primaryConnection.on('usable', async () => {
|
2021-08-05 20:14:19 +03:00
|
|
|
this._lastUpdateTime = Date.now()
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
this._keepAliveInterval = setInterval(async () => {
|
2021-08-05 20:14:19 +03:00
|
|
|
if (Date.now() - this._lastUpdateTime > 900_000) {
|
|
|
|
this._keepAliveAction()
|
|
|
|
this._lastUpdateTime = Date.now()
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
}, 60_000)
|
|
|
|
})
|
|
|
|
this.primaryConnection.on('update', (update) => {
|
2021-08-05 20:14:19 +03:00
|
|
|
this._lastUpdateTime = Date.now()
|
2021-04-08 12:19:38 +03:00
|
|
|
this._handleUpdate(update)
|
|
|
|
})
|
|
|
|
this.primaryConnection.on('wait', () =>
|
|
|
|
this._cleanupPrimaryConnection()
|
|
|
|
)
|
|
|
|
this.primaryConnection.on('key-change', async (key) => {
|
|
|
|
this.storage.setAuthKeyFor(this._primaryDc.id, key)
|
2021-04-17 18:56:51 +03:00
|
|
|
await this._saveStorage()
|
2021-04-08 12:19:38 +03:00
|
|
|
})
|
2021-06-05 18:56:43 +03:00
|
|
|
this.primaryConnection.on('error', (err) =>
|
|
|
|
this._emitError(err, this.primaryConnection)
|
|
|
|
)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the connection to the primary DC.
|
|
|
|
*
|
|
|
|
* You shouldn't usually call this method directly as it is called
|
|
|
|
* implicitly the first time you call {@link call}.
|
|
|
|
*/
|
|
|
|
async connect(): Promise<void> {
|
2021-11-23 00:03:59 +03:00
|
|
|
if (this._connected) {
|
|
|
|
await this._connected
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this._connected = createControllablePromise()
|
|
|
|
|
2021-04-17 18:56:51 +03:00
|
|
|
await this._loadStorage()
|
2021-04-08 12:19:38 +03:00
|
|
|
const primaryDc = await this.storage.getDefaultDc()
|
|
|
|
if (primaryDc !== null) this._primaryDc = primaryDc
|
|
|
|
|
|
|
|
this._setupPrimaryConnection()
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
await this.primaryConnection.setupKeys(
|
|
|
|
await this.storage.getAuthKeyFor(this._primaryDc.id)
|
2021-04-08 12:19:38 +03:00
|
|
|
)
|
2021-06-05 20:25:08 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!this.primaryConnection.getAuthKey() && this._importFrom) {
|
2021-06-05 20:25:08 +03:00
|
|
|
const buf = parseUrlSafeBase64(this._importFrom)
|
2021-06-06 15:20:41 +03:00
|
|
|
if (buf[0] !== 1)
|
|
|
|
throw new Error(`Invalid session string (version = ${buf[0]})`)
|
2021-06-05 20:25:08 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const reader = new TlBinaryReader(this._readerMap, buf, 1)
|
2021-06-05 20:25:08 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const flags = reader.int()
|
2021-06-05 20:25:08 +03:00
|
|
|
const hasSelf = flags & 1
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!(flags & 2) !== !this._testMode) {
|
|
|
|
throw new Error(
|
|
|
|
'This session string is not for the current backend'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-06-05 20:25:08 +03:00
|
|
|
const primaryDc = reader.object()
|
|
|
|
if (primaryDc._ !== 'dcOption') {
|
2021-06-06 15:20:41 +03:00
|
|
|
throw new Error(
|
|
|
|
`Invalid session string (dc._ = ${primaryDc._})`
|
|
|
|
)
|
2021-06-05 20:25:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
this._primaryDc = this.primaryConnection.params.dc = primaryDc
|
|
|
|
await this.storage.setDefaultDc(primaryDc)
|
|
|
|
|
|
|
|
if (hasSelf) {
|
2021-11-23 00:03:59 +03:00
|
|
|
const selfId = reader.int53()
|
2021-06-05 20:25:08 +03:00
|
|
|
const selfBot = reader.boolean()
|
|
|
|
|
|
|
|
await this.storage.setSelf({ userId: selfId, isBot: selfBot })
|
|
|
|
}
|
|
|
|
|
|
|
|
const key = reader.bytes()
|
2021-11-23 00:03:59 +03:00
|
|
|
await this.primaryConnection.setupKeys(key)
|
2021-06-05 20:25:08 +03:00
|
|
|
await this.storage.setAuthKeyFor(primaryDc.id, key)
|
|
|
|
|
|
|
|
await this._saveStorage(true)
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
this._connected.resolve()
|
|
|
|
this._connected = true
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
this.primaryConnection.connect()
|
|
|
|
}
|
|
|
|
|
2021-06-15 03:11:11 +03:00
|
|
|
/**
|
|
|
|
* Wait until this client is usable (i.e. connection is fully ready)
|
|
|
|
*/
|
|
|
|
async waitUntilUsable(): Promise<void> {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.primaryConnection.once('usable', resolve)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
/**
|
|
|
|
* Additional cleanup for subclasses.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _onClose(): void {}
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
|
|
|
* Close all connections and finalize the client.
|
|
|
|
*/
|
|
|
|
async close(): Promise<void> {
|
2021-11-23 00:03:59 +03:00
|
|
|
await this._onClose()
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
this._cleanupPrimaryConnection(true)
|
|
|
|
// close additional connections
|
|
|
|
this._additionalConnections.forEach((conn) => conn.destroy())
|
|
|
|
|
2021-04-17 18:56:51 +03:00
|
|
|
await this._saveStorage()
|
2021-04-08 12:19:38 +03:00
|
|
|
await this.storage.destroy?.()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function to find the DC by its ID.
|
|
|
|
*
|
|
|
|
* @param id Datacenter ID
|
2021-11-23 00:03:59 +03:00
|
|
|
* @param preferMedia Whether to prefer media-only DCs
|
2021-04-08 12:19:38 +03:00
|
|
|
* @param cdn Whether the needed DC is a CDN DC
|
|
|
|
*/
|
2021-11-23 00:03:59 +03:00
|
|
|
async getDcById(
|
|
|
|
id: number,
|
|
|
|
preferMedia = false,
|
|
|
|
cdn = false
|
|
|
|
): Promise<tl.RawDcOption> {
|
2021-04-08 12:19:38 +03:00
|
|
|
if (!this._config) {
|
|
|
|
this._config = await this.call({ _: 'help.getConfig' })
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cdn && !this._cdnConfig) {
|
|
|
|
this._cdnConfig = await this.call({ _: 'help.getCdnConfig' })
|
|
|
|
for (const key of this._cdnConfig.publicKeys) {
|
|
|
|
await addPublicKey(this._crypto, key.publicKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._useIpv6) {
|
|
|
|
// first try to find ipv6 dc
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
let found
|
|
|
|
if (preferMedia) {
|
|
|
|
found = this._config.dcOptions.find(
|
|
|
|
(it) =>
|
|
|
|
it.id === id &&
|
|
|
|
it.mediaOnly &&
|
|
|
|
it.cdn === cdn &&
|
|
|
|
it.ipv6 &&
|
|
|
|
!it.tcpoOnly
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found)
|
|
|
|
found = this._config.dcOptions.find(
|
|
|
|
(it) =>
|
|
|
|
it.id === id &&
|
|
|
|
it.cdn === cdn &&
|
|
|
|
it.ipv6 &&
|
|
|
|
!it.tcpoOnly
|
|
|
|
)
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
if (found) return found
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
let found
|
|
|
|
if (preferMedia) {
|
|
|
|
found = this._config.dcOptions.find(
|
|
|
|
(it) =>
|
|
|
|
it.id === id &&
|
|
|
|
it.mediaOnly &&
|
|
|
|
it.cdn === cdn &&
|
|
|
|
!it.tcpoOnly
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (!found)
|
|
|
|
found = this._config.dcOptions.find(
|
|
|
|
(it) => it.id === id && it.cdn === cdn && !it.tcpoOnly
|
|
|
|
)
|
2021-04-08 12:19:38 +03:00
|
|
|
if (found) return found
|
|
|
|
|
|
|
|
throw new Error(`Could not find${cdn ? ' CDN' : ''} DC ${id}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change primary DC and write that fact to the storage.
|
|
|
|
* Will immediately reconnect to another DC.
|
|
|
|
*
|
|
|
|
* @param newDc New DC or its ID
|
|
|
|
*/
|
|
|
|
async changeDc(newDc: tl.RawDcOption | number): Promise<void> {
|
|
|
|
if (typeof newDc === 'number') {
|
|
|
|
newDc = await this.getDcById(newDc)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._primaryDc = newDc
|
|
|
|
await this.storage.setDefaultDc(newDc)
|
2021-04-17 18:56:51 +03:00
|
|
|
await this._saveStorage()
|
2021-11-23 00:03:59 +03:00
|
|
|
await this.primaryConnection.changeDc(newDc)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Make an RPC call to the primary DC.
|
|
|
|
* This method handles DC migration, flood waits and retries automatically.
|
|
|
|
*
|
|
|
|
* If you want more low-level control, use
|
|
|
|
* `primaryConnection.sendForResult()` (which is what this method wraps)
|
|
|
|
*
|
|
|
|
* This method is still quite low-level and you shouldn't use this
|
2021-08-05 20:38:24 +03:00
|
|
|
* when using high-level API provided by `@mtcute/client`.
|
2021-04-08 12:19:38 +03:00
|
|
|
*
|
|
|
|
* @param message RPC method to call
|
|
|
|
* @param params Additional call parameters
|
|
|
|
*/
|
|
|
|
async call<T extends tl.RpcMethod>(
|
|
|
|
message: T,
|
|
|
|
params?: {
|
2021-05-04 14:07:40 +03:00
|
|
|
throwFlood?: boolean
|
2021-11-23 00:03:59 +03:00
|
|
|
connection?: SessionConnection
|
2021-05-16 02:52:13 +03:00
|
|
|
timeout?: number
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
): Promise<tl.RpcCallReturn[T['_']]> {
|
2021-11-23 00:03:59 +03:00
|
|
|
if (this._connected !== true) {
|
2021-04-08 12:19:38 +03:00
|
|
|
await this.connect()
|
|
|
|
}
|
|
|
|
|
|
|
|
// do not send requests that are in flood wait
|
|
|
|
if (message._ in this._floodWaitedRequests) {
|
|
|
|
const delta = Date.now() - this._floodWaitedRequests[message._]
|
|
|
|
if (delta <= 3000) {
|
|
|
|
// flood waits below 3 seconds are "ignored"
|
|
|
|
delete this._floodWaitedRequests[message._]
|
|
|
|
} else if (delta <= this._floodSleepThreshold) {
|
|
|
|
await sleep(delta)
|
|
|
|
delete this._floodWaitedRequests[message._]
|
|
|
|
} else {
|
2022-04-01 22:17:10 +03:00
|
|
|
throw new tl.errors.FloodWaitXError(delta / 1000)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 18:24:16 +03:00
|
|
|
const connection = params?.connection ?? this.primaryConnection
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
let lastError: Error | null = null
|
|
|
|
const stack = this._niceStacks ? new Error().stack : undefined
|
|
|
|
|
|
|
|
for (let i = 0; i < this._rpcRetryCount; i++) {
|
|
|
|
try {
|
2021-11-23 00:03:59 +03:00
|
|
|
const res = await connection.sendRpc(
|
2021-06-05 18:56:43 +03:00
|
|
|
message,
|
|
|
|
stack,
|
|
|
|
params?.timeout
|
|
|
|
)
|
2021-04-08 12:19:38 +03:00
|
|
|
await this._cachePeersFrom(res)
|
|
|
|
|
|
|
|
return res
|
|
|
|
} catch (e) {
|
|
|
|
lastError = e
|
|
|
|
|
2022-04-01 22:17:10 +03:00
|
|
|
if (e instanceof tl.errors.InternalError) {
|
2021-11-23 00:03:59 +03:00
|
|
|
this.log.warn('Telegram is having internal issues: %s', e)
|
2021-04-24 20:13:36 +03:00
|
|
|
if (e.message === 'WORKER_BUSY_TOO_LONG_RETRY') {
|
|
|
|
// according to tdlib, "it is dangerous to resend query without timeout, so use 1"
|
|
|
|
await sleep(1000)
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
2022-04-01 22:17:10 +03:00
|
|
|
e.constructor === tl.errors.FloodWaitXError ||
|
|
|
|
e.constructor === tl.errors.SlowmodeWaitXError ||
|
|
|
|
e.constructor === tl.errors.FloodTestPhoneWaitXError
|
2021-04-08 12:19:38 +03:00
|
|
|
) {
|
2022-04-01 22:17:10 +03:00
|
|
|
if (e.constructor !== tl.errors.SlowmodeWaitXError) {
|
2021-04-08 12:19:38 +03:00
|
|
|
// SLOW_MODE_WAIT is chat-specific, not request-specific
|
|
|
|
this._floodWaitedRequests[message._] =
|
|
|
|
Date.now() + e.seconds * 1000
|
|
|
|
}
|
|
|
|
|
|
|
|
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
|
|
|
|
// such a short amount will cause retries very fast leading to issues
|
|
|
|
if (e.seconds === 0) {
|
2022-04-01 22:17:10 +03:00
|
|
|
;(e as any).seconds = 1
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
params?.throwFlood !== true &&
|
|
|
|
e.seconds <= this._floodSleepThreshold
|
|
|
|
) {
|
2021-11-23 00:03:59 +03:00
|
|
|
this.log.info('Flood wait for %d seconds', e.seconds)
|
2021-04-08 12:19:38 +03:00
|
|
|
await sleep(e.seconds * 1000)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2021-06-05 18:56:43 +03:00
|
|
|
|
|
|
|
if (connection.params.dc.id === this._primaryDc.id) {
|
|
|
|
if (
|
2022-04-01 22:17:10 +03:00
|
|
|
e.constructor === tl.errors.PhoneMigrateXError ||
|
|
|
|
e.constructor === tl.errors.UserMigrateXError ||
|
|
|
|
e.constructor === tl.errors.NetworkMigrateXError
|
2021-06-05 18:56:43 +03:00
|
|
|
) {
|
2022-04-01 22:17:10 +03:00
|
|
|
this.log.info('Migrate error, new dc = %d', e.new_dc)
|
|
|
|
await this.changeDc(e.new_dc)
|
2021-06-05 18:56:43 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
2022-04-01 22:17:10 +03:00
|
|
|
if (e.constructor === tl.errors.AuthKeyUnregisteredError) {
|
2021-06-05 18:56:43 +03:00
|
|
|
// we can try re-exporting auth from the primary connection
|
2021-11-23 00:03:59 +03:00
|
|
|
this.log.warn('exported auth key error, re-exporting..')
|
2021-06-05 18:56:43 +03:00
|
|
|
|
|
|
|
const auth = await this.call({
|
|
|
|
_: 'auth.exportAuthorization',
|
|
|
|
dcId: connection.params.dc.id,
|
|
|
|
})
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
await connection.sendRpc({
|
2021-06-05 18:56:43 +03:00
|
|
|
_: 'auth.importAuthorization',
|
|
|
|
id: auth.id,
|
|
|
|
bytes: auth.bytes,
|
|
|
|
})
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
2021-06-05 18:56:43 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw lastError
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates an additional connection to a given DC.
|
|
|
|
* This will use auth key for that DC that was already stored
|
|
|
|
* in the session, or generate a new auth key by exporting
|
|
|
|
* authorization from primary DC and importing it to the new DC.
|
|
|
|
* New connection will use the same crypto provider, `initConnection`,
|
|
|
|
* transport and reconnection strategy as the primary connection
|
|
|
|
*
|
|
|
|
* This method is quite low-level and you shouldn't usually care about this
|
2021-08-05 20:38:24 +03:00
|
|
|
* when using high-level API provided by `@mtcute/client`.
|
2021-04-08 12:19:38 +03:00
|
|
|
*
|
|
|
|
* @param dcId DC id, to which the connection will be created
|
|
|
|
* @param cdn Whether that DC is a CDN DC
|
|
|
|
* @param inactivityTimeout
|
|
|
|
* Inactivity timeout for the connection (in ms), after which the transport will be closed.
|
|
|
|
* Note that connection can still be used normally, it's just the transport which is closed.
|
|
|
|
* Defaults to 5 min
|
|
|
|
*/
|
|
|
|
async createAdditionalConnection(
|
|
|
|
dcId: number,
|
2021-11-23 00:03:59 +03:00
|
|
|
params?: {
|
|
|
|
// todo proper docs
|
|
|
|
// default = false
|
|
|
|
media?: boolean
|
|
|
|
// default = fa;se
|
|
|
|
cdn?: boolean
|
|
|
|
// default = 300_000
|
|
|
|
inactivityTimeout?: number
|
|
|
|
// default = false
|
|
|
|
disableUpdates?: boolean
|
|
|
|
}
|
|
|
|
): Promise<SessionConnection> {
|
|
|
|
const dc = await this.getDcById(dcId, params?.media, params?.cdn)
|
|
|
|
const connection = new SessionConnection(
|
|
|
|
{
|
|
|
|
dc,
|
|
|
|
testMode: this._testMode,
|
|
|
|
crypto: this._crypto,
|
|
|
|
initConnection: this._initConnectionParams,
|
|
|
|
transportFactory: this._transportFactory,
|
|
|
|
reconnectionStrategy: this._reconnectionStrategy,
|
|
|
|
inactivityTimeout: params?.inactivityTimeout ?? 300_000,
|
|
|
|
layer: this._layer,
|
|
|
|
disableUpdates: params?.disableUpdates,
|
|
|
|
readerMap: this._readerMap,
|
|
|
|
writerMap: this._writerMap,
|
|
|
|
},
|
|
|
|
this.log.create('connection')
|
|
|
|
)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-05-23 13:35:03 +03:00
|
|
|
connection.on('error', (err) => this._emitError(err, connection))
|
2021-11-23 00:03:59 +03:00
|
|
|
await connection.setupKeys(await this.storage.getAuthKeyFor(dc.id))
|
2021-04-08 12:19:38 +03:00
|
|
|
connection.connect()
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!connection.getAuthKey()) {
|
|
|
|
this.log.info('exporting auth to DC %d', dcId)
|
2021-04-08 12:19:38 +03:00
|
|
|
const auth = await this.call({
|
|
|
|
_: 'auth.exportAuthorization',
|
|
|
|
dcId,
|
|
|
|
})
|
2021-11-23 00:03:59 +03:00
|
|
|
await connection.sendRpc({
|
2021-04-08 12:19:38 +03:00
|
|
|
_: 'auth.importAuthorization',
|
|
|
|
id: auth.id,
|
|
|
|
bytes: auth.bytes,
|
|
|
|
})
|
|
|
|
|
|
|
|
// connection.authKey was already generated at this point
|
2021-11-23 00:03:59 +03:00
|
|
|
this.storage.setAuthKeyFor(dc.id, connection.getAuthKey()!)
|
2021-04-17 18:56:51 +03:00
|
|
|
await this._saveStorage()
|
2021-06-16 18:21:17 +03:00
|
|
|
} else {
|
|
|
|
// in case the auth key is invalid
|
|
|
|
const dcId = dc.id
|
|
|
|
connection.on('key-change', async (key) => {
|
|
|
|
// we don't need to export, it will be done by `.call()`
|
|
|
|
// in case this error is returned
|
|
|
|
//
|
|
|
|
// even worse, exporting here will lead to a race condition,
|
|
|
|
// and may result in redundant re-exports.
|
|
|
|
|
|
|
|
this.storage.setAuthKeyFor(dcId, key)
|
|
|
|
await this._saveStorage()
|
|
|
|
})
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
this._additionalConnections.push(connection)
|
|
|
|
|
|
|
|
return connection
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy a connection that was previously created using {@link createAdditionalConnection}.
|
|
|
|
* Passing any other connection will not have any effect.
|
|
|
|
*
|
|
|
|
* @param connection Connection created with {@link createAdditionalConnection}
|
|
|
|
*/
|
|
|
|
async destroyAdditionalConnection(
|
2021-11-23 00:03:59 +03:00
|
|
|
connection: SessionConnection
|
2021-04-08 12:19:38 +03:00
|
|
|
): Promise<void> {
|
|
|
|
const idx = this._additionalConnections.indexOf(connection)
|
|
|
|
if (idx === -1) return
|
|
|
|
|
|
|
|
await connection.destroy()
|
|
|
|
this._additionalConnections.splice(idx, 1)
|
|
|
|
}
|
|
|
|
|
2021-05-23 13:35:03 +03:00
|
|
|
/**
|
|
|
|
* Change transport for the client.
|
|
|
|
*
|
|
|
|
* Can be used, for example, to change proxy at runtime
|
|
|
|
*
|
|
|
|
* This effectively calls `changeTransport()` on
|
|
|
|
* `primaryConnection` and all additional connections.
|
|
|
|
*
|
|
|
|
* @param factory New transport factory
|
|
|
|
*/
|
|
|
|
changeTransport(factory: TransportFactory): void {
|
|
|
|
this.primaryConnection.changeTransport(factory)
|
|
|
|
|
2021-06-05 18:56:43 +03:00
|
|
|
this._additionalConnections.forEach((conn) =>
|
|
|
|
conn.changeTransport(factory)
|
|
|
|
)
|
2021-05-23 13:35:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an error handler for the client
|
|
|
|
*
|
|
|
|
* @param handler
|
|
|
|
* Error handler. Called with one or two parameters.
|
|
|
|
* The first one is always the error, and the second is
|
|
|
|
* the connection in which the error has occurred, in case
|
|
|
|
* this was connection-related error.
|
|
|
|
*/
|
2021-06-05 18:56:43 +03:00
|
|
|
onError(
|
2021-11-23 00:03:59 +03:00
|
|
|
handler: (err: Error, connection?: SessionConnection) => void
|
2021-06-05 18:56:43 +03:00
|
|
|
): void {
|
2021-04-08 12:19:38 +03:00
|
|
|
this._onError = handler
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
protected _emitError(err: Error, connection?: SessionConnection): void {
|
2021-04-08 12:19:38 +03:00
|
|
|
if (this._onError) {
|
2021-05-23 13:35:03 +03:00
|
|
|
this._onError(err, connection)
|
2021-04-08 12:19:38 +03:00
|
|
|
} else {
|
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds all peers from a given object to entity cache in storage.
|
2021-05-22 14:41:11 +03:00
|
|
|
*
|
|
|
|
* @returns `true` if there were any `min` peers
|
2021-04-08 12:19:38 +03:00
|
|
|
*/
|
2021-06-06 15:20:41 +03:00
|
|
|
protected async _cachePeersFrom(obj: object): Promise<boolean> {
|
2021-04-08 12:19:38 +03:00
|
|
|
const parsedPeers: ITelegramStorage.PeerInfo[] = []
|
|
|
|
|
2021-05-22 14:41:11 +03:00
|
|
|
let hadMin = false
|
2021-08-14 12:57:26 +03:00
|
|
|
let count = 0
|
2021-04-08 12:19:38 +03:00
|
|
|
for (const peer of getAllPeersFrom(obj)) {
|
2021-05-22 14:41:11 +03:00
|
|
|
if ((peer as any).min) {
|
|
|
|
// absolutely incredible min peer handling, courtesy of levlam.
|
|
|
|
// see this thread: https://t.me/tdlibchat/15084
|
|
|
|
hadMin = true
|
2021-11-23 00:03:59 +03:00
|
|
|
this.log.debug('received min peer: %j', peer)
|
2021-04-08 12:19:38 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-08-14 12:57:26 +03:00
|
|
|
count += 1
|
|
|
|
|
2021-05-12 17:58:45 +03:00
|
|
|
switch (peer._) {
|
|
|
|
case 'user':
|
|
|
|
parsedPeers.push({
|
|
|
|
id: peer.id,
|
|
|
|
accessHash: peer.accessHash!,
|
2021-05-22 14:41:11 +03:00
|
|
|
username: peer.username?.toLowerCase(),
|
|
|
|
phone: peer.phone,
|
|
|
|
type: 'user',
|
|
|
|
full: peer,
|
2021-05-12 17:58:45 +03:00
|
|
|
})
|
|
|
|
break
|
|
|
|
case 'chat':
|
|
|
|
case 'chatForbidden':
|
|
|
|
parsedPeers.push({
|
|
|
|
id: -peer.id,
|
2021-11-23 00:03:59 +03:00
|
|
|
accessHash: Long.ZERO,
|
2021-05-22 14:41:11 +03:00
|
|
|
type: 'chat',
|
|
|
|
full: peer,
|
2021-05-12 17:58:45 +03:00
|
|
|
})
|
|
|
|
break
|
|
|
|
case 'channel':
|
|
|
|
case 'channelForbidden':
|
|
|
|
parsedPeers.push({
|
2021-11-23 00:03:59 +03:00
|
|
|
id: toggleChannelIdMark(peer.id),
|
2021-05-12 17:58:45 +03:00
|
|
|
accessHash: peer.accessHash!,
|
|
|
|
username:
|
|
|
|
peer._ === 'channel'
|
2021-05-22 14:41:11 +03:00
|
|
|
? peer.username?.toLowerCase()
|
|
|
|
: undefined,
|
|
|
|
type: 'channel',
|
|
|
|
full: peer,
|
2021-05-12 17:58:45 +03:00
|
|
|
})
|
|
|
|
break
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.storage.updatePeers(parsedPeers)
|
2021-05-22 14:41:11 +03:00
|
|
|
|
2021-08-14 12:57:26 +03:00
|
|
|
if (count > 0) {
|
2021-11-23 00:03:59 +03:00
|
|
|
this.log.debug('cached %d peers', count)
|
2021-08-14 12:57:26 +03:00
|
|
|
}
|
|
|
|
|
2021-05-22 14:41:11 +03:00
|
|
|
return hadMin
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
2021-06-05 20:25:08 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Export current session to a single *LONG* string, containing
|
|
|
|
* all the needed information.
|
|
|
|
*
|
|
|
|
* > **Warning!** Anyone with this string will be able
|
|
|
|
* > to authorize as you and do anything. Treat this
|
|
|
|
* > as your password, and never give it away!
|
|
|
|
* >
|
|
|
|
* > In case you have accidentally leaked this string,
|
|
|
|
* > make sure to revoke this session in account settings:
|
|
|
|
* > "Privacy & Security" > "Active sessions" >
|
2021-08-05 20:38:24 +03:00
|
|
|
* > find the one containing `MTCute` > Revoke,
|
2021-06-05 20:25:08 +03:00
|
|
|
* > or, in case this is a bot, revoke bot token
|
|
|
|
* > with [@BotFather](//t.me/botfather)
|
|
|
|
*/
|
|
|
|
async exportSession(): Promise<string> {
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!this.primaryConnection.getAuthKey())
|
2021-06-05 20:25:08 +03:00
|
|
|
throw new Error('Auth key is not generated yet')
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const writer = TlBinaryWriter.alloc(this._writerMap, 512)
|
2021-06-05 20:25:08 +03:00
|
|
|
|
|
|
|
const self = await this.storage.getSelf()
|
|
|
|
|
|
|
|
const version = 1
|
|
|
|
let flags = 0
|
|
|
|
|
|
|
|
if (self) {
|
|
|
|
flags |= 1
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (this._testMode) {
|
|
|
|
flags |= 2
|
|
|
|
}
|
|
|
|
|
2021-06-05 20:25:08 +03:00
|
|
|
writer.buffer[0] = version
|
|
|
|
writer.pos += 1
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
writer.int(flags)
|
2021-06-05 20:25:08 +03:00
|
|
|
writer.object(this._primaryDc)
|
|
|
|
|
|
|
|
if (self) {
|
2021-11-23 00:03:59 +03:00
|
|
|
writer.int53(self.userId)
|
2021-06-05 20:25:08 +03:00
|
|
|
writer.boolean(self.isBot)
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
writer.bytes(this.primaryConnection.getAuthKey()!)
|
2021-06-05 20:25:08 +03:00
|
|
|
|
|
|
|
return encodeUrlSafeBase64(writer.result())
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Request the session to be imported from the given session string.
|
|
|
|
*
|
|
|
|
* Note that the string will not be parsed and imported right away,
|
|
|
|
* instead, it will be imported when `connect()` is called
|
|
|
|
*
|
|
|
|
* Also note that the session will only be imported in case
|
|
|
|
* the storage is missing authorization (i.e. does not contain
|
|
|
|
* auth key for the primary DC), otherwise it will be ignored.
|
|
|
|
*/
|
|
|
|
importSession(session: string): void {
|
|
|
|
this._importFrom = session
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|