/* eslint-disable @typescript-eslint/no-explicit-any */ import EventEmitter from 'events' import { tl } from '@mtcute/tl' import { __tlReaderMap as defaultReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlWriterMap as defaultWriterMap } from '@mtcute/tl/binary/writer.js' import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' import { IMtStorageProvider } from '../storage/provider.js' import { StorageManager, StorageManagerExtraOptions } from '../storage/storage.js' import { MustEqual } from '../types/index.js' import { asyncResettable, DcOptions, defaultProductionDc, defaultProductionIpv6Dc, defaultTestDc, defaultTestIpv6Dc, ICryptoProvider, Logger, LogManager, } from '../utils/index.js' import { ConfigManager } from './config-manager.js' import { NetworkManager, NetworkManagerExtraParams, RpcCallOptions } from './network-manager.js' import { PersistentConnectionParams } from './persistent-connection.js' import { ReconnectionStrategy } from './reconnection.js' import { SessionConnection } from './session-connection.js' import { TransportFactory } from './transports/index.js' /** Options for {@link MtClient} */ export interface MtClientOptions { /** * API ID from my.telegram.org */ apiId: number /** * API hash from my.telegram.org */ apiHash: string /** * Storage to use for this client. */ storage: IMtStorageProvider /** Additional options for the storage manager */ storageOptions?: StorageManagerExtraOptions /** * Cryptography provider to allow delegating * crypto to native addon, worker, etc. */ crypto: ICryptoProvider /** * 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. * * @default Production DC 2. */ defaultDcs?: DcOptions /** * Whether to connect to test servers. * * If passed, {@link defaultDc} defaults to Test DC 2. * * **Must** be passed if using test servers, even if * you passed custom {@link defaultDc} */ testMode?: boolean /** * Additional options for initConnection call. * `apiId` and `query` are not available and will be ignored. * Omitted values will be filled with defaults */ initConnectionOptions?: Partial> /** * Transport factory to use in the client. * * @default platform-specific transport: WebSocket on the web, TCP in node */ transport: TransportFactory /** * Reconnection strategy. * * @default simple reconnection strategy: first 0ms, then up to 5s (increasing by 1s) */ reconnectionStrategy?: ReconnectionStrategy /** * 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 */ maxRetryCount?: 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 /** * mtcute can send all unknown RPC errors to [danog](https://github.com/danog)'s * [error reporting service](https://rpc.pwrtelegram.xyz/). * * This is fully anonymous (except maybe IP) and is only used to improve the library * and developer experience for everyone working with MTProto. This is fully opt-in, * and if you're too paranoid, you can disable it by manually passing `enableErrorReporting: false` to the client. * * @default false */ enableErrorReporting?: boolean /** * If true, RPC errors will have a stack trace of the initial `.call()` * or `.sendForResult()` call position, which drastically improves * debugging experience.
* If false, they will have a stack trace of mtcute internals. * * 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 /** * Extra parameters for {@link NetworkManager} */ network?: NetworkManagerExtraParams /** * Logger instance for the client. * If not passed, a new one will be created. */ logger?: Logger /** * Set logging level for the client. * Shorthand for `client.log.level = level`. * * See static members of {@link LogManager} for possible values. */ logLevel?: number /** * **EXPERT USE ONLY!** * * Override TL layer used for the connection. * * **Does not** change the schema used. */ overrideLayer?: number /** * **EXPERT USE ONLY** * * Override reader map used for the connection. */ readerMap?: TlReaderMap /** * **EXPERT USE ONLY** * * Override writer map used for the connection. */ writerMap?: TlWriterMap } /** * Basic MTProto client implementation, only doing the bare minimum * to make RPC calls and receive low-level updates, as well as providing * some APIs to manage that. */ export class MtClient extends EventEmitter { /** * Crypto provider taken from {@link MtClientOptions.crypto} */ readonly crypto: ICryptoProvider /** Storage manager */ readonly storage: StorageManager /** * "Test mode" taken from {@link MtClientOptions.testMode} */ protected readonly _testMode: boolean /** * Primary DCs taken from {@link MtClientOptions.defaultDcs}, * loaded from session or changed by other means (like redirecting). */ _defaultDcs: DcOptions private _niceStacks: boolean /** TL layer used by the client */ readonly _layer: number /** TL readers map used by the client */ readonly _readerMap: TlReaderMap /** TL writers map used by the client */ readonly _writerMap: TlWriterMap readonly _config = new ConfigManager(() => this.call({ _: 'help.getConfig' })) emitError: (err: unknown, connection?: SessionConnection) => void = console.error.bind(console) readonly log: Logger readonly network: NetworkManager constructor(readonly params: MtClientOptions) { super() this.log = params.logger ?? new LogManager() if (params.logLevel !== undefined) { this.log.mgr.level = params.logLevel } this.crypto = params.crypto this._testMode = Boolean(params.testMode) let dc = params.defaultDcs if (!dc) { if (params.testMode) { dc = params.useIpv6 ? defaultTestIpv6Dc : defaultTestDc } else { dc = params.useIpv6 ? defaultProductionIpv6Dc : defaultProductionDc } } this._defaultDcs = dc this._niceStacks = params.niceStacks ?? true this._layer = params.overrideLayer ?? tl.LAYER this._readerMap = params.readerMap ?? defaultReaderMap this._writerMap = params.writerMap ?? defaultWriterMap this.storage = new StorageManager({ provider: params.storage, log: this.log, readerMap: this._readerMap, writerMap: this._writerMap, ...params.storageOptions, }) this.network = new NetworkManager( { apiId: params.apiId, crypto: this.crypto, disableUpdates: params.disableUpdates ?? false, initConnectionOptions: params.initConnectionOptions, layer: this._layer, log: this.log, readerMap: this._readerMap, writerMap: this._writerMap, reconnectionStrategy: params.reconnectionStrategy, storage: this.storage, testMode: Boolean(params.testMode), transport: params.transport, emitError: this.emitError.bind(this), floodSleepThreshold: params.floodSleepThreshold ?? 10000, maxRetryCount: params.maxRetryCount ?? 5, isPremium: false, useIpv6: Boolean(params.useIpv6), enableErrorReporting: params.enableErrorReporting ?? false, onUsable: () => this.emit('usable'), onUpdate: (upd) => this.emit('update', upd), ...params.network, }, this._config, ) } private _prepare = asyncResettable(async () => { await this.crypto.initialize?.() await this.storage.load() const primaryDc = await this.storage.dcs.fetch() if (primaryDc !== null) this._defaultDcs = primaryDc }) /** * **ADVANCED** * * Do all the preparations, but don't connect just yet. * Useful when you want to do some preparations before * connecting, like setting up session. * * Call {@link connect} to actually connect. */ prepare() { return this._prepare.run() } private _connect = asyncResettable(async () => { await this._prepare.run() await this.network.connect(this._defaultDcs) }) /** * 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 { return this._connect.run() } /** * Close all connections and finalize the client. */ async close(): Promise { this._config.destroy() this.network.destroy() await this.storage.save() await this.storage.destroy?.() this._prepare.reset() this._connect.reset() } /** * Make an RPC call. * * The connection must have been {@link connect}-ed * before calling this method. * * This method is still quite low-level and you shouldn't use this * when using high-level API provided by `@mtcute/client`. * * @param message RPC method to call * @param params Additional call parameters */ async call( message: MustEqual, params?: RpcCallOptions, ): Promise { const stack = this._niceStacks ? new Error().stack : undefined // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.network.call(message, params, stack) } /** * Register an error handler for the client * * @param handler Error handler. */ onError(handler: (err: unknown) => void): void { this.emitError = handler } }