diff --git a/.config/eslint.cjs b/.config/eslint.cjs index f26e5229..8f16012a 100644 --- a/.config/eslint.cjs +++ b/.config/eslint.cjs @@ -160,7 +160,7 @@ module.exports = { 'simple-import-sort/imports': [ 'error', { - groups: [['^[a-z]'], ['^@mtcute'], ['^@/'], ['^~/'], ['^\\.']], + groups: [['^[a-z]'], ['^@?mtcute'], ['^@/'], ['^~/'], ['^\\.']], }, ], 'simple-import-sort/exports': 'error', diff --git a/e2e/cjs/utils.js b/e2e/cjs/utils.js index 1307ce20..402a563b 100644 --- a/e2e/cjs/utils.js +++ b/e2e/cjs/utils.js @@ -1,4 +1,4 @@ -const { MemoryStorage } = require('@mtcute/core/storage/memory.js') +const { MemoryStorage } = require('@mtcute/core') const { LogManager } = require('@mtcute/core/utils.js') exports.getApiParams = () => { diff --git a/e2e/esm/utils.js b/e2e/esm/utils.js index aa4c14d2..e5817cd7 100644 --- a/e2e/esm/utils.js +++ b/e2e/esm/utils.js @@ -1,4 +1,4 @@ -import { MemoryStorage } from '@mtcute/core/storage/memory.js' +import { MemoryStorage } from '@mtcute/core' import { LogManager } from '@mtcute/core/utils.js' export const getApiParams = () => { diff --git a/e2e/package.json b/e2e/package.json index 4d3ff1d6..7a986e88 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -2,7 +2,6 @@ "name": "mtcute-e2e", "private": true, "dependencies": { - "@mtcute/client": "*", "@mtcute/core": "*", "@mtcute/crypto-node": "*", "@mtcute/dispatcher": "*", diff --git a/e2e/ts/tests/01.auth.ts b/e2e/ts/tests/01.auth.ts index 67de54f7..834aed82 100644 --- a/e2e/ts/tests/01.auth.ts +++ b/e2e/ts/tests/01.auth.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { MtUnsupportedError, TelegramClient } from '@mtcute/client' +import { BaseTelegramClient, MtUnsupportedError, TelegramClient } from '@mtcute/core' import { getApiParams } from '../utils.js' @@ -14,11 +14,12 @@ describe('1. authorization', function () { this.timeout(300_000) it('should authorize in default dc', async () => { - const tg = new TelegramClient(getApiParams('dc2.session')) + const base = new BaseTelegramClient(getApiParams('dc2.session')) + const tg = new TelegramClient({ client: base }) // reset storage just in case - await tg.storage.load?.() - await tg.storage.reset(true) + await base.mt.storage.load() + await base.storage.clear(true) while (true) { const phone = `999662${getAccountId()}` @@ -45,11 +46,12 @@ describe('1. authorization', function () { }) it('should authorize in dc 1', async () => { - const tg = new TelegramClient(getApiParams('dc1.session')) + const base = new BaseTelegramClient(getApiParams('dc1.session')) + const tg = new TelegramClient({ client: base }) // reset storage just in case - await tg.storage.load?.() - await tg.storage.reset(true) + await base.mt.storage.load() + await base.mt.storage.clear(true) while (true) { const phone = `999661${getAccountId()}` diff --git a/e2e/ts/tests/02.methods.ts b/e2e/ts/tests/02.methods.ts index ea1ca009..21bc5da1 100644 --- a/e2e/ts/tests/02.methods.ts +++ b/e2e/ts/tests/02.methods.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { TelegramClient } from '@mtcute/client' +import { TelegramClient } from '@mtcute/core' import { getApiParams } from '../utils.js' diff --git a/e2e/ts/tests/03.files.ts b/e2e/ts/tests/03.files.ts index 4ac3deb4..e1f39e3e 100644 --- a/e2e/ts/tests/03.files.ts +++ b/e2e/ts/tests/03.files.ts @@ -3,7 +3,7 @@ import { expect } from 'chai' import { createHash } from 'crypto' import { describe, it } from 'mocha' -import { FileDownloadLocation, TelegramClient, Thumbnail } from '@mtcute/client' +import { FileDownloadLocation, TelegramClient, Thumbnail } from '@mtcute/core' import { sleep } from '@mtcute/core/utils.js' import { getApiParams } from '../utils.js' diff --git a/e2e/ts/tests/04.updates.ts b/e2e/ts/tests/04.updates.ts index 78284610..7e55c4b7 100644 --- a/e2e/ts/tests/04.updates.ts +++ b/e2e/ts/tests/04.updates.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { Message, TelegramClient } from '@mtcute/client' +import { Message, TelegramClient } from '@mtcute/core' import { getApiParams, waitFor } from '../utils.js' diff --git a/e2e/ts/utils.ts b/e2e/ts/utils.ts index ca6fb8c6..90f869f1 100644 --- a/e2e/ts/utils.ts +++ b/e2e/ts/utils.ts @@ -1,12 +1,11 @@ // eslint-disable-next-line no-restricted-imports import { join } from 'path' -import { BaseTelegramClientOptions, MaybeAsync } from '@mtcute/core' -import { MemoryStorage } from '@mtcute/core/storage/memory.js' +import { MaybePromise, MemoryStorage } from '@mtcute/core' import { LogManager, sleep } from '@mtcute/core/utils.js' import { SqliteStorage } from '@mtcute/sqlite' -export const getApiParams = (storage?: string): BaseTelegramClientOptions => { +export const getApiParams = (storage?: string) => { if (!process.env.API_ID || !process.env.API_HASH) { throw new Error('API_ID and API_HASH env variables must be set') } @@ -20,7 +19,7 @@ export const getApiParams = (storage?: string): BaseTelegramClientOptions => { } } -export async function waitFor(condition: () => MaybeAsync, timeout = 5000): Promise { +export async function waitFor(condition: () => MaybePromise, timeout = 5000): Promise { const start = Date.now() let lastError diff --git a/package.json b/package.json index 008511a4..1c0704cb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "mtcute", + "name": "mtcute-workspace", "private": true, "version": "0.6.0", "description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS", @@ -61,7 +61,7 @@ "ts-node": "10.9.1", "tsconfig-paths": "4.2.0", "typedoc": "0.25.3", - "typescript": "5.0.4", + "typescript": "5.1.6", "vite": "5.0.3", "vitest": "0.34.6" }, diff --git a/packages/client/README.md b/packages/client/README.md deleted file mode 100644 index b11abacb..00000000 --- a/packages/client/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# @mtcute/client - -📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_client.html) - -High-level Telegram client implementation over the `@mtcute/core` base library. - -## Features -- **Updates handling**: Implements proper updates handling, including ordering and gap recovery ([learn more](https://core.telegram.org/api/updates)) -- **Wrapper classes**: Easy-to-use classes that wrap the complex TL objects and provide a clean interface -- **High-level methods**: Methods that wrap the low-level API calls and provide a clean interface -- **Tree-shaking**: Only import the methods you need, and the rest will not be included into the bundle -- **Web support**: Works in the browser with no additional configuration - -## Usage - -```ts -import { TelegramClient } from '@mtcute/client' - -const tg = new TelegramClient({ - apiId: 12345, - apiHash: '0123456789abcdef0123456789abcdef', - // ... + supports all options from @mtcute/core ... -}) - -tg.start({ - phone: '+1234567890', - password: () => prompt('Enter password'), - code: () => prompt('Enter code'), -}, (user) => { - console.log(`Logged in as ${user.displayName}`) -}) -``` - -> **Note**: for web, prefer BaseTelegramClient over TelegramClient, -> as it is tree-shakeable – [learn more](https://mtcute.dev/guide/topics/treeshaking.html) diff --git a/packages/client/build.config.cjs b/packages/client/build.config.cjs deleted file mode 100644 index da490312..00000000 --- a/packages/client/build.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - esmOnlyDirectives: true, -} diff --git a/packages/client/package.json b/packages/client/package.json deleted file mode 100644 index 5282f59e..00000000 --- a/packages/client/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@mtcute/client", - "private": true, - "version": "0.6.0", - "description": "High-level API over @mtcute/core", - "author": "Alina Sireneva ", - "license": "MIT", - "main": "src/index.ts", - "type": "module", - "scripts": { - "build": "pnpm run -w build-package client", - "gen-client": "node ./scripts/generate-client.cjs", - "gen-updates": "node ./scripts/generate-updates.cjs" - }, - "distOnlyFields": { - "exports": { - ".": { - "import": "./esm/index.js", - "require": "./cjs/index.js" - }, - "./methods/*": { - "import": "./esm/methods/*", - "require": "./cjs/methods/*" - }, - "./utils.js": { - "import": "./esm/utils/index.js", - "require": "./cjs/utils/index.js" - } - } - }, - "browser": { - "./src/methods/files/_platform.js": "./src/methods/files/_platform.web.js", - "./src/methods/files/download-file.js": "./src/methods/files/download-file.web.js", - "./src/utils/platform/storage.js": "./src/utils/platform/storage.web.js" - }, - "dependencies": { - "@mtcute/core": "workspace:^", - "@mtcute/file-id": "workspace:^" - }, - "devDependencies": { - "@mtcute/test": "workspace:^" - } -} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts deleted file mode 100644 index 4aaf6b14..00000000 --- a/packages/client/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './client.js' -export * from './types/index.js' -export * from './utils/peer-utils.js' -export { createDummyUpdate } from './utils/updates-utils.js' -export * from '@mtcute/core' diff --git a/packages/client/src/methods/_init.ts b/packages/client/src/methods/_init.ts deleted file mode 100644 index f45e70a6..00000000 --- a/packages/client/src/methods/_init.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { BaseTelegramClientOptions, ITelegramStorage } from '@mtcute/core' -// @copy -import { MemoryStorage } from '@mtcute/core/src/storage/memory.js' - -import { TelegramClient } from '../client.js' -// @copy -import { Conversation } from '../types/conversation.js' -// @copy -import { _defaultStorageFactory } from '../utils/platform/storage.js' -// @copy -import { setupAuthState } from './auth/_state.js' -// @copy -import { - enableUpdatesProcessing, - makeParsedUpdateHandler, - ParsedUpdateHandlerParams, - UpdatesManagerParams, -} from './updates/index.js' - -// @extension -interface TelegramClientExt { - _disableUpdatesManager: boolean -} - -// @copy -interface TelegramClientOptions extends Omit { - /** - * Storage to use for this client. - * - * If a string is passed, it will be used as: - * - a path to a JSON file for Node.js - * - IndexedDB database name for browsers - * - * If omitted, {@link MemoryStorage} is used - */ - storage?: string | ITelegramStorage - - /** - * Parameters for updates manager. - */ - updates?: Omit - - /** - * **ADVANCED** - * - * If set to `true`, updates manager will not be created, - * and only raw TL Updates will be emitted. - * - * Unlike {@link TelegramClientOptions.disableUpdates}, this - * does not prevent the updates from being sent by the server, - * but disables proper handling of them (see [Working with Updates](https://core.telegram.org/api/updates)) - * - * This may be useful in some cases when you require more control over - * the updates or to minimize additional overhead from properly handling them - * for some very particular use cases. - * - * The updates **will not** be dispatched the normal way, instead - * you should manually add a handler using `client.network.setUpdateHandler`. - */ - disableUpdatesManager?: boolean - - /** - * If `true`, the updates that were handled by some {@link Conversation} - * will not be dispatched any further. - * - * @default true - */ - skipConversationUpdates?: boolean -} - -// @initialize=super -/** @internal */ -function _initializeClientSuper(this: TelegramClient, opts: TelegramClientOptions) { - if (typeof opts.storage === 'string') { - opts.storage = _defaultStorageFactory(opts.storage) - } else if (!opts.storage) { - opts.storage = new MemoryStorage() - } - - /* eslint-disable @typescript-eslint/no-unsafe-call */ - // @ts-expect-error codegen - super(opts) - /* eslint-enable @typescript-eslint/no-unsafe-call */ -} - -// @initialize -/** @internal */ -function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) { - this._disableUpdatesManager = opts.disableUpdatesManager ?? false - const skipConversationUpdates = opts.skipConversationUpdates ?? true - - if (!opts.disableUpdates && !opts.disableUpdatesManager) { - const { messageGroupingInterval, ...managerParams } = opts.updates ?? {} - - enableUpdatesProcessing(this, { - ...managerParams, - onUpdate: makeParsedUpdateHandler({ - messageGroupingInterval, - onUpdate: (update) => { - if (Conversation.handleUpdate(this, update) && skipConversationUpdates) return - - this.emit('update', update) - this.emit(update.name, update.data) - }, - onRawUpdate: (update, peers) => { - this.emit('raw_update', update, peers) - }, - }), - }) - } else { - setupAuthState(this) - } -} diff --git a/packages/client/src/methods/auth/_state.ts b/packages/client/src/methods/auth/_state.ts deleted file mode 100644 index bdd0f595..00000000 --- a/packages/client/src/methods/auth/_state.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable no-inner-declarations */ -import { BaseTelegramClient, MtArgumentError, MtUnsupportedError, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - -import { User } from '../../types/peers/user.js' - -const STATE_SYMBOL = Symbol('authState') - -/** @exported */ -export interface AuthState { - // local copy of "self" in storage, - // so we can use it w/out relying on storage. - // they are both loaded and saved to storage along with the updates - // (see methods/updates) - userId: number | null - isBot: boolean - selfUsername: string | null - selfChanged?: boolean -} - -/** - * Initialize auth state for the given client. - * - * Allows {@link getAuthState} to be used and is required for some methods. - * @noemit - */ -export function setupAuthState(client: BaseTelegramClient): void { - // eslint-disable-next-line - let state: AuthState = (client as any)[STATE_SYMBOL] - if (state) return - - // init - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state = (client as any)[STATE_SYMBOL] = { - userId: null, - isBot: false, - selfUsername: null, - } - - client.log.prefix = '[USER N/A] ' - - function onBeforeConnect() { - Promise.resolve(client.storage.getSelf()) - .then((self) => { - if (!self) return - - state.userId = self.userId - state.isBot = self.isBot - client.log.prefix = `[USER ${self.userId}] ` - }) - .catch((err) => client._emitError(err)) - } - - async function onBeforeStorageSave() { - if (state.selfChanged) { - await client.storage.setSelf( - state.userId ? - { - userId: state.userId, - isBot: state.isBot, - } : - null, - ) - state.selfChanged = false - } - } - - client.on('before_connect', onBeforeConnect) - client.beforeStorageSave(onBeforeStorageSave) - client.on('before_stop', () => { - client.off('before_connect', onBeforeConnect) - client.offBeforeStorageSave(onBeforeStorageSave) - }) -} - -/** - * Get auth state for the given client, containing - * information about the current user. - * - * Auth state must first be initialized with {@link setupAuthState}. - */ -export function getAuthState(client: BaseTelegramClient): AuthState { - // eslint-disable-next-line - let state: AuthState = (client as any)[STATE_SYMBOL] - - if (!state) { - throw new MtArgumentError('Auth state is not initialized, use setupAuthState()') - } - - return state -} - -/** @internal */ -export async function _onAuthorization( - client: BaseTelegramClient, - auth: tl.auth.TypeAuthorization, - bot = false, -): Promise { - if (auth._ === 'auth.authorizationSignUpRequired') { - throw new MtUnsupportedError( - 'Signup is no longer supported by Telegram for non-official clients. Please use your mobile device to sign up.', - ) - } - - assertTypeIs('_onAuthorization (@ auth.authorization -> user)', auth.user, 'user') - - const state = getAuthState(client) - state.userId = auth.user.id - state.isBot = bot - state.selfUsername = auth.user.username ?? null - state.selfChanged = true - - client.notifyLoggedIn(auth) - await client.saveStorage() - - // telegram ignores invokeWithoutUpdates for auth methods - if (client.network.params.disableUpdates) client.network.resetSessions() - - return new User(auth.user) -} - -/** - * Check if the given peer/input peer is referring to the current user - */ -export function isSelfPeer( - client: BaseTelegramClient, - peer: tl.TypeInputPeer | tl.TypePeer | tl.TypeInputUser, -): boolean { - const state = getAuthState(client) - - switch (peer._) { - case 'inputPeerSelf': - case 'inputUserSelf': - return true - case 'inputPeerUser': - case 'inputPeerUserFromMessage': - case 'inputUser': - case 'inputUserFromMessage': - case 'peerUser': - return peer.userId === state.userId - default: - return false - } -} diff --git a/packages/client/src/methods/auth/log-out.ts b/packages/client/src/methods/auth/log-out.ts deleted file mode 100644 index 784eef93..00000000 --- a/packages/client/src/methods/auth/log-out.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseTelegramClient } from '@mtcute/core' - -import { getAuthState } from './_state.js' - -/** - * Log out from Telegram account and optionally reset the session storage. - * - * When you log out, you can immediately log back in using - * the same {@link TelegramClient} instance. - * - * @returns On success, `true` is returned - */ -export async function logOut(client: BaseTelegramClient): Promise { - await client.call({ _: 'auth.logOut' }) - - const authState = getAuthState(client) - authState.userId = null - authState.isBot = false - authState.selfUsername = null - authState.selfChanged = true - - client.emit('logged_out') - - await client.storage.reset() - await client.saveStorage() - - return true -} diff --git a/packages/client/src/methods/auth/sign-in-bot.ts b/packages/client/src/methods/auth/sign-in-bot.ts deleted file mode 100644 index a0f29cb0..00000000 --- a/packages/client/src/methods/auth/sign-in-bot.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BaseTelegramClient } from '@mtcute/core' - -import { User } from '../../types/index.js' -import { _onAuthorization } from './_state.js' - -/** - * Authorize a bot using its token issued by [@BotFather](//t.me/BotFather) - * - * @param token Bot token issued by BotFather - * @returns Bot's {@link User} object - * @throws BadRequestError In case the bot token is invalid - */ -export async function signInBot(client: BaseTelegramClient, token: string): Promise { - const res = await client.call({ - _: 'auth.importBotAuthorization', - flags: 0, - apiId: client.params.apiId, - apiHash: client.params.apiHash, - botAuthToken: token, - }) - - return _onAuthorization(client, res, true) -} diff --git a/packages/client/src/methods/misc/chain-id.ts b/packages/client/src/methods/misc/chain-id.ts deleted file mode 100644 index c46d0aeb..00000000 --- a/packages/client/src/methods/misc/chain-id.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BaseTelegramClient, getMarkedPeerId, tl } from '@mtcute/core' - -import { getAuthState } from '../auth/_state.js' - -/** @internal */ -export function _getPeerChainId(client: BaseTelegramClient, peer: tl.TypeInputPeer, prefix = 'peer') { - const id = peer._ === 'inputPeerSelf' ? getAuthState(client).userId! : getMarkedPeerId(peer) - - return `${prefix}:${id}` -} diff --git a/packages/client/src/methods/updates/manager.ts b/packages/client/src/methods/updates/manager.ts deleted file mode 100644 index 96cc99e2..00000000 --- a/packages/client/src/methods/updates/manager.ts +++ /dev/null @@ -1,1842 +0,0 @@ -/* eslint-disable max-depth,max-params */ -import { assertNever, BaseTelegramClient, MaybeAsync, MtArgumentError, tl } from '@mtcute/core' -import { getBarePeerId, getMarkedPeerId, markedPeerIdToBare, toggleChannelIdMark } from '@mtcute/core/utils.js' - -import { PeersIndex } from '../../types/index.js' -import { isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/peer-utils.js' -import { RpsMeter } from '../../utils/rps-meter.js' -import { createDummyUpdatesContainer } from '../../utils/updates-utils.js' -import { getAuthState, setupAuthState } from '../auth/_state.js' -import { _getChannelsBatched, _getUsersBatched } from '../chats/batched-queries.js' -import { resolvePeer } from '../users/resolve-peer.js' -import { createUpdatesState, PendingUpdate, toPendingUpdate, UpdatesManagerParams, UpdatesState } from './types.js' -import { extractChannelIdFromUpdate, messageToUpdate } from './utils.js' - -// code in this file is very bad, thanks to Telegram's awesome updates mechanism - -/** - * Enable RPS meter. - * Only available in NodeJS v10.7.0 and newer - * - * > **Note**: This may have negative impact on performance - * - * @param size Sampling size - * @param time Window time - */ -export function enableRps(client: BaseTelegramClient, size?: number, time?: number): void { - const state = getState(client) - state.rpsIncoming = new RpsMeter(size, time) - state.rpsProcessing = new RpsMeter(size, time) -} - -/** - * Get current average incoming RPS - * - * Incoming RPS is calculated based on - * incoming update containers. Normally, - * they should be around the same, except - * rare situations when processing rps - * may peak. - */ -export function getCurrentRpsIncoming(client: BaseTelegramClient): number { - const state = getState(client) - - if (!state.rpsIncoming) { - throw new MtArgumentError('RPS meter is not enabled, use .enableRps() first') - } - - return state.rpsIncoming.getRps() -} - -/** - * Get current average processing RPS - * - * Processing RPS is calculated based on - * dispatched updates. Normally, - * they should be around the same, except - * rare situations when processing rps - * may peak. - */ -export function getCurrentRpsProcessing(client: BaseTelegramClient): number { - const state = getState(client) - - if (!state.rpsProcessing) { - throw new MtArgumentError('RPS meter is not enabled, use .enableRps() first') - } - - return state.rpsProcessing.getRps() -} - -/** - * Add updates handling capabilities to {@link BaseTelegramClient} - * - * {@link BaseTelegramClient} doesn't do any updates processing on its own, and instead - * dispatches raw TL updates to user of the class. - * - * This method enables updates processing according to Telegram's updates mechanism. - * - * > **Note**: you don't need to use this if you are using {@link TelegramClient} - * - * @param client Client instance - * @param params Updates manager parameters - * @noemit - */ -export function enableUpdatesProcessing(client: BaseTelegramClient, params: UpdatesManagerParams): void { - if (getState(client)) return - - setupAuthState(client) - - if (client.network.params.disableUpdates) { - throw new MtArgumentError('Updates must be enabled to use updates manager') - } - - const authState = getAuthState(client) - - const state = createUpdatesState(client, authState, params) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(client as any)[STATE_SYMBOL] = state - - function onLoggedIn(): void { - fetchUpdatesState(client, state).catch((err) => client._emitError(err)) - } - - function onLoggedOut(): void { - stopUpdatesLoop(client) - state.cpts.clear() - state.cptsMod.clear() - state.pts = state.qts = state.date = state.seq = undefined - } - - function onBeforeConnect(): void { - loadUpdatesStorage(client, state).catch((err) => client._emitError(err)) - } - - function onBeforeStorageSave(): Promise { - return saveUpdatesStorage(client, state).catch((err) => client._emitError(err)) - } - - function onKeepAlive() { - state.log.debug('no updates for >15 minutes, catching up') - handleUpdate(state, { _: 'updatesTooLong' }) - } - - state.postponedTimer.onTimeout(() => { - state.hasTimedoutPostponed = true - state.updatesLoopCv.notify() - }) - - client.on('logged_in', onLoggedIn) - client.on('logged_out', onLoggedOut) - client.on('before_connect', onBeforeConnect) - client.beforeStorageSave(onBeforeStorageSave) - client.on('keep_alive', onKeepAlive) - client.network.setUpdateHandler((upd, fromClient) => handleUpdate(state, upd, fromClient)) - - function cleanup() { - client.off('logged_in', onLoggedIn) - client.off('logged_out', onLoggedOut) - client.off('before_connect', onBeforeConnect) - client.offBeforeStorageSave(onBeforeStorageSave) - client.off('keep_alive', onKeepAlive) - client.off('before_stop', cleanup) - client.network.setUpdateHandler(() => {}) - stopUpdatesLoop(client) - } - - state.stop = cleanup - client.on('before_stop', cleanup) -} - -/** - * Disable updates processing. - * - * Basically reverts {@link enableUpdatesProcessing} - * - * @param client Client instance - * @noemit - */ -export function disableUpdatesProcessing(client: BaseTelegramClient): void { - const state = getState(client) - if (!state) return - - state.stop() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (client as any)[STATE_SYMBOL] -} - -/** - * Start updates loop. - * - * You must first call {@link enableUpdatesProcessing} to use this method. - * - * It is recommended to use this method in callback to {@link start}, - * or otherwise make sure the user is logged in. - * - * > **Note**: If you are using {@link UpdatesManagerParams.catchUp} option, - * > catching up will be done in background, you can't await it. - */ -export async function startUpdatesLoop(client: BaseTelegramClient): Promise { - const state = getState(client) - if (state.updatesLoopActive) return - - // otherwise we will catch up on the first update - if (!state.catchUpOnStart) { - await fetchUpdatesState(client, state) - } - - // start updates loop in background - state.updatesLoopActive = true - updatesLoop(client, state).catch((err) => client._emitError(err)) - - if (state.catchUpOnStart) { - catchUp(client) - } -} - -/** - * **ADVANCED** - * - * Manually stop updates loop. - * Usually done automatically when stopping the client with {@link close} - */ -export function stopUpdatesLoop(client: BaseTelegramClient): void { - const state = getState(client) - if (!state.updatesLoopActive) return - - for (const timer of state.channelDiffTimeouts.values()) { - clearTimeout(timer) - } - state.channelDiffTimeouts.clear() - - state.updatesLoopActive = false - state.pendingUpdateContainers.clear() - state.pendingUnorderedUpdates.clear() - state.pendingPtsUpdates.clear() - state.pendingQtsUpdates.clear() - state.pendingPtsUpdatesPostponed.clear() - state.pendingQtsUpdatesPostponed.clear() - state.postponedTimer.reset() - state.updatesLoopCv.notify() -} - -/** - * Catch up with the server by loading missed updates. - * - * > **Note**: In case the storage was not properly - * > closed the last time, "catching up" might - * > result in duplicate updates. - */ -export function catchUp(client: BaseTelegramClient): void { - const state = getState(client) - - state.log.debug('catch up requested') - - state.catchUpChannels = true - handleUpdate(state, { _: 'updatesTooLong' }) -} - -/** - * **ADVANCED** - * - * Notify the updates manager that some channel was "opened". - * Channel difference for "opened" channels will be fetched on a regular basis. - * This is a low-level method, prefer using {@link openChat} instead. - * - * Channel must be resolve-able with `resolvePeer` method (i.e. be in cache); - * base chat PTS must either be passed (e.g. from {@link Dialog}), or cached in storage. - * - * @param channelId Bare ID of the channel - * @param pts PTS of the channel, if known (e.g. from {@link Dialog}) - * @returns `true` if the channel was opened for the first time, `false` if it is already opened - */ -export function notifyChannelOpened(client: BaseTelegramClient, channelId: number, pts?: number): boolean { - // this method is intentionally very dumb to avoid making this file even more unreadable - - const state = getState(client) - - if (!state) { - throw new MtArgumentError('Updates processing is not enabled, use enableUpdatesProcessing() first') - } - - if (state.channelsOpened.has(channelId)) { - state.log.debug('channel %d opened again', channelId) - state.channelsOpened.set(channelId, state.channelsOpened.get(channelId)! + 1) - - return false - } - - state.channelsOpened.set(channelId, 1) - state.log.debug('channel %d opened (pts=%d)', channelId, pts) - - // force fetch channel difference - fetchChannelDifferenceViaUpdate(state, channelId, pts) - - return true -} - -/** - * **ADVANCED** - * - * Notify the updates manager that some channel was "closed". - * Basically the opposite of {@link notifyChannelOpened}. - * This is a low-level method, prefer using {@link closeChat} instead. - * - * @param channelId Bare channel ID - * @returns `true` if the chat was closed for the last time, `false` otherwise - */ -export function notifyChannelClosed(client: BaseTelegramClient, channelId: number): boolean { - const state = getState(client) - - if (!state) { - throw new MtArgumentError('Updates processing is not enabled, use enableUpdatesProcessing() first') - } - - const opened = state.channelsOpened.get(channelId)! - - if (opened === undefined) { - return false - } - - if (opened > 1) { - state.log.debug('channel %d closed, but is opened %d more times', channelId, opened - 1) - state.channelsOpened.set(channelId, opened - 1) - - return false - } - - state.channelsOpened.delete(channelId) - state.log.debug('channel %d closed', channelId) - - return true -} - -////////////////////////////////////////////// IMPLEMENTATION ////////////////////////////////////////////// - -const STATE_SYMBOL = Symbol('updatesState') - -function getState(client: BaseTelegramClient): UpdatesState { - // eslint-disable-next-line - return (client as any)[STATE_SYMBOL] -} - -async function fetchUpdatesState(client: BaseTelegramClient, state: UpdatesState): Promise { - await state.lock.acquire() - - state.log.debug('fetching initial state') - - try { - let fetchedState = await client.call({ _: 'updates.getState' }) - - state.log.debug( - 'updates.getState returned state: pts=%d, qts=%d, date=%d, seq=%d', - fetchedState.pts, - fetchedState.qts, - fetchedState.date, - fetchedState.seq, - ) - - // for some unknown fucking reason getState may return old qts - // call getDifference to get actual values :shrug: - const diff = await client.call({ - _: 'updates.getDifference', - pts: fetchedState.pts, - qts: fetchedState.qts, - date: fetchedState.date, - }) - - switch (diff._) { - case 'updates.differenceEmpty': - break - case 'updates.differenceTooLong': // shouldn't happen, but who knows? - (fetchedState as tl.Mutable).pts = diff.pts - break - case 'updates.differenceSlice': - fetchedState = diff.intermediateState - break - case 'updates.difference': - fetchedState = diff.state - break - default: - assertNever(diff) - } - - state.qts = fetchedState.qts - state.pts = fetchedState.pts - state.date = fetchedState.date - state.seq = fetchedState.seq - - state.log.debug( - 'loaded initial state: pts=%d, qts=%d, date=%d, seq=%d', - state.pts, - state.qts, - state.date, - state.seq, - ) - } catch (e) { - state.log.error('failed to fetch updates state: %s', e) - } - - state.lock.release() -} - -async function loadUpdatesStorage(client: BaseTelegramClient, state: UpdatesState): Promise { - const storedState = await client.storage.getUpdatesState() - - if (storedState) { - state.pts = state.oldPts = storedState[0] - state.qts = state.oldQts = storedState[1] - state.date = state.oldDate = storedState[2] - state.seq = state.oldSeq = storedState[3] - - state.log.debug( - 'loaded stored state: pts=%d, qts=%d, date=%d, seq=%d', - storedState[0], - storedState[1], - storedState[2], - storedState[3], - ) - } - // if no state, don't bother initializing properties - // since that means that there is no authorization, - // and thus fetchUpdatesState will be called -} - -async function saveUpdatesStorage(client: BaseTelegramClient, state: UpdatesState, save = false): Promise { - // before any authorization pts will be undefined - if (state.pts !== undefined) { - // if old* value is not available, assume it has changed. - if (state.oldPts === undefined || state.oldPts !== state.pts) { - await client.storage.setUpdatesPts(state.pts) - } - if (state.oldQts === undefined || state.oldQts !== state.qts) { - await client.storage.setUpdatesQts(state.qts!) - } - if (state.oldDate === undefined || state.oldDate !== state.date) { - await client.storage.setUpdatesDate(state.date!) - } - if (state.oldSeq === undefined || state.oldSeq !== state.seq) { - await client.storage.setUpdatesSeq(state.seq!) - } - - // update old* values - state.oldPts = state.pts - state.oldQts = state.qts - state.oldDate = state.date - state.oldSeq = state.seq - - await client.storage.setManyChannelPts(state.cptsMod) - state.cptsMod.clear() - - if (save) { - await client.storage.save?.() - } - } -} - -function addToNoDispatchIndex(state: UpdatesState, updates?: tl.TypeUpdates): void { - if (!updates) return - - const addUpdate = (upd: tl.TypeUpdate) => { - const channelId = extractChannelIdFromUpdate(upd) ?? 0 - const pts = 'pts' in upd ? upd.pts : undefined - - if (pts) { - const set = state.noDispatchPts.get(channelId) - if (!set) state.noDispatchPts.set(channelId, new Set([pts])) - else set.add(pts) - } - - const qts = 'qts' in upd ? upd.qts : undefined - - if (qts) { - state.noDispatchQts.add(qts) - } - - switch (upd._) { - case 'updateNewMessage': - case 'updateNewChannelMessage': { - const channelId = upd.message.peerId?._ === 'peerChannel' ? upd.message.peerId.channelId : 0 - - const set = state.noDispatchMsg.get(channelId) - if (!set) state.noDispatchMsg.set(channelId, new Set([upd.message.id])) - else set.add(upd.message.id) - - break - } - } - } - - switch (updates._) { - case 'updates': - case 'updatesCombined': - updates.updates.forEach(addUpdate) - break - case 'updateShortMessage': - case 'updateShortChatMessage': - case 'updateShortSentMessage': { - // these updates are only used for non-channel messages, so we use 0 - let set = state.noDispatchMsg.get(0) - if (!set) state.noDispatchMsg.set(0, new Set([updates.id])) - else set.add(updates.id) - - set = state.noDispatchPts.get(0) - if (!set) state.noDispatchPts.set(0, new Set([updates.pts])) - else set.add(updates.pts) - break - } - case 'updateShort': - addUpdate(updates.update) - break - case 'updatesTooLong': - break - default: - assertNever(updates) - } -} - -async function fetchMissingPeers( - client: BaseTelegramClient, - upd: tl.TypeUpdate, - peers: PeersIndex, - allowMissing = false, -): Promise> { - const missing = new Set() - - async function fetchPeer(peer?: tl.TypePeer | number) { - if (!peer) return true - - const bare = typeof peer === 'number' ? markedPeerIdToBare(peer) : getBarePeerId(peer) - - const marked = typeof peer === 'number' ? peer : getMarkedPeerId(peer) - const index = marked > 0 ? peers.users : peers.chats - - if (index.has(bare)) return true - if (missing.has(marked)) return false - - const cached = await client.storage.getFullPeerById(marked) - - if (!cached) { - missing.add(marked) - - return allowMissing - } - - // whatever, ts is not smart enough to understand - (index as Map).set(bare, cached) - - return true - } - - switch (upd._) { - case 'updateNewMessage': - case 'updateNewChannelMessage': - case 'updateEditMessage': - case 'updateEditChannelMessage': { - const msg = upd.message - if (msg._ === 'messageEmpty') return missing - - // ref: https://github.com/tdlib/td/blob/master/td/telegram/UpdatesManager.cpp - // (search by UpdatesManager::is_acceptable_update) - if (!(await fetchPeer(msg.peerId))) return missing - if (!(await fetchPeer(msg.fromId))) return missing - - if (msg.replyTo) { - if (msg.replyTo._ === 'messageReplyHeader' && !(await fetchPeer(msg.replyTo.replyToPeerId))) { - return missing - } - if (msg.replyTo._ === 'messageReplyStoryHeader' && !(await fetchPeer(msg.replyTo.userId))) { - return missing - } - } - - if (msg._ !== 'messageService') { - if ( - msg.fwdFrom && - (!(await fetchPeer(msg.fwdFrom.fromId)) || !(await fetchPeer(msg.fwdFrom.savedFromPeer))) - ) { - return missing - } - if (!(await fetchPeer(msg.viaBotId))) return missing - - if (msg.entities) { - for (const ent of msg.entities) { - if (ent._ === 'messageEntityMentionName') { - if (!(await fetchPeer(ent.userId))) return missing - } - } - } - - if (msg.media) { - switch (msg.media._) { - case 'messageMediaContact': - if (msg.media.userId && !(await fetchPeer(msg.media.userId))) { - return missing - } - } - } - } else { - switch (msg.action._) { - case 'messageActionChatCreate': - case 'messageActionChatAddUser': - case 'messageActionInviteToGroupCall': - for (const user of msg.action.users) { - if (!(await fetchPeer(user))) return missing - } - break - case 'messageActionChatJoinedByLink': - if (!(await fetchPeer(msg.action.inviterId))) { - return missing - } - break - case 'messageActionChatDeleteUser': - if (!(await fetchPeer(msg.action.userId))) return missing - break - case 'messageActionChatMigrateTo': - if (!(await fetchPeer(toggleChannelIdMark(msg.action.channelId)))) { - return missing - } - break - case 'messageActionChannelMigrateFrom': - if (!(await fetchPeer(-msg.action.chatId))) return missing - break - case 'messageActionGeoProximityReached': - if (!(await fetchPeer(msg.action.fromId))) return missing - if (!(await fetchPeer(msg.action.toId))) return missing - break - } - } - break - } - case 'updateDraftMessage': - if ('entities' in upd.draft && upd.draft.entities) { - for (const ent of upd.draft.entities) { - if (ent._ === 'messageEntityMentionName') { - if (!(await fetchPeer(ent.userId))) return missing - } - } - } - } - - return missing -} - -async function storeMessageReferences(client: BaseTelegramClient, msg: tl.TypeMessage): Promise { - if (msg._ === 'messageEmpty') return - - const peerId = msg.peerId - if (peerId._ !== 'peerChannel') return - - const channelId = toggleChannelIdMark(peerId.channelId) - - const promises: MaybeAsync[] = [] - - function store(peer?: tl.TypePeer | number | number[]): void { - if (!peer) return - - if (Array.isArray(peer)) { - peer.forEach(store) - - return - } - - const marked = typeof peer === 'number' ? peer : getMarkedPeerId(peer) - - promises.push(client.storage.saveReferenceMessage(marked, channelId, msg.id)) - } - - // reference: https://github.com/tdlib/td/blob/master/td/telegram/MessagesManager.cpp - // (search by get_message_user_ids, get_message_channel_ids) - store(msg.fromId) - - if (msg._ === 'message') { - store(msg.viaBotId) - store(msg.fwdFrom?.fromId) - - if (msg.media) { - switch (msg.media._) { - case 'messageMediaWebPage': - if (msg.media.webpage._ === 'webPage' && msg.media.webpage.attributes) { - for (const attr of msg.media.webpage.attributes) { - if (attr._ === 'webPageAttributeStory') { - store(attr.peer) - } - } - } - break - case 'messageMediaContact': - store(msg.media.userId) - break - case 'messageMediaStory': - store(msg.media.peer) - break - case 'messageMediaGiveaway': - store(msg.media.channels.map(toggleChannelIdMark)) - break - } - } - } else { - switch (msg.action._) { - case 'messageActionChatCreate': - case 'messageActionChatAddUser': - case 'messageActionInviteToGroupCall': - store(msg.action.users) - break - case 'messageActionChatDeleteUser': - store(msg.action.userId) - break - } - } - - if (msg.replyTo) { - switch (msg.replyTo._) { - case 'messageReplyHeader': - store(msg.replyTo.replyToPeerId) - store(msg.replyTo.replyFrom?.fromId) - break - case 'messageReplyStoryHeader': - store(msg.replyTo.userId) - break - } - // in fact, we can also use peers contained in the replied-to message, - // but we don't fetch it automatically, so we can't know which peers are there - } - - await Promise.all(promises) -} - -function isMessageEmpty(upd: tl.TypeUpdate): boolean { - return (upd as Extract).message?._ === 'messageEmpty' -} - -function handleUpdate(state: UpdatesState, update: tl.TypeUpdates, noDispatch = false): void { - if (noDispatch && state.noDispatchEnabled) { - addToNoDispatchIndex(state, update) - } - - state.log.debug( - 'received %s, queueing for processing. containers queue size: %d', - update._, - state.pendingUpdateContainers.length, - ) - state.rpsIncoming?.hit() - - switch (update._) { - case 'updatesTooLong': - case 'updateShortMessage': - case 'updateShortChatMessage': - case 'updateShort': - case 'updateShortSentMessage': - state.pendingUpdateContainers.add({ - upd: update, - seqStart: 0, - seqEnd: 0, - }) - break - case 'updates': - case 'updatesCombined': - state.pendingUpdateContainers.add({ - upd: update, - seqStart: update._ === 'updatesCombined' ? update.seqStart : update.seq, - seqEnd: update.seq, - }) - break - default: - assertNever(update) - } - - state.updatesLoopCv.notify() -} - -async function fetchChannelDifference( - client: BaseTelegramClient, - state: UpdatesState, - channelId: number, - fallbackPts?: number, -): Promise { - // clear timeout if any - if (state.channelDiffTimeouts.has(channelId)) { - clearTimeout(state.channelDiffTimeouts.get(channelId)) - state.channelDiffTimeouts.delete(channelId) - } - - let _pts: number | null | undefined = state.cpts.get(channelId) - - if (!_pts && state.catchUpChannels) { - _pts = await client.storage.getChannelPts(channelId) - } - if (!_pts) _pts = fallbackPts - - if (!_pts) { - state.log.debug('fetchChannelDifference failed for channel %d: base pts not available', channelId) - - return false - } - - const channel = toInputChannel(await resolvePeer(client, toggleChannelIdMark(channelId))) - - if (channel._ === 'inputChannel' && channel.accessHash.isZero()) { - state.log.debug('fetchChannelDifference failed for channel %d: input peer not found', channelId) - - return false - } - - // to make TS happy - let pts = _pts - let limit = state.auth.isBot ? 100000 : 100 - - if (pts <= 0) { - pts = 1 - limit = 1 - } - - let lastTimeout = 0 - - for (;;) { - const diff = await client.call({ - _: 'updates.getChannelDifference', - force: true, // Set to true to skip some possibly unneeded updates and reduce server-side load - channel, - pts, - limit, - filter: { _: 'channelMessagesFilterEmpty' }, - }) - - if (diff.timeout) lastTimeout = diff.timeout - - if (diff._ === 'updates.channelDifferenceEmpty') { - state.log.debug('getChannelDifference (cid = %d) returned channelDifferenceEmpty', channelId) - break - } - - const peers = PeersIndex.from(diff) - - if (diff._ === 'updates.channelDifferenceTooLong') { - if (diff.dialog._ === 'dialog') { - pts = diff.dialog.pts! - } - - state.log.warn( - 'getChannelDifference (cid = %d) returned channelDifferenceTooLong. new pts: %d, recent msgs: %d', - channelId, - pts, - diff.messages.length, - ) - - diff.messages.forEach((message) => { - state.log.debug( - 'processing message %d (%s) from TooLong diff for channel %d', - message.id, - message._, - channelId, - ) - - if (message._ === 'messageEmpty') return - - state.pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) - }) - break - } - - state.log.debug( - 'getChannelDifference (cid = %d) returned %d messages, %d updates. new pts: %d, final: %b', - channelId, - diff.newMessages.length, - diff.otherUpdates.length, - diff.pts, - diff.final, - ) - - diff.newMessages.forEach((message) => { - state.log.debug('processing message %d (%s) from diff for channel %d', message.id, message._, channelId) - - if (message._ === 'messageEmpty') return - - state.pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) - }) - - diff.otherUpdates.forEach((upd) => { - const parsed = toPendingUpdate(upd, peers, true) - - state.log.debug( - 'processing %s from diff for channel %d, pts_before: %d, pts: %d', - upd._, - channelId, - parsed.ptsBefore, - parsed.pts, - ) - - if (isMessageEmpty(upd)) return - - state.pendingUnorderedUpdates.pushBack(parsed) - }) - - pts = diff.pts - - if (diff.final) break - } - - state.cpts.set(channelId, pts) - state.cptsMod.set(channelId, pts) - - // schedule next fetch - if (lastTimeout !== 0 && state.channelsOpened.has(channelId)) { - state.log.debug('scheduling next fetch for channel %d in %d seconds', channelId, lastTimeout) - state.channelDiffTimeouts.set( - channelId, - setTimeout(() => fetchChannelDifferenceViaUpdate(state, channelId), lastTimeout * 1000), - ) - } - - return true -} - -function fetchChannelDifferenceLater( - client: BaseTelegramClient, - state: UpdatesState, - requestedDiff: Map>, - channelId: number, - fallbackPts?: number, -): void { - if (!requestedDiff.has(channelId)) { - requestedDiff.set( - channelId, - fetchChannelDifference(client, state, channelId, fallbackPts) - .catch((err) => { - state.log.warn('error fetching difference for %d: %s', channelId, err) - }) - .then((ok) => { - requestedDiff.delete(channelId) - - if (!ok) { - state.log.debug('channel difference for %d failed, falling back to common diff', channelId) - fetchDifferenceLater(client, state, requestedDiff) - } - }), - ) - } -} - -function fetchChannelDifferenceViaUpdate(state: UpdatesState, channelId: number, pts?: number): void { - handleUpdate( - state, - createDummyUpdatesContainer([ - { - _: 'updateChannelTooLong', - channelId, - pts, - }, - ]), - ) -} - -async function fetchDifference( - client: BaseTelegramClient, - state: UpdatesState, - requestedDiff: Map>, -): Promise { - for (;;) { - const diff = await client.call({ - _: 'updates.getDifference', - pts: state.pts!, - date: state.date!, - qts: state.qts!, - }) - - switch (diff._) { - case 'updates.differenceEmpty': - state.log.debug('updates.getDifference returned updates.differenceEmpty') - - return - case 'updates.differenceTooLong': - state.pts = diff.pts - state.log.debug('updates.getDifference returned updates.differenceTooLong') - - return - } - - const fetchedState = diff._ === 'updates.difference' ? diff.state : diff.intermediateState - - state.log.debug( - 'updates.getDifference returned %d messages, %d updates. new pts: %d, qts: %d, seq: %d, final: %b', - diff.newMessages.length, - diff.otherUpdates.length, - fetchedState.pts, - fetchedState.qts, - fetchedState.seq, - diff._ === 'updates.difference', - ) - - const peers = PeersIndex.from(diff) - - diff.newMessages.forEach((message) => { - state.log.debug('processing message %d in %j (%s) from common diff', message.id, message.peerId, message._) - - if (message._ === 'messageEmpty') return - - // pts does not need to be checked for them - state.pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) - }) - - diff.otherUpdates.forEach((upd) => { - if (upd._ === 'updateChannelTooLong') { - state.log.debug( - 'received updateChannelTooLong for channel %d in common diff (pts = %d), fetching diff', - upd.channelId, - upd.pts, - ) - - fetchChannelDifferenceLater(client, state, requestedDiff, upd.channelId, upd.pts) - - return - } - - if (isMessageEmpty(upd)) return - - const parsed = toPendingUpdate(upd, peers, true) - - if (parsed.channelId && parsed.ptsBefore) { - // we need to check pts for these updates, put into pts queue - state.pendingPtsUpdates.add(parsed) - } else { - // the updates are in order already, we can treat them as unordered - state.pendingUnorderedUpdates.pushBack(parsed) - } - - state.log.debug( - 'received %s from common diff, cid: %d, pts_before: %d, pts: %d, qts_before: %d', - upd._, - parsed.channelId, - parsed.ptsBefore, - parsed.pts, - parsed.qtsBefore, - ) - }) - - state.pts = fetchedState.pts - state.qts = fetchedState.qts - state.seq = fetchedState.seq - state.date = fetchedState.date - - if (diff._ === 'updates.difference') { - return - } - } -} - -function fetchDifferenceLater( - client: BaseTelegramClient, - state: UpdatesState, - requestedDiff: Map>, -): void { - if (!requestedDiff.has(0)) { - requestedDiff.set( - 0, - fetchDifference(client, state, requestedDiff) - .catch((err) => { - if (tl.RpcError.is(err, 'AUTH_KEY_UNREGISTERED')) { - // for some reason, when logging out telegram may send updatesTooLong - // in any case, we need to stop updates loop - stopUpdatesLoop(client) - - return - } - - state.log.warn('error fetching common difference: %s', err) - - if (tl.RpcError.is(err, 'PERSISTENT_TIMESTAMP_INVALID')) { - // this function never throws - return fetchUpdatesState(client, state) - } - }) - .then(() => { - requestedDiff.delete(0) - }), - ) - } -} - -async function onUpdate( - client: BaseTelegramClient, - state: UpdatesState, - pending: PendingUpdate, - requestedDiff: Map>, - postponed = false, - unordered = false, -): Promise { - const upd = pending.update - - let missing: Set | undefined = undefined - - // it is important to do this before updating pts - if (pending.peers.hasMin || pending.peers.empty) { - // even if we have min peers in difference, we can't do anything about them. - // we still want to collect them, so we can fetch them in the background. - // we won't wait for them, since that would block the updates loop - - state.log.debug('loading missing peers for %s (pts = %d, cid = %d)', upd._, pending.pts, pending.channelId) - missing = await fetchMissingPeers(client, upd, pending.peers, pending.fromDifference) - - if (!pending.fromDifference && missing.size) { - state.log.debug( - 'fetching difference because some peers were min (%J) and not cached for %s (pts = %d, cid = %d)', - missing, - upd._, - pending.pts, - pending.channelId, - ) - - if (pending.channelId && !(upd._ === 'updateNewChannelMessage' && upd.message._ === 'messageService')) { - // don't replace service messages, because they can be about bot's kicking - fetchChannelDifferenceLater(client, state, requestedDiff, pending.channelId, pending.ptsBefore) - } else { - fetchDifferenceLater(client, state, requestedDiff) - } - - return - } - - if (missing.size) { - state.log.debug( - 'peers still missing after fetching difference: %J for %s (pts = %d, cid = %d)', - missing, - upd._, - pending.pts, - pending.channelId, - ) - } - } - - // apply new pts/qts, if applicable - if (!unordered) { - // because unordered may contain pts/qts values when received from diff - - if (pending.pts) { - const localPts = pending.channelId ? state.cpts.get(pending.channelId) : state.pts - - if (localPts && pending.ptsBefore !== localPts) { - state.log.warn( - 'pts_before does not match local_pts for %s (cid = %d, pts_before = %d, pts = %d, local_pts = %d)', - upd._, - pending.channelId, - pending.ptsBefore, - pending.pts, - localPts, - ) - } - - state.log.debug( - 'applying new pts (cid = %d) because received %s: %d -> %d (before: %d, count: %d) (postponed = %s)', - pending.channelId, - upd._, - localPts, - pending.pts, - pending.ptsBefore, - pending.pts - pending.ptsBefore!, - postponed, - ) - - if (pending.channelId) { - state.cpts.set(pending.channelId, pending.pts) - state.cptsMod.set(pending.channelId, pending.pts) - } else { - state.pts = pending.pts - } - } - - if (pending.qtsBefore) { - state.log.debug( - 'applying new qts because received %s: %d -> %d (postponed = %s)', - upd._, - state.qts, - pending.qtsBefore + 1, - postponed, - ) - - state.qts = pending.qts - } - } - - if (isMessageEmpty(upd)) return - - state.rpsProcessing?.hit() - - // updates that are also used internally - switch (upd._) { - case 'mtcute.dummyUpdate': - // we just needed to apply new pts values - return - case 'updateDcOptions': { - const config = client.network.config.getNow() - - if (config) { - client.network.config.setConfig({ - ...config, - dcOptions: upd.dcOptions, - }) - } else { - client.network.config.update(true).catch((err) => client._emitError(err)) - } - break - } - case 'updateConfig': - client.network.config.update(true).catch((err) => client._emitError(err)) - break - case 'updateUserName': - if (upd.userId === state.auth.userId) { - state.auth.selfUsername = upd.usernames.find((it) => it.active)?.username ?? null - } - break - case 'updateDeleteChannelMessages': - if (!state.auth.isBot) { - await client.storage.deleteReferenceMessages(toggleChannelIdMark(upd.channelId), upd.messages) - } - break - case 'updateNewMessage': - case 'updateEditMessage': - case 'updateNewChannelMessage': - case 'updateEditChannelMessage': - if (!state.auth.isBot) { - await storeMessageReferences(client, upd.message) - } - break - } - - if (missing?.size) { - if (state.auth.isBot) { - state.log.warn( - 'missing peers (%J) after getDifference for %s (pts = %d, cid = %d)', - missing, - upd._, - pending.pts, - pending.channelId, - ) - } else { - // force save storage so the min peers are stored - await client.storage.save?.() - - for (const id of missing) { - Promise.resolve(client.storage.getPeerById(id)) - .then((peer): unknown => { - if (!peer) { - state.log.warn('cannot fetch full peer %d - getPeerById returned null', id) - - return - } - - // the peer will be automatically cached by the `.call()`, we don't have to do anything - if (isInputPeerChannel(peer)) { - return _getChannelsBatched(client, toInputChannel(peer)) - } else if (isInputPeerUser(peer)) { - return _getUsersBatched(client, toInputUser(peer)) - } - - state.log.warn('cannot fetch full peer %d - unknown peer type %s', id, peer._) - }) - .catch((err) => { - state.log.warn('error fetching full peer %d: %s', id, err) - }) - } - } - } - - // dispatch the update - if (state.noDispatchEnabled) { - const channelId = pending.channelId ?? 0 - const msgId = upd._ === 'updateNewMessage' || upd._ === 'updateNewChannelMessage' ? upd.message.id : undefined - - // we first need to remove it from each index, and then check if it was there - const foundByMsgId = msgId && state.noDispatchMsg.get(channelId)?.delete(msgId) - const foundByPts = state.noDispatchPts.get(channelId)?.delete(pending.pts!) - const foundByQts = state.noDispatchQts.delete(pending.qts!) - - if (foundByMsgId || foundByPts || foundByQts) { - state.log.debug('not dispatching %s because it is in no_dispatch index', upd._) - - return - } - } - - state.log.debug('dispatching %s (postponed = %s)', upd._, postponed) - state.handler(upd, pending.peers) -} - -async function updatesLoop(client: BaseTelegramClient, state: UpdatesState): Promise { - const { log } = state - - log.debug('updates loop started, state available? %b', state.pts) - - try { - if (!state.pts) { - await fetchUpdatesState(client, state) - } - - while (state.updatesLoopActive) { - if ( - !( - state.pendingUpdateContainers.length || - state.pendingPtsUpdates.length || - state.pendingQtsUpdates.length || - state.pendingUnorderedUpdates.length || - state.hasTimedoutPostponed - ) - ) { - await state.updatesLoopCv.wait() - } - if (!state.updatesLoopActive) break - - log.debug( - 'updates loop tick. pending containers: %d, pts: %d, pts_postponed: %d, qts: %d, qts_postponed: %d, unordered: %d', - state.pendingUpdateContainers.length, - state.pendingPtsUpdates.length, - state.pendingPtsUpdatesPostponed.length, - state.pendingQtsUpdates.length, - state.pendingQtsUpdatesPostponed.length, - state.pendingUnorderedUpdates.length, - ) - - const requestedDiff = new Map>() - - // first process pending containers - while (state.pendingUpdateContainers.length) { - const { upd, seqStart, seqEnd } = state.pendingUpdateContainers.popFront()! - - switch (upd._) { - case 'updatesTooLong': - log.debug('received updatesTooLong, fetching difference') - fetchDifferenceLater(client, state, requestedDiff) - break - case 'updatesCombined': - case 'updates': { - if (seqStart !== 0) { - // https://t.me/tdlibchat/5843 - const nextLocalSeq = state.seq! + 1 - log.debug( - 'received seq-ordered %s (seq_start = %d, seq_end = %d, size = %d)', - upd._, - seqStart, - seqEnd, - upd.updates.length, - ) - - if (nextLocalSeq > seqStart) { - log.debug( - 'ignoring updates group because already applied (by seq: exp %d, got %d)', - nextLocalSeq, - seqStart, - ) - // "the updates were already applied, and must be ignored" - continue - } - - if (nextLocalSeq < seqStart) { - log.debug( - 'fetching difference because gap detected (by seq: exp %d, got %d)', - nextLocalSeq, - seqStart, - ) - // "there's an updates gap that must be filled" - fetchDifferenceLater(client, state, requestedDiff) - } - } else { - log.debug('received %s (size = %d)', upd._, upd.updates.length) - } - - await client._cachePeersFrom(upd) - - const peers = PeersIndex.from(upd) - - for (const update of upd.updates) { - switch (update._) { - case 'updateChannelTooLong': - log.debug( - 'received updateChannelTooLong for channel %d (pts = %d) in container, fetching diff', - update.channelId, - update.pts, - ) - fetchChannelDifferenceLater( - client, - state, - requestedDiff, - update.channelId, - update.pts, - ) - continue - case 'updatePtsChanged': - // see https://github.com/tdlib/td/blob/07c1d53a6d3cb1fad58d2822e55eef6d57363581/td/telegram/UpdatesManager.cpp#L4051 - if (client.network.getPoolSize('main') > 1) { - // highload bot - state.log.debug( - 'updatePtsChanged received, resetting pts to 1 and fetching difference', - ) - state.pts = 1 - fetchDifferenceLater(client, state, requestedDiff) - } else { - state.log.debug('updatePtsChanged received, fetching updates state') - await fetchUpdatesState(client, state) - } - continue - } - - const parsed = toPendingUpdate(update, peers) - - if (parsed.ptsBefore !== undefined) { - state.pendingPtsUpdates.add(parsed) - } else if (parsed.qtsBefore !== undefined) { - state.pendingQtsUpdates.add(parsed) - } else { - state.pendingUnorderedUpdates.pushBack(parsed) - } - } - - if (seqEnd !== 0 && seqEnd > state.seq!) { - state.seq = seqEnd - state.date = upd.date - } - - break - } - case 'updateShort': { - log.debug('received short %s', upd._) - - const parsed = toPendingUpdate(upd.update, new PeersIndex()) - - if (parsed.ptsBefore !== undefined) { - state.pendingPtsUpdates.add(parsed) - } else if (parsed.qtsBefore !== undefined) { - state.pendingQtsUpdates.add(parsed) - } else { - state.pendingUnorderedUpdates.pushBack(parsed) - } - - break - } - case 'updateShortMessage': { - log.debug('received updateShortMessage') - - const message: tl.RawMessage = { - _: 'message', - out: upd.out, - mentioned: upd.mentioned, - mediaUnread: upd.mediaUnread, - silent: upd.silent, - id: upd.id, - fromId: { - _: 'peerUser', - userId: upd.out ? state.auth.userId! : upd.userId, - }, - peerId: { - _: 'peerUser', - userId: upd.userId, - }, - fwdFrom: upd.fwdFrom, - viaBotId: upd.viaBotId, - replyTo: upd.replyTo, - date: upd.date, - message: upd.message, - entities: upd.entities, - ttlPeriod: upd.ttlPeriod, - } - - const update: tl.RawUpdateNewMessage = { - _: 'updateNewMessage', - message, - pts: upd.pts, - ptsCount: upd.ptsCount, - } - - state.pendingPtsUpdates.add({ - update, - ptsBefore: upd.pts - upd.ptsCount, - pts: upd.pts, - peers: new PeersIndex(), - fromDifference: false, - }) - - break - } - case 'updateShortChatMessage': { - log.debug('received updateShortChatMessage') - - const message: tl.RawMessage = { - _: 'message', - out: upd.out, - mentioned: upd.mentioned, - mediaUnread: upd.mediaUnread, - silent: upd.silent, - id: upd.id, - fromId: { - _: 'peerUser', - userId: upd.fromId, - }, - peerId: { - _: 'peerChat', - chatId: upd.chatId, - }, - fwdFrom: upd.fwdFrom, - viaBotId: upd.viaBotId, - replyTo: upd.replyTo, - date: upd.date, - message: upd.message, - entities: upd.entities, - ttlPeriod: upd.ttlPeriod, - } - - const update: tl.RawUpdateNewMessage = { - _: 'updateNewMessage', - message, - pts: upd.pts, - ptsCount: upd.ptsCount, - } - - state.pendingPtsUpdates.add({ - update, - ptsBefore: upd.pts - upd.ptsCount, - pts: upd.pts, - peers: new PeersIndex(), - fromDifference: false, - }) - - break - } - case 'updateShortSentMessage': { - // should not happen - log.warn('received updateShortSentMessage') - break - } - default: - assertNever(upd) - } - } - - // process pts-ordered updates - while (state.pendingPtsUpdates.length) { - const pending = state.pendingPtsUpdates.popFront()! - const upd = pending.update - - // check pts - - let localPts: number | null = null - - if (!pending.channelId) localPts = state.pts! - else if (state.cpts.has(pending.channelId)) { - localPts = state.cpts.get(pending.channelId)! - } else if (state.catchUpChannels) { - // only load stored channel pts in case - // the user has enabled catching up. - // not loading stored pts effectively disables - // catching up, but doesn't interfere with further - // update gaps (i.e. first update received is considered - // to be the base state) - - const saved = await client.storage.getChannelPts(pending.channelId) - - if (saved) { - state.cpts.set(pending.channelId, saved) - localPts = saved - } - } - - if (localPts) { - const diff = localPts - pending.ptsBefore! - // PTS can only go up or drop cardinally - const isPtsDrop = diff > 1000009 - - if (diff > 0 && !isPtsDrop) { - // "the update was already applied, and must be ignored" - log.debug( - 'ignoring %s (cid = %d) because already applied (by pts: exp %d, got %d)', - upd._, - pending.channelId, - localPts, - pending.ptsBefore, - ) - continue - } - if (diff < 0) { - // "there's an update gap that must be filled" - // if the gap is less than 3, put the update into postponed queue - // otherwise, call getDifference - if (diff > -3) { - log.debug( - 'postponing %s for 0.5s (cid = %d) because small gap detected (by pts: exp %d, got %d, diff=%d)', - upd._, - pending.channelId, - localPts, - pending.ptsBefore, - diff, - ) - pending.timeout = Date.now() + 500 - state.pendingPtsUpdatesPostponed.add(pending) - state.postponedTimer.emitBefore(pending.timeout) - } else if (diff > -1000000) { - log.debug( - 'fetching difference after %s (cid = %d) because pts gap detected (by pts: exp %d, got %d, diff=%d)', - upd._, - pending.channelId, - localPts, - pending.ptsBefore, - diff, - ) - - if (pending.channelId) { - fetchChannelDifferenceLater(client, state, requestedDiff, pending.channelId) - } else { - fetchDifferenceLater(client, state, requestedDiff) - } - } else { - log.debug( - 'skipping all updates because pts gap is too big (by pts: exp %d, got %d, diff=%d)', - localPts, - pending.ptsBefore, - diff, - ) - - if (pending.channelId) { - state.cpts.set(pending.channelId, 0) - state.cptsMod.set(pending.channelId, 0) - } else { - await fetchUpdatesState(client, state) - } - } - continue - } - - if (isPtsDrop) { - log.debug('pts drop detected (%d -> %d)', localPts, pending.ptsBefore) - } - } - - await onUpdate(client, state, pending, requestedDiff) - } - - // process postponed pts-ordered updates - for (let item = state.pendingPtsUpdatesPostponed._first; item; item = item.n) { - // awesome fucking iteration because i'm so fucking tired and wanna kms - const pending = item.v - - const upd = pending.update - - let localPts - - if (!pending.channelId) localPts = state.pts! - else if (state.cpts.has(pending.channelId)) { - localPts = state.cpts.get(pending.channelId) - } - - // channel pts from storage will be available because we loaded it earlier - if (!localPts) { - log.warn('local pts not available for postponed %s (cid = %d), skipping', upd._, pending.channelId) - continue - } - - // check the pts to see if the gap was filled - if (localPts > pending.ptsBefore!) { - // "the update was already applied, and must be ignored" - log.debug( - 'ignoring postponed %s (cid = %d) because already applied (by pts: exp %d, got %d)', - upd._, - pending.channelId, - localPts, - pending.ptsBefore, - ) - state.pendingPtsUpdatesPostponed._remove(item) - continue - } - if (localPts < pending.ptsBefore!) { - // "there's an update gap that must be filled" - // if the timeout has not expired yet, keep the update in the queue - // otherwise, fetch diff - const now = Date.now() - - if (now < pending.timeout!) { - log.debug( - 'postponed %s (cid = %d) is still waiting (%dms left) (current pts %d, need %d)', - upd._, - pending.channelId, - pending.timeout! - now, - localPts, - pending.ptsBefore, - ) - } else { - log.debug( - "gap for postponed %s (cid = %d) wasn't filled, fetching diff (current pts %d, need %d)", - upd._, - pending.channelId, - localPts, - pending.ptsBefore, - ) - state.pendingPtsUpdatesPostponed._remove(item) - - if (pending.channelId) { - fetchChannelDifferenceLater(client, state, requestedDiff, pending.channelId) - } else { - fetchDifferenceLater(client, state, requestedDiff) - } - } - continue - } - - await onUpdate(client, state, pending, requestedDiff, true) - state.pendingPtsUpdatesPostponed._remove(item) - } - - // process qts-ordered updates - while (state.pendingQtsUpdates.length) { - const pending = state.pendingQtsUpdates.popFront()! - const upd = pending.update - - // check qts - const diff = state.qts! - pending.qtsBefore! - const isQtsDrop = diff > 1000009 - - if (diff > 0 && !isQtsDrop) { - // "the update was already applied, and must be ignored" - log.debug( - 'ignoring %s because already applied (by qts: exp %d, got %d)', - upd._, - state.qts!, - pending.qtsBefore, - ) - continue - } - if (state.qts! < pending.qtsBefore!) { - // "there's an update gap that must be filled" - // if the gap is less than 3, put the update into postponed queue - // otherwise, call getDifference - if (diff > -3) { - log.debug( - 'postponing %s for 0.5s because small gap detected (by qts: exp %d, got %d, diff=%d)', - upd._, - state.qts!, - pending.qtsBefore, - diff, - ) - pending.timeout = Date.now() + 500 - state.pendingQtsUpdatesPostponed.add(pending) - state.postponedTimer.emitBefore(pending.timeout) - } else { - log.debug( - 'fetching difference after %s because qts gap detected (by qts: exp %d, got %d, diff=%d)', - upd._, - state.qts!, - pending.qtsBefore, - diff, - ) - fetchDifferenceLater(client, state, requestedDiff) - } - continue - } - - if (isQtsDrop) { - log.debug('qts drop detected (%d -> %d)', state.qts, pending.qtsBefore) - } - - await onUpdate(client, state, pending, requestedDiff) - } - - // process postponed qts-ordered updates - for (let item = state.pendingQtsUpdatesPostponed._first; item; item = item.n) { - // awesome fucking iteration because i'm so fucking tired and wanna kms - const pending = item.v - const upd = pending.update - - // check the pts to see if the gap was filled - if (state.qts! > pending.qtsBefore!) { - // "the update was already applied, and must be ignored" - log.debug( - 'ignoring postponed %s because already applied (by qts: exp %d, got %d)', - upd._, - state.qts!, - pending.qtsBefore, - ) - continue - } - if (state.qts! < pending.qtsBefore!) { - // "there's an update gap that must be filled" - // if the timeout has not expired yet, keep the update in the queue - // otherwise, fetch diff - const now = Date.now() - - if (now < pending.timeout!) { - log.debug( - 'postponed %s is still waiting (%dms left) (current qts %d, need %d)', - upd._, - pending.timeout! - now, - state.qts!, - pending.qtsBefore, - ) - } else { - log.debug( - "gap for postponed %s wasn't filled, fetching diff (current qts %d, need %d)", - upd._, - state.qts!, - pending.qtsBefore, - ) - state.pendingQtsUpdatesPostponed._remove(item) - fetchDifferenceLater(client, state, requestedDiff) - } - continue - } - - // gap was filled, and the update can be applied - await onUpdate(client, state, pending, requestedDiff, true) - state.pendingQtsUpdatesPostponed._remove(item) - } - - state.hasTimedoutPostponed = false - - // wait for all pending diffs to load - while (requestedDiff.size) { - log.debug( - 'waiting for %d pending diffs before processing unordered: %J', - requestedDiff.size, - requestedDiff.keys(), - ) - - await Promise.all([...requestedDiff.values()]) - - // diff results may as well contain new diffs to be requested - log.debug( - 'pending diffs awaited, new diffs requested: %d (%J)', - requestedDiff.size, - requestedDiff.keys(), - ) - } - - // process unordered updates (or updates received from diff) - while (state.pendingUnorderedUpdates.length) { - const pending = state.pendingUnorderedUpdates.popFront()! - - await onUpdate(client, state, pending, requestedDiff, false, true) - } - - // onUpdate may also call getDiff in some cases, so we also need to check - // diff may also contain new updates, which will be processed in the next tick, - // but we don't want to postpone diff fetching - while (requestedDiff.size) { - log.debug( - 'waiting for %d pending diffs after processing unordered: %J', - requestedDiff.size, - requestedDiff.keys(), - ) - - await Promise.all([...requestedDiff.values()]) - - // diff results may as well contain new diffs to be requested - log.debug( - 'pending diffs awaited, new diffs requested: %d (%j)', - requestedDiff.size, - requestedDiff.keys(), - ) - } - - // save new update state - await saveUpdatesStorage(client, state, true) - } - - log.debug('updates loop stopped') - } catch (e) { - log.error('updates loop encountered error, restarting: %s', e) - updatesLoop(client, state).catch((err) => client._emitError(err)) - } -} diff --git a/packages/client/src/methods/updates/utils.ts b/packages/client/src/methods/updates/utils.ts deleted file mode 100644 index 6897d0f8..00000000 --- a/packages/client/src/methods/updates/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { tl } from '@mtcute/core' - -export function messageToUpdate(message: tl.TypeMessage): tl.TypeUpdate { - switch (message.peerId!._) { - case 'peerUser': - case 'peerChat': - return { - _: 'updateNewMessage', - message, - pts: 0, - ptsCount: 0, - } - case 'peerChannel': - return { - _: 'updateNewChannelMessage', - message, - pts: 0, - ptsCount: 0, - } - } -} - -export function extractChannelIdFromUpdate(upd: tl.TypeUpdate): number | undefined { - // holy shit - let res = 0 - - if ('channelId' in upd) { - res = upd.channelId - } else if ( - 'message' in upd && - typeof upd.message !== 'string' && - 'peerId' in upd.message && - upd.message.peerId && - 'channelId' in upd.message.peerId - ) { - res = upd.message.peerId.channelId - } - - if (res === 0) return undefined - - return res -} diff --git a/packages/client/src/methods/users/get-me.ts b/packages/client/src/methods/users/get-me.ts deleted file mode 100644 index 06a51379..00000000 --- a/packages/client/src/methods/users/get-me.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - -import { User } from '../../types/index.js' -import { getAuthState } from '../auth/_state.js' - -/** - * Get currently authorized user's full information - */ -export function getMe(client: BaseTelegramClient): Promise { - return client - .call({ - _: 'users.getUsers', - id: [ - { - _: 'inputUserSelf', - }, - ], - }) - .then(async ([user]) => { - assertTypeIs('getMe (@ users.getUsers)', user, 'user') - - const authState = getAuthState(client) - - if (authState.userId !== user.id) { - // there is such possibility, e.g. when - // using a string session without `self`, - // or logging out and re-logging in - // we need to update the fields accordingly, - // and force-save the session - authState.userId = user.id - authState.isBot = Boolean(user.bot) - authState.selfChanged = true - await client.saveStorage() - } - - authState.selfUsername = user.username ?? null - - return new User(user) - }) -} diff --git a/packages/client/src/methods/users/get-my-username.ts b/packages/client/src/methods/users/get-my-username.ts deleted file mode 100644 index b5a13871..00000000 --- a/packages/client/src/methods/users/get-my-username.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BaseTelegramClient } from '@mtcute/core' - -import { getAuthState } from '../auth/_state.js' - -/** - * Get currently authorized user's username. - * - * This method uses locally available information and - * does not call any API methods. - */ -export function getMyUsername(client: BaseTelegramClient): string | null { - return getAuthState(client).selfUsername -} diff --git a/packages/client/src/utils/platform/storage.ts b/packages/client/src/utils/platform/storage.ts deleted file mode 100644 index 3d580e99..00000000 --- a/packages/client/src/utils/platform/storage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { JsonFileStorage } from '@mtcute/core/src/storage/json-file.js' - -/** @internal */ -export const _defaultStorageFactory = (name: string) => { - return new JsonFileStorage(name) -} diff --git a/packages/client/src/utils/updates-utils.ts b/packages/client/src/utils/updates-utils.ts deleted file mode 100644 index 0351e19e..00000000 --- a/packages/client/src/utils/updates-utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' - -// dummy updates which are used for methods that return messages.affectedHistory. -// that is not an update, but it carries info about pts, and we need to handle it - -/** - * Create a dummy `updates` container with given updates. - */ -export function createDummyUpdatesContainer(updates: tl.TypeUpdate[], seq = 0): tl.TypeUpdates { - return { - _: 'updates', - seq, - date: 0, - chats: [], - users: [], - updates, - } -} - -/** - * Create a dummy update from PTS and PTS count. - * - * @param pts PTS - * @param ptsCount PTS count - * @param channelId Channel ID (bare), if applicable - */ -export function createDummyUpdate(pts: number, ptsCount: number, channelId = 0): tl.TypeUpdates { - return createDummyUpdatesContainer([ - { - _: 'mtcute.dummyUpdate', - channelId, - pts, - ptsCount, - }, - ]) -} - -/** @internal */ -export function assertIsUpdatesGroup( - ctx: string, - upd: tl.TypeUpdates, -): asserts upd is tl.RawUpdates | tl.RawUpdatesCombined { - switch (upd._) { - case 'updates': - case 'updatesCombined': - return - } - throw new MtTypeAssertionError(ctx, 'updates | updatesCombined', upd._) -} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json deleted file mode 100644 index af184bdc..00000000 --- a/packages/client/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist/esm", - "rootDir": "./src" - }, - "include": [ - "./src", - ], - "references": [ - { "path": "../core" }, - { "path": "../test" } - ] -} diff --git a/packages/client/typedoc.cjs b/packages/client/typedoc.cjs deleted file mode 100644 index 8afba39c..00000000 --- a/packages/client/typedoc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - extends: ['../../.config/typedoc/config.base.cjs'], - entryPoints: [ - './src/index.ts', - './src/utils/index.ts', - './src/methods/updates/index.ts', - ], - entryPointStrategy: 'expand', -} diff --git a/packages/client/utils.ts b/packages/client/utils.ts deleted file mode 100644 index 50894ad2..00000000 --- a/packages/client/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -// this file only exists as a hint to IDEs that we can use @mtcute/core/utils instead of @mtcute/core/src/utils. -// it is not present in the built package, just a DX improvement - -export * from './src/utils/index.js' diff --git a/packages/core/build.config.cjs b/packages/core/build.config.cjs index a112453f..1c80ab09 100644 --- a/packages/core/build.config.cjs +++ b/packages/core/build.config.cjs @@ -1,5 +1,6 @@ module.exports = ({ path, transformFile, packageDir, outDir }) => ({ esmOnlyDirectives: true, + esmImportDirectives: true, final() { const version = require(path.join(packageDir, 'package.json')).version const replaceVersion = (content) => content.replace('%VERSION%', version) diff --git a/packages/core/package.json b/packages/core/package.json index d5b3744c..d0ddda85 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,13 +2,15 @@ "name": "@mtcute/core", "private": true, "version": "0.6.0", - "description": "Core functions and base MTProto client", + "description": "Type-safe library for MTProto (Telegram API)", "author": "Alina Sireneva ", "license": "MIT", "main": "src/index.ts", "type": "module", "scripts": { - "build": "pnpm run -w build-package core" + "build": "pnpm run -w build-package core", + "gen-client": "node ./scripts/generate-client.cjs", + "gen-updates": "node ./scripts/generate-updates.cjs" }, "browser": { "./src/utils/platform/crypto.js": "./src/utils/platform/crypto.web.js", @@ -16,6 +18,11 @@ "./src/utils/platform/logging.js": "./src/utils/platform/logging.web.js", "./src/utils/platform/random.js": "./src/utils/platform/random.web.js", "./src/utils/platform/exit-hook.js": "./src/utils/platform/exit-hook.web.js", + "./src/highlevel/worker/platform/connect.js": "./src/highlevel/worker/platform/connect.web.js", + "./src/highlevel/worker/platform/register.js": "./src/highlevel/worker/platform/register.web.js", + "./src/highlevel/methods/files/_platform.js": "./src/highlevel/methods/files/_platform.web.js", + "./src/highlevel/methods/files/download-file.js": "./src/highlevel/methods/files/download-file.web.js", + "./src/highlevel/utils/platform/storage.js": "./src/highlevel/utils/platform/storage.web.js", "./src/storage/json-file.js": false }, "distOnlyFields": { @@ -39,6 +46,14 @@ "./storage/*": { "import": "./esm/storage/*", "require": "./cjs/storage/*" + }, + "./highlevel/*": { + "import": "./esm/highlevel/*", + "require": "./cjs/highlevel/*" + }, + "./methods/*": { + "import": "./esm/highlevel/methods/*", + "require": "./cjs/highlevel/methods/*" } } }, @@ -46,13 +61,13 @@ "@mtcute/tl": "workspace:^", "@mtcute/tl-runtime": "workspace:^", "@mtcute/wasm": "workspace:^", + "@mtcute/file-id": "workspace:^", "@types/events": "3.0.0", "events": "3.2.0", "long": "5.2.3" }, "devDependencies": { "@types/ws": "8.5.4", - "node-forge": "1.3.1", "@mtcute/test": "workspace:^", "ws": "8.13.0" } diff --git a/packages/client/scripts/generate-client.cjs b/packages/core/scripts/generate-client.cjs similarity index 92% rename from packages/client/scripts/generate-client.cjs rename to packages/core/scripts/generate-client.cjs index c78af227..9846a521 100644 --- a/packages/client/scripts/generate-client.cjs +++ b/packages/core/scripts/generate-client.cjs @@ -13,7 +13,7 @@ function findMethodAvailability(method) { return entry.available ?? null } -const targetDir = path.join(__dirname, '../src') +const targetDir = path.join(__dirname, '../src/highlevel') async function* getFiles(dir) { const dirents = await fs.promises.readdir(dir, { withFileTypes: true }) @@ -315,7 +315,7 @@ async function addSingleMethod(state, fileName) { const firstArg = stmt.parameters[0] - if (isExported && (!firstArg || firstArg.type.getText() !== 'BaseTelegramClient')) { + if (isExported && (!firstArg || firstArg.type.getText() !== 'ITelegramClient')) { continue } @@ -421,7 +421,8 @@ async function addSingleMethod(state, fileName) { } async function main() { - const output = fs.createWriteStream(path.join(__dirname, '../src/client.ts')) + const targetFile = path.join(__dirname, '../src/highlevel/client.ts') + const output = fs.createWriteStream(targetFile) const state = { imports: {}, fields: [], @@ -435,7 +436,7 @@ async function main() { files: {}, } - for await (const file of getFiles(path.join(__dirname, '../src/methods'))) { + for await (const file of getFiles(path.join(__dirname, '../src/highlevel/methods'))) { if (!file.startsWith('.') && file.endsWith('.ts') && !file.endsWith('.web.ts') && !file.endsWith('.test.ts')) { await addSingleMethod(state, file) } @@ -444,7 +445,9 @@ async function main() { output.write( '/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */\n' + '/* eslint-disable @typescript-eslint/no-unsafe-argument */\n' + - '/* THIS FILE WAS AUTO-GENERATED */\n', + '/* THIS FILE WAS AUTO-GENERATED */\n' + + "import EventEmitter from 'events'\n" + + "import Long from 'long'\n", ) Object.entries(state.imports).forEach(([module, items]) => { items = [...items] @@ -458,7 +461,7 @@ async function main() { output.write(`// from ${from}\n${code}\n`) }) - output.write('\nexport interface TelegramClient extends BaseTelegramClient {\n') + output.write('\nexport interface TelegramClient extends ITelegramClient {\n') output.write(`/** * Register a raw update handler @@ -547,7 +550,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`) `<${func.typeParameters.map((it) => it.getFullText()).join(', ')}>` : '' const rawParams = (func.parameters || []).filter( - (it) => !it.type || it.type.getText() !== 'BaseTelegramClient', + (it) => !it.type || it.type.getText() !== 'ITelegramClient', ) const parameters = rawParams .map((it) => { @@ -648,8 +651,8 @@ on(name: string, handler: (...args: any[]) => void): this\n`) if (hasOverloads) { classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc') } - classProtoDecls.push(` return ${origName}(this, ...args);`) - classProtoDecls.push('}\n') + classProtoDecls.push(` return ${origName}(this._client, ...args);`) + classProtoDecls.push('}') } } }, @@ -657,11 +660,13 @@ on(name: string, handler: (...args: any[]) => void): this\n`) output.write('}\n') output.write('\nexport type { TelegramClientOptions }\n') - output.write('\nexport class TelegramClient extends BaseTelegramClient {\n') + output.write('\nexport class TelegramClient extends EventEmitter implements ITelegramClient {\n') + output.write(' _client: ITelegramClient\n') state.fields.forEach(({ code }) => output.write(`protected ${code}\n`)) output.write('constructor(opts: TelegramClientOptions) {\n') + output.write(' super()\n') state.init.forEach((code) => { output.write(code + '\n') }) @@ -670,10 +675,47 @@ on(name: string, handler: (...args: any[]) => void): this\n`) classContents.forEach((line) => output.write(line + '\n')) output.write('}\n') classProtoDecls.forEach((line) => output.write(line + '\n')) + // proxied methods + ;[ + 'prepare', + 'connect', + 'close', + 'notifyLoggedIn', + 'notifyLoggedOut', + 'notifyChannelOpened', + 'notifyChannelClosed', + 'startUpdatesLoop', + 'stopUpdatesLoop', + 'call', + 'importSession', + 'exportSession', + 'onError', + 'emitError', + 'handleClientUpdate', + 'getApiCrenetials', + 'getPoolSize', + 'getPrimaryDcId', + 'computeSrpParams', + 'computeNewPasswordHash', + ].forEach((name) => { + output.write( + `TelegramClient.prototype.${name} = function(...args) {\n` + + ` return this._client.${name}(...args)\n` + + '}\n', + ) + }) + // disabled methods - they are used internally and we don't want to expose them + // if the user *really* needs them, they can use `client._client` to access the underlying client + ;['onServerUpdate', 'onUpdate'].forEach((name) => { + output.write( + `TelegramClient.prototype.${name} = function() {\n` + + ` throw new Error('${name} is not available for TelegramClient, use .on() methods instead')\n` + + '}\n', + ) + }) state.impls.forEach(({ name, code }) => output.write(`TelegramClient.prototype.${name} = ${code}\n`)) // format the resulting file with prettier - const targetFile = path.join(__dirname, '../src/client.ts') const prettierConfig = await prettier.resolveConfig(targetFile) let fullSource = await fs.promises.readFile(targetFile, 'utf-8') fullSource = await prettier.format(fullSource, { diff --git a/packages/client/scripts/generate-updates.cjs b/packages/core/scripts/generate-updates.cjs similarity index 97% rename from packages/client/scripts/generate-updates.cjs rename to packages/core/scripts/generate-updates.cjs index 180fc727..c814655d 100644 --- a/packages/client/scripts/generate-updates.cjs +++ b/packages/core/scripts/generate-updates.cjs @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-globals */ const fs = require('fs') const path = require('path') const prettier = require('prettier') @@ -90,7 +89,7 @@ function toSentence(type, stype = 'inline') { } function generateParsedUpdate() { - replaceSections('types/updates/index.ts', { + replaceSections('highlevel/types/updates/index.ts', { codegen: 'export type ParsedUpdate =\n' + types.map((typ) => ` | { name: '${typ.typeName}'; data: ${typ.updateType} }\n`).join(''), diff --git a/packages/client/scripts/update-types.txt b/packages/core/scripts/update-types.txt similarity index 100% rename from packages/client/scripts/update-types.txt rename to packages/core/scripts/update-types.txt diff --git a/packages/core/src/base-client.ts b/packages/core/src/base-client.ts deleted file mode 100644 index 8916c080..00000000 --- a/packages/core/src/base-client.ts +++ /dev/null @@ -1,460 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import EventEmitter from 'events' -import Long from 'long' - -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 { BaseTelegramClientOptions } from './base-client.types.js' -import { ConfigManager } from './network/config-manager.js' -import { SessionConnection, TransportFactory } from './network/index.js' -import { NetworkManager, RpcCallOptions } from './network/network-manager.js' -import { ITelegramStorage } from './storage/index.js' -import { MustEqual } from './types/index.js' -import { - ControllablePromise, - createControllablePromise, - defaultCryptoProviderFactory, - defaultProductionDc, - defaultProductionIpv6Dc, - defaultTestDc, - defaultTestIpv6Dc, - getAllPeersFrom, - ICryptoProvider, - LogManager, - readStringSession, - StringSessionData, - toggleChannelIdMark, - writeStringSession, -} from './utils/index.js' - -/** - * Basic Telegram client that only implements the bare minimum - * to make RPC calls and receive low-level updates. - */ -export class BaseTelegramClient extends EventEmitter { - /** - * Crypto provider taken from {@link BaseTelegramClientOptions.crypto} - */ - readonly crypto: ICryptoProvider - - /** - * Telegram storage taken from {@link BaseTelegramClientOptions.storage} - */ - readonly storage: ITelegramStorage - - /** - * "Test mode" taken from {@link BaseTelegramClientOptions.testMode} - */ - protected readonly _testMode: boolean - - /** - * Primary DCs taken from {@link BaseTelegramClientOptions.defaultDcs}, - * loaded from session or changed by other means (like redirecting). - */ - protected _defaultDcs: ITelegramStorage.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 - - /** Unix timestamp when the last update was received */ - protected _lastUpdateTime = 0 - - readonly _config = new ConfigManager(() => this.call({ _: 'help.getConfig' })) - - // not really connected, but rather "connect() was called" - private _connected: ControllablePromise | boolean = false - - _emitError: (err: unknown, connection?: SessionConnection) => void = console.error.bind(console) - - private _importFrom?: StringSessionData - private _importForce?: boolean - - readonly log = new LogManager('client') - readonly network: NetworkManager - - constructor(readonly params: BaseTelegramClientOptions) { - super() - - if (params.logLevel !== undefined) { - this.log.level = params.logLevel - } - - this.crypto = (params.crypto ?? defaultCryptoProviderFactory)() - this.storage = params.storage - 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.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), - keepAliveAction: this._keepAliveAction.bind(this), - enableErrorReporting: params.enableErrorReporting ?? false, - onUsable: () => this.emit('usable'), - ...(params.network ?? {}), - }, - this._config, - ) - - this.storage.setup?.(this.log, this._readerMap, this._writerMap) - } - - protected _keepAliveAction(): void { - this.emit('keep_alive') - } - - protected async _loadStorage(): Promise { - await this.storage.load?.() - } - - _beforeStorageSave: (() => Promise)[] = [] - - beforeStorageSave(cb: () => Promise): void { - this._beforeStorageSave.push(cb) - } - - offBeforeStorageSave(cb: () => Promise): void { - this._beforeStorageSave = this._beforeStorageSave.filter((x) => x !== cb) - } - - async saveStorage(): Promise { - for (const cb of this._beforeStorageSave) { - await cb() - } - await this.storage.save?.() - } - - /** - * 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 { - if (this._connected) { - // avoid double-connect - await this._connected - - return - } - - const promise = (this._connected = createControllablePromise()) - - await this.crypto.initialize?.() - await this._loadStorage() - const primaryDc = await this.storage.getDefaultDcs() - if (primaryDc !== null) this._defaultDcs = primaryDc - - const defaultDcAuthKey = await this.storage.getAuthKeyFor(this._defaultDcs.main.id) - - if ((this._importForce || !defaultDcAuthKey) && this._importFrom) { - const data = this._importFrom - - if (data.testMode !== this._testMode) { - throw new Error( - 'This session string is not for the current backend. ' + - `Session is ${data.testMode ? 'test' : 'prod'}, but the client is ${ - this._testMode ? 'test' : 'prod' - }`, - ) - } - - this._defaultDcs = data.primaryDcs - await this.storage.setDefaultDcs(data.primaryDcs) - - if (data.self) { - await this.storage.setSelf(data.self) - } - - // await this.primaryConnection.setupKeys(data.authKey) - await this.storage.setAuthKeyFor(data.primaryDcs.main.id, data.authKey) - - await this.saveStorage() - } - - this.emit('before_connect') - - this.network - .connect(this._defaultDcs) - .then(() => { - promise.resolve() - this._connected = true - }) - .catch((err: Error) => this._emitError(err)) - } - - /** - * Close all connections and finalize the client. - */ - async close(): Promise { - this.emit('before_close') - - this._config.destroy() - this.network.destroy() - - await this.saveStorage() - await this.storage.destroy?.() - - this.emit('closed') - } - - /** - * 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 - * 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 { - if (this._connected !== true) { - await this.connect() - } - - const stack = this._niceStacks ? new Error().stack : undefined - - const res = await this.network.call(message, params, stack) - - if (await this._cachePeersFrom(res)) { - await this.saveStorage() - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return res - } - - /** - * Create a Proxy that will call all methods with given call parameters - * (see {@link RpcCallOptions}}) - * - * This is useful when you don't call `call()` directly, but rather - * use high-level API provided by `@mtcute/client`, for example: - * - * ```ts - * const client = new TelegramClient(...) - * - * const someone = await client - * .withCallParams({ timeout: 500 }) - * .getUsers(...) - * ``` - */ - withCallParams(params: RpcCallOptions): this { - return new Proxy(this, { - get(target, prop, receiver) { - if (prop === 'call') { - return (message: tl.RpcMethod, paramsCustom?: RpcCallOptions) => - target.call(message, { - ...params, - ...paramsCustom, - }) - } - - return Reflect.get(target, prop, receiver) - }, - }) - } - - /** - * Shorthand for `withCallParams({ abortSignal })` - */ - withAbortSignal(signal: AbortSignal): this { - return this.withCallParams({ abortSignal: signal }) - } - - /** - * 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.network.changeTransport(factory) - } - - /** - * 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. - */ - onError(handler: (err: unknown, connection?: SessionConnection) => void): void { - this._emitError = handler - } - - notifyLoggedIn(auth: tl.auth.RawAuthorization): void { - this.network.notifyLoggedIn(auth) - this.emit('logged_in', auth) - } - - /** - * **ADVANCED** - * - * Adds all peers from a given object to entity cache in storage. - */ - async _cachePeersFrom(obj: object): Promise { - const parsedPeers: ITelegramStorage.PeerInfo[] = [] - - let count = 0 - - for (const peer of getAllPeersFrom(obj as tl.TlObject)) { - if ((peer as any).min) { - // no point in caching min peers as we can't use them - continue - } - - count += 1 - - switch (peer._) { - case 'user': - if (!peer.accessHash) { - this.log.warn('received user without access hash: %j', peer) - continue - } - - parsedPeers.push({ - id: peer.id, - accessHash: peer.accessHash, - username: peer.username?.toLowerCase(), - phone: peer.phone, - type: 'user', - full: peer, - }) - break - case 'chat': - case 'chatForbidden': - parsedPeers.push({ - id: -peer.id, - accessHash: Long.ZERO, - type: 'chat', - full: peer, - }) - break - case 'channel': - case 'channelForbidden': - if (!peer.accessHash) { - this.log.warn('received user without access hash: %j', peer) - continue - } - parsedPeers.push({ - id: toggleChannelIdMark(peer.id), - accessHash: peer.accessHash, - username: peer._ === 'channel' ? peer.username?.toLowerCase() : undefined, - type: 'channel', - full: peer, - }) - break - } - } - - if (count > 0) { - await this.storage.updatePeers(parsedPeers) - this.log.debug('cached %d peers', count) - - return true - } - - return false - } - - /** - * 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" > - * > find the one containing `mtcute` > Revoke, - * > or, in case this is a bot, revoke bot token - * > with [@BotFather](//t.me/botfather) - */ - async exportSession(): Promise { - const primaryDcs = (await this.storage.getDefaultDcs()) ?? this._defaultDcs - - const authKey = await this.storage.getAuthKeyFor(primaryDcs.main.id) - if (!authKey) throw new Error('Auth key is not ready yet') - - return writeStringSession(this._writerMap, { - version: 2, - self: await this.storage.getSelf(), - testMode: this._testMode, - primaryDcs, - authKey, - }) - } - - /** - * Request the session to be imported from the given session string. - * - * Note that the session will not be imported right away, - * instead, it will be imported once `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 (unless `force`). - * - * @param session Session string to import - * @param force Whether to overwrite existing session - */ - importSession(session: string | StringSessionData, force = false): void { - this._importFrom = typeof session === 'string' ? readStringSession(this._readerMap, session) : session - this._importForce = force - } -} diff --git a/packages/core/src/base-client.types.ts b/packages/core/src/base-client.types.ts deleted file mode 100644 index bbc0e8fa..00000000 --- a/packages/core/src/base-client.types.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { tl } from '@mtcute/tl' -import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' - -import { NetworkManagerExtraParams, ReconnectionStrategy, TransportFactory } from './network/index.js' -import { PersistentConnectionParams } from './network/persistent-connection.js' -import { ITelegramStorage } from './storage/abstract.js' -import { CryptoProviderFactory } from './utils/index.js' - -/** Options for {@link BaseTelegramClient} */ -export interface BaseTelegramClientOptions { - /** - * API ID from my.telegram.org - */ - apiId: number - /** - * API hash from my.telegram.org - */ - apiHash: string - - /** - * Storage to use for this client. - */ - 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. - * - * @default Production DC 2. - */ - defaultDcs?: ITelegramStorage.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 - - /** - * Set logging level for the client. - * - * 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 -} diff --git a/packages/core/src/highlevel/base.ts b/packages/core/src/highlevel/base.ts new file mode 100644 index 00000000..62e0e17c --- /dev/null +++ b/packages/core/src/highlevel/base.ts @@ -0,0 +1,297 @@ +/* eslint-disable @typescript-eslint/require-await */ +import { tl } from '@mtcute/tl' + +import { MtClient, MtClientOptions } from '../network/client.js' +import { ConnectionKind, RpcCallOptions } from '../network/network-manager.js' +import { StorageManagerExtraOptions } from '../storage/storage.js' +import { MtArgumentError } from '../types/errors.js' +import { MustEqual } from '../types/utils.js' +import { + asyncResettable, + computeNewPasswordHash, + computeSrpParams, + readStringSession, + StringSessionData, + writeStringSession, +} from '../utils/index.js' +import { LogManager } from '../utils/logger.js' +import { ITelegramClient } from './client.types.js' +import { ITelegramStorageProvider } from './storage/provider.js' +import { TelegramStorageManager, TelegramStorageManagerExtraOptions } from './storage/storage.js' +import { UpdatesManager } from './updates/manager.js' +import { RawUpdateHandler, UpdatesManagerParams } from './updates/types.js' + +export interface BaseTelegramClientOptions extends MtClientOptions { + storage: ITelegramStorageProvider + storageOptions?: StorageManagerExtraOptions & TelegramStorageManagerExtraOptions + updates?: UpdatesManagerParams | false +} + +export class BaseTelegramClient implements ITelegramClient { + readonly updates?: UpdatesManager + private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {} + + constructor(readonly params: BaseTelegramClientOptions) { + if (!params.disableUpdates && params.updates !== false) { + this.updates = new UpdatesManager(this, params.updates) + this._serverUpdatesHandler = this.updates.handleUpdate.bind(this.updates) + } + + this.mt.on('update', (update: tl.TypeUpdates) => { + this._serverUpdatesHandler(update) + }) + } + + readonly log = this.params.logger ?? new LogManager('client') + readonly mt = new MtClient({ + ...this.params, + logger: this.log.create('mtproto'), + }) + readonly crypto = this.mt.crypto + readonly storage = new TelegramStorageManager(this.mt.storage, { + provider: this.params.storage, + ...this.params.storageOptions, + }) + + private _prepare = asyncResettable(async () => { + await this.mt.prepare() + + const self = await this.storage.self.fetch() + this.log.prefix = `[USER ${self?.userId ?? 'n/a'}] ` + this.mt.network.setIsPremium(self?.isPremium ?? false) + + await this.updates?.prepare() + }) + + /** + * **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() + } + + // used in a hot path, avoid extra function calls + private _connected = false + private _connect = asyncResettable(async () => { + await this._prepare.run() + await this.mt.connect() + this._connected = true + }) + + /** + * 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() + } + + get isConnected(): boolean { + return this._connected + } + + async close(): Promise { + await this.mt.close() + this.updates?.stopLoop() + this._prepare.reset() + this._connect.reset() + this._connected = false + } + + async notifyLoggedIn(auth: tl.auth.TypeAuthorization | tl.RawUser): Promise { + const user = this.mt.network.notifyLoggedIn(auth) + + this.log.prefix = `[USER ${user.id}] ` + const self = await this.storage.self.storeFrom(user) + + this.updates?.notifyLoggedIn(self) + + return user + } + + async notifyLoggedOut(): Promise { + this.mt.network.notifyLoggedOut() + + this.log.prefix = '[USER n/a] ' + await this.storage.self.store(null) + } + + async notifyChannelOpened(channelId: number, pts?: number): Promise { + return this.updates?.notifyChannelOpened(channelId, pts) ?? false + } + + async notifyChannelClosed(channelId: number): Promise { + return this.updates?.notifyChannelClosed(channelId) ?? false + } + + async startUpdatesLoop(): Promise { + await this.updates?.startLoop() + } + + async stopUpdatesLoop(): Promise { + this.updates?.stopLoop() + } + + /** + * Make an RPC call + * + * 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 { + if (!this._connected) { + await this._connect.run() + } + + const res = await this.mt.call(message, params) + + await this.storage.peers.updatePeersFrom(res) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return res + } + + /** + * Import the session from the given session string. + * + * 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 (unless `force`). + * + * @param session Session string to import + * @param force Whether to overwrite existing session + */ + async importSession(session: string | StringSessionData, force = false): Promise { + await this.prepare() + + const defaultDcAuthKey = await this.mt.storage.provider.authKeys.get(this.mt._defaultDcs.main.id) + + if (defaultDcAuthKey && !force) return + + const data = typeof session === 'string' ? readStringSession(this.mt._readerMap, session) : session + + if (data.testMode && !this.params.testMode) { + throw new Error( + 'This session string is not for the current backend. ' + + `Session is ${data.testMode ? 'test' : 'prod'}, ` + + `but the client is ${this.params.testMode ? 'test' : 'prod'}`, + ) + } + + this.mt._defaultDcs = data.primaryDcs + await this.mt.storage.dcs.store(data.primaryDcs) + + if (data.self) { + await this.storage.self.store(data.self) + } + + await this.mt.storage.provider.authKeys.set(data.primaryDcs.main.id, data.authKey) + + await this.mt.storage.save() + } + + /** + * 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" > + * > find the one containing `mtcute` > Revoke, + * > or, in case this is a bot, revoke bot token + * > with [@BotFather](//t.me/botfather) + */ + async exportSession(): Promise { + await this._prepare.run() + + const primaryDcs = (await this.mt.storage.dcs.fetch()) ?? this.mt._defaultDcs + + const authKey = await this.mt.storage.provider.authKeys.get(primaryDcs.main.id) + if (!authKey) throw new Error('Auth key is not ready yet') + + return writeStringSession(this.mt._writerMap, { + version: 2, + self: await this.storage.self.fetch(), + testMode: Boolean(this.params.testMode), + primaryDcs, + authKey, + }) + } + + /** + * Register an error handler for the client + * + * @param handler Error handler. + */ + onError(handler: (err: unknown) => void): void { + this.mt.onError(handler) + } + + emitError(err: unknown): void { + this.mt.emitError(err) + } + + handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void { + this.updates?.handleClientUpdate(updates, noDispatch) + } + + onServerUpdate(handler: (update: tl.TypeUpdates) => void): void { + this._serverUpdatesHandler = handler + } + + onUpdate(handler: RawUpdateHandler): void { + if (!this.updates) { + throw new MtArgumentError('Updates manager is disabled') + } + + this.updates.setHandler(handler) + } + + async getApiCrenetials() { + return { + id: this.params.apiId, + hash: this.params.apiHash, + } + } + + async getPoolSize(kind: ConnectionKind, dcId?: number): Promise { + if (!this._connected) { + await this._connect.run() + } + + return this.mt.network.getPoolSize(kind, dcId) + } + + async getPrimaryDcId(): Promise { + if (!this._connected) { + await this._connect.run() + } + + return this.mt.network.getPrimaryDcId() + } + + computeSrpParams(request: tl.account.RawPassword, password: string): Promise { + return computeSrpParams(this.crypto, request, password) + } + computeNewPasswordHash(algo: tl.TypePasswordKdfAlgo, password: string): Promise { + return computeNewPasswordHash(this.crypto, algo, password) + } +} diff --git a/packages/client/src/client.ts b/packages/core/src/highlevel/client.ts similarity index 90% rename from packages/client/src/client.ts rename to packages/core/src/highlevel/client.ts index 989e9bcc..8ff17f80 100644 --- a/packages/client/src/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -1,32 +1,30 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* THIS FILE WAS AUTO-GENERATED */ -import { - BaseTelegramClient, - BaseTelegramClientOptions, - ITelegramStorage, - Long, - MaybeArray, - MaybeAsync, - PartialExcept, - PartialOnly, - tl, -} from '@mtcute/core' -import { MemoryStorage } from '@mtcute/core/src/storage/memory.js' -import { tdFileId } from '@mtcute/file-id' +import EventEmitter from 'events' +import Long from 'long' -import { AuthState, getAuthState, isSelfPeer, setupAuthState } from './methods/auth/_state.js' +import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { MemoryStorage } from '../storage/providers/memory/index.js' +import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../types/index.js' +import { StringSessionData } from '../utils/string-session.js' +import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js' +import { ITelegramClient } from './client.types.js' import { checkPassword } from './methods/auth/check-password.js' import { getPasswordHint } from './methods/auth/get-password-hint.js' import { logOut } from './methods/auth/log-out.js' import { recoverPassword } from './methods/auth/recover-password.js' import { resendCode } from './methods/auth/resend-code.js' +import { run } from './methods/auth/run.js' import { sendCode } from './methods/auth/send-code.js' import { sendRecoveryCode } from './methods/auth/send-recovery-code.js' import { signIn } from './methods/auth/sign-in.js' import { signInBot } from './methods/auth/sign-in-bot.js' import { start } from './methods/auth/start.js' import { startTest } from './methods/auth/start-test.js' +import { isSelfPeer } from './methods/auth/utils.js' import { answerCallbackQuery } from './methods/bots/answer-callback-query.js' import { answerInlineQuery } from './methods/bots/answer-inline-query.js' import { answerPreCheckoutQuery } from './methods/bots/answer-pre-checkout-query.js' @@ -214,22 +212,6 @@ import { sendStory } from './methods/stories/send-story.js' import { sendStoryReaction } from './methods/stories/send-story-reaction.js' import { togglePeerStoriesArchived } from './methods/stories/toggle-peer-stories-archived.js' import { toggleStoriesPinned } from './methods/stories/toggle-stories-pinned.js' -import { - enableUpdatesProcessing, - makeParsedUpdateHandler, - ParsedUpdateHandlerParams, - UpdatesManagerParams, -} from './methods/updates/index.js' -import { - catchUp, - enableRps, - getCurrentRpsIncoming, - getCurrentRpsProcessing, - notifyChannelClosed, - notifyChannelOpened, - startUpdatesLoop, - stopUpdatesLoop, -} from './methods/updates/manager.js' import { blockUser } from './methods/users/block-user.js' import { deleteProfilePhotos } from './methods/users/delete-profile-photos.js' import { editCloseFriends, editCloseFriendsRaw } from './methods/users/edit-close-friends.js' @@ -250,6 +232,7 @@ import { setMyUsername } from './methods/users/set-my-username.js' import { setOffline } from './methods/users/set-offline.js' import { unblockUser } from './methods/users/unblock-user.js' import { updateProfile } from './methods/users/update-profile.js' +import { ITelegramStorageProvider } from './storage/provider.js' import { Conversation } from './types/conversation.js' import { AllStories, @@ -331,45 +314,27 @@ import { UserStatusUpdate, UserTypingUpdate, } from './types/index.js' +import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from './updates/parsed.js' import { _defaultStorageFactory } from './utils/platform/storage.js' // from methods/_init.ts -interface TelegramClientOptions extends Omit { - /** - * Storage to use for this client. - * - * If a string is passed, it will be used as: - * - a path to a JSON file for Node.js - * - IndexedDB database name for browsers - * - * If omitted, {@link MemoryStorage} is used - */ - storage?: string | ITelegramStorage - - /** - * Parameters for updates manager. - */ - updates?: Omit - - /** - * **ADVANCED** - * - * If set to `true`, updates manager will not be created, - * and only raw TL Updates will be emitted. - * - * Unlike {@link TelegramClientOptions.disableUpdates}, this - * does not prevent the updates from being sent by the server, - * but disables proper handling of them (see [Working with Updates](https://core.telegram.org/api/updates)) - * - * This may be useful in some cases when you require more control over - * the updates or to minimize additional overhead from properly handling them - * for some very particular use cases. - * - * The updates **will not** be dispatched the normal way, instead - * you should manually add a handler using `client.network.setUpdateHandler`. - */ - disableUpdatesManager?: boolean - +// @copy +type TelegramClientOptions = ( + | (Omit & { + /** + * Storage to use for this client. + * + * If a string is passed, it will be used as: + * - a path to a JSON file for Node.js + * - IndexedDB database name for browsers + * + * If omitted, {@link MemoryStorage} is used + */ + storage?: string | ITelegramStorageProvider + }) + | { client: ITelegramClient } +) & { + updates?: Omit /** * If `true`, the updates that were handled by some {@link Conversation} * will not be dispatched any further. @@ -379,7 +344,7 @@ interface TelegramClientOptions extends Omit void): this - /** - * Get auth state for the given client, containing - * information about the current user. - * - * Auth state must first be initialized with {@link setupAuthState}. - * **Available**: ✅ both users and bots - * - */ - getAuthState(): AuthState - /** - * Check if the given peer/input peer is referring to the current user - * **Available**: ✅ both users and bots - * - */ - isSelfPeer(peer: tl.TypeInputPeer | tl.TypePeer | tl.TypeInputUser): boolean /** * Check your Two-Step verification password and log in * @@ -634,7 +584,6 @@ export interface TelegramClient extends BaseTelegramClient { * * @param params Parameters to be passed to {@link start} * @param then Function to be called after {@link start} returns - * @manual=noemit */ run(params: Parameters[1], then?: (user: User) => void | Promise): void /** @@ -723,7 +672,7 @@ export interface TelegramClient extends BaseTelegramClient { * This method handles both login and sign up, and also handles 2FV * * All parameters are `MaybeDynamic`, meaning you - * can either supply `T`, or a function that returns `MaybeAsync` + * can either supply `T`, or a function that returns `MaybePromise` * * This method is intended for simple and fast use in automated * scripts and bots. If you are developing a custom client, @@ -740,7 +689,7 @@ export interface TelegramClient extends BaseTelegramClient { * Note that passed session will be ignored in case storage already * contains authorization. */ - session?: string + session?: string | StringSessionData /** * Whether to overwrite existing session. @@ -776,7 +725,7 @@ export interface TelegramClient extends BaseTelegramClient { * If provided `code`/`password` is a constant string, providing an * invalid one will interrupt authorization flow. */ - invalidCodeCallback?: (type: 'code' | 'password') => MaybeAsync + invalidCodeCallback?: (type: 'code' | 'password') => MaybePromise /** * Whether to force code delivery through SMS @@ -792,8 +741,14 @@ export interface TelegramClient extends BaseTelegramClient { * @param code * @default `console.log`. */ - codeSentCallback?: (code: SentCode) => MaybeAsync + codeSentCallback?: (code: SentCode) => MaybePromise }): Promise + /** + * Check if the given peer/input peer is referring to the current user + * **Available**: ✅ both users and bots + * + */ + isSelfPeer(peer: tl.TypeInputPeer | tl.TypePeer | tl.TypeInputUser): boolean /** * Send an answer to a callback query. * @@ -4012,7 +3967,7 @@ export interface TelegramClient extends BaseTelegramClient { */ sendTyping( chatId: InputPeerLike, - status?: TypingStatus | tl.TypeSendMessageAction, + status?: Exclude | tl.TypeSendMessageAction, params?: { /** * For `upload_*` and history import actions, progress of the upload @@ -4901,106 +4856,6 @@ export interface TelegramClient extends BaseTelegramClient { */ peer?: InputPeerLike }): Promise - // code in this file is very bad, thanks to Telegram's awesome updates mechanism - /** - * Enable RPS meter. - * Only available in NodeJS v10.7.0 and newer - * - * > **Note**: This may have negative impact on performance - * - * **Available**: ✅ both users and bots - * - * @param size Sampling size - * @param time Window time - */ - enableRps(size?: number, time?: number): void - /** - * Get current average incoming RPS - * - * Incoming RPS is calculated based on - * incoming update containers. Normally, - * they should be around the same, except - * rare situations when processing rps - * may peak. - * **Available**: ✅ both users and bots - * - */ - getCurrentRpsIncoming(): number - /** - * Get current average processing RPS - * - * Processing RPS is calculated based on - * dispatched updates. Normally, - * they should be around the same, except - * rare situations when processing rps - * may peak. - * **Available**: ✅ both users and bots - * - */ - getCurrentRpsProcessing(): number - /** - * Start updates loop. - * - * You must first call {@link enableUpdatesProcessing} to use this method. - * - * It is recommended to use this method in callback to {@link start}, - * or otherwise make sure the user is logged in. - * - * > **Note**: If you are using {@link UpdatesManagerParams.catchUp} option, - * > catching up will be done in background, you can't await it. - * **Available**: ✅ both users and bots - * - */ - startUpdatesLoop(): Promise - /** - * **ADVANCED** - * - * Manually stop updates loop. - * Usually done automatically when stopping the client with {@link close} - * **Available**: ✅ both users and bots - * - */ - stopUpdatesLoop(): void - /** - * Catch up with the server by loading missed updates. - * - * > **Note**: In case the storage was not properly - * > closed the last time, "catching up" might - * > result in duplicate updates. - * **Available**: ✅ both users and bots - * - */ - catchUp(): void - /** - * **ADVANCED** - * - * Notify the updates manager that some channel was "opened". - * Channel difference for "opened" channels will be fetched on a regular basis. - * This is a low-level method, prefer using {@link openChat} instead. - * - * Channel must be resolve-able with `resolvePeer` method (i.e. be in cache); - * base chat PTS must either be passed (e.g. from {@link Dialog}), or cached in storage. - * - * **Available**: ✅ both users and bots - * - * @param channelId Bare ID of the channel - * @param pts PTS of the channel, if known (e.g. from {@link Dialog}) - * @returns `true` if the channel was opened for the first time, `false` if it is already opened - */ - notifyChannelOpened(channelId: number, pts?: number): boolean - /** - * **ADVANCED** - * - * Notify the updates manager that some channel was "closed". - * Basically the opposite of {@link notifyChannelOpened}. - * This is a low-level method, prefer using {@link closeChat} instead. - * - * **Available**: ✅ both users and bots - * - * @param channelId Bare channel ID - * @returns `true` if the chat was closed for the last time, `false` otherwise - */ - notifyChannelClosed(channelId: number): boolean /** * Block a user * @@ -5062,7 +4917,7 @@ export interface TelegramClient extends BaseTelegramClient { * **Available**: ✅ both users and bots * */ - getMyUsername(): string | null + getMyUsername(): Promise /** * Get a single profile picture of a user by its ID * @@ -5265,1053 +5120,837 @@ export interface TelegramClient extends BaseTelegramClient { export type { TelegramClientOptions } -export class TelegramClient extends BaseTelegramClient { - protected _disableUpdatesManager: boolean +export class TelegramClient extends EventEmitter implements ITelegramClient { + _client: ITelegramClient constructor(opts: TelegramClientOptions) { - if (typeof opts.storage === 'string') { - opts.storage = _defaultStorageFactory(opts.storage) - } else if (!opts.storage) { - opts.storage = new MemoryStorage() - } + super() - /* eslint-disable @typescript-eslint/no-unsafe-call */ - // @ts-expect-error codegen - super(opts) - /* eslint-enable @typescript-eslint/no-unsafe-call */ - this._disableUpdatesManager = opts.disableUpdatesManager ?? false - const skipConversationUpdates = opts.skipConversationUpdates ?? true - - if (!opts.disableUpdates && !opts.disableUpdatesManager) { - const { messageGroupingInterval, ...managerParams } = opts.updates ?? {} - - enableUpdatesProcessing(this, { - ...managerParams, - onUpdate: makeParsedUpdateHandler({ - messageGroupingInterval, - onUpdate: (update) => { - if (Conversation.handleUpdate(this, update) && skipConversationUpdates) return - - this.emit('update', update) - this.emit(update.name, update.data) - }, - onRawUpdate: (update, peers) => { - this.emit('raw_update', update, peers) - }, - }), - }) + if ('client' in opts) { + this._client = opts.client } else { - setupAuthState(this) + let storage: ITelegramStorageProvider + + if (typeof opts.storage === 'string') { + storage = _defaultStorageFactory(opts.storage) + } else if (!opts.storage) { + storage = new MemoryStorage() + } else { + storage = opts.storage + } + + this._client = new BaseTelegramClient({ + ...opts, + storage, + }) } + + // @ts-expect-error codegen + this.log = this._client.log + // @ts-expect-error codegen + this.storage = this._client.storage + + const skipConversationUpdates = opts.skipConversationUpdates ?? true + const { messageGroupingInterval } = opts.updates ?? {} + + this._client.onUpdate( + makeParsedUpdateHandler({ + messageGroupingInterval, + onUpdate: (update) => { + if (Conversation.handleUpdate(this._client, update) && skipConversationUpdates) return + + this.emit('update', update) + this.emit(update.name, update.data) + }, + onRawUpdate: (update, peers) => { + this.emit('raw_update', update, peers) + }, + }), + ) } } -TelegramClient.prototype.getAuthState = function (...args) { - return getAuthState(this, ...args) -} - -TelegramClient.prototype.isSelfPeer = function (...args) { - return isSelfPeer(this, ...args) -} - TelegramClient.prototype.checkPassword = function (...args) { - return checkPassword(this, ...args) + return checkPassword(this._client, ...args) } - TelegramClient.prototype.getPasswordHint = function (...args) { - return getPasswordHint(this, ...args) + return getPasswordHint(this._client, ...args) } - TelegramClient.prototype.logOut = function (...args) { - return logOut(this, ...args) + return logOut(this._client, ...args) } - TelegramClient.prototype.recoverPassword = function (...args) { - return recoverPassword(this, ...args) + return recoverPassword(this._client, ...args) } - TelegramClient.prototype.resendCode = function (...args) { - return resendCode(this, ...args) + return resendCode(this._client, ...args) +} +TelegramClient.prototype.run = function (...args) { + return run(this._client, ...args) } - TelegramClient.prototype.sendCode = function (...args) { - return sendCode(this, ...args) + return sendCode(this._client, ...args) } - TelegramClient.prototype.sendRecoveryCode = function (...args) { - return sendRecoveryCode(this, ...args) + return sendRecoveryCode(this._client, ...args) } - TelegramClient.prototype.signInBot = function (...args) { - return signInBot(this, ...args) + return signInBot(this._client, ...args) } - TelegramClient.prototype.signIn = function (...args) { - return signIn(this, ...args) + return signIn(this._client, ...args) } - TelegramClient.prototype.startTest = function (...args) { - return startTest(this, ...args) + return startTest(this._client, ...args) +} +TelegramClient.prototype.start = function (...args) { + return start(this._client, ...args) +} +TelegramClient.prototype.isSelfPeer = function (...args) { + return isSelfPeer(this._client, ...args) } - TelegramClient.prototype.answerCallbackQuery = function (...args) { - return answerCallbackQuery(this, ...args) + return answerCallbackQuery(this._client, ...args) } - TelegramClient.prototype.answerInlineQuery = function (...args) { - return answerInlineQuery(this, ...args) + return answerInlineQuery(this._client, ...args) } - TelegramClient.prototype.answerPreCheckoutQuery = function (...args) { - return answerPreCheckoutQuery(this, ...args) + return answerPreCheckoutQuery(this._client, ...args) } - TelegramClient.prototype.deleteMyCommands = function (...args) { - return deleteMyCommands(this, ...args) + return deleteMyCommands(this._client, ...args) } - TelegramClient.prototype.getBotInfo = function (...args) { - return getBotInfo(this, ...args) + return getBotInfo(this._client, ...args) } - TelegramClient.prototype.getBotMenuButton = function (...args) { - return getBotMenuButton(this, ...args) + return getBotMenuButton(this._client, ...args) } - TelegramClient.prototype.getCallbackAnswer = function (...args) { - return getCallbackAnswer(this, ...args) + return getCallbackAnswer(this._client, ...args) } - TelegramClient.prototype.getGameHighScores = function (...args) { - return getGameHighScores(this, ...args) + return getGameHighScores(this._client, ...args) } - TelegramClient.prototype.getInlineGameHighScores = function (...args) { - return getInlineGameHighScores(this, ...args) + return getInlineGameHighScores(this._client, ...args) } - TelegramClient.prototype.getMyCommands = function (...args) { - return getMyCommands(this, ...args) + return getMyCommands(this._client, ...args) } - TelegramClient.prototype.setBotInfo = function (...args) { - return setBotInfo(this, ...args) + return setBotInfo(this._client, ...args) } - TelegramClient.prototype.setBotMenuButton = function (...args) { - return setBotMenuButton(this, ...args) + return setBotMenuButton(this._client, ...args) } - TelegramClient.prototype.setGameScore = function (...args) { - return setGameScore(this, ...args) + return setGameScore(this._client, ...args) } - TelegramClient.prototype.setInlineGameScore = function (...args) { - return setInlineGameScore(this, ...args) + return setInlineGameScore(this._client, ...args) } - TelegramClient.prototype.setMyCommands = function (...args) { - return setMyCommands(this, ...args) + return setMyCommands(this._client, ...args) } - TelegramClient.prototype.setMyDefaultRights = function (...args) { - return setMyDefaultRights(this, ...args) + return setMyDefaultRights(this._client, ...args) } - TelegramClient.prototype.addChatMembers = function (...args) { - return addChatMembers(this, ...args) + return addChatMembers(this._client, ...args) } - TelegramClient.prototype.archiveChats = function (...args) { - return archiveChats(this, ...args) + return archiveChats(this._client, ...args) } - TelegramClient.prototype.banChatMember = function (...args) { - return banChatMember(this, ...args) + return banChatMember(this._client, ...args) } - TelegramClient.prototype.createChannel = function (...args) { - return createChannel(this, ...args) + return createChannel(this._client, ...args) } - TelegramClient.prototype.createGroup = function (...args) { - return createGroup(this, ...args) + return createGroup(this._client, ...args) } - TelegramClient.prototype.createSupergroup = function (...args) { - return createSupergroup(this, ...args) + return createSupergroup(this._client, ...args) } - TelegramClient.prototype.deleteChannel = function (...args) { - return deleteChannel(this, ...args) + return deleteChannel(this._client, ...args) } - TelegramClient.prototype.deleteSupergroup = function (...args) { - return deleteChannel(this, ...args) + return deleteChannel(this._client, ...args) } - TelegramClient.prototype.deleteChatPhoto = function (...args) { - return deleteChatPhoto(this, ...args) + return deleteChatPhoto(this._client, ...args) } - TelegramClient.prototype.deleteGroup = function (...args) { - return deleteGroup(this, ...args) + return deleteGroup(this._client, ...args) } - TelegramClient.prototype.deleteHistory = function (...args) { - return deleteHistory(this, ...args) + return deleteHistory(this._client, ...args) } - TelegramClient.prototype.deleteUserHistory = function (...args) { - return deleteUserHistory(this, ...args) + return deleteUserHistory(this._client, ...args) } - TelegramClient.prototype.editAdminRights = function (...args) { - return editAdminRights(this, ...args) + return editAdminRights(this._client, ...args) } - TelegramClient.prototype.getChatEventLog = function (...args) { - return getChatEventLog(this, ...args) + return getChatEventLog(this._client, ...args) } - TelegramClient.prototype.getChatMember = function (...args) { - return getChatMember(this, ...args) + return getChatMember(this._client, ...args) } - TelegramClient.prototype.getChatMembers = function (...args) { - return getChatMembers(this, ...args) + return getChatMembers(this._client, ...args) } - TelegramClient.prototype.getChatPreview = function (...args) { - return getChatPreview(this, ...args) + return getChatPreview(this._client, ...args) } - TelegramClient.prototype.getChat = function (...args) { - return getChat(this, ...args) + return getChat(this._client, ...args) } - TelegramClient.prototype.getFullChat = function (...args) { - return getFullChat(this, ...args) + return getFullChat(this._client, ...args) } - TelegramClient.prototype.getNearbyChats = function (...args) { - return getNearbyChats(this, ...args) + return getNearbyChats(this._client, ...args) } - TelegramClient.prototype.getSimilarChannels = function (...args) { - return getSimilarChannels(this, ...args) + return getSimilarChannels(this._client, ...args) } - TelegramClient.prototype.iterChatEventLog = function (...args) { - return iterChatEventLog(this, ...args) + return iterChatEventLog(this._client, ...args) } - TelegramClient.prototype.iterChatMembers = function (...args) { - return iterChatMembers(this, ...args) + return iterChatMembers(this._client, ...args) } - TelegramClient.prototype.joinChat = function (...args) { - return joinChat(this, ...args) + return joinChat(this._client, ...args) } - TelegramClient.prototype.kickChatMember = function (...args) { - return kickChatMember(this, ...args) + return kickChatMember(this._client, ...args) } - TelegramClient.prototype.leaveChat = function (...args) { - return leaveChat(this, ...args) + return leaveChat(this._client, ...args) } - TelegramClient.prototype.markChatUnread = function (...args) { - return markChatUnread(this, ...args) + return markChatUnread(this._client, ...args) } - TelegramClient.prototype.openChat = function (...args) { - return openChat(this, ...args) + return openChat(this._client, ...args) } - TelegramClient.prototype.closeChat = function (...args) { - return closeChat(this, ...args) + return closeChat(this._client, ...args) } - TelegramClient.prototype.reorderUsernames = function (...args) { - return reorderUsernames(this, ...args) + return reorderUsernames(this._client, ...args) } - TelegramClient.prototype.restrictChatMember = function (...args) { - return restrictChatMember(this, ...args) + return restrictChatMember(this._client, ...args) } - TelegramClient.prototype.saveDraft = function (...args) { - return saveDraft(this, ...args) + return saveDraft(this._client, ...args) } - TelegramClient.prototype.setChatColor = function (...args) { - return setChatColor(this, ...args) + return setChatColor(this._client, ...args) } - TelegramClient.prototype.setChatDefaultPermissions = function (...args) { - return setChatDefaultPermissions(this, ...args) + return setChatDefaultPermissions(this._client, ...args) } - TelegramClient.prototype.setChatDescription = function (...args) { - return setChatDescription(this, ...args) + return setChatDescription(this._client, ...args) } - TelegramClient.prototype.setChatPhoto = function (...args) { - return setChatPhoto(this, ...args) + return setChatPhoto(this._client, ...args) } - TelegramClient.prototype.setChatTitle = function (...args) { - return setChatTitle(this, ...args) + return setChatTitle(this._client, ...args) } - TelegramClient.prototype.setChatTtl = function (...args) { - return setChatTtl(this, ...args) + return setChatTtl(this._client, ...args) } - TelegramClient.prototype.setChatUsername = function (...args) { - return setChatUsername(this, ...args) + return setChatUsername(this._client, ...args) } - TelegramClient.prototype.setSlowMode = function (...args) { - return setSlowMode(this, ...args) + return setSlowMode(this._client, ...args) } - TelegramClient.prototype.toggleContentProtection = function (...args) { - return toggleContentProtection(this, ...args) + return toggleContentProtection(this._client, ...args) } - TelegramClient.prototype.toggleFragmentUsername = function (...args) { - return toggleFragmentUsername(this, ...args) + return toggleFragmentUsername(this._client, ...args) } - TelegramClient.prototype.toggleJoinRequests = function (...args) { - return toggleJoinRequests(this, ...args) + return toggleJoinRequests(this._client, ...args) } - TelegramClient.prototype.toggleJoinToSend = function (...args) { - return toggleJoinToSend(this, ...args) + return toggleJoinToSend(this._client, ...args) } - TelegramClient.prototype.unarchiveChats = function (...args) { - return unarchiveChats(this, ...args) + return unarchiveChats(this._client, ...args) } - TelegramClient.prototype.unbanChatMember = function (...args) { - return unbanChatMember(this, ...args) + return unbanChatMember(this._client, ...args) } - TelegramClient.prototype.unrestrictChatMember = function (...args) { - return unbanChatMember(this, ...args) + return unbanChatMember(this._client, ...args) } - TelegramClient.prototype.addContact = function (...args) { - return addContact(this, ...args) + return addContact(this._client, ...args) } - TelegramClient.prototype.deleteContacts = function (...args) { - return deleteContacts(this, ...args) + return deleteContacts(this._client, ...args) } - TelegramClient.prototype.getContacts = function (...args) { - return getContacts(this, ...args) + return getContacts(this._client, ...args) } - TelegramClient.prototype.importContacts = function (...args) { - return importContacts(this, ...args) + return importContacts(this._client, ...args) } - TelegramClient.prototype.createFolder = function (...args) { - return createFolder(this, ...args) + return createFolder(this._client, ...args) } - TelegramClient.prototype.deleteFolder = function (...args) { - return deleteFolder(this, ...args) + return deleteFolder(this._client, ...args) } - TelegramClient.prototype.editFolder = function (...args) { - return editFolder(this, ...args) + return editFolder(this._client, ...args) } - TelegramClient.prototype.findFolder = function (...args) { - return findFolder(this, ...args) + return findFolder(this._client, ...args) } - TelegramClient.prototype.getFolders = function (...args) { - return getFolders(this, ...args) + return getFolders(this._client, ...args) } - TelegramClient.prototype.getPeerDialogs = function (...args) { - return getPeerDialogs(this, ...args) + return getPeerDialogs(this._client, ...args) } - TelegramClient.prototype.iterDialogs = function (...args) { - return iterDialogs(this, ...args) + return iterDialogs(this._client, ...args) } - TelegramClient.prototype.setFoldersOrder = function (...args) { - return setFoldersOrder(this, ...args) + return setFoldersOrder(this._client, ...args) } - TelegramClient.prototype.downloadAsBuffer = function (...args) { - return downloadAsBuffer(this, ...args) + return downloadAsBuffer(this._client, ...args) } - TelegramClient.prototype.downloadToFile = function (...args) { - return downloadToFile(this, ...args) + return downloadToFile(this._client, ...args) } - TelegramClient.prototype.downloadAsIterable = function (...args) { - return downloadAsIterable(this, ...args) + return downloadAsIterable(this._client, ...args) } - TelegramClient.prototype.downloadAsStream = function (...args) { - return downloadAsStream(this, ...args) + return downloadAsStream(this._client, ...args) } - TelegramClient.prototype._normalizeInputFile = function (...args) { - return _normalizeInputFile(this, ...args) + return _normalizeInputFile(this._client, ...args) } - TelegramClient.prototype._normalizeInputMedia = function (...args) { - return _normalizeInputMedia(this, ...args) + return _normalizeInputMedia(this._client, ...args) } - TelegramClient.prototype.uploadFile = function (...args) { - return uploadFile(this, ...args) + return uploadFile(this._client, ...args) } - TelegramClient.prototype.uploadMedia = function (...args) { - return uploadMedia(this, ...args) + return uploadMedia(this._client, ...args) } - TelegramClient.prototype.createForumTopic = function (...args) { - return createForumTopic(this, ...args) + return createForumTopic(this._client, ...args) } - TelegramClient.prototype.deleteForumTopicHistory = function (...args) { - return deleteForumTopicHistory(this, ...args) + return deleteForumTopicHistory(this._client, ...args) } - TelegramClient.prototype.editForumTopic = function (...args) { - return editForumTopic(this, ...args) + return editForumTopic(this._client, ...args) } - TelegramClient.prototype.getForumTopicsById = function (...args) { - return getForumTopicsById(this, ...args) + return getForumTopicsById(this._client, ...args) } - TelegramClient.prototype.getForumTopics = function (...args) { - return getForumTopics(this, ...args) + return getForumTopics(this._client, ...args) } - TelegramClient.prototype.iterForumTopics = function (...args) { - return iterForumTopics(this, ...args) + return iterForumTopics(this._client, ...args) } - TelegramClient.prototype.reorderPinnedForumTopics = function (...args) { - return reorderPinnedForumTopics(this, ...args) + return reorderPinnedForumTopics(this._client, ...args) } - TelegramClient.prototype.toggleForumTopicClosed = function (...args) { - return toggleForumTopicClosed(this, ...args) + return toggleForumTopicClosed(this._client, ...args) } - TelegramClient.prototype.toggleForumTopicPinned = function (...args) { - return toggleForumTopicPinned(this, ...args) + return toggleForumTopicPinned(this._client, ...args) } - TelegramClient.prototype.toggleForum = function (...args) { - return toggleForum(this, ...args) + return toggleForum(this._client, ...args) } - TelegramClient.prototype.toggleGeneralTopicHidden = function (...args) { - return toggleGeneralTopicHidden(this, ...args) + return toggleGeneralTopicHidden(this._client, ...args) } - TelegramClient.prototype.createInviteLink = function (...args) { - return createInviteLink(this, ...args) + return createInviteLink(this._client, ...args) } - TelegramClient.prototype.editInviteLink = function (...args) { - return editInviteLink(this, ...args) + return editInviteLink(this._client, ...args) } - TelegramClient.prototype.exportInviteLink = function (...args) { - return exportInviteLink(this, ...args) + return exportInviteLink(this._client, ...args) } - TelegramClient.prototype.getInviteLinkMembers = function (...args) { - return getInviteLinkMembers(this, ...args) + return getInviteLinkMembers(this._client, ...args) } - TelegramClient.prototype.getInviteLink = function (...args) { - return getInviteLink(this, ...args) + return getInviteLink(this._client, ...args) } - TelegramClient.prototype.getInviteLinks = function (...args) { - return getInviteLinks(this, ...args) + return getInviteLinks(this._client, ...args) } - TelegramClient.prototype.getPrimaryInviteLink = function (...args) { - return getPrimaryInviteLink(this, ...args) + return getPrimaryInviteLink(this._client, ...args) } - TelegramClient.prototype.hideAllJoinRequests = function (...args) { - return hideAllJoinRequests(this, ...args) + return hideAllJoinRequests(this._client, ...args) } - TelegramClient.prototype.hideJoinRequest = function (...args) { - return hideJoinRequest(this, ...args) + return hideJoinRequest(this._client, ...args) } - TelegramClient.prototype.iterInviteLinkMembers = function (...args) { - return iterInviteLinkMembers(this, ...args) + return iterInviteLinkMembers(this._client, ...args) } - TelegramClient.prototype.iterInviteLinks = function (...args) { - return iterInviteLinks(this, ...args) + return iterInviteLinks(this._client, ...args) } - TelegramClient.prototype.revokeInviteLink = function (...args) { - return revokeInviteLink(this, ...args) + return revokeInviteLink(this._client, ...args) } - TelegramClient.prototype.closePoll = function (...args) { - return closePoll(this, ...args) + return closePoll(this._client, ...args) } - TelegramClient.prototype.deleteMessagesById = function (...args) { - return deleteMessagesById(this, ...args) + return deleteMessagesById(this._client, ...args) } - TelegramClient.prototype.deleteMessages = function (...args) { - return deleteMessages(this, ...args) + return deleteMessages(this._client, ...args) } - TelegramClient.prototype.deleteScheduledMessages = function (...args) { - return deleteScheduledMessages(this, ...args) + return deleteScheduledMessages(this._client, ...args) } - TelegramClient.prototype.editInlineMessage = function (...args) { - return editInlineMessage(this, ...args) + return editInlineMessage(this._client, ...args) } - TelegramClient.prototype.editMessage = function (...args) { - return editMessage(this, ...args) + return editMessage(this._client, ...args) } - TelegramClient.prototype.forwardMessagesById = function (...args) { - return forwardMessagesById(this, ...args) + return forwardMessagesById(this._client, ...args) } - TelegramClient.prototype.forwardMessages = function (...args) { - return forwardMessages(this, ...args) + return forwardMessages(this._client, ...args) } - TelegramClient.prototype.getCallbackQueryMessage = function (...args) { - return getCallbackQueryMessage(this, ...args) + return getCallbackQueryMessage(this._client, ...args) } - TelegramClient.prototype.getDiscussionMessage = function (...args) { - return getDiscussionMessage(this, ...args) + return getDiscussionMessage(this._client, ...args) } - TelegramClient.prototype.getHistory = function (...args) { - return getHistory(this, ...args) + return getHistory(this._client, ...args) } - TelegramClient.prototype.getMessageByLink = function (...args) { - return getMessageByLink(this, ...args) + return getMessageByLink(this._client, ...args) } - TelegramClient.prototype.getMessageGroup = function (...args) { - return getMessageGroup(this, ...args) + return getMessageGroup(this._client, ...args) } - TelegramClient.prototype.getMessageReactionsById = function (...args) { - return getMessageReactionsById(this, ...args) + return getMessageReactionsById(this._client, ...args) } - TelegramClient.prototype.getMessageReactions = function (...args) { - return getMessageReactions(this, ...args) + return getMessageReactions(this._client, ...args) } - TelegramClient.prototype.getMessagesUnsafe = function (...args) { - return getMessagesUnsafe(this, ...args) + return getMessagesUnsafe(this._client, ...args) } - TelegramClient.prototype.getMessages = function (...args) { - return getMessages(this, ...args) + return getMessages(this._client, ...args) } - TelegramClient.prototype.getReactionUsers = function (...args) { - return getReactionUsers(this, ...args) + return getReactionUsers(this._client, ...args) } - TelegramClient.prototype.getReplyTo = function (...args) { - return getReplyTo(this, ...args) + return getReplyTo(this._client, ...args) } - TelegramClient.prototype.getScheduledMessages = function (...args) { - return getScheduledMessages(this, ...args) + return getScheduledMessages(this._client, ...args) } - TelegramClient.prototype.iterHistory = function (...args) { - return iterHistory(this, ...args) + return iterHistory(this._client, ...args) } - TelegramClient.prototype.iterReactionUsers = function (...args) { - return iterReactionUsers(this, ...args) + return iterReactionUsers(this._client, ...args) } - TelegramClient.prototype.iterSearchGlobal = function (...args) { - return iterSearchGlobal(this, ...args) + return iterSearchGlobal(this._client, ...args) } - TelegramClient.prototype.iterSearchMessages = function (...args) { - return iterSearchMessages(this, ...args) + return iterSearchMessages(this._client, ...args) } - TelegramClient.prototype.pinMessage = function (...args) { - return pinMessage(this, ...args) + return pinMessage(this._client, ...args) } - TelegramClient.prototype.readHistory = function (...args) { - return readHistory(this, ...args) + return readHistory(this._client, ...args) } - TelegramClient.prototype.readReactions = function (...args) { - return readReactions(this, ...args) + return readReactions(this._client, ...args) } - TelegramClient.prototype.searchGlobal = function (...args) { - return searchGlobal(this, ...args) + return searchGlobal(this._client, ...args) } - TelegramClient.prototype.searchMessages = function (...args) { - return searchMessages(this, ...args) + return searchMessages(this._client, ...args) } - TelegramClient.prototype.answerText = function (...args) { - return answerText(this, ...args) + return answerText(this._client, ...args) } - TelegramClient.prototype.answerMedia = function (...args) { - return answerMedia(this, ...args) + return answerMedia(this._client, ...args) } - TelegramClient.prototype.answerMediaGroup = function (...args) { - return answerMediaGroup(this, ...args) + return answerMediaGroup(this._client, ...args) } - TelegramClient.prototype.commentText = function (...args) { - return commentText(this, ...args) + return commentText(this._client, ...args) } - TelegramClient.prototype.commentMedia = function (...args) { - return commentMedia(this, ...args) + return commentMedia(this._client, ...args) } - TelegramClient.prototype.commentMediaGroup = function (...args) { - return commentMediaGroup(this, ...args) + return commentMediaGroup(this._client, ...args) } - TelegramClient.prototype.sendCopyGroup = function (...args) { - return sendCopyGroup(this, ...args) + return sendCopyGroup(this._client, ...args) } - TelegramClient.prototype.sendCopy = function (...args) { - return sendCopy(this, ...args) + return sendCopy(this._client, ...args) } - TelegramClient.prototype.sendMediaGroup = function (...args) { - return sendMediaGroup(this, ...args) + return sendMediaGroup(this._client, ...args) } - TelegramClient.prototype.sendMedia = function (...args) { - return sendMedia(this, ...args) + return sendMedia(this._client, ...args) } - TelegramClient.prototype.quoteWithText = function (...args) { - return quoteWithText(this, ...args) + return quoteWithText(this._client, ...args) } - TelegramClient.prototype.quoteWithMedia = function (...args) { - return quoteWithMedia(this, ...args) + return quoteWithMedia(this._client, ...args) } - TelegramClient.prototype.quoteWithMediaGroup = function (...args) { - return quoteWithMediaGroup(this, ...args) + return quoteWithMediaGroup(this._client, ...args) } - TelegramClient.prototype.sendReaction = function (...args) { - return sendReaction(this, ...args) + return sendReaction(this._client, ...args) } - TelegramClient.prototype.replyText = function (...args) { - return replyText(this, ...args) + return replyText(this._client, ...args) } - TelegramClient.prototype.replyMedia = function (...args) { - return replyMedia(this, ...args) + return replyMedia(this._client, ...args) } - TelegramClient.prototype.replyMediaGroup = function (...args) { - return replyMediaGroup(this, ...args) + return replyMediaGroup(this._client, ...args) } - TelegramClient.prototype.sendScheduled = function (...args) { - return sendScheduled(this, ...args) + return sendScheduled(this._client, ...args) } - TelegramClient.prototype.sendText = function (...args) { - return sendText(this, ...args) + return sendText(this._client, ...args) } - TelegramClient.prototype.sendTyping = function (...args) { - return sendTyping(this, ...args) + return sendTyping(this._client, ...args) } - TelegramClient.prototype.sendVote = function (...args) { - return sendVote(this, ...args) + return sendVote(this._client, ...args) } - TelegramClient.prototype.translateMessage = function (...args) { - return translateMessage(this, ...args) + return translateMessage(this._client, ...args) } - TelegramClient.prototype.translateText = function (...args) { - return translateText(this, ...args) + return translateText(this._client, ...args) } - TelegramClient.prototype.unpinAllMessages = function (...args) { - return unpinAllMessages(this, ...args) + return unpinAllMessages(this._client, ...args) } - TelegramClient.prototype.unpinMessage = function (...args) { - return unpinMessage(this, ...args) + return unpinMessage(this._client, ...args) } - TelegramClient.prototype.initTakeoutSession = function (...args) { - return initTakeoutSession(this, ...args) + return initTakeoutSession(this._client, ...args) } - TelegramClient.prototype._normalizePrivacyRules = function (...args) { - return _normalizePrivacyRules(this, ...args) + return _normalizePrivacyRules(this._client, ...args) } - TelegramClient.prototype.changeCloudPassword = function (...args) { - return changeCloudPassword(this, ...args) + return changeCloudPassword(this._client, ...args) } - TelegramClient.prototype.enableCloudPassword = function (...args) { - return enableCloudPassword(this, ...args) + return enableCloudPassword(this._client, ...args) } - TelegramClient.prototype.verifyPasswordEmail = function (...args) { - return verifyPasswordEmail(this, ...args) + return verifyPasswordEmail(this._client, ...args) } - TelegramClient.prototype.resendPasswordEmail = function (...args) { - return resendPasswordEmail(this, ...args) + return resendPasswordEmail(this._client, ...args) } - TelegramClient.prototype.cancelPasswordEmail = function (...args) { - return cancelPasswordEmail(this, ...args) + return cancelPasswordEmail(this._client, ...args) } - TelegramClient.prototype.removeCloudPassword = function (...args) { - return removeCloudPassword(this, ...args) + return removeCloudPassword(this._client, ...args) } - TelegramClient.prototype.applyBoost = function (...args) { - return applyBoost(this, ...args) + return applyBoost(this._client, ...args) } - TelegramClient.prototype.canApplyBoost = function (...args) { - return canApplyBoost(this, ...args) + return canApplyBoost(this._client, ...args) } - TelegramClient.prototype.getBoostStats = function (...args) { - return getBoostStats(this, ...args) + return getBoostStats(this._client, ...args) } - TelegramClient.prototype.getBoosts = function (...args) { - return getBoosts(this, ...args) + return getBoosts(this._client, ...args) } - TelegramClient.prototype.getMyBoostSlots = function (...args) { - return getMyBoostSlots(this, ...args) + return getMyBoostSlots(this._client, ...args) } - TelegramClient.prototype.iterBoosters = function (...args) { - return iterBoosters(this, ...args) + return iterBoosters(this._client, ...args) } - TelegramClient.prototype.addStickerToSet = function (...args) { - return addStickerToSet(this, ...args) + return addStickerToSet(this._client, ...args) } - TelegramClient.prototype.createStickerSet = function (...args) { - return createStickerSet(this, ...args) + return createStickerSet(this._client, ...args) } - TelegramClient.prototype.deleteStickerFromSet = function (...args) { - return deleteStickerFromSet(this, ...args) + return deleteStickerFromSet(this._client, ...args) } - TelegramClient.prototype.getCustomEmojis = function (...args) { - return getCustomEmojis(this, ...args) + return getCustomEmojis(this._client, ...args) } - TelegramClient.prototype.getCustomEmojisFromMessages = function (...args) { - return getCustomEmojisFromMessages(this, ...args) + return getCustomEmojisFromMessages(this._client, ...args) } - TelegramClient.prototype.getInstalledStickers = function (...args) { - return getInstalledStickers(this, ...args) + return getInstalledStickers(this._client, ...args) } - TelegramClient.prototype.getStickerSet = function (...args) { - return getStickerSet(this, ...args) + return getStickerSet(this._client, ...args) } - TelegramClient.prototype.moveStickerInSet = function (...args) { - return moveStickerInSet(this, ...args) + return moveStickerInSet(this._client, ...args) } - TelegramClient.prototype.setChatStickerSet = function (...args) { - return setChatStickerSet(this, ...args) + return setChatStickerSet(this._client, ...args) } - TelegramClient.prototype.setStickerSetThumb = function (...args) { - return setStickerSetThumb(this, ...args) + return setStickerSetThumb(this._client, ...args) } - TelegramClient.prototype.canSendStory = function (...args) { - return canSendStory(this, ...args) + return canSendStory(this._client, ...args) } - TelegramClient.prototype.deleteStories = function (...args) { - return deleteStories(this, ...args) + return deleteStories(this._client, ...args) } - TelegramClient.prototype.editStory = function (...args) { - return editStory(this, ...args) + return editStory(this._client, ...args) } - TelegramClient.prototype.getAllStories = function (...args) { - return getAllStories(this, ...args) + return getAllStories(this._client, ...args) } - TelegramClient.prototype.getPeerStories = function (...args) { - return getPeerStories(this, ...args) + return getPeerStories(this._client, ...args) } - TelegramClient.prototype.getProfileStories = function (...args) { - return getProfileStories(this, ...args) + return getProfileStories(this._client, ...args) } - TelegramClient.prototype.getStoriesById = function (...args) { - return getStoriesById(this, ...args) + return getStoriesById(this._client, ...args) } - TelegramClient.prototype.getStoriesInteractions = function (...args) { - return getStoriesInteractions(this, ...args) + return getStoriesInteractions(this._client, ...args) } - TelegramClient.prototype.getStoryLink = function (...args) { - return getStoryLink(this, ...args) + return getStoryLink(this._client, ...args) } - TelegramClient.prototype.getStoryViewers = function (...args) { - return getStoryViewers(this, ...args) + return getStoryViewers(this._client, ...args) } - TelegramClient.prototype.hideMyStoriesViews = function (...args) { - return hideMyStoriesViews(this, ...args) + return hideMyStoriesViews(this._client, ...args) } - TelegramClient.prototype.incrementStoriesViews = function (...args) { - return incrementStoriesViews(this, ...args) + return incrementStoriesViews(this._client, ...args) } - TelegramClient.prototype.iterAllStories = function (...args) { - return iterAllStories(this, ...args) + return iterAllStories(this._client, ...args) } - TelegramClient.prototype.iterProfileStories = function (...args) { - return iterProfileStories(this, ...args) + return iterProfileStories(this._client, ...args) } - TelegramClient.prototype.iterStoryViewers = function (...args) { - return iterStoryViewers(this, ...args) + return iterStoryViewers(this._client, ...args) } - TelegramClient.prototype.readStories = function (...args) { - return readStories(this, ...args) + return readStories(this._client, ...args) } - TelegramClient.prototype.reportStory = function (...args) { - return reportStory(this, ...args) + return reportStory(this._client, ...args) } - TelegramClient.prototype.sendStoryReaction = function (...args) { - return sendStoryReaction(this, ...args) + return sendStoryReaction(this._client, ...args) } - TelegramClient.prototype.sendStory = function (...args) { - return sendStory(this, ...args) + return sendStory(this._client, ...args) } - TelegramClient.prototype.togglePeerStoriesArchived = function (...args) { - return togglePeerStoriesArchived(this, ...args) + return togglePeerStoriesArchived(this._client, ...args) } - TelegramClient.prototype.toggleStoriesPinned = function (...args) { - return toggleStoriesPinned(this, ...args) + return toggleStoriesPinned(this._client, ...args) } - -TelegramClient.prototype.enableRps = function (...args) { - return enableRps(this, ...args) -} - -TelegramClient.prototype.getCurrentRpsIncoming = function (...args) { - return getCurrentRpsIncoming(this, ...args) -} - -TelegramClient.prototype.getCurrentRpsProcessing = function (...args) { - return getCurrentRpsProcessing(this, ...args) -} - -TelegramClient.prototype.startUpdatesLoop = function (...args) { - return startUpdatesLoop(this, ...args) -} - -TelegramClient.prototype.stopUpdatesLoop = function (...args) { - return stopUpdatesLoop(this, ...args) -} - -TelegramClient.prototype.catchUp = function (...args) { - return catchUp(this, ...args) -} - -TelegramClient.prototype.notifyChannelOpened = function (...args) { - return notifyChannelOpened(this, ...args) -} - -TelegramClient.prototype.notifyChannelClosed = function (...args) { - return notifyChannelClosed(this, ...args) -} - TelegramClient.prototype.blockUser = function (...args) { - return blockUser(this, ...args) + return blockUser(this._client, ...args) } - TelegramClient.prototype.deleteProfilePhotos = function (...args) { - return deleteProfilePhotos(this, ...args) + return deleteProfilePhotos(this._client, ...args) } - TelegramClient.prototype.editCloseFriendsRaw = function (...args) { - return editCloseFriendsRaw(this, ...args) + return editCloseFriendsRaw(this._client, ...args) } - TelegramClient.prototype.editCloseFriends = function (...args) { - return editCloseFriends(this, ...args) + return editCloseFriends(this._client, ...args) } - TelegramClient.prototype.getCommonChats = function (...args) { - return getCommonChats(this, ...args) + return getCommonChats(this._client, ...args) } - TelegramClient.prototype.getGlobalTtl = function (...args) { - return getGlobalTtl(this, ...args) + return getGlobalTtl(this._client, ...args) } - TelegramClient.prototype.getMe = function (...args) { - return getMe(this, ...args) + return getMe(this._client, ...args) } - TelegramClient.prototype.getMyUsername = function (...args) { - return getMyUsername(this, ...args) + return getMyUsername(this._client, ...args) } - TelegramClient.prototype.getProfilePhoto = function (...args) { - return getProfilePhoto(this, ...args) + return getProfilePhoto(this._client, ...args) } - TelegramClient.prototype.getProfilePhotos = function (...args) { - return getProfilePhotos(this, ...args) + return getProfilePhotos(this._client, ...args) } - TelegramClient.prototype.getUsers = function (...args) { - return getUsers(this, ...args) + return getUsers(this._client, ...args) } - TelegramClient.prototype.iterProfilePhotos = function (...args) { - return iterProfilePhotos(this, ...args) + return iterProfilePhotos(this._client, ...args) } - // @ts-expect-error this kinda breaks typings for overloads, idc TelegramClient.prototype.resolvePeerMany = function (...args) { // @ts-expect-error this kinda breaks typings for overloads, idc - return resolvePeerMany(this, ...args) + return resolvePeerMany(this._client, ...args) } - TelegramClient.prototype.resolvePeer = function (...args) { - return resolvePeer(this, ...args) + return resolvePeer(this._client, ...args) } - TelegramClient.prototype.setGlobalTtl = function (...args) { - return setGlobalTtl(this, ...args) + return setGlobalTtl(this._client, ...args) } - TelegramClient.prototype.setMyEmojiStatus = function (...args) { - return setMyEmojiStatus(this, ...args) + return setMyEmojiStatus(this._client, ...args) } - TelegramClient.prototype.setMyProfilePhoto = function (...args) { - return setMyProfilePhoto(this, ...args) + return setMyProfilePhoto(this._client, ...args) } - TelegramClient.prototype.setMyUsername = function (...args) { - return setMyUsername(this, ...args) + return setMyUsername(this._client, ...args) } - TelegramClient.prototype.setOffline = function (...args) { - return setOffline(this, ...args) + return setOffline(this._client, ...args) } - TelegramClient.prototype.unblockUser = function (...args) { - return unblockUser(this, ...args) + return unblockUser(this._client, ...args) } - TelegramClient.prototype.updateProfile = function (...args) { - return updateProfile(this, ...args) + return updateProfile(this._client, ...args) +} +TelegramClient.prototype.prepare = function (...args) { + return this._client.prepare(...args) +} +TelegramClient.prototype.connect = function (...args) { + return this._client.connect(...args) +} +TelegramClient.prototype.close = function (...args) { + return this._client.close(...args) +} +TelegramClient.prototype.notifyLoggedIn = function (...args) { + return this._client.notifyLoggedIn(...args) +} +TelegramClient.prototype.notifyLoggedOut = function (...args) { + return this._client.notifyLoggedOut(...args) +} +TelegramClient.prototype.notifyChannelOpened = function (...args) { + return this._client.notifyChannelOpened(...args) +} +TelegramClient.prototype.notifyChannelClosed = function (...args) { + return this._client.notifyChannelClosed(...args) +} +TelegramClient.prototype.startUpdatesLoop = function (...args) { + return this._client.startUpdatesLoop(...args) +} +TelegramClient.prototype.stopUpdatesLoop = function (...args) { + return this._client.stopUpdatesLoop(...args) +} +TelegramClient.prototype.call = function (...args) { + return this._client.call(...args) +} +TelegramClient.prototype.importSession = function (...args) { + return this._client.importSession(...args) +} +TelegramClient.prototype.exportSession = function (...args) { + return this._client.exportSession(...args) +} +TelegramClient.prototype.onError = function (...args) { + return this._client.onError(...args) +} +TelegramClient.prototype.emitError = function (...args) { + return this._client.emitError(...args) +} +TelegramClient.prototype.handleClientUpdate = function (...args) { + return this._client.handleClientUpdate(...args) +} +TelegramClient.prototype.getApiCrenetials = function (...args) { + return this._client.getApiCrenetials(...args) +} +TelegramClient.prototype.getPoolSize = function (...args) { + return this._client.getPoolSize(...args) +} +TelegramClient.prototype.getPrimaryDcId = function (...args) { + return this._client.getPrimaryDcId(...args) +} +TelegramClient.prototype.computeSrpParams = function (...args) { + return this._client.computeSrpParams(...args) +} +TelegramClient.prototype.computeNewPasswordHash = function (...args) { + return this._client.computeNewPasswordHash(...args) +} +TelegramClient.prototype.onServerUpdate = function () { + throw new Error('onServerUpdate is not available for TelegramClient, use .on() methods instead') +} +TelegramClient.prototype.onUpdate = function () { + throw new Error('onUpdate is not available for TelegramClient, use .on() methods instead') } - -TelegramClient.prototype.run = - // @manual-impl=run - /** @internal */ - function _run( - this: TelegramClient, - params: Parameters[1], - then?: (user: User) => void | Promise, - ) { - this.start(params) - .then(then) - .catch((err) => this._emitError(err)) - } -TelegramClient.prototype.start = - // @manual-impl=start - /** @internal */ - async function _start(this: TelegramClient, params: Parameters[1]) { - const user = await start(this, params) - - if (!this.network.params.disableUpdates && !this._disableUpdatesManager) { - await this.startUpdatesLoop() - } - - return user - } diff --git a/packages/core/src/highlevel/client.types.ts b/packages/core/src/highlevel/client.types.ts new file mode 100644 index 00000000..7a671388 --- /dev/null +++ b/packages/core/src/highlevel/client.types.ts @@ -0,0 +1,49 @@ +import { tl } from '@mtcute/tl' + +import type { ConnectionKind, RpcCallOptions } from '../network/index.js' +import type { MustEqual, PublicPart } from '../types/utils.js' +import type { Logger } from '../utils/logger.js' +import type { StringSessionData } from '../utils/string-session.js' +import type { TelegramStorageManager } from './storage/storage.js' +import type { RawUpdateHandler } from './updates/types.js' + +// NB: when adding new methods, don't forget to add them to: +// - worker/port.ts +// - generate-client script + +export interface ITelegramClient { + readonly log: Logger + readonly storage: PublicPart + + prepare(): Promise + connect(): Promise + close(): Promise + notifyLoggedIn(auth: tl.auth.TypeAuthorization | tl.RawUser): Promise + notifyLoggedOut(): Promise + notifyChannelOpened(channelId: number, pts?: number): Promise + notifyChannelClosed(channelId: number): Promise + startUpdatesLoop(): Promise + stopUpdatesLoop(): Promise + call( + message: MustEqual, + params?: RpcCallOptions, + ): Promise + importSession(session: string | StringSessionData, force?: boolean): Promise + exportSession(): Promise + onError(handler: (err: unknown) => void): void + emitError(err: unknown): void + handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void + + onServerUpdate(handler: (update: tl.TypeUpdates) => void): void + onUpdate(handler: RawUpdateHandler): void + + getApiCrenetials(): Promise<{ id: number; hash: string }> + // todo - this is only used for file dl/ul, which should probably be moved + // to the client to allow moving the thing to worker + // or at least load this once at startup (and then these methods can be made sync) + getPoolSize(kind: ConnectionKind, dcId?: number): Promise + getPrimaryDcId(): Promise + + computeSrpParams(request: tl.account.RawPassword, password: string): Promise + computeNewPasswordHash(algo: tl.TypePasswordKdfAlgo, password: string): Promise +} diff --git a/packages/core/src/highlevel/index.ts b/packages/core/src/highlevel/index.ts new file mode 100644 index 00000000..573e6f07 --- /dev/null +++ b/packages/core/src/highlevel/index.ts @@ -0,0 +1,6 @@ +export * from './base.js' +export * from './client.js' +export * from './client.types.js' +export * from './storage/index.js' +export * from './types/index.js' +export * from './updates/index.js' diff --git a/packages/client/src/methods/README.md b/packages/core/src/highlevel/methods/README.md similarity index 93% rename from packages/client/src/methods/README.md rename to packages/core/src/highlevel/methods/README.md index ca13f810..fcb30f28 100644 --- a/packages/client/src/methods/README.md +++ b/packages/core/src/highlevel/methods/README.md @@ -57,7 +57,7 @@ Example: ```typescript // @initialize -function _initializeAwesomeExtension(client: BaseTelegramClient) { +function _initializeAwesomeExtension(client: ITelegramClient) { this._field1 = 42 this._field2 = 'uwu' } @@ -74,7 +74,7 @@ Example: // @exported export type FooOrBar = Foo | Bar -export function getFooOrBar(client: BaseTelegramClient): FooOrBar { +export function getFooOrBar(client: ITelegramClient): FooOrBar { return new Foo() } ``` diff --git a/packages/client/src/methods/_imports.ts b/packages/core/src/highlevel/methods/_imports.ts similarity index 82% rename from packages/client/src/methods/_imports.ts rename to packages/core/src/highlevel/methods/_imports.ts index 3ee4cd99..138d8da4 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/core/src/highlevel/methods/_imports.ts @@ -1,19 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -// @copy -import { - BaseTelegramClient, - BaseTelegramClientOptions, - ITelegramStorage, - Long, - MaybeArray, - MaybeAsync, - PartialExcept, - PartialOnly, - tl, -} from '@mtcute/core' + // @copy import { tdFileId } from '@mtcute/file-id' +// @copy +import { tl } from '@mtcute/tl' +// @copy +import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../../types/index.js' +// @copy +import { StringSessionData } from '../../utils/string-session.js' +// @copy +import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' +// @copy +import { ITelegramClient } from '../client.types.js' // @copy import { AllStories, diff --git a/packages/core/src/highlevel/methods/_init.ts b/packages/core/src/highlevel/methods/_init.ts new file mode 100644 index 00000000..f5e7588d --- /dev/null +++ b/packages/core/src/highlevel/methods/_init.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// @copy +import { MemoryStorage } from '../../storage/providers/memory/index.js' +import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' +import { TelegramClient } from '../client.js' +import { ITelegramClient } from '../client.types.js' +// @copy +import { ITelegramStorageProvider } from '../storage/provider.js' +// @copy +import { Conversation } from '../types/conversation.js' +// @copy +import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from '../updates/parsed.js' +// @copy +import { _defaultStorageFactory } from '../utils/platform/storage.js' + +// @copy +type TelegramClientOptions = ((Omit & { + /** + * Storage to use for this client. + * + * If a string is passed, it will be used as: + * - a path to a JSON file for Node.js + * - IndexedDB database name for browsers + * + * If omitted, {@link MemoryStorage} is used + */ + storage?: string | ITelegramStorageProvider +}) | ({ client: ITelegramClient })) & { + updates?: Omit + /** + * If `true`, the updates that were handled by some {@link Conversation} + * will not be dispatched any further. + * + * @default true + */ + skipConversationUpdates?: boolean +} + +// // @initialize=super +// /** @internal */ +// function _initializeClientSuper(this: TelegramClient, opts: TelegramClientOptions) { +// if (typeof opts.storage === 'string') { +// opts.storage = _defaultStorageFactory(opts.storage) +// } else if (!opts.storage) { +// opts.storage = new MemoryStorage() +// } + +// /* eslint-disable @typescript-eslint/no-unsafe-call */ +// // @ts-expect-error codegen +// super(opts) +// /* eslint-enable @typescript-eslint/no-unsafe-call */ +// } + +// @initialize +/** @internal */ +function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) { + if ('client' in opts) { + this._client = opts.client + } else { + let storage: ITelegramStorageProvider + + if (typeof opts.storage === 'string') { + storage = _defaultStorageFactory(opts.storage) + } else if (!opts.storage) { + storage = new MemoryStorage() + } else { + storage = opts.storage + } + + this._client = new BaseTelegramClient({ + ...opts, + storage, + }) + } + + // @ts-expect-error codegen + this.log = this._client.log + // @ts-expect-error codegen + this.storage = this._client.storage + + const skipConversationUpdates = opts.skipConversationUpdates ?? true + const { messageGroupingInterval } = opts.updates ?? {} + + this._client.onUpdate(makeParsedUpdateHandler({ + messageGroupingInterval, + onUpdate: (update) => { + if (Conversation.handleUpdate(this._client, update) && skipConversationUpdates) return + + this.emit('update', update) + this.emit(update.name, update.data) + }, + onRawUpdate: (update, peers) => { + this.emit('raw_update', update, peers) + }, + })) +} diff --git a/packages/client/src/methods/auth/check-password.ts b/packages/core/src/highlevel/methods/auth/check-password.ts similarity index 60% rename from packages/client/src/methods/auth/check-password.ts rename to packages/core/src/highlevel/methods/auth/check-password.ts index 88c594e9..12b1620a 100644 --- a/packages/client/src/methods/auth/check-password.ts +++ b/packages/core/src/highlevel/methods/auth/check-password.ts @@ -1,8 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { computeSrpParams } from '@mtcute/core/utils.js' - +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' -import { _onAuthorization } from './_state.js' +import { _onAuthorization } from './utils.js' /** * Check your Two-Step verification password and log in @@ -11,11 +9,10 @@ import { _onAuthorization } from './_state.js' * @returns The authorized user * @throws BadRequestError In case the password is invalid */ -export async function checkPassword(client: BaseTelegramClient, password: string): Promise { +export async function checkPassword(client: ITelegramClient, password: string): Promise { const res = await client.call({ _: 'auth.checkPassword', - password: await computeSrpParams( - client.crypto, + password: await client.computeSrpParams( await client.call({ _: 'account.getPassword', }), diff --git a/packages/client/src/methods/auth/get-password-hint.ts b/packages/core/src/highlevel/methods/auth/get-password-hint.ts similarity index 63% rename from packages/client/src/methods/auth/get-password-hint.ts rename to packages/core/src/highlevel/methods/auth/get-password-hint.ts index 9bc05030..7cee79da 100644 --- a/packages/client/src/methods/auth/get-password-hint.ts +++ b/packages/core/src/highlevel/methods/auth/get-password-hint.ts @@ -1,11 +1,11 @@ -import { BaseTelegramClient } from '@mtcute/core' +import { ITelegramClient } from '../../client.types.js' /** * Get your Two-Step Verification password hint. * * @returns The password hint as a string, if any */ -export function getPasswordHint(client: BaseTelegramClient): Promise { +export function getPasswordHint(client: ITelegramClient): Promise { return client .call({ _: 'account.getPassword', diff --git a/packages/core/src/highlevel/methods/auth/log-out.ts b/packages/core/src/highlevel/methods/auth/log-out.ts new file mode 100644 index 00000000..fe284668 --- /dev/null +++ b/packages/core/src/highlevel/methods/auth/log-out.ts @@ -0,0 +1,16 @@ +import { ITelegramClient } from '../../client.types.js' + +/** + * Log out from Telegram account and optionally reset the session storage. + * + * When you log out, you can immediately log back in using + * the same {@link TelegramClient} instance. + * + * @returns On success, `true` is returned + */ +export async function logOut(client: ITelegramClient): Promise { + await client.call({ _: 'auth.logOut' }) + await client.notifyLoggedOut() + + return true +} diff --git a/packages/client/src/methods/auth/recover-password.ts b/packages/core/src/highlevel/methods/auth/recover-password.ts similarity index 80% rename from packages/client/src/methods/auth/recover-password.ts rename to packages/core/src/highlevel/methods/auth/recover-password.ts index dfeaa5e2..45a30ed0 100644 --- a/packages/client/src/methods/auth/recover-password.ts +++ b/packages/core/src/highlevel/methods/auth/recover-password.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' -import { _onAuthorization } from './_state.js' +import { _onAuthorization } from './utils.js' /** * Recover your password with a recovery code and log in. @@ -10,7 +9,7 @@ import { _onAuthorization } from './_state.js' * @throws BadRequestError In case the code is invalid */ export async function recoverPassword( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** The recovery code sent via email */ recoveryCode: string diff --git a/packages/client/src/methods/auth/resend-code.ts b/packages/core/src/highlevel/methods/auth/resend-code.ts similarity index 79% rename from packages/client/src/methods/auth/resend-code.ts rename to packages/core/src/highlevel/methods/auth/resend-code.ts index ec49825a..9f93ce68 100644 --- a/packages/client/src/methods/auth/resend-code.ts +++ b/packages/core/src/highlevel/methods/auth/resend-code.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - -import { SentCode } from '../../types/index.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { SentCode } from '../../types/auth/sent-code.js' import { normalizePhoneNumber } from '../../utils/misc-utils.js' /** @@ -11,7 +10,7 @@ import { normalizePhoneNumber } from '../../utils/misc-utils.js' * {@link SentCode} object returned by {@link sendCode} */ export async function resendCode( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Phone number in international format */ phone: string diff --git a/packages/client/src/methods/auth/run.ts b/packages/core/src/highlevel/methods/auth/run.ts similarity index 60% rename from packages/client/src/methods/auth/run.ts rename to packages/core/src/highlevel/methods/auth/run.ts index 0a074435..a0d8b23f 100644 --- a/packages/client/src/methods/auth/run.ts +++ b/packages/core/src/highlevel/methods/auth/run.ts @@ -1,6 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - -import { TelegramClient } from '../../index.js' +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' import { start } from './start.js' @@ -14,22 +12,13 @@ import { start } from './start.js' * * @param params Parameters to be passed to {@link start} * @param then Function to be called after {@link start} returns - * @manual=noemit */ export function run( - client: BaseTelegramClient, + client: ITelegramClient, params: Parameters[1], then?: (user: User) => void | Promise, ): void { start(client, params) .then(then) - .catch((err) => client._emitError(err)) -} - -// @manual-impl=run -/** @internal */ -function _run(this: TelegramClient, params: Parameters[1], then?: (user: User) => void | Promise) { - this.start(params) - .then(then) - .catch((err) => this._emitError(err)) + .catch((err) => client.emitError(err)) } diff --git a/packages/client/src/methods/auth/send-code.ts b/packages/core/src/highlevel/methods/auth/send-code.ts similarity index 67% rename from packages/client/src/methods/auth/send-code.ts rename to packages/core/src/highlevel/methods/auth/send-code.ts index 754d5d71..aa74c715 100644 --- a/packages/client/src/methods/auth/send-code.ts +++ b/packages/core/src/highlevel/methods/auth/send-code.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - -import { SentCode } from '../../types/index.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { SentCode } from '../../types/auth/sent-code.js' import { normalizePhoneNumber } from '../../utils/misc-utils.js' /** @@ -10,7 +9,7 @@ import { normalizePhoneNumber } from '../../utils/misc-utils.js' * @returns An object containing information about the sent confirmation code */ export async function sendCode( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Phone number in international format */ phone: string @@ -18,11 +17,13 @@ export async function sendCode( ): Promise { const phone = normalizePhoneNumber(params.phone) + const { id, hash } = await client.getApiCrenetials() + const res = await client.call({ _: 'auth.sendCode', phoneNumber: phone, - apiId: client.params.apiId, - apiHash: client.params.apiHash, + apiId: id, + apiHash: hash, settings: { _: 'codeSettings' }, }) diff --git a/packages/client/src/methods/auth/send-recovery-code.ts b/packages/core/src/highlevel/methods/auth/send-recovery-code.ts similarity index 68% rename from packages/client/src/methods/auth/send-recovery-code.ts rename to packages/core/src/highlevel/methods/auth/send-recovery-code.ts index d6a4e6ce..c6cab6f3 100644 --- a/packages/client/src/methods/auth/send-recovery-code.ts +++ b/packages/core/src/highlevel/methods/auth/send-recovery-code.ts @@ -1,11 +1,11 @@ -import { BaseTelegramClient } from '@mtcute/core' +import { ITelegramClient } from '../../client.types.js' /** * Send a code to email needed to recover your password * * @returns String containing email pattern to which the recovery code was sent */ -export function sendRecoveryCode(client: BaseTelegramClient): Promise { +export function sendRecoveryCode(client: ITelegramClient): Promise { return client .call({ _: 'auth.requestPasswordRecovery', diff --git a/packages/core/src/highlevel/methods/auth/sign-in-bot.ts b/packages/core/src/highlevel/methods/auth/sign-in-bot.ts new file mode 100644 index 00000000..4af5231f --- /dev/null +++ b/packages/core/src/highlevel/methods/auth/sign-in-bot.ts @@ -0,0 +1,24 @@ +import { ITelegramClient } from '../../client.types.js' +import { User } from '../../types/peers/user.js' +import { _onAuthorization } from './utils.js' + +/** + * Authorize a bot using its token issued by [@BotFather](//t.me/BotFather) + * + * @param token Bot token issued by BotFather + * @returns Bot's {@link User} object + * @throws BadRequestError In case the bot token is invalid + */ +export async function signInBot(client: ITelegramClient, token: string): Promise { + const { id, hash } = await client.getApiCrenetials() + + const res = await client.call({ + _: 'auth.importBotAuthorization', + flags: 0, + apiId: id, + apiHash: hash, + botAuthToken: token, + }) + + return _onAuthorization(client, res) +} diff --git a/packages/client/src/methods/auth/sign-in.ts b/packages/core/src/highlevel/methods/auth/sign-in.ts similarity index 84% rename from packages/client/src/methods/auth/sign-in.ts rename to packages/core/src/highlevel/methods/auth/sign-in.ts index 43e2d3ab..739a2c7b 100644 --- a/packages/client/src/methods/auth/sign-in.ts +++ b/packages/core/src/highlevel/methods/auth/sign-in.ts @@ -1,8 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - -import { User } from '../../types/index.js' +import { ITelegramClient } from '../../client.types.js' +import { User } from '../../types/peers/user.js' import { normalizePhoneNumber } from '../../utils/misc-utils.js' -import { _onAuthorization } from './_state.js' +import { _onAuthorization } from './utils.js' /** * Authorize a user in Telegram with a valid confirmation code. @@ -12,7 +11,7 @@ import { _onAuthorization } from './_state.js' * @throws SessionPasswordNeededError In case a password is needed to sign in */ export async function signIn( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Phone number in international format */ phone: string diff --git a/packages/client/src/methods/auth/start-test.ts b/packages/core/src/highlevel/methods/auth/start-test.ts similarity index 92% rename from packages/client/src/methods/auth/start-test.ts rename to packages/core/src/highlevel/methods/auth/start-test.ts index 14038b4a..ca3537e2 100644 --- a/packages/client/src/methods/auth/start-test.ts +++ b/packages/core/src/highlevel/methods/auth/start-test.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' import { logOut } from './log-out.js' import { start } from './start.js' @@ -15,7 +15,7 @@ import { start } from './start.js' * @param params Additional parameters */ export async function startTest( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Whether to log out if current session is logged in. @@ -63,7 +63,7 @@ export async function startTest( throw new MtArgumentError(`${phone} has invalid DC ID (${id})`) } } else { - let dcId = client.network.getPrimaryDcId() + let dcId = await client.getPrimaryDcId() if (params.dcId) { if (!availableDcs.find((dc) => dc.id === params!.dcId)) { diff --git a/packages/client/src/methods/auth/start.ts b/packages/core/src/highlevel/methods/auth/start.ts similarity index 88% rename from packages/client/src/methods/auth/start.ts rename to packages/core/src/highlevel/methods/auth/start.ts index 42a633b5..223ca918 100644 --- a/packages/client/src/methods/auth/start.ts +++ b/packages/core/src/highlevel/methods/auth/start.ts @@ -1,8 +1,13 @@ /* eslint-disable no-console */ -import { BaseTelegramClient, MaybeAsync, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import type { TelegramClient } from '../../client.js' -import { MaybeDynamic, SentCode, User } from '../../types/index.js' +import { MtArgumentError } from '../../../types/errors.js' +import { MaybePromise } from '../../../types/utils.js' +import { StringSessionData } from '../../../utils/string-session.js' +import { ITelegramClient } from '../../client.types.js' +import { SentCode } from '../../types/auth/sent-code.js' +import { User } from '../../types/peers/user.js' +import { MaybeDynamic } from '../../types/utils.js' import { normalizePhoneNumber, resolveMaybeDynamic } from '../../utils/misc-utils.js' import { getMe } from '../users/get-me.js' import { checkPassword } from './check-password.js' @@ -11,7 +16,6 @@ import { sendCode } from './send-code.js' import { signIn } from './sign-in.js' import { signInBot } from './sign-in-bot.js' -// @manual // @available=both /** * Start the client in an interactive and declarative manner, @@ -20,14 +24,14 @@ import { signInBot } from './sign-in-bot.js' * This method handles both login and sign up, and also handles 2FV * * All parameters are `MaybeDynamic`, meaning you - * can either supply `T`, or a function that returns `MaybeAsync` + * can either supply `T`, or a function that returns `MaybePromise` * * This method is intended for simple and fast use in automated * scripts and bots. If you are developing a custom client, * you'll probably need to use other auth methods. */ export async function start( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * String session exported using {@link TelegramClient.exportSession}. @@ -37,7 +41,7 @@ export async function start( * Note that passed session will be ignored in case storage already * contains authorization. */ - session?: string + session?: string | StringSessionData /** * Whether to overwrite existing session. @@ -73,7 +77,7 @@ export async function start( * If provided `code`/`password` is a constant string, providing an * invalid one will interrupt authorization flow. */ - invalidCodeCallback?: (type: 'code' | 'password') => MaybeAsync + invalidCodeCallback?: (type: 'code' | 'password') => MaybePromise /** * Whether to force code delivery through SMS @@ -89,11 +93,11 @@ export async function start( * @param code * @default `console.log`. */ - codeSentCallback?: (code: SentCode) => MaybeAsync + codeSentCallback?: (code: SentCode) => MaybePromise }, ): Promise { if (params.session) { - client.importSession(params.session, params.sessionForce) + await client.importSession(params.session, params.sessionForce) } try { @@ -103,7 +107,7 @@ export async function start( client.log.info('Logged in as %s (ID: %s, username: %s, bot: %s)', me.displayName, me.id, me.username, me.isBot) - client.network.setIsPremium(me.isPremium) + await client.notifyLoggedIn(me.raw) return me } catch (e) { @@ -211,15 +215,3 @@ export async function start( throw new MtArgumentError('Failed to log in with provided credentials') } - -// @manual-impl=start -/** @internal */ -async function _start(this: TelegramClient, params: Parameters[1]) { - const user = await start(this, params) - - if (!this.network.params.disableUpdates && !this._disableUpdatesManager) { - await this.startUpdatesLoop() - } - - return user -} diff --git a/packages/core/src/highlevel/methods/auth/utils.ts b/packages/core/src/highlevel/methods/auth/utils.ts new file mode 100644 index 00000000..8e02ec65 --- /dev/null +++ b/packages/core/src/highlevel/methods/auth/utils.ts @@ -0,0 +1,39 @@ +import { tl } from '@mtcute/tl' + +import { ITelegramClient } from '../../client.types.js' +import { User } from '../../types/peers/user.js' + +/** @internal */ +export async function _onAuthorization( + client: ITelegramClient, + auth: tl.auth.TypeAuthorization, +): Promise { + const user = await client.notifyLoggedIn(auth) + + return new User(user) +} + +/** + * Check if the given peer/input peer is referring to the current user + */ +export function isSelfPeer( + client: ITelegramClient, + peer: tl.TypeInputPeer | tl.TypePeer | tl.TypeInputUser, +): boolean { + const state = client.storage.self.getCached() + if (!state) return false + + switch (peer._) { + case 'inputPeerSelf': + case 'inputUserSelf': + return true + case 'inputPeerUser': + case 'inputPeerUserFromMessage': + case 'inputUser': + case 'inputUserFromMessage': + case 'peerUser': + return peer.userId === state.userId + default: + return false + } +} diff --git a/packages/client/src/methods/bots/answer-callback-query.ts b/packages/core/src/highlevel/methods/bots/answer-callback-query.ts similarity index 90% rename from packages/client/src/methods/bots/answer-callback-query.ts rename to packages/core/src/highlevel/methods/bots/answer-callback-query.ts index eb48918f..19a5ea03 100644 --- a/packages/client/src/methods/bots/answer-callback-query.ts +++ b/packages/core/src/highlevel/methods/bots/answer-callback-query.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, Long } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import Long from 'long' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { CallbackQuery } from '../../types/updates/callback-query.js' /** @@ -10,7 +11,7 @@ import { CallbackQuery } from '../../types/updates/callback-query.js' * @param params Parameters of the answer */ export async function answerCallbackQuery( - client: BaseTelegramClient, + client: ITelegramClient, queryId: Long | CallbackQuery, params?: { /** diff --git a/packages/client/src/methods/bots/answer-inline-query.ts b/packages/core/src/highlevel/methods/bots/answer-inline-query.ts similarity index 96% rename from packages/client/src/methods/bots/answer-inline-query.ts rename to packages/core/src/highlevel/methods/bots/answer-inline-query.ts index 79021e38..631be8fc 100644 --- a/packages/client/src/methods/bots/answer-inline-query.ts +++ b/packages/core/src/highlevel/methods/bots/answer-inline-query.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { ITelegramClient } from '../../client.types.js' import { BotInline, InputInlineResult } from '../../types/bots/index.js' import { InlineQuery } from '../../types/updates/inline-query.js' @@ -11,7 +14,7 @@ import { InlineQuery } from '../../types/updates/inline-query.js' * @param params Additional parameters */ export async function answerInlineQuery( - client: BaseTelegramClient, + client: ITelegramClient, queryId: tl.Long | InlineQuery, results: InputInlineResult[], params?: { diff --git a/packages/client/src/methods/bots/answer-pre-checkout-query.ts b/packages/core/src/highlevel/methods/bots/answer-pre-checkout-query.ts similarity index 77% rename from packages/client/src/methods/bots/answer-pre-checkout-query.ts rename to packages/core/src/highlevel/methods/bots/answer-pre-checkout-query.ts index 18bde294..2b4740fe 100644 --- a/packages/client/src/methods/bots/answer-pre-checkout-query.ts +++ b/packages/core/src/highlevel/methods/bots/answer-pre-checkout-query.ts @@ -1,6 +1,9 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import type { PreCheckoutQuery } from '../../types/updates/pre-checkout-query.js' /** @@ -9,7 +12,7 @@ import type { PreCheckoutQuery } from '../../types/updates/pre-checkout-query.js * @param queryId Pre-checkout query ID */ export async function answerPreCheckoutQuery( - client: BaseTelegramClient, + client: ITelegramClient, queryId: tl.Long | PreCheckoutQuery, params?: { /** If pre-checkout is rejected, error message to show to the user */ diff --git a/packages/client/src/methods/bots/delete-my-commands.ts b/packages/core/src/highlevel/methods/bots/delete-my-commands.ts similarity index 90% rename from packages/client/src/methods/bots/delete-my-commands.ts rename to packages/core/src/highlevel/methods/bots/delete-my-commands.ts index 61ff23e5..4926ba85 100644 --- a/packages/client/src/methods/bots/delete-my-commands.ts +++ b/packages/core/src/highlevel/methods/bots/delete-my-commands.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { BotCommands } from '../../types/index.js' import { _normalizeCommandScope } from './normalize-command-scope.js' @@ -11,7 +12,7 @@ import { _normalizeCommandScope } from './normalize-command-scope.js' * Learn more about scopes in the [Bot API docs](https://core.telegram.org/bots/api#botcommandscope) */ export async function deleteMyCommands( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Scope of the commands. diff --git a/packages/client/src/methods/bots/get-bot-info.ts b/packages/core/src/highlevel/methods/bots/get-bot-info.ts similarity index 88% rename from packages/client/src/methods/bots/get-bot-info.ts rename to packages/core/src/highlevel/methods/bots/get-bot-info.ts index 5c383660..8b16ac10 100644 --- a/packages/client/src/methods/bots/get-bot-info.ts +++ b/packages/core/src/highlevel/methods/bots/get-bot-info.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Gets information about a bot the current uzer owns (or the current bot) */ export async function getBotInfo( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * When called by a user, a bot the user owns must be specified. diff --git a/packages/client/src/methods/bots/get-bot-menu-button.ts b/packages/core/src/highlevel/methods/bots/get-bot-menu-button.ts similarity index 64% rename from packages/client/src/methods/bots/get-bot-menu-button.ts rename to packages/core/src/highlevel/methods/bots/get-bot-menu-button.ts index 73fde523..4164dd22 100644 --- a/packages/client/src/methods/bots/get-bot-menu-button.ts +++ b/packages/core/src/highlevel/methods/bots/get-bot-menu-button.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -7,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' /** * Fetches the menu button set for the given user. */ -export async function getBotMenuButton(client: BaseTelegramClient, user: InputPeerLike): Promise { +export async function getBotMenuButton(client: ITelegramClient, user: InputPeerLike): Promise { return await client.call({ _: 'bots.getBotMenuButton', userId: toInputUser(await resolvePeer(client, user), user), diff --git a/packages/client/src/methods/bots/get-callback-answer.ts b/packages/core/src/highlevel/methods/bots/get-callback-answer.ts similarity index 86% rename from packages/client/src/methods/bots/get-callback-answer.ts rename to packages/core/src/highlevel/methods/bots/get-callback-answer.ts index 025d80de..81022b47 100644 --- a/packages/client/src/methods/bots/get-callback-answer.ts +++ b/packages/core/src/highlevel/methods/bots/get-callback-answer.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { computeSrpParams, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, normalizeInputMessageId } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param params */ export async function getCallbackAnswer( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** Data contained in the button */ data: string | Uint8Array @@ -44,7 +45,7 @@ export async function getCallbackAnswer( if (params?.password) { const pwd = await client.call({ _: 'account.getPassword' }) - password = await computeSrpParams(client.crypto, pwd, params.password) + password = await client.computeSrpParams(pwd, params.password) } return await client.call( diff --git a/packages/client/src/methods/bots/get-game-high-scores.ts b/packages/core/src/highlevel/methods/bots/get-game-high-scores.ts similarity index 93% rename from packages/client/src/methods/bots/get-game-high-scores.ts rename to packages/core/src/highlevel/methods/bots/get-game-high-scores.ts index f1225dda..c3fa6bc2 100644 --- a/packages/client/src/methods/bots/get-game-high-scores.ts +++ b/packages/core/src/highlevel/methods/bots/get-game-high-scores.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { GameHighScore, InputMessageId, InputPeerLike, normalizeInputMessageId, PeersIndex } from '../../types/index.js' import { normalizeInlineId } from '../../utils/inline-utils.js' import { toInputUser } from '../../utils/peer-utils.js' @@ -9,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Get high scores of a game */ export async function getGameHighScores( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** ID of the user to find high scores for */ userId?: InputPeerLike @@ -47,7 +48,7 @@ export async function getGameHighScores( * @param userId ID of the user to find high scores for */ export async function getInlineGameHighScores( - client: BaseTelegramClient, + client: ITelegramClient, messageId: string | tl.TypeInputBotInlineMessageID, userId?: InputPeerLike, ): Promise { diff --git a/packages/client/src/methods/bots/get-my-commands.ts b/packages/core/src/highlevel/methods/bots/get-my-commands.ts similarity index 89% rename from packages/client/src/methods/bots/get-my-commands.ts rename to packages/core/src/highlevel/methods/bots/get-my-commands.ts index 56a8695e..af933e56 100644 --- a/packages/client/src/methods/bots/get-my-commands.ts +++ b/packages/core/src/highlevel/methods/bots/get-my-commands.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { BotCommands } from '../../types/index.js' import { _normalizeCommandScope } from './normalize-command-scope.js' @@ -10,7 +11,7 @@ import { _normalizeCommandScope } from './normalize-command-scope.js' * Learn more about scopes in the [Bot API docs](https://core.telegram.org/bots/api#botcommandscope) */ export async function getMyCommands( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Scope of the commands. diff --git a/packages/client/src/methods/bots/normalize-command-scope.ts b/packages/core/src/highlevel/methods/bots/normalize-command-scope.ts similarity index 86% rename from packages/client/src/methods/bots/normalize-command-scope.ts rename to packages/core/src/highlevel/methods/bots/normalize-command-scope.ts index 587cd05c..65eb11fe 100644 --- a/packages/client/src/methods/bots/normalize-command-scope.ts +++ b/packages/core/src/highlevel/methods/bots/normalize-command-scope.ts @@ -1,12 +1,14 @@ -import { assertNever, BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { assertNever } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { BotCommands } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @internal */ export async function _normalizeCommandScope( - client: BaseTelegramClient, + client: ITelegramClient, scope: tl.TypeBotCommandScope | BotCommands.IntermediateScope, ): Promise { if (tl.isAnyBotCommandScope(scope)) return scope diff --git a/packages/client/src/methods/bots/set-bot-info.ts b/packages/core/src/highlevel/methods/bots/set-bot-info.ts similarity index 89% rename from packages/client/src/methods/bots/set-bot-info.ts rename to packages/core/src/highlevel/methods/bots/set-bot-info.ts index c22f8e9e..ddbd2303 100644 --- a/packages/client/src/methods/bots/set-bot-info.ts +++ b/packages/core/src/highlevel/methods/bots/set-bot-info.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Sets information about a bot the current uzer owns (or the current bot) */ export async function setBotInfo( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * When called by a user, a bot the user owns must be specified. diff --git a/packages/client/src/methods/bots/set-bot-menu-button.ts b/packages/core/src/highlevel/methods/bots/set-bot-menu-button.ts similarity index 75% rename from packages/client/src/methods/bots/set-bot-menu-button.ts rename to packages/core/src/highlevel/methods/bots/set-bot-menu-button.ts index 06090f58..8378e66e 100644 --- a/packages/client/src/methods/bots/set-bot-menu-button.ts +++ b/packages/core/src/highlevel/methods/bots/set-bot-menu-button.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Sets a menu button for the given user. */ export async function setBotMenuButton( - client: BaseTelegramClient, + client: ITelegramClient, user: InputPeerLike, button: tl.TypeBotMenuButton, ): Promise { diff --git a/packages/client/src/methods/bots/set-game-score.ts b/packages/core/src/highlevel/methods/bots/set-game-score.ts similarity index 93% rename from packages/client/src/methods/bots/set-game-score.ts rename to packages/core/src/highlevel/methods/bots/set-game-score.ts index e470c19f..2403b624 100644 --- a/packages/client/src/methods/bots/set-game-score.ts +++ b/packages/core/src/highlevel/methods/bots/set-game-score.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, InputPeerLike, Message, normalizeInputMessageId } from '../../types/index.js' import { normalizeInlineId } from '../../utils/inline-utils.js' import { toInputUser } from '../../utils/peer-utils.js' @@ -14,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns The modified message */ export async function setGameScore( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** ID of the user who has scored */ userId: InputPeerLike @@ -67,7 +68,7 @@ export async function setGameScore( * @param params */ export async function setInlineGameScore( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** ID of the inline message */ messageId: string | tl.TypeInputBotInlineMessageID diff --git a/packages/client/src/methods/bots/set-my-commands.ts b/packages/core/src/highlevel/methods/bots/set-my-commands.ts similarity index 89% rename from packages/client/src/methods/bots/set-my-commands.ts rename to packages/core/src/highlevel/methods/bots/set-my-commands.ts index ae7e66a8..a19124b5 100644 --- a/packages/client/src/methods/bots/set-my-commands.ts +++ b/packages/core/src/highlevel/methods/bots/set-my-commands.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { BotCommands } from '../../types/index.js' import { _normalizeCommandScope } from './normalize-command-scope.js' @@ -10,7 +11,7 @@ import { _normalizeCommandScope } from './normalize-command-scope.js' * Learn more about scopes in the [Bot API docs](https://core.telegram.org/bots/api#botcommandscope) */ export async function setMyCommands( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * New list of bot commands for the given scope. diff --git a/packages/client/src/methods/bots/set-my-default-rights.ts b/packages/core/src/highlevel/methods/bots/set-my-default-rights.ts similarity index 79% rename from packages/client/src/methods/bots/set-my-default-rights.ts rename to packages/core/src/highlevel/methods/bots/set-my-default-rights.ts index da1c30c0..96cac184 100644 --- a/packages/client/src/methods/bots/set-my-default-rights.ts +++ b/packages/core/src/highlevel/methods/bots/set-my-default-rights.ts @@ -1,11 +1,13 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' + +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Sets the default chat permissions for the bot in the supergroup or channel. */ export async function setMyDefaultRights( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Whether to target groups or channels. */ target: 'channel' | 'group' diff --git a/packages/client/src/methods/chats/add-chat-members.ts b/packages/core/src/highlevel/methods/chats/add-chat-members.ts similarity index 88% rename from packages/client/src/methods/chats/add-chat-members.ts rename to packages/core/src/highlevel/methods/chats/add-chat-members.ts index 813ef40d..9afec6a3 100644 --- a/packages/client/src/methods/chats/add-chat-members.ts +++ b/packages/core/src/highlevel/methods/chats/add-chat-members.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel, toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +12,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * @param users ID(s) of the user(s) to add */ export async function addChatMembers( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, users: MaybeArray, params: { @@ -41,7 +41,7 @@ export async function addChatMembers( userId: p, fwdLimit: forwardCount, }) - client.network.handleUpdate(updates) + client.handleClientUpdate(updates) } } else if (isInputPeerChannel(chat)) { const updates = await client.call({ @@ -49,7 +49,6 @@ export async function addChatMembers( channel: toInputChannel(chat), users: await resolvePeerMany(client, users, toInputUser), }) - - client.network.handleUpdate(updates) + client.handleClientUpdate(updates) } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') } diff --git a/packages/client/src/methods/chats/archive-chats.ts b/packages/core/src/highlevel/methods/chats/archive-chats.ts similarity index 69% rename from packages/client/src/methods/chats/archive-chats.ts rename to packages/core/src/highlevel/methods/chats/archive-chats.ts index a0c78e55..1e0e53b0 100644 --- a/packages/client/src/methods/chats/archive-chats.ts +++ b/packages/core/src/highlevel/methods/chats/archive-chats.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeerMany } from '../users/resolve-peer-many.js' @@ -8,7 +8,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * * @param chats Chat ID(s), username(s), phone number(s), `"me"` or `"self"` */ -export async function archiveChats(client: BaseTelegramClient, chats: MaybeArray): Promise { +export async function archiveChats(client: ITelegramClient, chats: MaybeArray): Promise { if (!Array.isArray(chats)) chats = [chats] const resolvedPeers = await resolvePeerMany(client, chats) @@ -21,5 +21,5 @@ export async function archiveChats(client: BaseTelegramClient, chats: MaybeArray folderId: 1, })), }) - client.network.handleUpdate(updates) + client.handleClientUpdate(updates) } diff --git a/packages/client/src/methods/chats/ban-chat-member.ts b/packages/core/src/highlevel/methods/chats/ban-chat-member.ts similarity index 95% rename from packages/client/src/methods/chats/ban-chat-member.ts rename to packages/core/src/highlevel/methods/chats/ban-chat-member.ts index 526be26e..41760939 100644 --- a/packages/client/src/methods/chats/ban-chat-member.ts +++ b/packages/core/src/highlevel/methods/chats/ban-chat-member.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel, toInputUser } from '../../utils/peer-utils.js' import { _findMessageInUpdate } from '../messages/find-in-update.js' @@ -16,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Service message about removed user, if one was generated. */ export async function banChatMember( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike diff --git a/packages/client/src/methods/chats/batched-queries.ts b/packages/core/src/highlevel/methods/chats/batched-queries.ts similarity index 94% rename from packages/client/src/methods/chats/batched-queries.ts rename to packages/core/src/highlevel/methods/chats/batched-queries.ts index c5ce5343..2e8eeed0 100644 --- a/packages/client/src/methods/chats/batched-queries.ts +++ b/packages/core/src/highlevel/methods/chats/batched-queries.ts @@ -1,5 +1,7 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { isInputPeerChannel, isInputPeerChat, @@ -8,7 +10,6 @@ import { toInputUser, } from '../../utils/peer-utils.js' import { batchedQuery } from '../../utils/query-batcher.js' -import { getAuthState } from '../auth/_state.js' /** @internal */ export const _getUsersBatched = batchedQuery({ @@ -27,7 +28,7 @@ export const _getUsersBatched = batchedQuery { if (isInputPeerUser(peer)) { diff --git a/packages/client/src/methods/chats/create-channel.ts b/packages/core/src/highlevel/methods/chats/create-channel.ts similarity index 77% rename from packages/client/src/methods/chats/create-channel.ts rename to packages/core/src/highlevel/methods/chats/create-channel.ts index 15c8acde..dbbea136 100644 --- a/packages/client/src/methods/chats/create-channel.ts +++ b/packages/core/src/highlevel/methods/chats/create-channel.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Chat } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** * Create a new broadcast channel @@ -9,7 +8,7 @@ import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' * @returns Newly created channel */ export async function createChannel( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Channel title @@ -33,7 +32,7 @@ export async function createChannel( assertIsUpdatesGroup('channels.createChannel', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } diff --git a/packages/client/src/methods/chats/create-group.ts b/packages/core/src/highlevel/methods/chats/create-group.ts similarity index 84% rename from packages/client/src/methods/chats/create-group.ts rename to packages/core/src/highlevel/methods/chats/create-group.ts index a0e01ec5..2ca837a0 100644 --- a/packages/client/src/methods/chats/create-group.ts +++ b/packages/core/src/highlevel/methods/chats/create-group.ts @@ -1,8 +1,8 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike } from '../../types/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { toInputUser } from '../../utils/peer-utils.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { resolvePeerMany } from '../users/resolve-peer-many.js' /** @@ -12,7 +12,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * instead. */ export async function createGroup( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Group title @@ -48,7 +48,7 @@ export async function createGroup( assertIsUpdatesGroup('messages.createChat', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } diff --git a/packages/client/src/methods/chats/create-supergroup.ts b/packages/core/src/highlevel/methods/chats/create-supergroup.ts similarity index 83% rename from packages/client/src/methods/chats/create-supergroup.ts rename to packages/core/src/highlevel/methods/chats/create-supergroup.ts index 70db4510..a11419d0 100644 --- a/packages/client/src/methods/chats/create-supergroup.ts +++ b/packages/core/src/highlevel/methods/chats/create-supergroup.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Chat } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** * Create a new supergroup @@ -9,7 +8,7 @@ import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' * @returns Newly created supergroup */ export async function createSupergroup( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Supergroup title @@ -47,7 +46,7 @@ export async function createSupergroup( assertIsUpdatesGroup('channels.createChannel', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } diff --git a/packages/client/src/methods/chats/delete-channel.ts b/packages/core/src/highlevel/methods/chats/delete-channel.ts similarity index 69% rename from packages/client/src/methods/chats/delete-channel.ts rename to packages/core/src/highlevel/methods/chats/delete-channel.ts index 49d6dab7..31b5fd0e 100644 --- a/packages/client/src/methods/chats/delete-channel.ts +++ b/packages/core/src/highlevel/methods/chats/delete-channel.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,10 +9,10 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID or username */ -export async function deleteChannel(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function deleteChannel(client: ITelegramClient, chatId: InputPeerLike): Promise { const res = await client.call({ _: 'channels.deleteChannel', channel: toInputChannel(await resolvePeer(client, chatId), chatId), }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/delete-chat-photo.ts b/packages/core/src/highlevel/methods/chats/delete-chat-photo.ts similarity index 82% rename from packages/client/src/methods/chats/delete-chat-photo.ts rename to packages/core/src/highlevel/methods/chats/delete-chat-photo.ts index 49a73a70..3098803e 100644 --- a/packages/client/src/methods/chats/delete-chat-photo.ts +++ b/packages/core/src/highlevel/methods/chats/delete-chat-photo.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID or username */ -export async function deleteChatPhoto(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function deleteChatPhoto(client: ITelegramClient, chatId: InputPeerLike): Promise { const chat = await resolvePeer(client, chatId) let res @@ -29,5 +28,5 @@ export async function deleteChatPhoto(client: BaseTelegramClient, chatId: InputP }) } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/delete-group.ts b/packages/core/src/highlevel/methods/chats/delete-group.ts similarity index 74% rename from packages/client/src/methods/chats/delete-group.ts rename to packages/core/src/highlevel/methods/chats/delete-group.ts index 7b14d703..5764c887 100644 --- a/packages/client/src/methods/chats/delete-group.ts +++ b/packages/core/src/highlevel/methods/chats/delete-group.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChat } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID */ -export async function deleteGroup(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function deleteGroup(client: ITelegramClient, chatId: InputPeerLike): Promise { const chat = await resolvePeer(client, chatId) if (!isInputPeerChat(chat)) throw new MtInvalidPeerTypeError(chatId, 'chat') @@ -20,7 +19,7 @@ export async function deleteGroup(client: BaseTelegramClient, chatId: InputPeerL chatId: chat.chatId, userId: { _: 'inputUserSelf' }, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) const r = await client.call({ _: 'messages.deleteChat', diff --git a/packages/client/src/methods/chats/delete-history.ts b/packages/core/src/highlevel/methods/chats/delete-history.ts similarity index 80% rename from packages/client/src/methods/chats/delete-history.ts rename to packages/core/src/highlevel/methods/chats/delete-history.ts index 4433ead5..118c920e 100644 --- a/packages/client/src/methods/chats/delete-history.ts +++ b/packages/core/src/highlevel/methods/chats/delete-history.ts @@ -1,15 +1,14 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { isInputPeerChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** * Delete communication history (for private chats and legacy groups) */ export async function deleteHistory( - client: BaseTelegramClient, + client: ITelegramClient, chat: InputPeerLike, params?: { /** @@ -45,8 +44,8 @@ export async function deleteHistory( }) if (isInputPeerChannel(peer)) { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) } else { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) } } diff --git a/packages/client/src/methods/chats/delete-user-history.ts b/packages/core/src/highlevel/methods/chats/delete-user-history.ts similarity index 73% rename from packages/client/src/methods/chats/delete-user-history.ts rename to packages/core/src/highlevel/methods/chats/delete-user-history.ts index 5bdc2536..a9ae8944 100644 --- a/packages/client/src/methods/chats/delete-user-history.ts +++ b/packages/core/src/highlevel/methods/chats/delete-user-history.ts @@ -1,15 +1,16 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { toInputChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** * Delete all messages of a user (or channel) in a supergroup */ export async function deleteUserHistory( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike @@ -29,5 +30,5 @@ export async function deleteUserHistory( participant: peer, }) - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount, (channel as tl.RawInputChannel).channelId)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount, (channel as tl.RawInputChannel).channelId)) } diff --git a/packages/client/src/methods/chats/edit-admin-rights.ts b/packages/core/src/highlevel/methods/chats/edit-admin-rights.ts similarity index 86% rename from packages/client/src/methods/chats/edit-admin-rights.ts rename to packages/core/src/highlevel/methods/chats/edit-admin-rights.ts index 830e58b7..b5dac765 100644 --- a/packages/client/src/methods/chats/edit-admin-rights.ts +++ b/packages/core/src/highlevel/methods/chats/edit-admin-rights.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel, toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Edit supergroup/channel admin rights of a user. */ export async function editAdminRights( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike @@ -36,5 +37,5 @@ export async function editAdminRights( rank, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/get-chat-event-log.ts b/packages/core/src/highlevel/methods/chats/get-chat-event-log.ts similarity index 95% rename from packages/client/src/methods/chats/get-chat-event-log.ts rename to packages/core/src/highlevel/methods/chats/get-chat-event-log.ts index 87c9b276..27120083 100644 --- a/packages/client/src/methods/chats/get-chat-event-log.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat-event-log.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { ITelegramClient } from '../../client.types.js' import { ChatEvent, InputPeerLike, PeersIndex } from '../../types/index.js' import { InputChatEventFilters, normalizeChatEventFilters } from '../../types/peers/chat-event/filters.js' import { toInputChannel, toInputUser } from '../../utils/peer-utils.js' @@ -20,7 +23,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * @param params */ export async function getChatEventLog( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/chats/get-chat-member.ts b/packages/core/src/highlevel/methods/chats/get-chat-member.ts similarity index 92% rename from packages/client/src/methods/chats/get-chat-member.ts rename to packages/core/src/highlevel/methods/chats/get-chat-member.ts index 4234afc1..4c337184 100644 --- a/packages/client/src/methods/chats/get-chat-member.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat-member.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ChatMember, InputPeerLike, MtInvalidPeerTypeError, PeersIndex } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, isInputPeerUser, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -13,7 +14,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Chat member, or `null` if user is not a member of the chat */ export async function getChatMember( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/chats/get-chat-members.ts b/packages/core/src/highlevel/methods/chats/get-chat-members.ts similarity index 94% rename from packages/client/src/methods/chats/get-chat-members.ts rename to packages/core/src/highlevel/methods/chats/get-chat-members.ts index b1911394..998672f1 100644 --- a/packages/client/src/methods/chats/get-chat-members.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat-members.ts @@ -1,6 +1,10 @@ -import { assertNever, BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { assertNever } from '../../../types/utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayWithTotal, ChatMember, InputPeerLike, MtInvalidPeerTypeError, PeersIndex } from '../../types/index.js' import { makeArrayWithTotal } from '../../utils/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' @@ -15,7 +19,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param params Additional parameters */ export async function getChatMembers( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/chats/get-chat-preview.ts b/packages/core/src/highlevel/methods/chats/get-chat-preview.ts similarity index 79% rename from packages/client/src/methods/chats/get-chat-preview.ts rename to packages/core/src/highlevel/methods/chats/get-chat-preview.ts index 5cbec222..07c4f0d7 100644 --- a/packages/client/src/methods/chats/get-chat-preview.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat-preview.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { ChatPreview, MtPeerNotFoundError } from '../../types/index.js' import { INVITE_LINK_REGEX } from '../../utils/peer-utils.js' @@ -12,7 +12,7 @@ import { INVITE_LINK_REGEX } from '../../utils/peer-utils.js' * In case you are trying to get info about private chat that you have already joined. * Use {@link getChat} or {@link getFullChat} instead. */ -export async function getChatPreview(client: BaseTelegramClient, inviteLink: string): Promise { +export async function getChatPreview(client: ITelegramClient, inviteLink: string): Promise { const m = inviteLink.match(INVITE_LINK_REGEX) if (!m) throw new MtArgumentError('Invalid invite link') diff --git a/packages/client/src/methods/chats/get-chat.ts b/packages/core/src/highlevel/methods/chats/get-chat.ts similarity index 85% rename from packages/client/src/methods/chats/get-chat.ts rename to packages/core/src/highlevel/methods/chats/get-chat.ts index 51cac4d8..aaa51ccb 100644 --- a/packages/client/src/methods/chats/get-chat.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike, MtPeerNotFoundError } from '../../types/index.js' import { INVITE_LINK_REGEX } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +14,7 @@ import { _getRawPeerBatched } from './batched-queries.js' * In case you are trying to get info about private chat that you haven't joined. * Use {@link getChatPreview} instead. */ -export async function getChat(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function getChat(client: ITelegramClient, chatId: InputPeerLike): Promise { if (typeof chatId === 'string') { const m = chatId.match(INVITE_LINK_REGEX) diff --git a/packages/client/src/methods/chats/get-full-chat.ts b/packages/core/src/highlevel/methods/chats/get-full-chat.ts similarity index 87% rename from packages/client/src/methods/chats/get-full-chat.ts rename to packages/core/src/highlevel/methods/chats/get-full-chat.ts index c2596ee5..57401d17 100644 --- a/packages/client/src/methods/chats/get-full-chat.ts +++ b/packages/core/src/highlevel/methods/chats/get-full-chat.ts @@ -1,5 +1,7 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike } from '../../types/index.js' import { INVITE_LINK_REGEX, @@ -20,7 +22,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * In case you are trying to get info about private chat that you haven't joined. * Use {@link getChatPreview} instead. */ -export async function getFullChat(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function getFullChat(client: ITelegramClient, chatId: InputPeerLike): Promise { if (typeof chatId === 'string') { const m = chatId.match(INVITE_LINK_REGEX) diff --git a/packages/client/src/methods/chats/get-nearby-chats.ts b/packages/core/src/highlevel/methods/chats/get-nearby-chats.ts similarity index 68% rename from packages/client/src/methods/chats/get-nearby-chats.ts rename to packages/core/src/highlevel/methods/chats/get-nearby-chats.ts index 376811ca..6dab21b2 100644 --- a/packages/client/src/methods/chats/get-nearby-chats.ts +++ b/packages/core/src/highlevel/methods/chats/get-nearby-chats.ts @@ -1,8 +1,10 @@ -import { BaseTelegramClient, getMarkedPeerId, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Chat } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** * Get nearby chats @@ -10,7 +12,7 @@ import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' * @param latitude Latitude of the location * @param longitude Longitude of the location */ -export async function getNearbyChats(client: BaseTelegramClient, latitude: number, longitude: number): Promise { +export async function getNearbyChats(client: ITelegramClient, latitude: number, longitude: number): Promise { const res = await client.call({ _: 'contacts.getLocated', geoPoint: { @@ -21,7 +23,7 @@ export async function getNearbyChats(client: BaseTelegramClient, latitude: numbe }) assertIsUpdatesGroup('contacts.getLocated', res) - client.network.handleUpdate(res, true) + // client.handleClientUpdate(res, true) if (!res.updates.length) return [] diff --git a/packages/client/src/methods/chats/get-similar-channels.ts b/packages/core/src/highlevel/methods/chats/get-similar-channels.ts similarity index 93% rename from packages/client/src/methods/chats/get-similar-channels.ts rename to packages/core/src/highlevel/methods/chats/get-similar-channels.ts index 4dc37d9c..0ddd5588 100644 --- a/packages/client/src/methods/chats/get-similar-channels.ts +++ b/packages/core/src/highlevel/methods/chats/get-similar-channels.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ArrayWithTotal, Chat, InputPeerLike } from '../../types/index.js' import { makeArrayWithTotal } from '../../utils/misc-utils.js' import { toInputChannel } from '../../utils/peer-utils.js' @@ -16,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * > Returns empty array in case there are no similar channels available. */ export async function getSimilarChannels( - client: BaseTelegramClient, + client: ITelegramClient, channel: InputPeerLike, ): Promise> { const res = await client.call({ diff --git a/packages/client/src/methods/chats/iter-chat-event-log.ts b/packages/core/src/highlevel/methods/chats/iter-chat-event-log.ts similarity index 94% rename from packages/client/src/methods/chats/iter-chat-event-log.ts rename to packages/core/src/highlevel/methods/chats/iter-chat-event-log.ts index 706efc94..2220687b 100644 --- a/packages/client/src/methods/chats/iter-chat-event-log.ts +++ b/packages/core/src/highlevel/methods/chats/iter-chat-event-log.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { ITelegramClient } from '../../client.types.js' import { ChatEvent, InputPeerLike } from '../../types/index.js' import { normalizeChatEventFilters } from '../../types/peers/chat-event/filters.js' import { toInputChannel, toInputUser } from '../../utils/peer-utils.js' @@ -16,7 +19,7 @@ import { getChatEventLog } from './get-chat-event-log.js' * @param params */ export async function* iterChatEventLog( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/chats/iter-chat-members.ts b/packages/core/src/highlevel/methods/chats/iter-chat-members.ts similarity index 95% rename from packages/client/src/methods/chats/iter-chat-members.ts rename to packages/core/src/highlevel/methods/chats/iter-chat-members.ts index 18917f40..7ea0ba00 100644 --- a/packages/client/src/methods/chats/iter-chat-members.ts +++ b/packages/core/src/highlevel/methods/chats/iter-chat-members.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatMember, InputPeerLike } from '../../types/index.js' import { isInputPeerChat } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -16,7 +15,7 @@ import { getChatMembers } from './get-chat-members.js' * @param params Additional parameters */ export async function* iterChatMembers( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/chats/join-chat.ts b/packages/core/src/highlevel/methods/chats/join-chat.ts similarity index 80% rename from packages/client/src/methods/chats/join-chat.ts rename to packages/core/src/highlevel/methods/chats/join-chat.ts index 7fb3d5f6..d4124b17 100644 --- a/packages/client/src/methods/chats/join-chat.ts +++ b/packages/core/src/highlevel/methods/chats/join-chat.ts @@ -1,8 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike } from '../../types/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { INVITE_LINK_REGEX, toInputChannel } from '../../utils/peer-utils.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -16,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Chat identifier. Either an invite link (`t.me/joinchat/*`), a username (`@username`) * or ID of the linked supergroup or channel. */ -export async function joinChat(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function joinChat(client: ITelegramClient, chatId: InputPeerLike): Promise { if (typeof chatId === 'string') { const m = chatId.match(INVITE_LINK_REGEX) @@ -27,7 +26,7 @@ export async function joinChat(client: BaseTelegramClient, chatId: InputPeerLike }) assertIsUpdatesGroup('messages.importChatInvite', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } @@ -40,7 +39,7 @@ export async function joinChat(client: BaseTelegramClient, chatId: InputPeerLike assertIsUpdatesGroup('channels.joinChannel', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } diff --git a/packages/client/src/methods/chats/kick-chat-member.ts b/packages/core/src/highlevel/methods/chats/kick-chat-member.ts similarity index 89% rename from packages/client/src/methods/chats/kick-chat-member.ts rename to packages/core/src/highlevel/methods/chats/kick-chat-member.ts index fea2124f..15a0f495 100644 --- a/packages/client/src/methods/chats/kick-chat-member.ts +++ b/packages/core/src/highlevel/methods/chats/kick-chat-member.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { sleep } from '@mtcute/core/utils.js' - +import { sleep } from '../../../utils/misc-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message } from '../../types/index.js' import { isInputPeerChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -15,7 +14,7 @@ import { unbanChatMember } from './unban-chat-member.js' * @returns Service message about removed user, if one was generated. */ export async function kickChatMember( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike diff --git a/packages/client/src/methods/chats/leave-chat.ts b/packages/core/src/highlevel/methods/chats/leave-chat.ts similarity index 87% rename from packages/client/src/methods/chats/leave-chat.ts rename to packages/core/src/highlevel/methods/chats/leave-chat.ts index 65f16f28..ddb89538 100644 --- a/packages/client/src/methods/chats/leave-chat.ts +++ b/packages/core/src/highlevel/methods/chats/leave-chat.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +10,7 @@ import { deleteHistory } from './delete-history.js' * @param chatId Chat ID or username */ export async function leaveChat( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** @@ -27,14 +26,14 @@ export async function leaveChat( _: 'channels.leaveChannel', channel: toInputChannel(chat), }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } else if (isInputPeerChat(chat)) { const res = await client.call({ _: 'messages.deleteChatUser', chatId: chat.chatId, userId: { _: 'inputUserSelf' }, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) if (params?.clear) { await deleteHistory(client, chat) diff --git a/packages/client/src/methods/chats/mark-chat-unread.ts b/packages/core/src/highlevel/methods/chats/mark-chat-unread.ts similarity index 66% rename from packages/client/src/methods/chats/mark-chat-unread.ts rename to packages/core/src/highlevel/methods/chats/mark-chat-unread.ts index 300c397b..eb491dd4 100644 --- a/packages/client/src/methods/chats/mark-chat-unread.ts +++ b/packages/core/src/highlevel/methods/chats/mark-chat-unread.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID */ -export async function markChatUnread(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function markChatUnread(client: ITelegramClient, chatId: InputPeerLike): Promise { const r = await client.call({ _: 'messages.markDialogUnread', peer: { diff --git a/packages/client/src/methods/chats/open-chat.ts b/packages/core/src/highlevel/methods/chats/open-chat.ts similarity index 67% rename from packages/client/src/methods/chats/open-chat.ts rename to packages/core/src/highlevel/methods/chats/open-chat.ts index 7317ebca..addd8a08 100644 --- a/packages/client/src/methods/chats/open-chat.ts +++ b/packages/core/src/highlevel/methods/chats/open-chat.ts @@ -1,9 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/peers/index.js' import { isInputPeerChannel } from '../../utils/peer-utils.js' import { getPeerDialogs } from '../dialogs/get-peer-dialogs.js' -import { notifyChannelClosed, notifyChannelOpened } from '../updates/manager.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -14,15 +12,13 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chat Chat to open */ -export async function openChat(client: BaseTelegramClient, chat: InputPeerLike): Promise { +export async function openChat(client: ITelegramClient, chat: InputPeerLike): Promise { const peer = await resolvePeer(client, chat) if (isInputPeerChannel(peer)) { const [dialog] = await getPeerDialogs(client, peer) - if (!client.network.params.disableUpdates) { - notifyChannelOpened(client, peer.channelId, dialog.raw.pts) - } + await client.notifyChannelOpened(peer.channelId, dialog.raw.pts) } // todo: once we have proper dialogs/peers db, we should also @@ -38,11 +34,11 @@ export async function openChat(client: BaseTelegramClient, chat: InputPeerLike): * * @param chat Chat to open */ -export async function closeChat(client: BaseTelegramClient, chat: InputPeerLike): Promise { +export async function closeChat(client: ITelegramClient, chat: InputPeerLike): Promise { const peer = await resolvePeer(client, chat) - if (isInputPeerChannel(peer) && !client.network.params.disableUpdates) { - notifyChannelClosed(client, peer.channelId) + if (isInputPeerChannel(peer)) { + await client.notifyChannelClosed(peer.channelId) } // todo: once we have proper dialogs/peers db, we should also diff --git a/packages/client/src/methods/chats/reorder-usernames.ts b/packages/core/src/highlevel/methods/chats/reorder-usernames.ts similarity index 79% rename from packages/client/src/methods/chats/reorder-usernames.ts rename to packages/core/src/highlevel/methods/chats/reorder-usernames.ts index 2c2ff167..9a217aae 100644 --- a/packages/client/src/methods/chats/reorder-usernames.ts +++ b/packages/core/src/highlevel/methods/chats/reorder-usernames.ts @@ -1,8 +1,8 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' -import { assertTrue, isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/index.js' -import { isSelfPeer } from '../auth/_state.js' +import { isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/index.js' +import { isSelfPeer } from '../auth/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -11,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param peerId Bot, channel or "me"/"self" */ export async function reorderUsernames( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, order: string[], ): Promise { diff --git a/packages/client/src/methods/chats/restrict-chat-member.ts b/packages/core/src/highlevel/methods/chats/restrict-chat-member.ts similarity index 92% rename from packages/client/src/methods/chats/restrict-chat-member.ts rename to packages/core/src/highlevel/methods/chats/restrict-chat-member.ts index 290cfb0e..a9544506 100644 --- a/packages/client/src/methods/chats/restrict-chat-member.ts +++ b/packages/core/src/highlevel/methods/chats/restrict-chat-member.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { normalizeDate } from '../../utils/misc-utils.js' import { isInputPeerChannel, toInputChannel } from '../../utils/peer-utils.js' @@ -9,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Restrict a user in a supergroup. */ export async function restrictChatMember( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike @@ -56,5 +57,5 @@ export async function restrictChatMember( ...restrictions, }, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/save-draft.ts b/packages/core/src/highlevel/methods/chats/save-draft.ts similarity index 86% rename from packages/client/src/methods/chats/save-draft.ts rename to packages/core/src/highlevel/methods/chats/save-draft.ts index d2bb4dd1..73dc7d18 100644 --- a/packages/client/src/methods/chats/save-draft.ts +++ b/packages/core/src/highlevel/methods/chats/save-draft.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param draft Draft message, or `null` to delete. */ export async function saveDraft( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, draft: null | Omit, ): Promise { diff --git a/packages/client/src/methods/chats/set-chat-color.ts b/packages/core/src/highlevel/methods/chats/set-chat-color.ts similarity index 83% rename from packages/client/src/methods/chats/set-chat-color.ts rename to packages/core/src/highlevel/methods/chats/set-chat-color.ts index e16cc050..247db161 100644 --- a/packages/client/src/methods/chats/set-chat-color.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-color.ts @@ -1,8 +1,11 @@ -import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' -import { assertTrue, isInputPeerChannel, isInputPeerUser, toInputChannel } from '../../utils/index.js' -import { isSelfPeer } from '../auth/_state.js' +import { isInputPeerChannel, isInputPeerUser, toInputChannel } from '../../utils/index.js' +import { isSelfPeer } from '../auth/utils.js' import { resolvePeer } from '../users/resolve-peer.js' // @available=user @@ -10,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Set peer color and optionally background pattern */ export async function setChatColor( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Peer where to update the color. @@ -58,7 +61,7 @@ export async function setChatColor( backgroundEmojiId, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return } diff --git a/packages/client/src/methods/chats/set-chat-default-permissions.ts b/packages/core/src/highlevel/methods/chats/set-chat-default-permissions.ts similarity index 84% rename from packages/client/src/methods/chats/set-chat-default-permissions.ts rename to packages/core/src/highlevel/methods/chats/set-chat-default-permissions.ts index bd99fcd4..576f8b7f 100644 --- a/packages/client/src/methods/chats/set-chat-default-permissions.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-default-permissions.ts @@ -1,7 +1,8 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -17,7 +18,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * and passing `{}` (empty object) will lift any restrictions */ export async function setChatDefaultPermissions( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, restrictions: Omit, ): Promise { @@ -35,7 +36,7 @@ export async function setChatDefaultPermissions( assertIsUpdatesGroup('messages.editChatDefaultBannedRights', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new Chat(res.chats[0]) } diff --git a/packages/client/src/methods/chats/set-chat-description.ts b/packages/core/src/highlevel/methods/chats/set-chat-description.ts similarity index 81% rename from packages/client/src/methods/chats/set-chat-description.ts rename to packages/core/src/highlevel/methods/chats/set-chat-description.ts index 794d3893..df01fbb7 100644 --- a/packages/client/src/methods/chats/set-chat-description.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-description.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -13,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param description New chat description, 0-255 characters */ export async function setChatDescription( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, description: string, ): Promise { diff --git a/packages/client/src/methods/chats/set-chat-photo.ts b/packages/core/src/highlevel/methods/chats/set-chat-photo.ts similarity index 90% rename from packages/client/src/methods/chats/set-chat-photo.ts rename to packages/core/src/highlevel/methods/chats/set-chat-photo.ts index 931da031..ef90a111 100644 --- a/packages/client/src/methods/chats/set-chat-photo.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-photo.ts @@ -1,7 +1,10 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { fileIdToInputPhoto, tdFileId } from '@mtcute/file-id' +import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike, InputPeerLike, isUploadedFile, MtInvalidPeerTypeError } from '../../types/index.js' +import { fileIdToInputPhoto } from '../../utils/convert-file-id.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { uploadFile } from '../files/upload-file.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * You must be an administrator and have the appropriate permissions. */ export async function setChatPhoto( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike @@ -99,5 +102,5 @@ export async function setChatPhoto( photo, }) } - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/set-chat-title.ts b/packages/core/src/highlevel/methods/chats/set-chat-title.ts similarity index 81% rename from packages/client/src/methods/chats/set-chat-title.ts rename to packages/core/src/highlevel/methods/chats/set-chat-title.ts index 1765b10e..d44ffc6d 100644 --- a/packages/client/src/methods/chats/set-chat-title.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-title.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID or username * @param title New chat title, 1-255 characters */ -export async function setChatTitle(client: BaseTelegramClient, chatId: InputPeerLike, title: string): Promise { +export async function setChatTitle(client: ITelegramClient, chatId: InputPeerLike, title: string): Promise { const chat = await resolvePeer(client, chatId) let res @@ -30,5 +29,5 @@ export async function setChatTitle(client: BaseTelegramClient, chatId: InputPeer }) } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/set-chat-ttl.ts b/packages/core/src/highlevel/methods/chats/set-chat-ttl.ts similarity index 71% rename from packages/client/src/methods/chats/set-chat-ttl.ts rename to packages/core/src/highlevel/methods/chats/set-chat-ttl.ts index 317bc08e..1c87130a 100644 --- a/packages/client/src/methods/chats/set-chat-ttl.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-ttl.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID * @param period New TTL period, in seconds (or 0 to disable) */ -export async function setChatTtl(client: BaseTelegramClient, chatId: InputPeerLike, period: number): Promise { +export async function setChatTtl(client: ITelegramClient, chatId: InputPeerLike, period: number): Promise { await client.call({ _: 'messages.setHistoryTTL', peer: await resolvePeer(client, chatId), diff --git a/packages/client/src/methods/chats/set-chat-username.ts b/packages/core/src/highlevel/methods/chats/set-chat-username.ts similarity index 83% rename from packages/client/src/methods/chats/set-chat-username.ts rename to packages/core/src/highlevel/methods/chats/set-chat-username.ts index f9a57b79..a1e218ba 100644 --- a/packages/client/src/methods/chats/set-chat-username.ts +++ b/packages/core/src/highlevel/methods/chats/set-chat-username.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param username New username, or `null` to remove */ export async function setChatUsername( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, username: string | null, ): Promise { diff --git a/packages/client/src/methods/chats/set-slow-mode.ts b/packages/core/src/highlevel/methods/chats/set-slow-mode.ts similarity index 75% rename from packages/client/src/methods/chats/set-slow-mode.ts rename to packages/core/src/highlevel/methods/chats/set-slow-mode.ts index c684597f..6b05a0d1 100644 --- a/packages/client/src/methods/chats/set-slow-mode.ts +++ b/packages/core/src/highlevel/methods/chats/set-slow-mode.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -13,11 +12,11 @@ import { resolvePeer } from '../users/resolve-peer.js' * Users will be able to send a message only once per this interval. * Valid values are: `0 (off), 10, 30, 60 (1m), 300 (5m), 900 (15m) or 3600 (1h)` */ -export async function setSlowMode(client: BaseTelegramClient, chatId: InputPeerLike, seconds = 0): Promise { +export async function setSlowMode(client: ITelegramClient, chatId: InputPeerLike, seconds = 0): Promise { const res = await client.call({ _: 'channels.toggleSlowMode', channel: toInputChannel(await resolvePeer(client, chatId), chatId), seconds, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/toggle-content-protection.ts b/packages/core/src/highlevel/methods/chats/toggle-content-protection.ts similarity index 82% rename from packages/client/src/methods/chats/toggle-content-protection.ts rename to packages/core/src/highlevel/methods/chats/toggle-content-protection.ts index 5c79dbf2..70a6b6c5 100644 --- a/packages/client/src/methods/chats/toggle-content-protection.ts +++ b/packages/core/src/highlevel/methods/chats/toggle-content-protection.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param enabled Whether content protection should be enabled */ export async function toggleContentProtection( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, enabled = false, ): Promise { @@ -19,5 +18,5 @@ export async function toggleContentProtection( peer: await resolvePeer(client, chatId), enabled, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/toggle-fragment-username.ts b/packages/core/src/highlevel/methods/chats/toggle-fragment-username.ts similarity index 84% rename from packages/client/src/methods/chats/toggle-fragment-username.ts rename to packages/core/src/highlevel/methods/chats/toggle-fragment-username.ts index 934b2f1f..e55fede5 100644 --- a/packages/client/src/methods/chats/toggle-fragment-username.ts +++ b/packages/core/src/highlevel/methods/chats/toggle-fragment-username.ts @@ -1,8 +1,8 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' -import { assertTrue, isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/index.js' -import { isSelfPeer } from '../auth/_state.js' +import { isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/index.js' +import { isSelfPeer } from '../auth/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -12,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * > using {@link setUsername}/{@link setChatUsername} */ export async function toggleFragmentUsername( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Peer ID whose username to toggle */ peerId: InputPeerLike diff --git a/packages/client/src/methods/chats/toggle-join-requests.ts b/packages/core/src/highlevel/methods/chats/toggle-join-requests.ts similarity index 86% rename from packages/client/src/methods/chats/toggle-join-requests.ts rename to packages/core/src/highlevel/methods/chats/toggle-join-requests.ts index ebae5309..ed6bb5c1 100644 --- a/packages/client/src/methods/chats/toggle-join-requests.ts +++ b/packages/core/src/highlevel/methods/chats/toggle-join-requests.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param enabled Whether join requests should be enabled */ export async function toggleJoinRequests( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, enabled = false, ): Promise { @@ -23,5 +22,5 @@ export async function toggleJoinRequests( channel: toInputChannel(await resolvePeer(client, chatId), chatId), enabled, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/toggle-join-to-send.ts b/packages/core/src/highlevel/methods/chats/toggle-join-to-send.ts similarity index 86% rename from packages/client/src/methods/chats/toggle-join-to-send.ts rename to packages/core/src/highlevel/methods/chats/toggle-join-to-send.ts index a1f9a35e..2374f018 100644 --- a/packages/client/src/methods/chats/toggle-join-to-send.ts +++ b/packages/core/src/highlevel/methods/chats/toggle-join-to-send.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param enabled Whether join-to-send setting should be enabled */ export async function toggleJoinToSend( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, enabled = false, ): Promise { @@ -23,5 +22,5 @@ export async function toggleJoinToSend( channel: toInputChannel(await resolvePeer(client, chatId), chatId), enabled, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/unarchive-chats.ts b/packages/core/src/highlevel/methods/chats/unarchive-chats.ts similarity index 68% rename from packages/client/src/methods/chats/unarchive-chats.ts rename to packages/core/src/highlevel/methods/chats/unarchive-chats.ts index b00c5626..63ec7500 100644 --- a/packages/client/src/methods/chats/unarchive-chats.ts +++ b/packages/core/src/highlevel/methods/chats/unarchive-chats.ts @@ -1,5 +1,7 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chats Chat ID(s), username(s), phone number(s), `"me"` or `"self"` */ -export async function unarchiveChats(client: BaseTelegramClient, chats: MaybeArray): Promise { +export async function unarchiveChats(client: ITelegramClient, chats: MaybeArray): Promise { if (!Array.isArray(chats)) chats = [chats] const folderPeers: tl.TypeInputFolderPeer[] = [] @@ -25,5 +27,5 @@ export async function unarchiveChats(client: BaseTelegramClient, chats: MaybeArr _: 'folders.editPeerFolders', folderPeers, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/chats/unban-chat-member.ts b/packages/core/src/highlevel/methods/chats/unban-chat-member.ts similarity index 91% rename from packages/client/src/methods/chats/unban-chat-member.ts rename to packages/core/src/highlevel/methods/chats/unban-chat-member.ts index 9407dad1..66c8ef09 100644 --- a/packages/client/src/methods/chats/unban-chat-member.ts +++ b/packages/core/src/highlevel/methods/chats/unban-chat-member.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types/index.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * This method acts as a no-op in case a legacy group is passed. */ export async function unbanChatMember( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike @@ -38,7 +37,7 @@ export async function unbanChatMember( }, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } else if (isInputPeerChat(chat)) { // no-op // } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') diff --git a/packages/client/src/methods/contacts/add-contact.ts b/packages/core/src/highlevel/methods/contacts/add-contact.ts similarity index 87% rename from packages/client/src/methods/contacts/add-contact.ts rename to packages/core/src/highlevel/methods/contacts/add-contact.ts index 79e06379..aee93f8a 100644 --- a/packages/client/src/methods/contacts/add-contact.ts +++ b/packages/core/src/highlevel/methods/contacts/add-contact.ts @@ -1,15 +1,14 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, User } from '../../types/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { toInputUser } from '../../utils/peer-utils.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** * Add an existing Telegram user as a contact */ export async function addContact( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** User ID, username or phone number */ userId: InputPeerLike @@ -52,7 +51,7 @@ export async function addContact( assertIsUpdatesGroup('contacts.addContact', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return new User(res.users[0]) } diff --git a/packages/client/src/methods/contacts/delete-contacts.ts b/packages/core/src/highlevel/methods/contacts/delete-contacts.ts similarity index 73% rename from packages/client/src/methods/contacts/delete-contacts.ts rename to packages/core/src/highlevel/methods/contacts/delete-contacts.ts index b28313d7..8d033ae4 100644 --- a/packages/client/src/methods/contacts/delete-contacts.ts +++ b/packages/core/src/highlevel/methods/contacts/delete-contacts.ts @@ -1,8 +1,8 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, MtInvalidPeerTypeError, User } from '../../types/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { toInputUser } from '../../utils/peer-utils.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { resolvePeerMany } from '../users/resolve-peer-many.js' /** @@ -13,7 +13,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * * @param userIds User IDs, usernames or phone numbers */ -export async function deleteContacts(client: BaseTelegramClient, userIds: MaybeArray): Promise { +export async function deleteContacts(client: ITelegramClient, userIds: MaybeArray): Promise { if (!Array.isArray(userIds)) userIds = [userIds] const inputPeers = await resolvePeerMany(client, userIds, toInputUser) @@ -29,7 +29,7 @@ export async function deleteContacts(client: BaseTelegramClient, userIds: MaybeA assertIsUpdatesGroup('contacts.deleteContacts', res) - client.network.handleUpdate(res) + client.handleClientUpdate(res) return res.users.map((user) => new User(user)) } diff --git a/packages/client/src/methods/contacts/get-contacts.ts b/packages/core/src/highlevel/methods/contacts/get-contacts.ts similarity index 59% rename from packages/client/src/methods/contacts/get-contacts.ts rename to packages/core/src/highlevel/methods/contacts/get-contacts.ts index d6b8b4a5..90c18e5a 100644 --- a/packages/client/src/methods/contacts/get-contacts.ts +++ b/packages/core/src/highlevel/methods/contacts/get-contacts.ts @@ -1,12 +1,13 @@ -import { BaseTelegramClient, Long } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import Long from 'long' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' /** * Get list of contacts from your Telegram contacts list. */ -export async function getContacts(client: BaseTelegramClient): Promise { +export async function getContacts(client: ITelegramClient): Promise { const res = await client.call({ _: 'contacts.getContacts', hash: Long.ZERO, diff --git a/packages/client/src/methods/contacts/import-contacts.ts b/packages/core/src/highlevel/methods/contacts/import-contacts.ts similarity index 74% rename from packages/client/src/methods/contacts/import-contacts.ts rename to packages/core/src/highlevel/methods/contacts/import-contacts.ts index f79c40cb..16d1a397 100644 --- a/packages/client/src/methods/contacts/import-contacts.ts +++ b/packages/core/src/highlevel/methods/contacts/import-contacts.ts @@ -1,4 +1,9 @@ -import { BaseTelegramClient, Long, PartialOnly, tl } from '@mtcute/core' +import Long from 'long' + +import { tl } from '@mtcute/tl' + +import { PartialOnly } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' /** * Import contacts to your Telegram contacts list. @@ -6,7 +11,7 @@ import { BaseTelegramClient, Long, PartialOnly, tl } from '@mtcute/core' * @param contacts List of contacts */ export async function importContacts( - client: BaseTelegramClient, + client: ITelegramClient, contacts: PartialOnly, 'clientId'>[], ): Promise { let seq = Long.ZERO diff --git a/packages/client/src/methods/dialogs/create-folder.ts b/packages/core/src/highlevel/methods/dialogs/create-folder.ts similarity index 82% rename from packages/client/src/methods/dialogs/create-folder.ts rename to packages/core/src/highlevel/methods/dialogs/create-folder.ts index b3e40c69..7700dbd5 100644 --- a/packages/client/src/methods/dialogs/create-folder.ts +++ b/packages/core/src/highlevel/methods/dialogs/create-folder.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, PartialExcept, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { PartialExcept } from '../../../types/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { getFolders } from './get-folders.js' /** @@ -13,7 +15,7 @@ import { getFolders } from './get-folders.js' * @returns Newly created folder */ export async function createFolder( - client: BaseTelegramClient, + client: ITelegramClient, folder: PartialExcept, ): Promise { let id = folder.id diff --git a/packages/client/src/methods/dialogs/delete-folder.ts b/packages/core/src/highlevel/methods/dialogs/delete-folder.ts similarity index 50% rename from packages/client/src/methods/dialogs/delete-folder.ts rename to packages/core/src/highlevel/methods/dialogs/delete-folder.ts index e861639e..68baafc7 100644 --- a/packages/client/src/methods/dialogs/delete-folder.ts +++ b/packages/core/src/highlevel/methods/dialogs/delete-folder.ts @@ -1,12 +1,14 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' + +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Delete a folder by its ID * * @param id Folder ID or folder itself */ -export async function deleteFolder(client: BaseTelegramClient, id: number | tl.RawDialogFilter): Promise { +export async function deleteFolder(client: ITelegramClient, id: number | tl.RawDialogFilter): Promise { const r = await client.call({ _: 'messages.updateDialogFilter', id: typeof id === 'number' ? id : id.id, diff --git a/packages/client/src/methods/dialogs/edit-folder.ts b/packages/core/src/highlevel/methods/dialogs/edit-folder.ts similarity index 86% rename from packages/client/src/methods/dialogs/edit-folder.ts rename to packages/core/src/highlevel/methods/dialogs/edit-folder.ts index 3dfbd5cf..de75c52b 100644 --- a/packages/client/src/methods/dialogs/edit-folder.ts +++ b/packages/core/src/highlevel/methods/dialogs/edit-folder.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { getFolders } from './get-folders.js' /** @@ -9,7 +11,7 @@ import { getFolders } from './get-folders.js' * @returns Modified folder */ export async function editFolder( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Folder, folder ID or name. diff --git a/packages/client/src/methods/dialogs/find-folder.ts b/packages/core/src/highlevel/methods/dialogs/find-folder.ts similarity index 86% rename from packages/client/src/methods/dialogs/find-folder.ts rename to packages/core/src/highlevel/methods/dialogs/find-folder.ts index 655f60a9..009efd63 100644 --- a/packages/client/src/methods/dialogs/find-folder.ts +++ b/packages/core/src/highlevel/methods/dialogs/find-folder.ts @@ -1,5 +1,7 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { getFolders } from './get-folders.js' /** @@ -12,7 +14,7 @@ import { getFolders } from './get-folders.js' * @param params Search parameters. At least one must be set. */ export async function findFolder( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Folder title */ title?: string diff --git a/packages/client/src/methods/dialogs/get-folders.ts b/packages/core/src/highlevel/methods/dialogs/get-folders.ts similarity index 74% rename from packages/client/src/methods/dialogs/get-folders.ts rename to packages/core/src/highlevel/methods/dialogs/get-folders.ts index cd709fd0..5e21e461 100644 --- a/packages/client/src/methods/dialogs/get-folders.ts +++ b/packages/core/src/highlevel/methods/dialogs/get-folders.ts @@ -1,11 +1,13 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { InputDialogFolder } from '../../types/index.js' /** * Get list of folders. */ -export async function getFolders(client: BaseTelegramClient): Promise { +export async function getFolders(client: ITelegramClient): Promise { return client.call({ _: 'messages.getDialogFilters', }) @@ -13,7 +15,7 @@ export async function getFolders(client: BaseTelegramClient): Promise { if (typeof folder === 'string' || typeof folder === 'number') { diff --git a/packages/client/src/methods/dialogs/get-peer-dialogs.ts b/packages/core/src/highlevel/methods/dialogs/get-peer-dialogs.ts similarity index 73% rename from packages/client/src/methods/dialogs/get-peer-dialogs.ts rename to packages/core/src/highlevel/methods/dialogs/get-peer-dialogs.ts index 85ea32a4..96e80deb 100644 --- a/packages/client/src/methods/dialogs/get-peer-dialogs.ts +++ b/packages/core/src/highlevel/methods/dialogs/get-peer-dialogs.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { Dialog } from '../../types/messages/dialog.js' import { InputPeerLike } from '../../types/peers/index.js' import { resolvePeerMany } from '../users/resolve-peer-many.js' @@ -9,7 +9,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * * @param peers Peers for which to fetch dialogs. */ -export async function getPeerDialogs(client: BaseTelegramClient, peers: MaybeArray): Promise { +export async function getPeerDialogs(client: ITelegramClient, peers: MaybeArray): Promise { if (!Array.isArray(peers)) peers = [peers] const res = await client.call({ diff --git a/packages/client/src/methods/dialogs/iter-dialogs.ts b/packages/core/src/highlevel/methods/dialogs/iter-dialogs.ts similarity index 97% rename from packages/client/src/methods/dialogs/iter-dialogs.ts rename to packages/core/src/highlevel/methods/dialogs/iter-dialogs.ts index 170754a3..2613068e 100644 --- a/packages/client/src/methods/dialogs/iter-dialogs.ts +++ b/packages/core/src/highlevel/methods/dialogs/iter-dialogs.ts @@ -1,5 +1,9 @@ -import { BaseTelegramClient, Long, MtUnsupportedError, tl } from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { MtUnsupportedError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Dialog, InputDialogFolder } from '../../types/index.js' import { normalizeDate } from '../../utils/misc-utils.js' import { _normalizeInputFolder } from './get-folders.js' @@ -15,7 +19,7 @@ import { _normalizeInputFolder } from './get-folders.js' * @param params Fetch parameters */ export async function* iterDialogs( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Offset message date used as an anchor for pagination. diff --git a/packages/client/src/methods/dialogs/set-folders-order.ts b/packages/core/src/highlevel/methods/dialogs/set-folders-order.ts similarity index 54% rename from packages/client/src/methods/dialogs/set-folders-order.ts rename to packages/core/src/highlevel/methods/dialogs/set-folders-order.ts index 9e1a39b3..4a78e3e6 100644 --- a/packages/client/src/methods/dialogs/set-folders-order.ts +++ b/packages/core/src/highlevel/methods/dialogs/set-folders-order.ts @@ -1,12 +1,12 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Reorder folders * * @param order New order of folders (folder IDs, where default = 0) */ -export async function setFoldersOrder(client: BaseTelegramClient, order: number[]): Promise { +export async function setFoldersOrder(client: ITelegramClient, order: number[]): Promise { const r = await client.call({ _: 'messages.updateDialogFiltersOrder', order, diff --git a/packages/client/src/methods/files/_platform.ts b/packages/core/src/highlevel/methods/files/_platform.ts similarity index 100% rename from packages/client/src/methods/files/_platform.ts rename to packages/core/src/highlevel/methods/files/_platform.ts diff --git a/packages/client/src/methods/files/_platform.web.ts b/packages/core/src/highlevel/methods/files/_platform.web.ts similarity index 88% rename from packages/client/src/methods/files/_platform.web.ts rename to packages/core/src/highlevel/methods/files/_platform.web.ts index 3fad2b9f..600b1cad 100644 --- a/packages/client/src/methods/files/_platform.web.ts +++ b/packages/core/src/highlevel/methods/files/_platform.web.ts @@ -1,4 +1,4 @@ -import { MtArgumentError } from '@mtcute/core' +import { MtArgumentError } from '../../../types/errors.js' /** @internal */ export function _createFileStream(): never { diff --git a/packages/client/src/methods/files/download-buffer.ts b/packages/core/src/highlevel/methods/files/download-buffer.ts similarity index 84% rename from packages/client/src/methods/files/download-buffer.ts rename to packages/core/src/highlevel/methods/files/download-buffer.ts index 85099991..9b1e0754 100644 --- a/packages/client/src/methods/files/download-buffer.ts +++ b/packages/core/src/highlevel/methods/files/download-buffer.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { concatBuffers } from '@mtcute/core/utils.js' - +import { concatBuffers } from '../../../utils/buffer-utils.js' +import { ITelegramClient } from '../../client.types.js' import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' import { downloadAsIterable } from './download-iterable.js' @@ -13,7 +12,7 @@ import { downloadAsIterable } from './download-iterable.js' * @param params File download parameters */ export async function downloadAsBuffer( - client: BaseTelegramClient, + client: ITelegramClient, location: FileDownloadLocation, params?: FileDownloadParameters, ): Promise { diff --git a/packages/client/src/methods/files/download-file.ts b/packages/core/src/highlevel/methods/files/download-file.ts similarity index 93% rename from packages/client/src/methods/files/download-file.ts rename to packages/core/src/highlevel/methods/files/download-file.ts index 331dd6a6..01c127e5 100644 --- a/packages/client/src/methods/files/download-file.ts +++ b/packages/core/src/highlevel/methods/files/download-file.ts @@ -2,8 +2,7 @@ import { createWriteStream, rmSync } from 'fs' import { writeFile } from 'fs/promises' -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' import { downloadAsIterable } from './download-iterable.js' @@ -15,7 +14,7 @@ import { downloadAsIterable } from './download-iterable.js' * @param params File download parameters */ export async function downloadToFile( - client: BaseTelegramClient, + client: ITelegramClient, filename: string, location: FileDownloadLocation, params?: FileDownloadParameters, diff --git a/packages/client/src/methods/files/download-file.web.ts b/packages/core/src/highlevel/methods/files/download-file.web.ts similarity index 66% rename from packages/client/src/methods/files/download-file.web.ts rename to packages/core/src/highlevel/methods/files/download-file.web.ts index 0855eca7..cdf92fed 100644 --- a/packages/client/src/methods/files/download-file.web.ts +++ b/packages/core/src/highlevel/methods/files/download-file.web.ts @@ -1,4 +1,4 @@ -import { MtUnsupportedError } from '@mtcute/core' +import { MtUnsupportedError } from '../../../types/errors.js' export function downloadToFile() { throw new MtUnsupportedError('Downloading to file is only supported in NodeJS') diff --git a/packages/client/src/methods/files/download-iterable.ts b/packages/core/src/highlevel/methods/files/download-iterable.ts similarity index 91% rename from packages/client/src/methods/files/download-iterable.ts rename to packages/core/src/highlevel/methods/files/download-iterable.ts index fcfcc34a..8f4302cb 100644 --- a/packages/client/src/methods/files/download-iterable.ts +++ b/packages/core/src/highlevel/methods/files/download-iterable.ts @@ -1,8 +1,12 @@ -import { BaseTelegramClient, ConnectionKind, MtArgumentError, MtUnsupportedError, tl } from '@mtcute/core' -import { ConditionVariable } from '@mtcute/core/utils.js' -import { fileIdToInputFileLocation, fileIdToInputWebFileLocation, parseFileId } from '@mtcute/file-id' +import { parseFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { ConnectionKind } from '../../../network/network-manager.js' +import { MtArgumentError, MtUnsupportedError } from '../../../types/errors.js' +import { ConditionVariable } from '../../../utils/condition-variable.js' +import { ITelegramClient } from '../../client.types.js' import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' +import { fileIdToInputFileLocation, fileIdToInputWebFileLocation } from '../../utils/convert-file-id.js' import { determinePartSize } from '../../utils/file-utils.js' // small files (less than 128 kb) are downloaded using the "downloadSmall" pool @@ -19,7 +23,7 @@ const REQUESTS_PER_CONNECTION = 3 // some arbitrary magic value that seems to wo * @param params Download parameters */ export async function* downloadAsIterable( - client: BaseTelegramClient, + client: ITelegramClient, input: FileDownloadLocation, params?: FileDownloadParameters, ): AsyncIterableIterator { @@ -64,7 +68,8 @@ export async function* downloadAsIterable( const isWeb = tl.isAnyInputWebFileLocation(location) // we will receive a FileMigrateError in case this is invalid - if (!dcId) dcId = client.network.getPrimaryDcId() + const primaryDcId = await client.getPrimaryDcId() + if (!dcId) dcId = primaryDcId const partSizeKb = params?.partSize ?? (fileSize ? determinePartSize(fileSize) : 64) @@ -88,11 +93,11 @@ export async function* downloadAsIterable( let connectionKind: ConnectionKind if (isSmall) { - connectionKind = dcId === client.network.getPrimaryDcId() ? 'main' : 'downloadSmall' + connectionKind = dcId === primaryDcId ? 'main' : 'downloadSmall' } else { connectionKind = 'download' } - const poolSize = client.network.getPoolSize(connectionKind, dcId) + const poolSize = await client.getPoolSize(connectionKind, dcId) client.log.debug( 'Downloading file of size %d from dc %d using %s connection pool (pool size: %d)', diff --git a/packages/client/src/methods/files/download-stream.ts b/packages/core/src/highlevel/methods/files/download-stream.ts similarity index 93% rename from packages/client/src/methods/files/download-stream.ts rename to packages/core/src/highlevel/methods/files/download-stream.ts index 5c5c52de..a0235f67 100644 --- a/packages/client/src/methods/files/download-stream.ts +++ b/packages/core/src/highlevel/methods/files/download-stream.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' import { bufferToStream } from '../../utils/stream-utils.js' import { downloadAsIterable } from './download-iterable.js' @@ -11,7 +10,7 @@ import { downloadAsIterable } from './download-iterable.js' * @param params File download parameters */ export function downloadAsStream( - client: BaseTelegramClient, + client: ITelegramClient, location: FileDownloadLocation, params?: FileDownloadParameters, ): ReadableStream { diff --git a/packages/client/src/methods/files/normalize-file-to-document.ts b/packages/core/src/highlevel/methods/files/normalize-file-to-document.ts similarity index 81% rename from packages/client/src/methods/files/normalize-file-to-document.ts rename to packages/core/src/highlevel/methods/files/normalize-file-to-document.ts index 3b49c420..a10ae664 100644 --- a/packages/client/src/methods/files/normalize-file-to-document.ts +++ b/packages/core/src/highlevel/methods/files/normalize-file-to-document.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike } from '../../types/index.js' import { _normalizeInputMedia } from './normalize-input-media.js' @@ -8,7 +9,7 @@ import { _normalizeInputMedia } from './normalize-input-media.js' * @internal */ export async function _normalizeFileToDocument( - client: BaseTelegramClient, + client: ITelegramClient, file: InputFileLike | tl.TypeInputDocument, params: { progressCallback?: (uploaded: number, total: number) => void diff --git a/packages/client/src/methods/files/normalize-input-file.ts b/packages/core/src/highlevel/methods/files/normalize-input-file.ts similarity index 88% rename from packages/client/src/methods/files/normalize-input-file.ts rename to packages/core/src/highlevel/methods/files/normalize-input-file.ts index c3e35b9e..82b2a79a 100644 --- a/packages/client/src/methods/files/normalize-input-file.ts +++ b/packages/core/src/highlevel/methods/files/normalize-input-file.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike, isUploadedFile } from '../../types/files/index.js' import { uploadFile } from './upload-file.js' @@ -9,7 +11,7 @@ import { uploadFile } from './upload-file.js' * uploading it if needed. */ export async function _normalizeInputFile( - client: BaseTelegramClient, + client: ITelegramClient, input: InputFileLike, params: { progressCallback?: (uploaded: number, total: number) => void diff --git a/packages/client/src/methods/files/normalize-input-media.ts b/packages/core/src/highlevel/methods/files/normalize-input-media.ts similarity index 97% rename from packages/client/src/methods/files/normalize-input-media.ts rename to packages/core/src/highlevel/methods/files/normalize-input-media.ts index 93747f42..0e411c21 100644 --- a/packages/client/src/methods/files/normalize-input-media.ts +++ b/packages/core/src/highlevel/methods/files/normalize-input-media.ts @@ -1,10 +1,14 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' -import { fileIdToInputDocument, fileIdToInputPhoto, parseFileId, tdFileId } from '@mtcute/file-id' +import Long from 'long' +import { parseFileId, tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { isUploadedFile } from '../../types/files/uploaded-file.js' import { UploadFileLike } from '../../types/files/utils.js' import { InputMediaLike } from '../../types/media/input-media.js' +import { fileIdToInputDocument, fileIdToInputPhoto } from '../../utils/convert-file-id.js' import { extractFileName } from '../../utils/file-utils.js' import { normalizeDate } from '../../utils/misc-utils.js' import { encodeWaveform } from '../../utils/voice-utils.js' @@ -18,7 +22,7 @@ import { uploadFile } from './upload-file.js' * uploading the file if needed. */ export async function _normalizeInputMedia( - client: BaseTelegramClient, + client: ITelegramClient, media: InputMediaLike, params: { progressCallback?: (uploaded: number, total: number) => void diff --git a/packages/client/src/methods/files/upload-file.ts b/packages/core/src/highlevel/methods/files/upload-file.ts similarity index 96% rename from packages/client/src/methods/files/upload-file.ts rename to packages/core/src/highlevel/methods/files/upload-file.ts index 19ed541f..8a98e898 100644 --- a/packages/client/src/methods/files/upload-file.ts +++ b/packages/core/src/highlevel/methods/files/upload-file.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { UploadedFile, UploadFileLike } from '../../types/index.js' import { guessFileMime } from '../../utils/file-type.js' import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js' @@ -31,7 +33,7 @@ const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB * @param params Upload parameters */ export async function uploadFile( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Upload file source. @@ -194,7 +196,7 @@ export async function uploadFile( const partSize = partSizeKb * 1024 let partCount = fileSize === -1 ? -1 : ~~((fileSize + partSize - 1) / partSize) - const maxPartCount = client.network.params.isPremium ? MAX_PART_COUNT_PREMIUM : MAX_PART_COUNT + const maxPartCount = client.storage.self.getCached()?.isPremium ? MAX_PART_COUNT_PREMIUM : MAX_PART_COUNT if (partCount > maxPartCount) { throw new MtArgumentError(`File is too large (max ${maxPartCount} parts, got ${partCount})`) @@ -204,7 +206,7 @@ export async function uploadFile( const isSmall = fileSize !== -1 && fileSize < SMALL_FILE_MAX_SIZE const connectionKind = isSmall ? 'main' : 'upload' // streamed uploads must be serialized, otherwise we'll get FILE_PART_SIZE_INVALID - const connectionPoolSize = Math.min(client.network.getPoolSize(connectionKind), partCount) + const connectionPoolSize = Math.min(await client.getPoolSize(connectionKind), partCount) const requestsPerConnection = params.requestsPerConnection ?? REQUESTS_PER_CONNECTION client.log.debug( diff --git a/packages/client/src/methods/files/upload-media.ts b/packages/core/src/highlevel/methods/files/upload-media.ts similarity index 90% rename from packages/client/src/methods/files/upload-media.ts rename to packages/core/src/highlevel/methods/files/upload-media.ts index d1842d87..f72c2bdd 100644 --- a/packages/client/src/methods/files/upload-media.ts +++ b/packages/core/src/highlevel/methods/files/upload-media.ts @@ -1,6 +1,7 @@ -import { assertNever, BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { assertTypeIs, assertTypeIsNot } from '@mtcute/core/utils.js' - +import { MtArgumentError } from '../../../types/errors.js' +import { assertNever } from '../../../types/utils.js' +import { assertTypeIs, assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputMediaLike, InputPeerLike, MessageMedia, Photo, RawDocument } from '../../types/index.js' import { parseDocument } from '../../types/media/document-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -18,7 +19,7 @@ import { _normalizeInputMedia } from './normalize-input-media.js' * @param params Upload parameters */ export async function uploadMedia( - client: BaseTelegramClient, + client: ITelegramClient, media: InputMediaLike, params: { /** diff --git a/packages/client/src/methods/forums/create-forum-topic.ts b/packages/core/src/highlevel/methods/forums/create-forum-topic.ts similarity index 90% rename from packages/client/src/methods/forums/create-forum-topic.ts rename to packages/core/src/highlevel/methods/forums/create-forum-topic.ts index 8c5b138b..16ab1c2d 100644 --- a/packages/client/src/methods/forums/create-forum-topic.ts +++ b/packages/core/src/highlevel/methods/forums/create-forum-topic.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { _findMessageInUpdate } from '../messages/find-in-update.js' @@ -14,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Service message for the created topic */ export async function createForumTopic( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/forums/delete-forum-topic-history.ts b/packages/core/src/highlevel/methods/forums/delete-forum-topic-history.ts similarity index 73% rename from packages/client/src/methods/forums/delete-forum-topic-history.ts rename to packages/core/src/highlevel/methods/forums/delete-forum-topic-history.ts index aeb686b3..e6634103 100644 --- a/packages/client/src/methods/forums/delete-forum-topic-history.ts +++ b/packages/core/src/highlevel/methods/forums/delete-forum-topic-history.ts @@ -1,9 +1,8 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' - +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import type { ForumTopic, InputPeerLike } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { toInputChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -13,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param topicId ID of the topic (i.e. its top message ID) */ export async function deleteForumTopicHistory( - client: BaseTelegramClient, + client: ITelegramClient, chat: InputPeerLike, topicId: number | ForumTopic, ): Promise { @@ -26,5 +25,5 @@ export async function deleteForumTopicHistory( topMsgId: typeof topicId === 'number' ? topicId : topicId.id, }) - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount, channel.channelId)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount, channel.channelId)) } diff --git a/packages/client/src/methods/forums/edit-forum-topic.ts b/packages/core/src/highlevel/methods/forums/edit-forum-topic.ts similarity index 92% rename from packages/client/src/methods/forums/edit-forum-topic.ts rename to packages/core/src/highlevel/methods/forums/edit-forum-topic.ts index 60f8889d..3504caf3 100644 --- a/packages/client/src/methods/forums/edit-forum-topic.ts +++ b/packages/core/src/highlevel/methods/forums/edit-forum-topic.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { ITelegramClient } from '../../client.types.js' import type { ForumTopic, InputPeerLike, Message } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { _findMessageInUpdate } from '../messages/find-in-update.js' @@ -15,7 +18,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Service message about the modification */ export async function editForumTopic( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/forums/get-forum-topics-by-id.ts b/packages/core/src/highlevel/methods/forums/get-forum-topics-by-id.ts similarity index 82% rename from packages/client/src/methods/forums/get-forum-topics-by-id.ts rename to packages/core/src/highlevel/methods/forums/get-forum-topics-by-id.ts index 01bbb15f..32cb8ee9 100644 --- a/packages/client/src/methods/forums/get-forum-topics-by-id.ts +++ b/packages/core/src/highlevel/methods/forums/get-forum-topics-by-id.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { ForumTopic, InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID or username */ export async function getForumTopicsById( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, ids: MaybeArray, ): Promise { diff --git a/packages/client/src/methods/forums/get-forum-topics.ts b/packages/core/src/highlevel/methods/forums/get-forum-topics.ts similarity index 95% rename from packages/client/src/methods/forums/get-forum-topics.ts rename to packages/core/src/highlevel/methods/forums/get-forum-topics.ts index f2e870eb..e0018bea 100644 --- a/packages/client/src/methods/forums/get-forum-topics.ts +++ b/packages/core/src/highlevel/methods/forums/get-forum-topics.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, ForumTopic, InputPeerLike } from '../../types/index.js' import { makeArrayPaginated } from '../../utils/index.js' import { toInputChannel } from '../../utils/peer-utils.js' @@ -24,7 +23,7 @@ const defaultOffset: GetForumTopicsOffset = { * @param chatId Chat ID or username */ export async function getForumTopics( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/forums/iter-forum-topics.ts b/packages/core/src/highlevel/methods/forums/iter-forum-topics.ts similarity index 94% rename from packages/client/src/methods/forums/iter-forum-topics.ts rename to packages/core/src/highlevel/methods/forums/iter-forum-topics.ts index ee8db2d9..28d83634 100644 --- a/packages/client/src/methods/forums/iter-forum-topics.ts +++ b/packages/core/src/highlevel/methods/forums/iter-forum-topics.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ForumTopic, InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +10,7 @@ import { getForumTopics } from './get-forum-topics.js' * @param chatId Chat ID or username */ export async function* iterForumTopics( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/forums/reorder-pinned-forum-topics.ts b/packages/core/src/highlevel/methods/forums/reorder-pinned-forum-topics.ts similarity index 91% rename from packages/client/src/methods/forums/reorder-pinned-forum-topics.ts rename to packages/core/src/highlevel/methods/forums/reorder-pinned-forum-topics.ts index 73e0cc2c..4ee86846 100644 --- a/packages/client/src/methods/forums/reorder-pinned-forum-topics.ts +++ b/packages/core/src/highlevel/methods/forums/reorder-pinned-forum-topics.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import type { ForumTopic, InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Only admins with `manageTopics` permission can do this. */ export async function reorderPinnedForumTopics( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/forums/toggle-forum-topic-closed.ts b/packages/core/src/highlevel/methods/forums/toggle-forum-topic-closed.ts similarity index 93% rename from packages/client/src/methods/forums/toggle-forum-topic-closed.ts rename to packages/core/src/highlevel/methods/forums/toggle-forum-topic-closed.ts index db0730ed..34f35935 100644 --- a/packages/client/src/methods/forums/toggle-forum-topic-closed.ts +++ b/packages/core/src/highlevel/methods/forums/toggle-forum-topic-closed.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import type { ForumTopic, InputPeerLike, Message } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { _findMessageInUpdate } from '../messages/find-in-update.js' @@ -13,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Service message about the modification */ export async function toggleForumTopicClosed( - client: BaseTelegramClient, + client: ITelegramClient, parmas: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/forums/toggle-forum-topic-pinned.ts b/packages/core/src/highlevel/methods/forums/toggle-forum-topic-pinned.ts similarity index 91% rename from packages/client/src/methods/forums/toggle-forum-topic-pinned.ts rename to packages/core/src/highlevel/methods/forums/toggle-forum-topic-pinned.ts index 903bd856..42ef5ead 100644 --- a/packages/client/src/methods/forums/toggle-forum-topic-pinned.ts +++ b/packages/core/src/highlevel/methods/forums/toggle-forum-topic-pinned.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ForumTopic, InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Only admins with `manageTopics` permission can do this. */ export async function toggleForumTopicPinned( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/forums/toggle-forum.ts b/packages/core/src/highlevel/methods/forums/toggle-forum.ts similarity index 72% rename from packages/client/src/methods/forums/toggle-forum.ts rename to packages/core/src/highlevel/methods/forums/toggle-forum.ts index 186422d7..7d53a09e 100644 --- a/packages/client/src/methods/forums/toggle-forum.ts +++ b/packages/core/src/highlevel/methods/forums/toggle-forum.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,11 +11,11 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID or username * @param enabled Whether the supergroup should be a forum */ -export async function toggleForum(client: BaseTelegramClient, chatId: InputPeerLike, enabled = false): Promise { +export async function toggleForum(client: ITelegramClient, chatId: InputPeerLike, enabled = false): Promise { const res = await client.call({ _: 'channels.toggleForum', channel: toInputChannel(await resolvePeer(client, chatId), chatId), enabled, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/forums/toggle-general-topic-hidden.ts b/packages/core/src/highlevel/methods/forums/toggle-general-topic-hidden.ts similarity index 93% rename from packages/client/src/methods/forums/toggle-general-topic-hidden.ts rename to packages/core/src/highlevel/methods/forums/toggle-general-topic-hidden.ts index f7a1427b..d9f631eb 100644 --- a/packages/client/src/methods/forums/toggle-general-topic-hidden.ts +++ b/packages/core/src/highlevel/methods/forums/toggle-general-topic-hidden.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message } from '../../types/index.js' import { toInputChannel } from '../../utils/peer-utils.js' import { _findMessageInUpdate } from '../messages/find-in-update.js' @@ -13,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Service message about the modification */ export async function toggleGeneralTopicHidden( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID or username */ chatId: InputPeerLike diff --git a/packages/client/src/methods/invite-links/create-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/create-invite-link.ts similarity index 94% rename from packages/client/src/methods/invite-links/create-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/create-invite-link.ts index 3efc4eef..0ac56e0b 100644 --- a/packages/client/src/methods/invite-links/create-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/create-invite-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike } from '../../types/index.js' import { normalizeDate } from '../../utils/misc-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -13,7 +12,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param params */ export async function createInviteLink( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/invite-links/edit-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/edit-invite-link.ts similarity index 95% rename from packages/client/src/methods/invite-links/edit-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/edit-invite-link.ts index ea22269e..3df68628 100644 --- a/packages/client/src/methods/invite-links/edit-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/edit-invite-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types/index.js' import { normalizeDate } from '../../utils/misc-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -16,7 +15,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Modified invite link */ export async function editInviteLink( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat ID */ chatId: InputPeerLike diff --git a/packages/client/src/methods/invite-links/export-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/export-invite-link.ts similarity index 76% rename from packages/client/src/methods/invite-links/export-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/export-invite-link.ts index ef63c1f8..209ad3fb 100644 --- a/packages/client/src/methods/invite-links/export-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/export-invite-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat IDs */ -export async function exportInviteLink(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function exportInviteLink(client: ITelegramClient, chatId: InputPeerLike): Promise { const res = await client.call({ _: 'messages.exportChatInvite', peer: await resolvePeer(client, chatId), diff --git a/packages/client/src/methods/invite-links/get-invite-link-members.ts b/packages/core/src/highlevel/methods/invite-links/get-invite-link-members.ts similarity index 95% rename from packages/client/src/methods/invite-links/get-invite-link-members.ts rename to packages/core/src/highlevel/methods/invite-links/get-invite-link-members.ts index 0fd4acb7..5b4e473e 100644 --- a/packages/client/src/methods/invite-links/get-invite-link-members.ts +++ b/packages/core/src/highlevel/methods/invite-links/get-invite-link-members.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, ChatInviteLink, ChatInviteLinkMember, InputPeerLike, PeersIndex } from '../../types/index.js' import { makeArrayPaginated, normalizeDate, toInputUser } from '../../utils/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param params Additional params */ export async function getInviteLinkMembers( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/invite-links/get-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/get-invite-link.ts similarity index 87% rename from packages/client/src/methods/invite-links/get-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/get-invite-link.ts index 3485e78c..c7dfca86 100644 --- a/packages/client/src/methods/invite-links/get-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/get-invite-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param link The invite link */ export async function getInviteLink( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, link: string, ): Promise { diff --git a/packages/client/src/methods/invite-links/get-invite-links.ts b/packages/core/src/highlevel/methods/invite-links/get-invite-links.ts similarity index 96% rename from packages/client/src/methods/invite-links/get-invite-links.ts rename to packages/core/src/highlevel/methods/invite-links/get-invite-links.ts index 58990952..97d327bd 100644 --- a/packages/client/src/methods/invite-links/get-invite-links.ts +++ b/packages/core/src/highlevel/methods/invite-links/get-invite-links.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, ChatInviteLink, InputPeerLike, PeersIndex } from '../../types/index.js' import { makeArrayPaginated } from '../../utils/index.js' import { toInputUser } from '../../utils/peer-utils.js' @@ -23,7 +22,7 @@ export interface GetInviteLinksOffset { * @param params */ export async function getInviteLinks( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/invite-links/get-primary-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/get-primary-invite-link.ts similarity index 79% rename from packages/client/src/methods/invite-links/get-primary-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/get-primary-invite-link.ts index c34d95a2..2100ffae 100644 --- a/packages/client/src/methods/invite-links/get-primary-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/get-primary-invite-link.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' - +import { MtTypeAssertionError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID */ -export async function getPrimaryInviteLink(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function getPrimaryInviteLink(client: ITelegramClient, chatId: InputPeerLike): Promise { const res = await client.call({ _: 'messages.getExportedChatInvites', peer: await resolvePeer(client, chatId), diff --git a/packages/client/src/methods/invite-links/hide-all-join-requests.ts b/packages/core/src/highlevel/methods/invite-links/hide-all-join-requests.ts similarity index 90% rename from packages/client/src/methods/invite-links/hide-all-join-requests.ts rename to packages/core/src/highlevel/methods/invite-links/hide-all-join-requests.ts index b1a479c1..ff765938 100644 --- a/packages/client/src/methods/invite-links/hide-all-join-requests.ts +++ b/packages/core/src/highlevel/methods/invite-links/hide-all-join-requests.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import type { ChatInviteLink, InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -7,7 +6,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Approve or decline multiple join requests to a chat. */ export async function hideAllJoinRequests( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat/channel ID */ chatId: InputPeerLike diff --git a/packages/client/src/methods/invite-links/hide-join-request.ts b/packages/core/src/highlevel/methods/invite-links/hide-join-request.ts similarity index 90% rename from packages/client/src/methods/invite-links/hide-join-request.ts rename to packages/core/src/highlevel/methods/invite-links/hide-join-request.ts index 0ee2029e..66257157 100644 --- a/packages/client/src/methods/invite-links/hide-join-request.ts +++ b/packages/core/src/highlevel/methods/invite-links/hide-join-request.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +7,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Approve or decline join request to a chat. */ export async function hideJoinRequest( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Chat/channel ID */ chatId: InputPeerLike diff --git a/packages/client/src/methods/invite-links/iter-invite-link-members.ts b/packages/core/src/highlevel/methods/invite-links/iter-invite-link-members.ts similarity index 95% rename from packages/client/src/methods/invite-links/iter-invite-link-members.ts rename to packages/core/src/highlevel/methods/invite-links/iter-invite-link-members.ts index d0edcfa4..a4213b38 100644 --- a/packages/client/src/methods/invite-links/iter-invite-link-members.ts +++ b/packages/core/src/highlevel/methods/invite-links/iter-invite-link-members.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLinkMember, InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getInviteLinkMembers } from './get-invite-link-members.js' @@ -12,7 +11,7 @@ import { getInviteLinkMembers } from './get-invite-link-members.js' * @param params Additional params */ export async function* iterInviteLinkMembers( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/invite-links/iter-invite-links.ts b/packages/core/src/highlevel/methods/invite-links/iter-invite-links.ts similarity index 95% rename from packages/client/src/methods/invite-links/iter-invite-links.ts rename to packages/core/src/highlevel/methods/invite-links/iter-invite-links.ts index 7d3d92d7..96acb43c 100644 --- a/packages/client/src/methods/invite-links/iter-invite-links.ts +++ b/packages/core/src/highlevel/methods/invite-links/iter-invite-links.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getInviteLinks } from './get-invite-links.js' @@ -16,7 +15,7 @@ import { getInviteLinks } from './get-invite-links.js' * @param params */ export async function* iterInviteLinks( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/invite-links/revoke-invite-link.ts b/packages/core/src/highlevel/methods/invite-links/revoke-invite-link.ts similarity index 92% rename from packages/client/src/methods/invite-links/revoke-invite-link.ts rename to packages/core/src/highlevel/methods/invite-links/revoke-invite-link.ts index 14df0ef9..da9f5891 100644 --- a/packages/client/src/methods/invite-links/revoke-invite-link.ts +++ b/packages/core/src/highlevel/methods/invite-links/revoke-invite-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns If `link` is a primary invite, newly generated invite link, otherwise the revoked link */ export async function revokeInviteLink( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, link: string | ChatInviteLink, ): Promise { diff --git a/packages/client/src/methods/messages/close-poll.ts b/packages/core/src/highlevel/methods/messages/close-poll.ts similarity index 79% rename from packages/client/src/methods/messages/close-poll.ts rename to packages/core/src/highlevel/methods/messages/close-poll.ts index bb0ead38..8518920f 100644 --- a/packages/client/src/methods/messages/close-poll.ts +++ b/packages/core/src/highlevel/methods/messages/close-poll.ts @@ -1,8 +1,10 @@ -import { BaseTelegramClient, Long, MtTypeAssertionError } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import Long from 'long' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, normalizeInputMessageId, PeersIndex, Poll } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -12,7 +14,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * will be able to vote in it */ export async function closePoll( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** * Whether to dispatch the edit message event @@ -41,7 +43,7 @@ export async function closePoll( assertIsUpdatesGroup('messages.editMessage', res) - client.network.handleUpdate(res, !params.shouldDispatch) + client.handleClientUpdate(res, !params.shouldDispatch) const upd = res.updates[0] assertTypeIs('messages.editMessage (@ .updates[0])', upd, 'updateMessagePoll') diff --git a/packages/client/src/methods/messages/delete-messages.ts b/packages/core/src/highlevel/methods/messages/delete-messages.ts similarity index 90% rename from packages/client/src/methods/messages/delete-messages.ts rename to packages/core/src/highlevel/methods/messages/delete-messages.ts index 98cb3093..cb4f8967 100644 --- a/packages/client/src/methods/messages/delete-messages.ts +++ b/packages/core/src/highlevel/methods/messages/delete-messages.ts @@ -1,8 +1,9 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { isInputPeerChannel, toInputChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' import { deleteScheduledMessages } from './delete-scheduled-messages.js' @@ -24,7 +25,7 @@ export interface DeleteMessagesParams { * @param ids Message(s) ID(s) to delete. */ export async function deleteMessagesById( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, ids: number[], params?: DeleteMessagesParams, @@ -52,7 +53,7 @@ export async function deleteMessagesById( upd = createDummyUpdate(res.pts, res.ptsCount) } - client.network.handleUpdate(upd) + client.handleClientUpdate(upd) } /** @@ -61,7 +62,7 @@ export async function deleteMessagesById( * @param messages Message(s) to delete */ export async function deleteMessages( - client: BaseTelegramClient, + client: ITelegramClient, messages: Message[], params?: DeleteMessagesParams, ): Promise { diff --git a/packages/client/src/methods/messages/delete-scheduled-messages.ts b/packages/core/src/highlevel/methods/messages/delete-scheduled-messages.ts similarity index 82% rename from packages/client/src/methods/messages/delete-scheduled-messages.ts rename to packages/core/src/highlevel/methods/messages/delete-scheduled-messages.ts index 07ee3ca1..9a5d9961 100644 --- a/packages/client/src/methods/messages/delete-scheduled-messages.ts +++ b/packages/core/src/highlevel/methods/messages/delete-scheduled-messages.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param ids Message(s) ID(s) to delete. */ export async function deleteScheduledMessages( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, ids: number[], ): Promise { @@ -22,5 +21,5 @@ export async function deleteScheduledMessages( id: ids, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/client/src/methods/messages/edit-inline-message.ts b/packages/core/src/highlevel/methods/messages/edit-inline-message.ts similarity index 96% rename from packages/client/src/methods/messages/edit-inline-message.ts rename to packages/core/src/highlevel/methods/messages/edit-inline-message.ts index e5870fa5..463a4ffc 100644 --- a/packages/client/src/methods/messages/edit-inline-message.ts +++ b/packages/core/src/highlevel/methods/messages/edit-inline-message.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { BotKeyboard, InputMediaLike, InputText, ReplyMarkup } from '../../types/index.js' import { normalizeInlineId } from '../../utils/inline-utils.js' import { _normalizeInputMedia } from '../files/normalize-input-media.js' @@ -14,7 +15,7 @@ import { _normalizeInputText } from '../misc/normalize-text.js' * @param params */ export async function editInlineMessage( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Inline message ID, either as a TL object, or as a diff --git a/packages/client/src/methods/messages/edit-message.ts b/packages/core/src/highlevel/methods/messages/edit-message.ts similarity index 96% rename from packages/client/src/methods/messages/edit-message.ts rename to packages/core/src/highlevel/methods/messages/edit-message.ts index 9ed80c1b..57208451 100644 --- a/packages/client/src/methods/messages/edit-message.ts +++ b/packages/core/src/highlevel/methods/messages/edit-message.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { BotKeyboard, InputMediaLike, @@ -22,7 +23,7 @@ import { _findMessageInUpdate } from './find-in-update.js' * @param params */ export async function editMessage( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** * New message text diff --git a/packages/client/src/methods/messages/find-in-update.ts b/packages/core/src/highlevel/methods/messages/find-in-update.ts similarity index 80% rename from packages/client/src/methods/messages/find-in-update.ts rename to packages/core/src/highlevel/methods/messages/find-in-update.ts index 3018f178..b49c2458 100644 --- a/packages/client/src/methods/messages/find-in-update.ts +++ b/packages/core/src/highlevel/methods/messages/find-in-update.ts @@ -1,16 +1,18 @@ /* eslint-disable max-params */ -import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/index.js' import { PeersIndex } from '../../types/peers/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** * @internal * @noemit */ export function _findMessageInUpdate( - client: BaseTelegramClient, + client: ITelegramClient, res: tl.TypeUpdates, isEdit?: boolean, noDispatch?: boolean, @@ -21,7 +23,7 @@ export function _findMessageInUpdate( * @noemit */ export function _findMessageInUpdate( - client: BaseTelegramClient, + client: ITelegramClient, res: tl.TypeUpdates, isEdit?: boolean, noDispatch?: boolean, @@ -33,7 +35,7 @@ export function _findMessageInUpdate( * @noemit */ export function _findMessageInUpdate( - client: BaseTelegramClient, + client: ITelegramClient, res: tl.TypeUpdates, isEdit = false, noDispatch = true, @@ -41,7 +43,7 @@ export function _findMessageInUpdate( ): Message | null { assertIsUpdatesGroup('_findMessageInUpdate', res) - client.network.handleUpdate(res, noDispatch) + client.handleClientUpdate(res, noDispatch) for (const u of res.updates) { if ( diff --git a/packages/client/src/methods/messages/forward-messages.ts b/packages/core/src/highlevel/methods/messages/forward-messages.ts similarity index 92% rename from packages/client/src/methods/messages/forward-messages.ts rename to packages/core/src/highlevel/methods/messages/forward-messages.ts index 797ca44c..a108f586 100644 --- a/packages/client/src/methods/messages/forward-messages.ts +++ b/packages/core/src/highlevel/methods/messages/forward-messages.ts @@ -1,9 +1,9 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' - +import { MtArgumentError } from '../../../types/errors.js' +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message, PeersIndex } from '../../types/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { normalizeDate } from '../../utils/misc-utils.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' // @exported @@ -74,7 +74,7 @@ export interface ForwardMessageOptions { * @returns Newly sent, forwarded messages in the destination chat. */ export async function forwardMessagesById( - client: BaseTelegramClient, + client: ITelegramClient, params: ForwardMessageOptions & { /** Source chat ID, username, phone, `"me"` or `"self"` */ fromChatId: InputPeerLike @@ -109,7 +109,7 @@ export async function forwardMessagesById( assertIsUpdatesGroup('messages.forwardMessages', res) - client.network.handleUpdate(res, !params.shouldDispatch) + client.handleClientUpdate(res, !params.shouldDispatch) const peers = PeersIndex.from(res) @@ -133,7 +133,7 @@ export async function forwardMessagesById( * > **Note**: all messages must be from the same chat. */ export async function forwardMessages( - client: BaseTelegramClient, + client: ITelegramClient, params: ForwardMessageOptions & { messages: Message[] }, diff --git a/packages/client/src/methods/messages/get-callback-query-message.ts b/packages/core/src/highlevel/methods/messages/get-callback-query-message.ts similarity index 91% rename from packages/client/src/methods/messages/get-callback-query-message.ts rename to packages/core/src/highlevel/methods/messages/get-callback-query-message.ts index 339c3266..83872c7f 100644 --- a/packages/client/src/methods/messages/get-callback-query-message.ts +++ b/packages/core/src/highlevel/methods/messages/get-callback-query-message.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' import type { CallbackQuery } from '../../types/updates/callback-query.js' @@ -13,7 +14,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * in the given callback query. */ export async function getCallbackQueryMessage( - client: BaseTelegramClient, + client: ITelegramClient, id: | CallbackQuery | tl.RawUpdateBotCallbackQuery diff --git a/packages/client/src/methods/messages/get-discussion-message.ts b/packages/core/src/highlevel/methods/messages/get-discussion-message.ts similarity index 93% rename from packages/client/src/methods/messages/get-discussion-message.ts rename to packages/core/src/highlevel/methods/messages/get-discussion-message.ts index 784c43b5..31a53b4e 100644 --- a/packages/client/src/methods/messages/get-discussion-message.ts +++ b/packages/core/src/highlevel/methods/messages/get-discussion-message.ts @@ -1,12 +1,13 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, Message, normalizeInputMessageId } from '../../types/messages/index.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' import { resolvePeer } from '../users/resolve-peer.js' /** @internal */ export async function _getDiscussionMessage( - client: BaseTelegramClient, + client: ITelegramClient, peer: InputPeerLike, message: number, ): Promise<[tl.TypeInputPeer, number]> { @@ -53,7 +54,7 @@ export async function _getDiscussionMessage( * @param message ID of the channel post */ export async function getDiscussionMessage( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId, ): Promise { const { chatId, message } = normalizeInputMessageId(params) diff --git a/packages/client/src/methods/messages/get-history.ts b/packages/core/src/highlevel/methods/messages/get-history.ts similarity index 94% rename from packages/client/src/methods/messages/get-history.ts rename to packages/core/src/highlevel/methods/messages/get-history.ts index 71f25bdc..7c1ffbb1 100644 --- a/packages/client/src/methods/messages/get-history.ts +++ b/packages/core/src/highlevel/methods/messages/get-history.ts @@ -1,6 +1,9 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputPeerLike, Message, PeersIndex } from '../../types/index.js' import { makeArrayPaginated } from '../../utils/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -23,7 +26,7 @@ const defaultOffset: GetHistoryOffset = { * @param params Additional fetch parameters */ export async function getHistory( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/messages/get-message-by-link.ts b/packages/core/src/highlevel/methods/messages/get-message-by-link.ts similarity index 67% rename from packages/client/src/methods/messages/get-message-by-link.ts rename to packages/core/src/highlevel/methods/messages/get-message-by-link.ts index 9500da95..5adb6415 100644 --- a/packages/client/src/methods/messages/get-message-by-link.ts +++ b/packages/core/src/highlevel/methods/messages/get-message-by-link.ts @@ -1,7 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, toggleChannelIdMark } from '@mtcute/core' -import { links } from '@mtcute/core/utils.js' - -import { Message } from '../../index.js' +import { MtArgumentError } from '../../../types/errors.js' +import { links } from '../../../utils/links/index.js' +import { toggleChannelIdMark } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' +import { Message } from '../../types/messages/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { _getDiscussionMessage } from './get-discussion-message.js' import { getMessages } from './get-messages.js' @@ -9,7 +10,7 @@ import { getMessages } from './get-messages.js' /** * Given a message link (e.g. `t.me/durov/1`), fetch the relevant message. */ -export async function getMessageByLink(client: BaseTelegramClient, link: string): Promise { +export async function getMessageByLink(client: ITelegramClient, link: string): Promise { const parsed = links.message.parse(link) if (!parsed) { diff --git a/packages/client/src/methods/messages/get-message-group.ts b/packages/core/src/highlevel/methods/messages/get-message-group.ts similarity index 80% rename from packages/client/src/methods/messages/get-message-group.ts rename to packages/core/src/highlevel/methods/messages/get-message-group.ts index 13aed8d6..7ed26ec3 100644 --- a/packages/client/src/methods/messages/get-message-group.ts +++ b/packages/core/src/highlevel/methods/messages/get-message-group.ts @@ -1,6 +1,6 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { isPresent } from '@mtcute/core/utils.js' - +import { MtArgumentError } from '../../../types/errors.js' +import { isPresent } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, Message, normalizeInputMessageId } from '../../types/index.js' import { isInputPeerChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +12,7 @@ import { getMessages } from './get-messages.js' * @param chatId Chat ID * @param message ID of one of the messages in the group */ -export async function getMessageGroup(client: BaseTelegramClient, params: InputMessageId): Promise { +export async function getMessageGroup(client: ITelegramClient, params: InputMessageId): Promise { const { chatId, message } = normalizeInputMessageId(params) // awesome hack stolen from pyrogram diff --git a/packages/client/src/methods/messages/get-message-reactions.ts b/packages/core/src/highlevel/methods/messages/get-message-reactions.ts similarity index 87% rename from packages/client/src/methods/messages/get-message-reactions.ts rename to packages/core/src/highlevel/methods/messages/get-message-reactions.ts index 1f49d6d7..23ce59eb 100644 --- a/packages/client/src/methods/messages/get-message-reactions.ts +++ b/packages/core/src/highlevel/methods/messages/get-message-reactions.ts @@ -1,8 +1,8 @@ -import { BaseTelegramClient, getMarkedPeerId } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message, MessageReactions, PeersIndex } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -17,7 +17,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Reactions to corresponding messages, or `null` if there are none */ export async function getMessageReactionsById( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, messages: number[], ): Promise<(MessageReactions | null)[]> { @@ -62,7 +62,7 @@ export async function getMessageReactionsById( * @returns Reactions to corresponding messages, or `null` if there are none */ export async function getMessageReactions( - client: BaseTelegramClient, + client: ITelegramClient, messages: Message[], ): Promise<(MessageReactions | null)[]> { return getMessageReactionsById( diff --git a/packages/client/src/methods/messages/get-messages-unsafe.ts b/packages/core/src/highlevel/methods/messages/get-messages-unsafe.ts similarity index 84% rename from packages/client/src/methods/messages/get-messages-unsafe.ts rename to packages/core/src/highlevel/methods/messages/get-messages-unsafe.ts index a4542953..b69cf050 100644 --- a/packages/client/src/methods/messages/get-messages-unsafe.ts +++ b/packages/core/src/highlevel/methods/messages/get-messages-unsafe.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Message, PeersIndex } from '../../types/index.js' /** @@ -19,7 +21,7 @@ import { Message, PeersIndex } from '../../types/index.js' * (i.e. `getMessages(msg.chat.id, msg.id, true).id === msg.replyToMessageId`) */ export async function getMessagesUnsafe( - client: BaseTelegramClient, + client: ITelegramClient, messageIds: MaybeArray, fromReply = false, ): Promise<(Message | null)[]> { diff --git a/packages/client/src/methods/messages/get-messages.ts b/packages/core/src/highlevel/methods/messages/get-messages.ts similarity index 88% rename from packages/client/src/methods/messages/get-messages.ts rename to packages/core/src/highlevel/methods/messages/get-messages.ts index d79bbc9c..969a0ca1 100644 --- a/packages/client/src/methods/messages/get-messages.ts +++ b/packages/core/src/highlevel/methods/messages/get-messages.ts @@ -1,10 +1,11 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/index.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' import { isInputPeerChannel, toInputChannel } from '../../utils/peer-utils.js' -import { getAuthState } from '../auth/_state.js' import { resolvePeer } from '../users/resolve-peer.js' // @available=both @@ -21,7 +22,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * (i.e. `getMessages(msg.chat.id, msg.id, true).id === msg.replyToMessageId`) */ export async function getMessages( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, messageIds: MaybeArray, fromReply = false, @@ -64,7 +65,7 @@ export async function getMessages( // (channels have their own message numbering) switch (peer._) { case 'inputPeerSelf': - if (selfId === undefined) selfId = getAuthState(client).userId + if (selfId === undefined) selfId = client.storage.self.getCached()?.userId ?? null if (!(msg.peerId._ === 'peerUser' && msg.peerId.userId === selfId)) { return null diff --git a/packages/client/src/methods/messages/get-reaction-users.ts b/packages/core/src/highlevel/methods/messages/get-reaction-users.ts similarity index 94% rename from packages/client/src/methods/messages/get-reaction-users.ts rename to packages/core/src/highlevel/methods/messages/get-reaction-users.ts index 2f23eb45..4c7753dd 100644 --- a/packages/client/src/methods/messages/get-reaction-users.ts +++ b/packages/core/src/highlevel/methods/messages/get-reaction-users.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputMessageId, @@ -21,7 +20,7 @@ export type GetReactionUsersOffset = string * @param params */ export async function getReactionUsers( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** * Get only reactions with the specified emoji diff --git a/packages/client/src/methods/messages/get-reply-to.ts b/packages/core/src/highlevel/methods/messages/get-reply-to.ts similarity index 82% rename from packages/client/src/methods/messages/get-reply-to.ts rename to packages/core/src/highlevel/methods/messages/get-reply-to.ts index bf107dd5..caac3236 100644 --- a/packages/client/src/methods/messages/get-reply-to.ts +++ b/packages/core/src/highlevel/methods/messages/get-reply-to.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/index.js' import { getMessages } from './get-messages.js' import { getMessagesUnsafe } from './get-messages-unsafe.js' @@ -11,7 +10,7 @@ import { getMessagesUnsafe } from './get-messages-unsafe.js' * the message itself may have been deleted, in which case * this method will also return `null`. */ -export async function getReplyTo(client: BaseTelegramClient, message: Message): Promise { +export async function getReplyTo(client: ITelegramClient, message: Message): Promise { if (!message.replyToMessage?.id) { return null } diff --git a/packages/client/src/methods/messages/get-scheduled-messages.ts b/packages/core/src/highlevel/methods/messages/get-scheduled-messages.ts similarity index 83% rename from packages/client/src/methods/messages/get-scheduled-messages.ts rename to packages/core/src/highlevel/methods/messages/get-scheduled-messages.ts index 17e6d1e7..24f6df23 100644 --- a/packages/client/src/methods/messages/get-scheduled-messages.ts +++ b/packages/core/src/highlevel/methods/messages/get-scheduled-messages.ts @@ -1,6 +1,6 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' - +import { MaybeArray } from '../../../types/utils.js' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message, PeersIndex } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +14,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param messageIds Scheduled messages IDs */ export async function getScheduledMessages( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, messageIds: MaybeArray, ): Promise<(Message | null)[]> { diff --git a/packages/client/src/methods/messages/iter-history.ts b/packages/core/src/highlevel/methods/messages/iter-history.ts similarity index 94% rename from packages/client/src/methods/messages/iter-history.ts rename to packages/core/src/highlevel/methods/messages/iter-history.ts index 988f6fa7..6bb80793 100644 --- a/packages/client/src/methods/messages/iter-history.ts +++ b/packages/core/src/highlevel/methods/messages/iter-history.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getHistory } from './get-history.js' @@ -11,7 +10,7 @@ import { getHistory } from './get-history.js' * @param params Additional fetch parameters */ export async function* iterHistory( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/messages/iter-reaction-users.ts b/packages/core/src/highlevel/methods/messages/iter-reaction-users.ts similarity index 94% rename from packages/client/src/methods/messages/iter-reaction-users.ts rename to packages/core/src/highlevel/methods/messages/iter-reaction-users.ts index c30c3c02..7bb8c556 100644 --- a/packages/client/src/methods/messages/iter-reaction-users.ts +++ b/packages/core/src/highlevel/methods/messages/iter-reaction-users.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { normalizeInputMessageId, normalizeInputReaction, PeerReaction } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getReactionUsers } from './get-reaction-users.js' @@ -14,7 +13,7 @@ import { getReactionUsers } from './get-reaction-users.js' * @param params */ export async function* iterReactionUsers( - client: BaseTelegramClient, + client: ITelegramClient, params: Parameters[1] & { /** * Limit the number of events returned. diff --git a/packages/client/src/methods/messages/iter-search-global.ts b/packages/core/src/highlevel/methods/messages/iter-search-global.ts similarity index 95% rename from packages/client/src/methods/messages/iter-search-global.ts rename to packages/core/src/highlevel/methods/messages/iter-search-global.ts index 7f63a1d1..55ee704a 100644 --- a/packages/client/src/methods/messages/iter-search-global.ts +++ b/packages/core/src/highlevel/methods/messages/iter-search-global.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Message, SearchFilters } from '../../types/index.js' import { normalizeDate } from '../../utils/index.js' import { searchGlobal } from './search-global.js' @@ -14,7 +13,7 @@ import { searchGlobal } from './search-global.js' * @param params Search parameters */ export async function* iterSearchGlobal( - client: BaseTelegramClient, + client: ITelegramClient, params?: Parameters[1] & { /** * Limits the number of messages to be retrieved. diff --git a/packages/client/src/methods/messages/iter-search-messages.ts b/packages/core/src/highlevel/methods/messages/iter-search-messages.ts similarity index 96% rename from packages/client/src/methods/messages/iter-search-messages.ts rename to packages/core/src/highlevel/methods/messages/iter-search-messages.ts index b61c2799..cc5dd005 100644 --- a/packages/client/src/methods/messages/iter-search-messages.ts +++ b/packages/core/src/highlevel/methods/messages/iter-search-messages.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Message, SearchFilters } from '../../types/index.js' import { normalizeDate } from '../../utils/misc-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +13,7 @@ import { searchMessages } from './search-messages.js' * @param params Additional search parameters */ export async function* iterSearchMessages( - client: BaseTelegramClient, + client: ITelegramClient, params?: Parameters[1] & { /** * Limits the number of messages to be retrieved. diff --git a/packages/client/src/methods/messages/pin-message.ts b/packages/core/src/highlevel/methods/messages/pin-message.ts similarity index 94% rename from packages/client/src/methods/messages/pin-message.ts rename to packages/core/src/highlevel/methods/messages/pin-message.ts index 2e92840b..da8e15b1 100644 --- a/packages/client/src/methods/messages/pin-message.ts +++ b/packages/core/src/highlevel/methods/messages/pin-message.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, Message, normalizeInputMessageId } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { _findMessageInUpdate } from './find-in-update.js' @@ -13,7 +12,7 @@ import { _findMessageInUpdate } from './find-in-update.js' * @returns Service message about pinned message, if one was generated. */ export async function pinMessage( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** Whether to send a notification (only for legacy groups and supergroups) */ notify?: boolean diff --git a/packages/client/src/methods/messages/read-history.ts b/packages/core/src/highlevel/methods/messages/read-history.ts similarity index 74% rename from packages/client/src/methods/messages/read-history.ts rename to packages/core/src/highlevel/methods/messages/read-history.ts index 5fd1569b..09e3f042 100644 --- a/packages/client/src/methods/messages/read-history.ts +++ b/packages/core/src/highlevel/methods/messages/read-history.ts @@ -1,9 +1,8 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { isInputPeerChannel, toInputChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -12,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID */ export async function readHistory( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** @@ -39,9 +38,9 @@ export async function readHistory( }) if (isInputPeerChannel(peer)) { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) } else { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) } } @@ -59,6 +58,6 @@ export async function readHistory( peer, maxId, }) - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) } } diff --git a/packages/client/src/methods/messages/read-reactions.ts b/packages/core/src/highlevel/methods/messages/read-reactions.ts similarity index 51% rename from packages/client/src/methods/messages/read-reactions.ts rename to packages/core/src/highlevel/methods/messages/read-reactions.ts index d0fbdd06..94f55430 100644 --- a/packages/client/src/methods/messages/read-reactions.ts +++ b/packages/core/src/highlevel/methods/messages/read-reactions.ts @@ -1,7 +1,6 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' +import { createDummyUpdate } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -9,10 +8,10 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param chatId Chat ID */ -export async function readReactions(client: BaseTelegramClient, chatId: InputPeerLike): Promise { +export async function readReactions(client: ITelegramClient, chatId: InputPeerLike): Promise { const res = await client.call({ _: 'messages.readReactions', peer: await resolvePeer(client, chatId), }) - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) } diff --git a/packages/client/src/methods/messages/search-global.ts b/packages/core/src/highlevel/methods/messages/search-global.ts similarity index 93% rename from packages/client/src/methods/messages/search-global.ts rename to packages/core/src/highlevel/methods/messages/search-global.ts index d49d6009..db12fa5f 100644 --- a/packages/client/src/methods/messages/search-global.ts +++ b/packages/core/src/highlevel/methods/messages/search-global.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, Message, PeersIndex, SearchFilters } from '../../types/index.js' import { makeArrayPaginated, normalizeDate } from '../../utils/index.js' @@ -25,7 +26,7 @@ const defaultOffset: SearchGlobalOffset = { * @param params Search parameters */ export async function searchGlobal( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Text query string. Use `"@"` to search for mentions. diff --git a/packages/client/src/methods/messages/search-messages.ts b/packages/core/src/highlevel/methods/messages/search-messages.ts similarity index 95% rename from packages/client/src/methods/messages/search-messages.ts rename to packages/core/src/highlevel/methods/messages/search-messages.ts index 592e2a5e..977909b6 100644 --- a/packages/client/src/methods/messages/search-messages.ts +++ b/packages/core/src/highlevel/methods/messages/search-messages.ts @@ -1,6 +1,9 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputPeerLike, Message, PeersIndex, SearchFilters } from '../../types/index.js' import { makeArrayPaginated, normalizeDate } from '../../utils/misc-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -15,7 +18,7 @@ export type SearchMessagesOffset = number * @param params Additional search parameters */ export async function searchMessages( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Text query string. Required for text-only messages, diff --git a/packages/client/src/methods/messages/send-answer.ts b/packages/core/src/highlevel/methods/messages/send-answer.ts similarity index 92% rename from packages/client/src/methods/messages/send-answer.ts rename to packages/core/src/highlevel/methods/messages/send-answer.ts index a7dfa3bd..1b63c147 100644 --- a/packages/client/src/methods/messages/send-answer.ts +++ b/packages/core/src/highlevel/methods/messages/send-answer.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' import { ParametersSkip2 } from '../../types/utils.js' import { sendMedia } from './send-media.js' @@ -8,7 +7,7 @@ import { sendText } from './send-text.js' /** Send a text to the same chat (and topic, if applicable) as a given message */ export function answerText( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -24,7 +23,7 @@ export function answerText( /** Send a media to the same chat (and topic, if applicable) as a given message */ export function answerMedia( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -40,7 +39,7 @@ export function answerMedia( /** Send a media group to the same chat (and topic, if applicable) as a given message */ export function answerMediaGroup( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { diff --git a/packages/client/src/methods/messages/send-comment.ts b/packages/core/src/highlevel/methods/messages/send-comment.ts similarity index 93% rename from packages/client/src/methods/messages/send-comment.ts rename to packages/core/src/highlevel/methods/messages/send-comment.ts index c1c378f4..e61dc532 100644 --- a/packages/client/src/methods/messages/send-comment.ts +++ b/packages/core/src/highlevel/methods/messages/send-comment.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' import { ParametersSkip2 } from '../../types/utils.js' import { sendMedia } from './send-media.js' @@ -18,7 +18,7 @@ import { sendText } from './send-text.js' * To check if a post has comments, use {@link Message#replies}.hasComments */ export function commentText( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -47,7 +47,7 @@ export function commentText( * To check if a post has comments, use {@link Message#replies}.hasComments */ export function commentMedia( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -76,7 +76,7 @@ export function commentMedia( * To check if a post has comments, use {@link Message#replies}.hasComments */ export function commentMediaGroup( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { diff --git a/packages/client/src/methods/messages/send-common.ts b/packages/core/src/highlevel/methods/messages/send-common.ts similarity index 95% rename from packages/client/src/methods/messages/send-common.ts rename to packages/core/src/highlevel/methods/messages/send-common.ts index f779d62e..6d22eed8 100644 --- a/packages/client/src/methods/messages/send-common.ts +++ b/packages/core/src/highlevel/methods/messages/send-common.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' import { MtMessageNotFoundError } from '../../types/errors.js' import { Message } from '../../types/messages/message.js' import { TextWithEntities } from '../../types/misc/entities.js' @@ -109,7 +112,7 @@ export interface CommonSendParams { * @noemit */ export async function _processCommonSendParameters( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params: CommonSendParams, ) { diff --git a/packages/client/src/methods/messages/send-copy-group.ts b/packages/core/src/highlevel/methods/messages/send-copy-group.ts similarity index 89% rename from packages/client/src/methods/messages/send-copy-group.ts rename to packages/core/src/highlevel/methods/messages/send-copy-group.ts index dfb4f175..6b295a4b 100644 --- a/packages/client/src/methods/messages/send-copy-group.ts +++ b/packages/core/src/highlevel/methods/messages/send-copy-group.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { isPresent } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { isPresent } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' import { InputPeerLike } from '../../types/peers/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -20,7 +22,7 @@ export interface SendCopyGroupParams extends CommonSendParams { * Note that all the provided messages must be in the same message group */ export async function sendCopyGroup( - client: BaseTelegramClient, + client: ITelegramClient, params: SendCopyGroupParams & ( | { diff --git a/packages/client/src/methods/messages/send-copy.ts b/packages/core/src/highlevel/methods/messages/send-copy.ts similarity index 92% rename from packages/client/src/methods/messages/send-copy.ts rename to packages/core/src/highlevel/methods/messages/send-copy.ts index 033b1eb1..d6ffd771 100644 --- a/packages/client/src/methods/messages/send-copy.ts +++ b/packages/core/src/highlevel/methods/messages/send-copy.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, getMarkedPeerId, MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../../types/errors.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, InputText, Message, MtMessageNotFoundError, ReplyMarkup } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getMessages } from './get-messages.js' @@ -33,7 +34,7 @@ export interface SendCopyParams extends CommonSendParams { * it can't be copied. */ export async function sendCopy( - client: BaseTelegramClient, + client: ITelegramClient, params: SendCopyParams & ( | { diff --git a/packages/client/src/methods/messages/send-media-group.ts b/packages/core/src/highlevel/methods/messages/send-media-group.ts similarity index 93% rename from packages/client/src/methods/messages/send-media-group.ts rename to packages/core/src/highlevel/methods/messages/send-media-group.ts index 18b748ff..8eab137e 100644 --- a/packages/client/src/methods/messages/send-media-group.ts +++ b/packages/core/src/highlevel/methods/messages/send-media-group.ts @@ -1,10 +1,11 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputMediaLike } from '../../types/media/input-media.js' import { Message } from '../../types/messages/message.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { _normalizeInputMedia } from '../files/normalize-input-media.js' import { _normalizeInputText } from '../misc/normalize-text.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -23,7 +24,7 @@ import { _processCommonSendParameters, CommonSendParams } from './send-common.js * @link InputMedia */ export async function sendMediaGroup( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, medias: (InputMediaLike | string)[], params?: CommonSendParams & { @@ -109,7 +110,7 @@ export async function sendMediaGroup( ) assertIsUpdatesGroup('sendMediaGroup', res) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res) const peers = PeersIndex.from(res) diff --git a/packages/client/src/methods/messages/send-media.ts b/packages/core/src/highlevel/methods/messages/send-media.ts similarity index 96% rename from packages/client/src/methods/messages/send-media.ts rename to packages/core/src/highlevel/methods/messages/send-media.ts index b954117a..414ecdeb 100644 --- a/packages/client/src/methods/messages/send-media.ts +++ b/packages/core/src/highlevel/methods/messages/send-media.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' - +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js' import { InputMediaLike } from '../../types/media/input-media.js' import { Message } from '../../types/messages/message.js' @@ -25,7 +24,7 @@ import { _processCommonSendParameters, CommonSendParams } from './send-common.js * @link InputMedia */ export async function sendMedia( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, media: InputMediaLike | string, params?: CommonSendParams & { diff --git a/packages/client/src/methods/messages/send-quote.ts b/packages/core/src/highlevel/methods/messages/send-quote.ts similarity index 90% rename from packages/client/src/methods/messages/send-quote.ts rename to packages/core/src/highlevel/methods/messages/send-quote.ts index ce68e01d..caa4eb47 100644 --- a/packages/client/src/methods/messages/send-quote.ts +++ b/packages/core/src/highlevel/methods/messages/send-quote.ts @@ -1,7 +1,10 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { InputPeerLike, TextWithEntities } from '../../index.js' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' +import { TextWithEntities } from '../../types/misc/entities.js' +import { InputPeerLike } from '../../types/peers/peer.js' import { sendMedia } from './send-media.js' import { sendMediaGroup } from './send-media-group.js' import { sendText } from './send-text.js' @@ -56,7 +59,7 @@ function extractQuote(message: Message, from: number, to: number): TextWithEntit /** Send a text in reply to a given quote */ export function quoteWithText( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, params: QuoteParamsFrom[3]> & { /** Text to send */ @@ -74,7 +77,7 @@ export function quoteWithText( /** Send a media in reply to a given quote */ export function quoteWithMedia( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, params: QuoteParamsFrom[3]> & { /** Media to send */ @@ -92,7 +95,7 @@ export function quoteWithMedia( /** Send a media group in reply to a given quote */ export function quoteWithMediaGroup( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, params: QuoteParamsFrom[3]> & { /** Media group to send */ diff --git a/packages/client/src/methods/messages/send-reaction.ts b/packages/core/src/highlevel/methods/messages/send-reaction.ts similarity index 90% rename from packages/client/src/methods/messages/send-reaction.ts rename to packages/core/src/highlevel/methods/messages/send-reaction.ts index 0af47a5b..c781b02e 100644 --- a/packages/client/src/methods/messages/send-reaction.ts +++ b/packages/core/src/highlevel/methods/messages/send-reaction.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, InputReaction, @@ -7,7 +7,7 @@ import { normalizeInputMessageId, normalizeInputReaction, } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' import { _findMessageInUpdate } from './find-in-update.js' @@ -19,7 +19,7 @@ import { _findMessageInUpdate } from './find-in-update.js' * The message is normally available for users, but may not be available for bots in PMs. */ export async function sendReaction( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** Reaction emoji (or `null` to remove reaction) */ emoji?: MaybeArray | null diff --git a/packages/client/src/methods/messages/send-reply.ts b/packages/core/src/highlevel/methods/messages/send-reply.ts similarity index 89% rename from packages/client/src/methods/messages/send-reply.ts rename to packages/core/src/highlevel/methods/messages/send-reply.ts index 87348a6f..2f64b325 100644 --- a/packages/client/src/methods/messages/send-reply.ts +++ b/packages/core/src/highlevel/methods/messages/send-reply.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Message } from '../../types/messages/message.js' import { ParametersSkip2 } from '../../types/utils.js' import { sendMedia } from './send-media.js' @@ -8,7 +7,7 @@ import { sendText } from './send-text.js' /** Send a text in reply to a given message */ export function replyText( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -20,7 +19,7 @@ export function replyText( /** Send a media in reply to a given message */ export function replyMedia( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { @@ -32,7 +31,7 @@ export function replyMedia( /** Send a media group in reply to a given message */ export function replyMediaGroup( - client: BaseTelegramClient, + client: ITelegramClient, message: Message, ...params: ParametersSkip2 ): ReturnType { diff --git a/packages/client/src/methods/messages/send-scheduled.ts b/packages/core/src/highlevel/methods/messages/send-scheduled.ts similarity index 80% rename from packages/client/src/methods/messages/send-scheduled.ts rename to packages/core/src/highlevel/methods/messages/send-scheduled.ts index 2cc1828d..3b3777a5 100644 --- a/packages/client/src/methods/messages/send-scheduled.ts +++ b/packages/core/src/highlevel/methods/messages/send-scheduled.ts @@ -1,7 +1,9 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Message, PeersIndex } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -15,7 +17,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param ids ID(s) of the messages */ export async function sendScheduled( - client: BaseTelegramClient, + client: ITelegramClient, peer: InputPeerLike, ids: MaybeArray, ): Promise { @@ -28,7 +30,7 @@ export async function sendScheduled( }) assertIsUpdatesGroup('sendScheduled', res) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res, true) const peers = PeersIndex.from(res) diff --git a/packages/client/src/methods/messages/send-text.test.ts b/packages/core/src/highlevel/methods/messages/send-text.test.ts similarity index 92% rename from packages/client/src/methods/messages/send-text.test.ts rename to packages/core/src/highlevel/methods/messages/send-text.test.ts index 0fc31431..bcf44d99 100644 --- a/packages/client/src/methods/messages/send-text.test.ts +++ b/packages/core/src/highlevel/methods/messages/send-text.test.ts @@ -1,9 +1,9 @@ +import Long from 'long' import { describe, expect, it, vi } from 'vitest' -import { Long, toggleChannelIdMark } from '@mtcute/core' import { createStub, StubTelegramClient } from '@mtcute/test' -import { getAuthState, setupAuthState } from '../auth/_state.js' +import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { sendText } from './send-text.js' const stubUser = createStub('user', { @@ -103,9 +103,13 @@ describe('sendText', () => { it('should correctly handle updateShortSentMessage with cached peer', async () => { const client = new StubTelegramClient() + await client.storage.self.store({ + userId: stubUser.id, + isBot: false, + isPremium: false, + usernames: [], + }) await client.registerPeers(stubUser) - setupAuthState(client) - getAuthState(client).userId = stubUser.id client.respondWith('messages.sendMessage', () => createStub('updateShortSentMessage', { @@ -128,8 +132,12 @@ describe('sendText', () => { it('should correctly handle updateShortSentMessage without cached peer', async () => { const client = new StubTelegramClient() - setupAuthState(client) - getAuthState(client).userId = stubUser.id + await client.storage.self.store({ + userId: stubUser.id, + isBot: false, + isPremium: false, + usernames: [], + }) const getUsersFn = client.respondWith( 'users.getUsers', diff --git a/packages/client/src/methods/messages/send-text.ts b/packages/core/src/highlevel/methods/messages/send-text.ts similarity index 87% rename from packages/client/src/methods/messages/send-text.ts rename to packages/core/src/highlevel/methods/messages/send-text.ts index 6e698a05..daaf2092 100644 --- a/packages/client/src/methods/messages/send-text.ts +++ b/packages/core/src/highlevel/methods/messages/send-text.ts @@ -1,13 +1,15 @@ -import { BaseTelegramClient, getMarkedPeerId, MtTypeAssertionError, tl } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { randomLong } from '../../../utils/long-utils.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js' import { Message } from '../../types/messages/message.js' import { InputText } from '../../types/misc/entities.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { inputPeerToPeer } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' -import { getAuthState } from '../auth/_state.js' import { _getRawPeerBatched } from '../chats/batched-queries.js' import { _normalizeInputText } from '../misc/normalize-text.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -23,7 +25,7 @@ import { _processCommonSendParameters, CommonSendParams } from './send-common.js * @param params Additional sending parameters */ export async function sendText( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, text: InputText, params?: CommonSendParams & { @@ -80,7 +82,7 @@ export async function sendText( _: 'message', id: res.id, peerId: inputPeerToPeer(peer), - fromId: { _: 'peerUser', userId: getAuthState(client).userId! }, + fromId: { _: 'peerUser', userId: client.storage.self.getCached()!.userId }, message, date: res.date, out: res.out, @@ -90,14 +92,14 @@ export async function sendText( // is this needed? // this._date = res.date - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) const peers = new PeersIndex() const fetchPeer = async (peer: tl.TypePeer | tl.TypeInputPeer): Promise => { const id = getMarkedPeerId(peer) - let cached = await client.storage.getFullPeerById(id) + let cached = await client.storage.peers.getCompleteById(id) if (!cached) { cached = await _getRawPeerBatched(client, await resolvePeer(client, peer)) diff --git a/packages/client/src/methods/messages/send-typing.ts b/packages/core/src/highlevel/methods/messages/send-typing.ts similarity index 89% rename from packages/client/src/methods/messages/send-typing.ts rename to packages/core/src/highlevel/methods/messages/send-typing.ts index 3cb16fc4..5855f6d1 100644 --- a/packages/client/src/methods/messages/send-typing.ts +++ b/packages/core/src/highlevel/methods/messages/send-typing.ts @@ -1,6 +1,8 @@ -import { assertNever, BaseTelegramClient, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertNever } from '../../../types/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, TypingStatus } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -17,9 +19,9 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param params */ export async function sendTyping( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, - status: TypingStatus | tl.TypeSendMessageAction = 'typing', + status: Exclude | tl.TypeSendMessageAction = 'typing', params?: { /** * For `upload_*` and history import actions, progress of the upload diff --git a/packages/client/src/methods/messages/send-vote.ts b/packages/core/src/highlevel/methods/messages/send-vote.ts similarity index 82% rename from packages/client/src/methods/messages/send-vote.ts rename to packages/core/src/highlevel/methods/messages/send-vote.ts index 0afcf767..4327b5ac 100644 --- a/packages/client/src/methods/messages/send-vote.ts +++ b/packages/core/src/highlevel/methods/messages/send-vote.ts @@ -1,8 +1,10 @@ -import { BaseTelegramClient, getMarkedPeerId, MaybeArray, MtArgumentError, MtTypeAssertionError } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - +import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' +import { MaybeArray } from '../../../types/utils.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, MtMessageNotFoundError, normalizeInputMessageId, PeersIndex, Poll } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' import { resolvePeer } from '../users/resolve-peer.js' import { getMessages } from './get-messages.js' @@ -10,7 +12,7 @@ import { getMessages } from './get-messages.js' * Send or retract a vote in a poll. */ export async function sendVote( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** * Selected options, or `null` to retract. @@ -61,7 +63,7 @@ export async function sendVote( assertIsUpdatesGroup('messages.sendVote', res) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res, true) const upd = res.updates[0] assertTypeIs('messages.sendVote (@ .updates[0])', upd, 'updateMessagePoll') diff --git a/packages/client/src/methods/messages/translate-message.ts b/packages/core/src/highlevel/methods/messages/translate-message.ts similarity index 90% rename from packages/client/src/methods/messages/translate-message.ts rename to packages/core/src/highlevel/methods/messages/translate-message.ts index bc54d4b5..c61b6cbc 100644 --- a/packages/client/src/methods/messages/translate-message.ts +++ b/packages/core/src/highlevel/methods/messages/translate-message.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, normalizeInputMessageId, TextWithEntities } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Returns `null` if it could not translate the message. */ export async function translateMessage( - client: BaseTelegramClient, + client: ITelegramClient, params: InputMessageId & { /** Target language (two-letter ISO 639-1 language code) */ toLanguage: string diff --git a/packages/client/src/methods/messages/translate-text.ts b/packages/core/src/highlevel/methods/messages/translate-text.ts similarity index 86% rename from packages/client/src/methods/messages/translate-text.ts rename to packages/core/src/highlevel/methods/messages/translate-text.ts index bdfce90a..22b5706c 100644 --- a/packages/client/src/methods/messages/translate-text.ts +++ b/packages/core/src/highlevel/methods/messages/translate-text.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' - +import { MtTypeAssertionError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { InputText, TextWithEntities } from '../../types/misc/entities.js' import { _normalizeInputText } from '../misc/normalize-text.js' @@ -10,7 +10,7 @@ import { _normalizeInputText } from '../misc/normalize-text.js' * @param toLanguage Target language (two-letter ISO 639-1 language code) */ export async function translateText( - client: BaseTelegramClient, + client: ITelegramClient, text: InputText, toLanguage: string, ): Promise { diff --git a/packages/client/src/methods/messages/unpin-all-messages.ts b/packages/core/src/highlevel/methods/messages/unpin-all-messages.ts similarity index 70% rename from packages/client/src/methods/messages/unpin-all-messages.ts rename to packages/core/src/highlevel/methods/messages/unpin-all-messages.ts index 6645ac47..a9e187a9 100644 --- a/packages/client/src/methods/messages/unpin-all-messages.ts +++ b/packages/core/src/highlevel/methods/messages/unpin-all-messages.ts @@ -1,8 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' +import { createDummyUpdate } from '../../updates/utils.js' import { isInputPeerChannel } from '../../utils/peer-utils.js' -import { createDummyUpdate } from '../../utils/updates-utils.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -11,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat or user ID */ export async function unpinAllMessages( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, params?: { /** @@ -31,8 +30,8 @@ export async function unpinAllMessages( }) if (isInputPeerChannel(peer)) { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount, peer.channelId)) } else { - client.network.handleUpdate(createDummyUpdate(res.pts, res.ptsCount)) + client.handleClientUpdate(createDummyUpdate(res.pts, res.ptsCount)) } } diff --git a/packages/client/src/methods/messages/unpin-message.ts b/packages/core/src/highlevel/methods/messages/unpin-message.ts similarity index 77% rename from packages/client/src/methods/messages/unpin-message.ts rename to packages/core/src/highlevel/methods/messages/unpin-message.ts index 84c46389..04b940fd 100644 --- a/packages/client/src/methods/messages/unpin-message.ts +++ b/packages/core/src/highlevel/methods/messages/unpin-message.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputMessageId, normalizeInputMessageId } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param chatId Chat ID, username, phone number, `"self"` or `"me"` * @param messageId Message ID */ -export async function unpinMessage(client: BaseTelegramClient, params: InputMessageId): Promise { +export async function unpinMessage(client: ITelegramClient, params: InputMessageId): Promise { const { chatId, message } = normalizeInputMessageId(params) const res = await client.call({ @@ -22,5 +21,5 @@ export async function unpinMessage(client: BaseTelegramClient, params: InputMess unpin: true, }) - client.network.handleUpdate(res) + client.handleClientUpdate(res) } diff --git a/packages/core/src/highlevel/methods/misc/chain-id.ts b/packages/core/src/highlevel/methods/misc/chain-id.ts new file mode 100644 index 00000000..c9a786fb --- /dev/null +++ b/packages/core/src/highlevel/methods/misc/chain-id.ts @@ -0,0 +1,11 @@ +import { tl } from '@mtcute/tl' + +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' + +/** @internal */ +export function _getPeerChainId(client: ITelegramClient, peer: tl.TypeInputPeer, prefix = 'peer') { + const id = peer._ === 'inputPeerSelf' ? client.storage.self.getCached()!.userId : getMarkedPeerId(peer) + + return `${prefix}:${id}` +} diff --git a/packages/client/src/methods/misc/init-takeout-session.ts b/packages/core/src/highlevel/methods/misc/init-takeout-session.ts similarity index 79% rename from packages/client/src/methods/misc/init-takeout-session.ts rename to packages/core/src/highlevel/methods/misc/init-takeout-session.ts index c30699e6..4df278db 100644 --- a/packages/client/src/methods/misc/init-takeout-session.ts +++ b/packages/core/src/highlevel/methods/misc/init-takeout-session.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { TakeoutSession } from '../../types/index.js' /** @@ -8,7 +9,7 @@ import { TakeoutSession } from '../../types/index.js' * @param params Takeout session parameters */ export async function initTakeoutSession( - client: BaseTelegramClient, + client: ITelegramClient, params: Omit, ): Promise { return new TakeoutSession( diff --git a/packages/client/src/methods/misc/normalize-privacy-rules.ts b/packages/core/src/highlevel/methods/misc/normalize-privacy-rules.ts similarity index 92% rename from packages/client/src/methods/misc/normalize-privacy-rules.ts rename to packages/core/src/highlevel/methods/misc/normalize-privacy-rules.ts index 60100f31..8c9edcf3 100644 --- a/packages/client/src/methods/misc/normalize-privacy-rules.ts +++ b/packages/core/src/highlevel/methods/misc/normalize-privacy-rules.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPrivacyRule } from '../../types/index.js' import { toInputUser } from '../../utils/index.js' import { resolvePeerMany } from '../users/resolve-peer-many.js' @@ -9,7 +10,7 @@ import { resolvePeerMany } from '../users/resolve-peer-many.js' * resolving the peers if needed. */ export async function _normalizePrivacyRules( - client: BaseTelegramClient, + client: ITelegramClient, rules: InputPrivacyRule[], ): Promise { const res: tl.TypeInputPrivacyRule[] = [] diff --git a/packages/client/src/methods/misc/normalize-text.ts b/packages/core/src/highlevel/methods/misc/normalize-text.ts similarity index 91% rename from packages/client/src/methods/misc/normalize-text.ts rename to packages/core/src/highlevel/methods/misc/normalize-text.ts index cce1afec..42ab4388 100644 --- a/packages/client/src/methods/misc/normalize-text.ts +++ b/packages/core/src/highlevel/methods/misc/normalize-text.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputText } from '../../types/misc/entities.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +9,7 @@ const empty: [string, undefined] = ['', undefined] /** @internal */ export async function _normalizeInputText( - client: BaseTelegramClient, + client: ITelegramClient, input?: InputText, ): Promise<[string, tl.TypeMessageEntity[] | undefined]> { if (!input) { diff --git a/packages/client/src/methods/password/change-cloud-password.ts b/packages/core/src/highlevel/methods/password/change-cloud-password.ts similarity index 64% rename from packages/client/src/methods/password/change-cloud-password.ts rename to packages/core/src/highlevel/methods/password/change-cloud-password.ts index b563136f..102e4daf 100644 --- a/packages/client/src/methods/password/change-cloud-password.ts +++ b/packages/core/src/highlevel/methods/password/change-cloud-password.ts @@ -1,11 +1,11 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { assertTypeIs, computeNewPasswordHash, computeSrpParams } from '@mtcute/core/utils.js' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' /** * Change your 2FA password */ export async function changeCloudPassword( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Current password as plaintext */ currentPassword: string @@ -24,10 +24,9 @@ export async function changeCloudPassword( } const algo = pwd.newAlgo - assertTypeIs('account.getPassword', algo, 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow') - const oldSrp = await computeSrpParams(client.crypto, pwd, currentPassword) - const newHash = await computeNewPasswordHash(client.crypto, algo, newPassword) + const oldSrp = await client.computeSrpParams(pwd, currentPassword) + const newHash = await client.computeNewPasswordHash(algo, newPassword) await client.call({ _: 'account.updatePasswordSettings', diff --git a/packages/client/src/methods/password/enable-cloud-password.ts b/packages/core/src/highlevel/methods/password/enable-cloud-password.ts similarity index 81% rename from packages/client/src/methods/password/enable-cloud-password.ts rename to packages/core/src/highlevel/methods/password/enable-cloud-password.ts index 9111eac8..518eecf4 100644 --- a/packages/client/src/methods/password/enable-cloud-password.ts +++ b/packages/core/src/highlevel/methods/password/enable-cloud-password.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { assertTypeIs, computeNewPasswordHash } from '@mtcute/core/utils.js' +import { MtArgumentError } from '../../../types/errors.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Enable 2FA password on your account @@ -10,7 +11,7 @@ import { assertTypeIs, computeNewPasswordHash } from '@mtcute/core/utils.js' * and the call this method again */ export async function enableCloudPassword( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** 2FA password as plaintext */ password: string @@ -31,7 +32,7 @@ export async function enableCloudPassword( const algo = pwd.newAlgo assertTypeIs('account.getPassword', algo, 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow') - const newHash = await computeNewPasswordHash(client.crypto, algo, password) + const newHash = await client.computeNewPasswordHash(algo, password) await client.call({ _: 'account.updatePasswordSettings', diff --git a/packages/client/src/methods/password/password-email.ts b/packages/core/src/highlevel/methods/password/password-email.ts similarity index 64% rename from packages/client/src/methods/password/password-email.ts rename to packages/core/src/highlevel/methods/password/password-email.ts index 51ba89e6..a721a418 100644 --- a/packages/client/src/methods/password/password-email.ts +++ b/packages/core/src/highlevel/methods/password/password-email.ts @@ -1,12 +1,12 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Verify an email to use as 2FA recovery method * * @param code Code which was sent via email */ -export async function verifyPasswordEmail(client: BaseTelegramClient, code: string): Promise { +export async function verifyPasswordEmail(client: ITelegramClient, code: string): Promise { const r = await client.call({ _: 'account.confirmPasswordEmail', code, @@ -18,7 +18,7 @@ export async function verifyPasswordEmail(client: BaseTelegramClient, code: stri /** * Resend the code to verify an email to use as 2FA recovery method. */ -export async function resendPasswordEmail(client: BaseTelegramClient): Promise { +export async function resendPasswordEmail(client: ITelegramClient): Promise { const r = await client.call({ _: 'account.resendPasswordEmail', }) @@ -29,7 +29,7 @@ export async function resendPasswordEmail(client: BaseTelegramClient): Promise { +export async function cancelPasswordEmail(client: ITelegramClient): Promise { const r = await client.call({ _: 'account.cancelPasswordEmail', }) diff --git a/packages/client/src/methods/password/remove-cloud-password.ts b/packages/core/src/highlevel/methods/password/remove-cloud-password.ts similarity index 65% rename from packages/client/src/methods/password/remove-cloud-password.ts rename to packages/core/src/highlevel/methods/password/remove-cloud-password.ts index fabbeb8a..e9936207 100644 --- a/packages/client/src/methods/password/remove-cloud-password.ts +++ b/packages/core/src/highlevel/methods/password/remove-cloud-password.ts @@ -1,19 +1,19 @@ -import { BaseTelegramClient, MtArgumentError } from '@mtcute/core' -import { computeSrpParams } from '@mtcute/core/utils.js' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' /** * Remove 2FA password from your account * * @param password 2FA password as plaintext */ -export async function removeCloudPassword(client: BaseTelegramClient, password: string): Promise { +export async function removeCloudPassword(client: ITelegramClient, password: string): Promise { const pwd = await client.call({ _: 'account.getPassword' }) if (!pwd.hasPassword) { throw new MtArgumentError('Cloud password is not enabled') } - const oldSrp = await computeSrpParams(client.crypto, pwd, password) + const oldSrp = await client.computeSrpParams(pwd, password) await client.call({ _: 'account.updatePasswordSettings', diff --git a/packages/client/src/methods/premium/apply-boost.ts b/packages/core/src/highlevel/methods/premium/apply-boost.ts similarity index 65% rename from packages/client/src/methods/premium/apply-boost.ts rename to packages/core/src/highlevel/methods/premium/apply-boost.ts index 3dc7618c..59851546 100644 --- a/packages/client/src/methods/premium/apply-boost.ts +++ b/packages/core/src/highlevel/methods/premium/apply-boost.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +7,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param peerId Peer ID to boost */ -export async function applyBoost(client: BaseTelegramClient, peerId: InputPeerLike): Promise { +export async function applyBoost(client: ITelegramClient, peerId: InputPeerLike): Promise { await client.call({ _: 'premium.applyBoost', peer: await resolvePeer(client, peerId), diff --git a/packages/client/src/methods/premium/can-apply-boost.ts b/packages/core/src/highlevel/methods/premium/can-apply-boost.ts similarity index 91% rename from packages/client/src/methods/premium/can-apply-boost.ts rename to packages/core/src/highlevel/methods/premium/can-apply-boost.ts index 0c221a3e..71148135 100644 --- a/packages/client/src/methods/premium/can-apply-boost.ts +++ b/packages/core/src/highlevel/methods/premium/can-apply-boost.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Chat } from '../../types/index.js' import { BoostSlot } from '../../types/premium/boost-slot.js' import { getMyBoostSlots } from './get-my-boost-slots.js' @@ -22,7 +21,7 @@ export type CanApplyBoostResult = * - `.reason == "need_premium"` if the user needs Premium to boost * - In all cases, `slots` will contain all the current user's boost slots */ -export async function canApplyBoost(client: BaseTelegramClient): Promise { +export async function canApplyBoost(client: ITelegramClient): Promise { const myBoosts = await getMyBoostSlots(client) if (!myBoosts.length) { diff --git a/packages/client/src/methods/premium/get-boost-stats.ts b/packages/core/src/highlevel/methods/premium/get-boost-stats.ts similarity index 73% rename from packages/client/src/methods/premium/get-boost-stats.ts rename to packages/core/src/highlevel/methods/premium/get-boost-stats.ts index 193e04ba..4324def0 100644 --- a/packages/client/src/methods/premium/get-boost-stats.ts +++ b/packages/core/src/highlevel/methods/premium/get-boost-stats.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { BoostStats } from '../../types/premium/boost-stats.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @returns IDs of stories that were removed */ -export async function getBoostStats(client: BaseTelegramClient, peerId: InputPeerLike): Promise { +export async function getBoostStats(client: ITelegramClient, peerId: InputPeerLike): Promise { const res = await client.call({ _: 'premium.getBoostsStatus', peer: await resolvePeer(client, peerId), diff --git a/packages/client/src/methods/premium/get-boosts.ts b/packages/core/src/highlevel/methods/premium/get-boosts.ts similarity index 92% rename from packages/client/src/methods/premium/get-boosts.ts rename to packages/core/src/highlevel/methods/premium/get-boosts.ts index ba56ac1c..e920063f 100644 --- a/packages/client/src/methods/premium/get-boosts.ts +++ b/packages/core/src/highlevel/methods/premium/get-boosts.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputPeerLike, PeersIndex } from '../../types/index.js' import { Boost } from '../../types/premium/boost.js' import { makeArrayPaginated } from '../../utils/index.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Get boosts of a channel */ export async function getBoosts( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/premium/get-my-boost-slots.ts b/packages/core/src/highlevel/methods/premium/get-my-boost-slots.ts similarity index 77% rename from packages/client/src/methods/premium/get-my-boost-slots.ts rename to packages/core/src/highlevel/methods/premium/get-my-boost-slots.ts index d5cddc9f..e7a57c76 100644 --- a/packages/client/src/methods/premium/get-my-boost-slots.ts +++ b/packages/core/src/highlevel/methods/premium/get-my-boost-slots.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { PeersIndex } from '../../types/index.js' import { BoostSlot } from '../../types/premium/boost-slot.js' @@ -9,7 +8,7 @@ import { BoostSlot } from '../../types/premium/boost-slot.js' * Includes information about the currently boosted channels, * as well as the slots that can be used to boost other channels. */ -export async function getMyBoostSlots(client: BaseTelegramClient): Promise { +export async function getMyBoostSlots(client: ITelegramClient): Promise { const res = await client.call({ _: 'premium.getMyBoosts', }) diff --git a/packages/client/src/methods/premium/iter-boosters.ts b/packages/core/src/highlevel/methods/premium/iter-boosters.ts similarity index 94% rename from packages/client/src/methods/premium/iter-boosters.ts rename to packages/core/src/highlevel/methods/premium/iter-boosters.ts index 76f62f51..fa6a4b31 100644 --- a/packages/client/src/methods/premium/iter-boosters.ts +++ b/packages/core/src/highlevel/methods/premium/iter-boosters.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { Boost } from '../../types/premium/boost.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -13,7 +12,7 @@ import { getBoosts } from './get-boosts.js' * @returns IDs of stories that were removed */ export async function* iterBoosters( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/stickers/add-sticker-to-set.ts b/packages/core/src/highlevel/methods/stickers/add-sticker-to-set.ts similarity index 95% rename from packages/client/src/methods/stickers/add-sticker-to-set.ts rename to packages/core/src/highlevel/methods/stickers/add-sticker-to-set.ts index 53a1b109..d2ccd9bf 100644 --- a/packages/client/src/methods/stickers/add-sticker-to-set.ts +++ b/packages/core/src/highlevel/methods/stickers/add-sticker-to-set.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputStickerSet, InputStickerSetItem, @@ -21,7 +20,7 @@ import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js * @returns Modfiied sticker set */ export async function addStickerToSet( - client: BaseTelegramClient, + client: ITelegramClient, setId: InputStickerSet, sticker: InputStickerSetItem, params?: { diff --git a/packages/client/src/methods/stickers/create-sticker-set.ts b/packages/core/src/highlevel/methods/stickers/create-sticker-set.ts similarity index 97% rename from packages/client/src/methods/stickers/create-sticker-set.ts rename to packages/core/src/highlevel/methods/stickers/create-sticker-set.ts index 419abdee..e343bd97 100644 --- a/packages/client/src/methods/stickers/create-sticker-set.ts +++ b/packages/core/src/highlevel/methods/stickers/create-sticker-set.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike, InputPeerLike, @@ -23,7 +24,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Newly created sticker set */ export async function createStickerSet( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Owner of the sticker set (must be user). diff --git a/packages/client/src/methods/stickers/delete-sticker-from-set.ts b/packages/core/src/highlevel/methods/stickers/delete-sticker-from-set.ts similarity index 75% rename from packages/client/src/methods/stickers/delete-sticker-from-set.ts rename to packages/core/src/highlevel/methods/stickers/delete-sticker-from-set.ts index 6e08fcdd..2ba7f0be 100644 --- a/packages/client/src/methods/stickers/delete-sticker-from-set.ts +++ b/packages/core/src/highlevel/methods/stickers/delete-sticker-from-set.ts @@ -1,7 +1,9 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { fileIdToInputDocument, tdFileId } from '@mtcute/file-id' +import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { StickerSet } from '../../types/index.js' +import { fileIdToInputDocument } from '../../utils/convert-file-id.js' /** * Delete a sticker from a sticker set @@ -15,7 +17,7 @@ import { StickerSet } from '../../types/index.js' * @returns Modfiied sticker set */ export async function deleteStickerFromSet( - client: BaseTelegramClient, + client: ITelegramClient, sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument, ): Promise { if (tdFileId.isFileIdLike(sticker)) { diff --git a/packages/client/src/methods/stickers/get-custom-emojis.ts b/packages/core/src/highlevel/methods/stickers/get-custom-emojis.ts similarity index 74% rename from packages/client/src/methods/stickers/get-custom-emojis.ts rename to packages/core/src/highlevel/methods/stickers/get-custom-emojis.ts index e68cff5b..2ef7ed34 100644 --- a/packages/client/src/methods/stickers/get-custom-emojis.ts +++ b/packages/core/src/highlevel/methods/stickers/get-custom-emojis.ts @@ -1,6 +1,10 @@ -import { BaseTelegramClient, MaybeArray, MtTypeAssertionError, tl } from '@mtcute/core' -import { assertTypeIs, LongSet } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { MaybeArray } from '../../../types/utils.js' +import { LongSet } from '../../../utils/long-utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { Message, Sticker } from '../../types/index.js' import { parseDocument } from '../../types/media/document-utils.js' @@ -9,7 +13,7 @@ import { parseDocument } from '../../types/media/document-utils.js' * * @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId}) */ -export async function getCustomEmojis(client: BaseTelegramClient, ids: tl.Long[]): Promise { +export async function getCustomEmojis(client: ITelegramClient, ids: tl.Long[]): Promise { const res = await client.call({ _: 'messages.getCustomEmojiDocuments', documentId: ids, @@ -32,7 +36,7 @@ export async function getCustomEmojis(client: BaseTelegramClient, ids: tl.Long[] * Given one or more messages, extract all unique custom emojis from it and fetch them */ export async function getCustomEmojisFromMessages( - client: BaseTelegramClient, + client: ITelegramClient, messages: MaybeArray, ): Promise { const set = new LongSet() diff --git a/packages/client/src/methods/stickers/get-installed-stickers.ts b/packages/core/src/highlevel/methods/stickers/get-installed-stickers.ts similarity index 71% rename from packages/client/src/methods/stickers/get-installed-stickers.ts rename to packages/core/src/highlevel/methods/stickers/get-installed-stickers.ts index 115959f6..5f310198 100644 --- a/packages/client/src/methods/stickers/get-installed-stickers.ts +++ b/packages/core/src/highlevel/methods/stickers/get-installed-stickers.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, Long } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import Long from 'long' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { StickerSet } from '../../types/index.js' /** @@ -11,7 +12,7 @@ import { StickerSet } from '../../types/index.js' * > Use {@link StickerSet.getFull} or {@link getStickerSet} * > to get a stickerset that will include the stickers */ -export async function getInstalledStickers(client: BaseTelegramClient): Promise { +export async function getInstalledStickers(client: ITelegramClient): Promise { const res = await client.call({ _: 'messages.getAllStickers', hash: Long.ZERO, diff --git a/packages/client/src/methods/stickers/get-sticker-set.ts b/packages/core/src/highlevel/methods/stickers/get-sticker-set.ts similarity index 72% rename from packages/client/src/methods/stickers/get-sticker-set.ts rename to packages/core/src/highlevel/methods/stickers/get-sticker-set.ts index 7bb7e0ea..8b1f616a 100644 --- a/packages/client/src/methods/stickers/get-sticker-set.ts +++ b/packages/core/src/highlevel/methods/stickers/get-sticker-set.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types/index.js' /** @@ -7,7 +6,7 @@ import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../typ * * @param setId Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID */ -export async function getStickerSet(client: BaseTelegramClient, setId: InputStickerSet): Promise { +export async function getStickerSet(client: ITelegramClient, setId: InputStickerSet): Promise { const res = await client.call({ _: 'messages.getStickerSet', stickerset: normalizeInputStickerSet(setId), diff --git a/packages/client/src/methods/stickers/move-sticker-in-set.ts b/packages/core/src/highlevel/methods/stickers/move-sticker-in-set.ts similarity index 78% rename from packages/client/src/methods/stickers/move-sticker-in-set.ts rename to packages/core/src/highlevel/methods/stickers/move-sticker-in-set.ts index ae2dfe89..24114d27 100644 --- a/packages/client/src/methods/stickers/move-sticker-in-set.ts +++ b/packages/core/src/highlevel/methods/stickers/move-sticker-in-set.ts @@ -1,7 +1,9 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { fileIdToInputDocument, tdFileId } from '@mtcute/file-id' +import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { StickerSet } from '../../types/index.js' +import { fileIdToInputDocument } from '../../utils/convert-file-id.js' /** * Move a sticker in a sticker set @@ -18,7 +20,7 @@ import { StickerSet } from '../../types/index.js' */ export async function moveStickerInSet( - client: BaseTelegramClient, + client: ITelegramClient, sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument, position: number, ): Promise { diff --git a/packages/client/src/methods/stickers/set-chat-sticker-set.ts b/packages/core/src/highlevel/methods/stickers/set-chat-sticker-set.ts similarity index 78% rename from packages/client/src/methods/stickers/set-chat-sticker-set.ts rename to packages/core/src/highlevel/methods/stickers/set-chat-sticker-set.ts index a7ca956e..d230c51f 100644 --- a/packages/client/src/methods/stickers/set-chat-sticker-set.ts +++ b/packages/core/src/highlevel/methods/stickers/set-chat-sticker-set.ts @@ -1,7 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, InputStickerSet, normalizeInputStickerSet } from '../../types/index.js' -import { assertTrue, toInputChannel } from '../../utils/index.js' +import { toInputChannel } from '../../utils/index.js' import { resolvePeer } from '../users/resolve-peer.js' /** @@ -13,7 +13,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns Modified sticker set */ export async function setChatStickerSet( - client: BaseTelegramClient, + client: ITelegramClient, chatId: InputPeerLike, setId: InputStickerSet, ): Promise { diff --git a/packages/client/src/methods/stickers/set-sticker-set-thumb.ts b/packages/core/src/highlevel/methods/stickers/set-sticker-set-thumb.ts similarity index 89% rename from packages/client/src/methods/stickers/set-sticker-set-thumb.ts rename to packages/core/src/highlevel/methods/stickers/set-sticker-set-thumb.ts index 6487c46b..82922d10 100644 --- a/packages/client/src/methods/stickers/set-sticker-set-thumb.ts +++ b/packages/core/src/highlevel/methods/stickers/set-sticker-set-thumb.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike, InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types/index.js' import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js' @@ -12,7 +13,7 @@ import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js * @returns Modified sticker set */ export async function setStickerSetThumb( - client: BaseTelegramClient, + client: ITelegramClient, id: InputStickerSet, thumb: InputFileLike | tl.TypeInputDocument, params?: { diff --git a/packages/client/src/methods/stories/can-send-story.ts b/packages/core/src/highlevel/methods/stories/can-send-story.ts similarity index 82% rename from packages/client/src/methods/stories/can-send-story.ts rename to packages/core/src/highlevel/methods/stories/can-send-story.ts index 2a962dcf..34fd33cf 100644 --- a/packages/client/src/methods/stories/can-send-story.ts +++ b/packages/core/src/highlevel/methods/stories/can-send-story.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -15,7 +16,7 @@ export type CanSendStoryResult = true | 'need_admin' | 'need_boosts' * - `"need_admin"` if the user is not an admin in the chat * - `"need_boosts"` if the channel doesn't have enough boosts */ -export async function canSendStory(client: BaseTelegramClient, peerId: InputPeerLike): Promise { +export async function canSendStory(client: ITelegramClient, peerId: InputPeerLike): Promise { try { const res = await client.call({ _: 'stories.canSendStory', diff --git a/packages/client/src/methods/stories/delete-stories.ts b/packages/core/src/highlevel/methods/stories/delete-stories.ts similarity index 83% rename from packages/client/src/methods/stories/delete-stories.ts rename to packages/core/src/highlevel/methods/stories/delete-stories.ts index f8c5cfb4..c3fed166 100644 --- a/packages/client/src/methods/stories/delete-stories.ts +++ b/packages/core/src/highlevel/methods/stories/delete-stories.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns IDs of stories that were removed */ export async function deleteStories( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Story IDs to delete diff --git a/packages/client/src/methods/stories/edit-story.ts b/packages/core/src/highlevel/methods/stories/edit-story.ts similarity index 95% rename from packages/client/src/methods/stories/edit-story.ts rename to packages/core/src/highlevel/methods/stories/edit-story.ts index f3eee571..70a6c04b 100644 --- a/packages/client/src/methods/stories/edit-story.ts +++ b/packages/core/src/highlevel/methods/stories/edit-story.ts @@ -1,5 +1,6 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { ITelegramClient } from '../../client.types.js' import { InputMediaLike, InputPeerLike, InputPrivacyRule, InputText, Story } from '../../types/index.js' import { _normalizeInputMedia } from '../files/normalize-input-media.js' import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js' @@ -13,7 +14,7 @@ import { _findStoryInUpdate } from './find-in-update.js' * @returns Edited story */ export async function editStory( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Story ID to edit diff --git a/packages/client/src/methods/stories/find-in-update.ts b/packages/core/src/highlevel/methods/stories/find-in-update.ts similarity index 53% rename from packages/client/src/methods/stories/find-in-update.ts rename to packages/core/src/highlevel/methods/stories/find-in-update.ts index 2c02e925..8f0f6644 100644 --- a/packages/client/src/methods/stories/find-in-update.ts +++ b/packages/core/src/highlevel/methods/stories/find-in-update.ts @@ -1,14 +1,16 @@ -import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' -import { assertTypeIs, hasValueAtKey } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { assertTypeIs, hasValueAtKey } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { PeersIndex, Story } from '../../types/index.js' -import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** @internal */ -export function _findStoryInUpdate(client: BaseTelegramClient, res: tl.TypeUpdates): Story { +export function _findStoryInUpdate(client: ITelegramClient, res: tl.TypeUpdates): Story { assertIsUpdatesGroup('_findStoryInUpdate', res) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res, true) const peers = PeersIndex.from(res) const updateStory = res.updates.find(hasValueAtKey('_', 'updateStory')) diff --git a/packages/client/src/methods/stories/get-all-stories.ts b/packages/core/src/highlevel/methods/stories/get-all-stories.ts similarity index 83% rename from packages/client/src/methods/stories/get-all-stories.ts rename to packages/core/src/highlevel/methods/stories/get-all-stories.ts index fdf05c9f..da22f9a1 100644 --- a/packages/client/src/methods/stories/get-all-stories.ts +++ b/packages/core/src/highlevel/methods/stories/get-all-stories.ts @@ -1,13 +1,12 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' - +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { AllStories } from '../../types/index.js' /** * Get all stories (e.g. to load the top bar) */ export async function getAllStories( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Offset from which to fetch stories diff --git a/packages/client/src/methods/stories/get-peer-stories.ts b/packages/core/src/highlevel/methods/stories/get-peer-stories.ts similarity index 72% rename from packages/client/src/methods/stories/get-peer-stories.ts rename to packages/core/src/highlevel/methods/stories/get-peer-stories.ts index f3fa6082..90f88b90 100644 --- a/packages/client/src/methods/stories/get-peer-stories.ts +++ b/packages/core/src/highlevel/methods/stories/get-peer-stories.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, PeersIndex, PeerStories } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +7,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * * @param peerId Peer ID whose stories to fetch */ -export async function getPeerStories(client: BaseTelegramClient, peerId: InputPeerLike): Promise { +export async function getPeerStories(client: ITelegramClient, peerId: InputPeerLike): Promise { const res = await client.call({ _: 'stories.getPeerStories', peer: await resolvePeer(client, peerId), diff --git a/packages/client/src/methods/stories/get-profile-stories.ts b/packages/core/src/highlevel/methods/stories/get-profile-stories.ts similarity index 91% rename from packages/client/src/methods/stories/get-profile-stories.ts rename to packages/core/src/highlevel/methods/stories/get-profile-stories.ts index 3cd11451..e3f21672 100644 --- a/packages/client/src/methods/stories/get-profile-stories.ts +++ b/packages/core/src/highlevel/methods/stories/get-profile-stories.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputPeerLike, PeersIndex, Story } from '../../types/index.js' import { makeArrayPaginated } from '../../utils/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Get profile stories */ export async function getProfileStories( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/stories/get-stories-by-id.ts b/packages/core/src/highlevel/methods/stories/get-stories-by-id.ts similarity index 79% rename from packages/client/src/methods/stories/get-stories-by-id.ts rename to packages/core/src/highlevel/methods/stories/get-stories-by-id.ts index 91ce3d52..b079dfd6 100644 --- a/packages/client/src/methods/stories/get-stories-by-id.ts +++ b/packages/core/src/highlevel/methods/stories/get-stories-by-id.ts @@ -1,6 +1,6 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' - +import { MaybeArray } from '../../../types/utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, PeersIndex, Story } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param storyIds Story IDs */ export async function getStoriesById( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyIds: MaybeArray, ): Promise { diff --git a/packages/client/src/methods/stories/get-stories-interactions.ts b/packages/core/src/highlevel/methods/stories/get-stories-interactions.ts similarity index 84% rename from packages/client/src/methods/stories/get-stories-interactions.ts rename to packages/core/src/highlevel/methods/stories/get-stories-interactions.ts index a1cb21a7..090b74c5 100644 --- a/packages/client/src/methods/stories/get-stories-interactions.ts +++ b/packages/core/src/highlevel/methods/stories/get-stories-interactions.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, PeersIndex, StoryInteractions } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * The result will be in the same order as the input IDs */ export async function getStoriesInteractions( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyIds: MaybeArray, ): Promise { diff --git a/packages/client/src/methods/stories/get-story-link.ts b/packages/core/src/highlevel/methods/stories/get-story-link.ts similarity index 88% rename from packages/client/src/methods/stories/get-story-link.ts rename to packages/core/src/highlevel/methods/stories/get-story-link.ts index 8ddcf963..ed44704e 100644 --- a/packages/client/src/methods/stories/get-story-link.ts +++ b/packages/core/src/highlevel/methods/stories/get-story-link.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -12,7 +11,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * I have no idea why is this an RPC call, but whatever */ export async function getStoryLink( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyId: number, ): Promise { diff --git a/packages/client/src/methods/stories/get-story-viewers.ts b/packages/core/src/highlevel/methods/stories/get-story-viewers.ts similarity index 94% rename from packages/client/src/methods/stories/get-story-viewers.ts rename to packages/core/src/highlevel/methods/stories/get-story-viewers.ts index 6d03ecf3..317c5426 100644 --- a/packages/client/src/methods/stories/get-story-viewers.ts +++ b/packages/core/src/highlevel/methods/stories/get-story-viewers.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, StoryViewersList } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -7,7 +6,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Get viewers list of a story */ export async function getStoryViewers( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyId: number, params?: { diff --git a/packages/client/src/methods/stories/hide-my-stories-views.ts b/packages/core/src/highlevel/methods/stories/hide-my-stories-views.ts similarity index 77% rename from packages/client/src/methods/stories/hide-my-stories-views.ts rename to packages/core/src/highlevel/methods/stories/hide-my-stories-views.ts index 42e09bcb..c552744b 100644 --- a/packages/client/src/methods/stories/hide-my-stories-views.ts +++ b/packages/core/src/highlevel/methods/stories/hide-my-stories-views.ts @@ -1,7 +1,8 @@ -import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' - +import { MtTypeAssertionError } from '../../../types/errors.js' +import { hasValueAtKey } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { StoriesStealthMode } from '../../types/stories/stealth-mode.js' -import { assertIsUpdatesGroup, hasValueAtKey } from '../../utils/index.js' +import { assertIsUpdatesGroup } from '../../updates/utils.js' /** * Hide own stories views (activate so called "stealth mode") @@ -9,7 +10,7 @@ import { assertIsUpdatesGroup, hasValueAtKey } from '../../utils/index.js' * Currently has a cooldown of 1 hour, and throws FLOOD_WAIT error if it is on cooldown. */ export async function hideMyStoriesViews( - client: BaseTelegramClient, + client: ITelegramClient, params?: { /** * Whether to hide views from the last 5 minutes @@ -35,7 +36,7 @@ export async function hideMyStoriesViews( }) assertIsUpdatesGroup('hideMyStoriesViews', res) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res, true) const upd = res.updates.find(hasValueAtKey('_', 'updateStoriesStealthMode')) diff --git a/packages/client/src/methods/stories/increment-stories-views.ts b/packages/core/src/highlevel/methods/stories/increment-stories-views.ts similarity index 79% rename from packages/client/src/methods/stories/increment-stories-views.ts rename to packages/core/src/highlevel/methods/stories/increment-stories-views.ts index d29bf1c9..e91f24d9 100644 --- a/packages/client/src/methods/stories/increment-stories-views.ts +++ b/packages/core/src/highlevel/methods/stories/increment-stories-views.ts @@ -1,6 +1,6 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { MaybeArray } from '../../../types/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -14,7 +14,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param ids ID(s) of the stories to increment views of (max 200) */ export async function incrementStoriesViews( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, ids: MaybeArray, ): Promise { diff --git a/packages/client/src/methods/stories/iter-all-stories.ts b/packages/core/src/highlevel/methods/stories/iter-all-stories.ts similarity index 91% rename from packages/client/src/methods/stories/iter-all-stories.ts rename to packages/core/src/highlevel/methods/stories/iter-all-stories.ts index 418c302e..27da69a3 100644 --- a/packages/client/src/methods/stories/iter-all-stories.ts +++ b/packages/core/src/highlevel/methods/stories/iter-all-stories.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { PeerStories } from '../../types/index.js' import { getAllStories } from './get-all-stories.js' @@ -9,7 +8,7 @@ import { getAllStories } from './get-all-stories.js' * Wrapper over {@link getAllStories} */ export async function* iterAllStories( - client: BaseTelegramClient, + client: ITelegramClient, params?: Parameters[1] & { /** * Maximum number of stories to fetch diff --git a/packages/client/src/methods/stories/iter-profile-stories.ts b/packages/core/src/highlevel/methods/stories/iter-profile-stories.ts similarity index 94% rename from packages/client/src/methods/stories/iter-profile-stories.ts rename to packages/core/src/highlevel/methods/stories/iter-profile-stories.ts index 30a9af9a..7632b64b 100644 --- a/packages/client/src/methods/stories/iter-profile-stories.ts +++ b/packages/core/src/highlevel/methods/stories/iter-profile-stories.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Story } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getProfileStories } from './get-profile-stories.js' @@ -8,7 +7,7 @@ import { getProfileStories } from './get-profile-stories.js' * Iterate over profile stories. Wrapper over {@link getProfileStories} */ export async function* iterProfileStories( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/stories/iter-story-viewers.ts b/packages/core/src/highlevel/methods/stories/iter-story-viewers.ts similarity index 94% rename from packages/client/src/methods/stories/iter-story-viewers.ts rename to packages/core/src/highlevel/methods/stories/iter-story-viewers.ts index 66acd5fe..9c2110a8 100644 --- a/packages/client/src/methods/stories/iter-story-viewers.ts +++ b/packages/core/src/highlevel/methods/stories/iter-story-viewers.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, StoryViewer } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' import { getStoryViewers } from './get-story-viewers.js' @@ -9,7 +8,7 @@ import { getStoryViewers } from './get-story-viewers.js' * Wrapper over {@link getStoryViewers} */ export async function* iterStoryViewers( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyId: number, params?: Parameters[3] & { diff --git a/packages/client/src/methods/stories/read-stories.ts b/packages/core/src/highlevel/methods/stories/read-stories.ts similarity index 73% rename from packages/client/src/methods/stories/read-stories.ts rename to packages/core/src/highlevel/methods/stories/read-stories.ts index 53c0c459..94fd40d9 100644 --- a/packages/client/src/methods/stories/read-stories.ts +++ b/packages/core/src/highlevel/methods/stories/read-stories.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -11,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @param peerId Peer ID whose stories to mark as read * @returns IDs of the stores that were marked as read */ -export async function readStories(client: BaseTelegramClient, peerId: InputPeerLike, maxId: number): Promise { +export async function readStories(client: ITelegramClient, peerId: InputPeerLike, maxId: number): Promise { return client.call({ _: 'stories.readStories', peer: await resolvePeer(client, peerId), diff --git a/packages/client/src/methods/stories/report-story.ts b/packages/core/src/highlevel/methods/stories/report-story.ts similarity index 79% rename from packages/client/src/methods/stories/report-story.ts rename to packages/core/src/highlevel/methods/stories/report-story.ts index 324773d9..682e87a4 100644 --- a/packages/client/src/methods/stories/report-story.ts +++ b/packages/core/src/highlevel/methods/stories/report-story.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -8,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Report a story (or multiple stories) to the moderation team */ export async function reportStory( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, storyIds: MaybeArray, params?: { diff --git a/packages/client/src/methods/stories/send-story-reaction.ts b/packages/core/src/highlevel/methods/stories/send-story-reaction.ts similarity index 85% rename from packages/client/src/methods/stories/send-story-reaction.ts rename to packages/core/src/highlevel/methods/stories/send-story-reaction.ts index 9921b8a7..b44a1c1a 100644 --- a/packages/client/src/methods/stories/send-story-reaction.ts +++ b/packages/core/src/highlevel/methods/stories/send-story-reaction.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, InputReaction, normalizeInputReaction } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -7,7 +6,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * Send (or remove) a reaction to a story */ export async function sendStoryReaction( - client: BaseTelegramClient, + client: ITelegramClient, params: { peerId: InputPeerLike storyId: number @@ -28,5 +27,5 @@ export async function sendStoryReaction( addToRecent, }) - client.network.handleUpdate(res, true) + client.handleClientUpdate(res, true) } diff --git a/packages/client/src/methods/stories/send-story.ts b/packages/core/src/highlevel/methods/stories/send-story.ts similarity index 94% rename from packages/client/src/methods/stories/send-story.ts rename to packages/core/src/highlevel/methods/stories/send-story.ts index 6adac18f..87457ba2 100644 --- a/packages/client/src/methods/stories/send-story.ts +++ b/packages/core/src/highlevel/methods/stories/send-story.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { randomLong } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { randomLong } from '../../../utils/long-utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputMediaLike, InputPeerLike, InputPrivacyRule, InputText, Story } from '../../types/index.js' import { _normalizeInputMedia } from '../files/normalize-input-media.js' import { _normalizePrivacyRules } from '../misc/normalize-privacy-rules.js' @@ -14,7 +15,7 @@ import { _findStoryInUpdate } from './find-in-update.js' * @returns Created story */ export async function sendStory( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Peer ID to send story as diff --git a/packages/client/src/methods/stories/toggle-peer-stories-archived.ts b/packages/core/src/highlevel/methods/stories/toggle-peer-stories-archived.ts similarity index 79% rename from packages/client/src/methods/stories/toggle-peer-stories-archived.ts rename to packages/core/src/highlevel/methods/stories/toggle-peer-stories-archived.ts index d4a85b37..00419c79 100644 --- a/packages/client/src/methods/stories/toggle-peer-stories-archived.ts +++ b/packages/core/src/highlevel/methods/stories/toggle-peer-stories-archived.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * This **does not** archive the chat with that peer, only stories. */ export async function togglePeerStoriesArchived( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, archived: boolean, ): Promise { diff --git a/packages/client/src/methods/stories/toggle-stories-pinned.ts b/packages/core/src/highlevel/methods/stories/toggle-stories-pinned.ts similarity index 86% rename from packages/client/src/methods/stories/toggle-stories-pinned.ts rename to packages/core/src/highlevel/methods/stories/toggle-stories-pinned.ts index 02faa14c..1dbf803c 100644 --- a/packages/client/src/methods/stories/toggle-stories-pinned.ts +++ b/packages/core/src/highlevel/methods/stories/toggle-stories-pinned.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -9,7 +9,7 @@ import { resolvePeer } from '../users/resolve-peer.js' * @returns IDs of stories that were toggled */ export async function toggleStoriesPinned( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * Story ID(s) to toggle diff --git a/packages/client/src/methods/users/block-user.ts b/packages/core/src/highlevel/methods/users/block-user.ts similarity index 60% rename from packages/client/src/methods/users/block-user.ts rename to packages/core/src/highlevel/methods/users/block-user.ts index baecc529..11c6274a 100644 --- a/packages/client/src/methods/users/block-user.ts +++ b/packages/core/src/highlevel/methods/users/block-user.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from './resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from './resolve-peer.js' * * @param id User ID, username or phone number */ -export async function blockUser(client: BaseTelegramClient, id: InputPeerLike): Promise { +export async function blockUser(client: ITelegramClient, id: InputPeerLike): Promise { const r = await client.call({ _: 'contacts.block', id: await resolvePeer(client, id), diff --git a/packages/client/src/methods/users/delete-profile-photos.ts b/packages/core/src/highlevel/methods/users/delete-profile-photos.ts similarity index 68% rename from packages/client/src/methods/users/delete-profile-photos.ts rename to packages/core/src/highlevel/methods/users/delete-profile-photos.ts index d1bb3861..656af894 100644 --- a/packages/client/src/methods/users/delete-profile-photos.ts +++ b/packages/core/src/highlevel/methods/users/delete-profile-photos.ts @@ -1,5 +1,8 @@ -import { BaseTelegramClient, MaybeArray, tl } from '@mtcute/core' -import { fileIdToInputPhoto } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' +import { fileIdToInputPhoto } from '../../utils/convert-file-id.js' /** * Delete your own profile photos @@ -7,7 +10,7 @@ import { fileIdToInputPhoto } from '@mtcute/file-id' * @param ids ID(s) of the photos. Can be file IDs or raw TL objects */ export async function deleteProfilePhotos( - client: BaseTelegramClient, + client: ITelegramClient, ids: MaybeArray, ): Promise { if (!Array.isArray(ids)) ids = [ids] diff --git a/packages/client/src/methods/users/edit-close-friends.ts b/packages/core/src/highlevel/methods/users/edit-close-friends.ts similarity index 67% rename from packages/client/src/methods/users/edit-close-friends.ts rename to packages/core/src/highlevel/methods/users/edit-close-friends.ts index 337bc6d7..5859650d 100644 --- a/packages/client/src/methods/users/edit-close-friends.ts +++ b/packages/core/src/highlevel/methods/users/edit-close-friends.ts @@ -1,7 +1,7 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' -import { assertTrue, toInputUser } from '../../utils/index.js' +import { toInputUser } from '../../utils/index.js' import { resolvePeerMany } from './resolve-peer-many.js' /** @@ -9,7 +9,7 @@ import { resolvePeerMany } from './resolve-peer-many.js' * * @param ids User IDs */ -export async function editCloseFriendsRaw(client: BaseTelegramClient, ids: number[]): Promise { +export async function editCloseFriendsRaw(client: ITelegramClient, ids: number[]): Promise { const r = await client.call({ _: 'contacts.editCloseFriends', id: ids, @@ -23,7 +23,7 @@ export async function editCloseFriendsRaw(client: BaseTelegramClient, ids: numbe * * @param ids User IDs */ -export async function editCloseFriends(client: BaseTelegramClient, ids: InputPeerLike[]): Promise { +export async function editCloseFriends(client: ITelegramClient, ids: InputPeerLike[]): Promise { const r = await client.call({ _: 'contacts.editCloseFriends', id: await resolvePeerMany(client, ids, toInputUser).then((r) => diff --git a/packages/client/src/methods/users/get-common-chats.ts b/packages/core/src/highlevel/methods/users/get-common-chats.ts similarity index 78% rename from packages/client/src/methods/users/get-common-chats.ts rename to packages/core/src/highlevel/methods/users/get-common-chats.ts index eb56194f..44050388 100644 --- a/packages/client/src/methods/users/get-common-chats.ts +++ b/packages/core/src/highlevel/methods/users/get-common-chats.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { Chat, InputPeerLike } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from './resolve-peer.js' @@ -10,7 +9,7 @@ import { resolvePeer } from './resolve-peer.js' * @param userId User's ID, username or phone number * @throws MtInvalidPeerTypeError */ -export async function getCommonChats(client: BaseTelegramClient, userId: InputPeerLike): Promise { +export async function getCommonChats(client: ITelegramClient, userId: InputPeerLike): Promise { return client .call({ _: 'messages.getCommonChats', diff --git a/packages/client/src/methods/users/get-global-ttl.ts b/packages/core/src/highlevel/methods/users/get-global-ttl.ts similarity index 62% rename from packages/client/src/methods/users/get-global-ttl.ts rename to packages/core/src/highlevel/methods/users/get-global-ttl.ts index 76f54ed8..bdfc1f62 100644 --- a/packages/client/src/methods/users/get-global-ttl.ts +++ b/packages/core/src/highlevel/methods/users/get-global-ttl.ts @@ -1,9 +1,9 @@ -import { BaseTelegramClient } from '@mtcute/core' +import { ITelegramClient } from '../../client.types.js' /** * Gets the current default value of the Time-To-Live setting, applied to all new chats. */ -export async function getGlobalTtl(client: BaseTelegramClient): Promise { +export async function getGlobalTtl(client: ITelegramClient): Promise { return client .call({ _: 'messages.getDefaultHistoryTTL', diff --git a/packages/core/src/highlevel/methods/users/get-me.ts b/packages/core/src/highlevel/methods/users/get-me.ts new file mode 100644 index 00000000..ee44317d --- /dev/null +++ b/packages/core/src/highlevel/methods/users/get-me.ts @@ -0,0 +1,25 @@ +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { User } from '../../types/index.js' + +/** + * Get currently authorized user's full information + */ +export function getMe(client: ITelegramClient): Promise { + return client + .call({ + _: 'users.getUsers', + id: [ + { + _: 'inputUserSelf', + }, + ], + }) + .then(async ([user]) => { + assertTypeIs('getMe (@ users.getUsers)', user, 'user') + + await client.storage.self.storeFrom(user) + + return new User(user) + }) +} diff --git a/packages/core/src/highlevel/methods/users/get-my-username.ts b/packages/core/src/highlevel/methods/users/get-my-username.ts new file mode 100644 index 00000000..1f78faa9 --- /dev/null +++ b/packages/core/src/highlevel/methods/users/get-my-username.ts @@ -0,0 +1,11 @@ +import { ITelegramClient } from '../../client.types.js' + +/** + * Get currently authorized user's username. + * + * This method uses locally available information and + * does not call any API methods. + */ +export async function getMyUsername(client: ITelegramClient): Promise { + return client.storage.self.fetch().then((self) => self?.usernames[0] ?? null) +} diff --git a/packages/client/src/methods/users/get-profile-photo.ts b/packages/core/src/highlevel/methods/users/get-profile-photo.ts similarity index 81% rename from packages/client/src/methods/users/get-profile-photo.ts rename to packages/core/src/highlevel/methods/users/get-profile-photo.ts index a2cbcce0..9cb7cf96 100644 --- a/packages/client/src/methods/users/get-profile-photo.ts +++ b/packages/core/src/highlevel/methods/users/get-profile-photo.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Photo } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { resolvePeer } from './resolve-peer.js' @@ -13,7 +14,7 @@ import { resolvePeer } from './resolve-peer.js' * @param params */ export async function getProfilePhoto( - client: BaseTelegramClient, + client: ITelegramClient, userId: InputPeerLike, photoId: tl.Long, ): Promise { diff --git a/packages/client/src/methods/users/get-profile-photos.ts b/packages/core/src/highlevel/methods/users/get-profile-photos.ts similarity index 86% rename from packages/client/src/methods/users/get-profile-photos.ts rename to packages/core/src/highlevel/methods/users/get-profile-photos.ts index c79d2d29..9b8df9ae 100644 --- a/packages/client/src/methods/users/get-profile-photos.ts +++ b/packages/core/src/highlevel/methods/users/get-profile-photos.ts @@ -1,6 +1,9 @@ -import { BaseTelegramClient, Long, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { ArrayPaginated, InputPeerLike, Photo } from '../../types/index.js' import { makeArrayPaginated } from '../../utils/index.js' import { toInputUser } from '../../utils/peer-utils.js' @@ -13,7 +16,7 @@ import { resolvePeer } from './resolve-peer.js' * @param params */ export async function getProfilePhotos( - client: BaseTelegramClient, + client: ITelegramClient, userId: InputPeerLike, params?: { /** diff --git a/packages/client/src/methods/users/get-users.test.ts b/packages/core/src/highlevel/methods/users/get-users.test.ts similarity index 92% rename from packages/client/src/methods/users/get-users.test.ts rename to packages/core/src/highlevel/methods/users/get-users.test.ts index 3c02fa6b..84994bb3 100644 --- a/packages/client/src/methods/users/get-users.test.ts +++ b/packages/core/src/highlevel/methods/users/get-users.test.ts @@ -1,9 +1,9 @@ +import Long from 'long' import { describe, expect, it } from 'vitest' -import { Long } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' import { createStub, StubTelegramClient } from '@mtcute/test' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { User } from '../../types/index.js' import { getUsers } from './get-users.js' diff --git a/packages/client/src/methods/users/get-users.ts b/packages/core/src/highlevel/methods/users/get-users.ts similarity index 82% rename from packages/client/src/methods/users/get-users.ts rename to packages/core/src/highlevel/methods/users/get-users.ts index 61be0420..4306824a 100644 --- a/packages/client/src/methods/users/get-users.ts +++ b/packages/core/src/highlevel/methods/users/get-users.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient, MaybeArray } from '@mtcute/core' - +import { MaybeArray } from '../../../types/utils.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, User } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { _getUsersBatched } from '../chats/batched-queries.js' @@ -14,7 +14,7 @@ import { resolvePeerMany } from './resolve-peer-many.js' * * @param ids Users' identifiers. Can be ID, username, phone number, `"me"`, `"self"` or TL object */ -export async function getUsers(client: BaseTelegramClient, ids: MaybeArray): Promise<(User | null)[]> { +export async function getUsers(client: ITelegramClient, ids: MaybeArray): Promise<(User | null)[]> { if (!Array.isArray(ids)) { // avoid unnecessary overhead of Promise.all and resolvePeerMany const res = await _getUsersBatched(client, toInputUser(await resolvePeer(client, ids))) diff --git a/packages/client/src/methods/users/iter-profile-photos.ts b/packages/core/src/highlevel/methods/users/iter-profile-photos.ts similarity index 94% rename from packages/client/src/methods/users/iter-profile-photos.ts rename to packages/core/src/highlevel/methods/users/iter-profile-photos.ts index 92ce1b0b..d076efe5 100644 --- a/packages/client/src/methods/users/iter-profile-photos.ts +++ b/packages/core/src/highlevel/methods/users/iter-profile-photos.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike, Photo } from '../../types/index.js' import { toInputUser } from '../../utils/peer-utils.js' import { getProfilePhotos } from './get-profile-photos.js' @@ -12,7 +11,7 @@ import { resolvePeer } from './resolve-peer.js' * @param params */ export async function* iterProfilePhotos( - client: BaseTelegramClient, + client: ITelegramClient, userId: InputPeerLike, params?: Parameters[2] & { /** diff --git a/packages/client/src/methods/users/resolve-peer-many.ts b/packages/core/src/highlevel/methods/users/resolve-peer-many.ts similarity index 89% rename from packages/client/src/methods/users/resolve-peer-many.ts rename to packages/core/src/highlevel/methods/users/resolve-peer-many.ts index 8263ee9e..b6a19765 100644 --- a/packages/client/src/methods/users/resolve-peer-many.ts +++ b/packages/core/src/highlevel/methods/users/resolve-peer-many.ts @@ -1,6 +1,7 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { ConditionVariable } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { ConditionVariable } from '../../../utils/condition-variable.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from './resolve-peer.js' @@ -15,7 +16,7 @@ import { resolvePeer } from './resolve-peer.js' * @param normalizer Normalization function */ export async function resolvePeerMany( - client: BaseTelegramClient, + client: ITelegramClient, peerIds: InputPeerLike[], normalizer: (obj: tl.TypeInputPeer) => T | null, ): Promise @@ -27,13 +28,13 @@ export async function resolvePeerMany +export async function resolvePeerMany(client: ITelegramClient, peerIds: InputPeerLike[]): Promise /** * @internal */ export async function resolvePeerMany( - client: BaseTelegramClient, + client: ITelegramClient, peerIds: InputPeerLike[], normalizer?: (obj: tl.TypeInputPeer) => tl.TypeInputPeer | tl.TypeInputUser | tl.TypeInputChannel | null, ): Promise<(tl.TypeInputPeer | tl.TypeInputUser | tl.TypeInputChannel)[]> { diff --git a/packages/client/src/methods/users/resolve-peer.test.ts b/packages/core/src/highlevel/methods/users/resolve-peer.test.ts similarity index 97% rename from packages/client/src/methods/users/resolve-peer.test.ts rename to packages/core/src/highlevel/methods/users/resolve-peer.test.ts index fe66b488..6d6bec08 100644 --- a/packages/client/src/methods/users/resolve-peer.test.ts +++ b/packages/core/src/highlevel/methods/users/resolve-peer.test.ts @@ -1,6 +1,6 @@ +import Long from 'long' import { describe, expect, it, vi } from 'vitest' -import { Long } from '@mtcute/core' import { createStub, StubTelegramClient } from '@mtcute/test' import { Chat, MtPeerNotFoundError, User } from '../../types/index.js' @@ -55,8 +55,8 @@ describe('resolvePeer', () => { accessHash: Long.fromBits(111, 222), }), ) - await client.storage.saveReferenceMessage(123, -1000000000456, 789) - await client.storage.saveReferenceMessage(-1000000000123, -1000000000456, 789) + await client.storage.refMsgs.store(123, -1000000000456, 789) + await client.storage.refMsgs.store(-1000000000123, -1000000000456, 789) const resolved = await resolvePeer(client, { _: 'mtcute.dummyInputPeerMinUser', @@ -124,7 +124,7 @@ describe('resolvePeer', () => { accessHash: Long.fromBits(111, 222), }), ) - await client.storage.saveReferenceMessage(123, -1000000000456, 789) + await client.storage.refMsgs.store(123, -1000000000456, 789) const resolved = await resolvePeer(client, 123) @@ -182,7 +182,7 @@ describe('resolvePeer', () => { accessHash: Long.fromBits(111, 222), }), ) - await client.storage.saveReferenceMessage(-1000000000123, -1000000000456, 789) + await client.storage.refMsgs.store(-1000000000123, -1000000000456, 789) const resolved = await resolvePeer(client, -1000000000123) diff --git a/packages/client/src/methods/users/resolve-peer.ts b/packages/core/src/highlevel/methods/users/resolve-peer.ts similarity index 87% rename from packages/client/src/methods/users/resolve-peer.ts rename to packages/core/src/highlevel/methods/users/resolve-peer.ts index 1db862b7..f68cb103 100644 --- a/packages/client/src/methods/users/resolve-peer.ts +++ b/packages/core/src/highlevel/methods/users/resolve-peer.ts @@ -1,13 +1,10 @@ -import { - BaseTelegramClient, - getBasicPeerType, - getMarkedPeerId, - Long, - MtTypeAssertionError, - tl, - toggleChannelIdMark, -} from '@mtcute/core' +import Long from 'long' +import { tl } from '@mtcute/tl' + +import { MtTypeAssertionError } from '../../../types/errors.js' +import { getMarkedPeerId, parseMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils.js' +import { ITelegramClient } from '../../client.types.js' import { MtPeerNotFoundError } from '../../types/errors.js' import { InputPeerLike } from '../../types/peers/index.js' import { toInputPeer } from '../../utils/peer-utils.js' @@ -21,7 +18,7 @@ import { toInputPeer } from '../../utils/peer-utils.js' * @param force Whether to force re-fetch the peer from the server (only for usernames and phone numbers) */ export async function resolvePeer( - client: BaseTelegramClient, + client: ITelegramClient, peerId: InputPeerLike, force = false, ): Promise { @@ -51,7 +48,7 @@ export async function resolvePeer( } if (typeof peerId === 'number' && !force) { - const fromStorage = await client.storage.getPeerById(peerId) + const fromStorage = await client.storage.peers.getById(peerId) if (fromStorage) return fromStorage } @@ -64,7 +61,7 @@ export async function resolvePeer( if (peerId.match(/^\d+$/)) { // phone number - const fromStorage = await client.storage.getPeerByPhone(peerId) + const fromStorage = await client.storage.peers.getByPhone(peerId) if (fromStorage) return fromStorage res = await client.call({ @@ -74,7 +71,7 @@ export async function resolvePeer( } else { // username if (!force) { - const fromStorage = await client.storage.getPeerByUsername(peerId.toLowerCase()) + const fromStorage = await client.storage.peers.getByUsername(peerId) if (fromStorage) return fromStorage } @@ -140,28 +137,25 @@ export async function resolvePeer( // particularly, when we're a bot or we're referencing a user // who we have "seen" recently // if it's not the case, we'll get an `PEER_ID_INVALID` error anyways - const peerType = getBasicPeerType(peerId) + const [peerType, bareId] = parseMarkedPeerId(peerId) switch (peerType) { case 'user': return { _: 'inputPeerUser', - userId: peerId, + userId: bareId, accessHash: Long.ZERO, } case 'chat': return { _: 'inputPeerChat', - chatId: -peerId, + chatId: bareId, } - case 'channel': { - const id = toggleChannelIdMark(peerId) - + case 'channel': return { _: 'inputPeerChannel', - channelId: id, + channelId: bareId, accessHash: Long.ZERO, } - } } } diff --git a/packages/client/src/methods/users/set-global-ttl.ts b/packages/core/src/highlevel/methods/users/set-global-ttl.ts similarity index 60% rename from packages/client/src/methods/users/set-global-ttl.ts rename to packages/core/src/highlevel/methods/users/set-global-ttl.ts index 264972f6..5b68fdba 100644 --- a/packages/client/src/methods/users/set-global-ttl.ts +++ b/packages/core/src/highlevel/methods/users/set-global-ttl.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Changes the current default value of the Time-To-Live setting, @@ -7,7 +7,7 @@ import { assertTrue } from '@mtcute/core/utils.js' * * @param period New TTL period, in seconds (or 0 to disable) */ -export async function setGlobalTtl(client: BaseTelegramClient, period: number): Promise { +export async function setGlobalTtl(client: ITelegramClient, period: number): Promise { const r = await client.call({ _: 'messages.setDefaultHistoryTTL', period, diff --git a/packages/client/src/methods/users/set-my-emoji-status.ts b/packages/core/src/highlevel/methods/users/set-my-emoji-status.ts similarity index 80% rename from packages/client/src/methods/users/set-my-emoji-status.ts rename to packages/core/src/highlevel/methods/users/set-my-emoji-status.ts index 0294b4f9..e6fe644c 100644 --- a/packages/client/src/methods/users/set-my-emoji-status.ts +++ b/packages/core/src/highlevel/methods/users/set-my-emoji-status.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { assertTrue, normalizeDate } from '../../utils/index.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { normalizeDate } from '../../utils/index.js' /** * Set an emoji status for the current user @@ -8,7 +10,7 @@ import { assertTrue, normalizeDate } from '../../utils/index.js' * @param emoji Custom emoji ID or `null` to remove the emoji */ export async function setMyEmojiStatus( - client: BaseTelegramClient, + client: ITelegramClient, emoji: tl.Long | null, params?: { /** diff --git a/packages/client/src/methods/users/set-my-profile-photo.ts b/packages/core/src/highlevel/methods/users/set-my-profile-photo.ts similarity index 85% rename from packages/client/src/methods/users/set-my-profile-photo.ts rename to packages/core/src/highlevel/methods/users/set-my-profile-photo.ts index b88fe373..5b92f400 100644 --- a/packages/client/src/methods/users/set-my-profile-photo.ts +++ b/packages/core/src/highlevel/methods/users/set-my-profile-photo.ts @@ -1,7 +1,10 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { fileIdToInputPhoto, tdFileId } from '@mtcute/file-id' +import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { ITelegramClient } from '../../client.types.js' import { InputFileLike, Photo } from '../../types/index.js' +import { fileIdToInputPhoto } from '../../utils/convert-file-id.js' import { _normalizeInputFile } from '../files/normalize-input-file.js' /** @@ -10,7 +13,7 @@ import { _normalizeInputFile } from '../files/normalize-input-file.js' * You can also pass a file ID or an InputPhoto to re-use existing photo. */ export async function setMyProfilePhoto( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** Media type (photo or video) */ type: 'photo' | 'video' diff --git a/packages/client/src/methods/users/set-my-username.ts b/packages/core/src/highlevel/methods/users/set-my-username.ts similarity index 63% rename from packages/client/src/methods/users/set-my-username.ts rename to packages/core/src/highlevel/methods/users/set-my-username.ts index e738d198..15a2bb2f 100644 --- a/packages/client/src/methods/users/set-my-username.ts +++ b/packages/core/src/highlevel/methods/users/set-my-username.ts @@ -1,7 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' -import { getAuthState } from '../auth/_state.js' /** * Change username of the current user. @@ -11,7 +9,7 @@ import { getAuthState } from '../auth/_state.js' * * @param username New username (5-32 chars, allowed chars: `a-zA-Z0-9_`), or `null` to remove */ -export async function setMyUsername(client: BaseTelegramClient, username: string | null): Promise { +export async function setMyUsername(client: ITelegramClient, username: string | null): Promise { if (username === null) username = '' const res = await client.call({ @@ -19,7 +17,7 @@ export async function setMyUsername(client: BaseTelegramClient, username: string username, }) - getAuthState(client).selfUsername = username || null + await client.storage.self.update({ username }) return new User(res) } diff --git a/packages/client/src/methods/users/set-offline.ts b/packages/core/src/highlevel/methods/users/set-offline.ts similarity index 54% rename from packages/client/src/methods/users/set-offline.ts rename to packages/core/src/highlevel/methods/users/set-offline.ts index aa650a9d..1a42c358 100644 --- a/packages/client/src/methods/users/set-offline.ts +++ b/packages/core/src/highlevel/methods/users/set-offline.ts @@ -1,12 +1,12 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' /** * Change user status to offline or online * * @param offline Whether the user is currently offline */ -export async function setOffline(client: BaseTelegramClient, offline = true): Promise { +export async function setOffline(client: ITelegramClient, offline = true): Promise { const r = await client.call({ _: 'account.updateStatus', offline, diff --git a/packages/client/src/methods/users/unblock-user.ts b/packages/core/src/highlevel/methods/users/unblock-user.ts similarity index 61% rename from packages/client/src/methods/users/unblock-user.ts rename to packages/core/src/highlevel/methods/users/unblock-user.ts index 6f092d0c..35a07a15 100644 --- a/packages/client/src/methods/users/unblock-user.ts +++ b/packages/core/src/highlevel/methods/users/unblock-user.ts @@ -1,6 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { assertTrue } from '@mtcute/core/utils.js' - +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' import { InputPeerLike } from '../../types/index.js' import { resolvePeer } from './resolve-peer.js' @@ -9,7 +8,7 @@ import { resolvePeer } from './resolve-peer.js' * * @param id User ID, username or phone number */ -export async function unblockUser(client: BaseTelegramClient, id: InputPeerLike): Promise { +export async function unblockUser(client: ITelegramClient, id: InputPeerLike): Promise { const r = await client.call({ _: 'contacts.unblock', id: await resolvePeer(client, id), diff --git a/packages/client/src/methods/users/update-profile.ts b/packages/core/src/highlevel/methods/users/update-profile.ts similarity index 89% rename from packages/client/src/methods/users/update-profile.ts rename to packages/core/src/highlevel/methods/users/update-profile.ts index 97585db4..6e8a60ec 100644 --- a/packages/client/src/methods/users/update-profile.ts +++ b/packages/core/src/highlevel/methods/users/update-profile.ts @@ -1,5 +1,4 @@ -import { BaseTelegramClient } from '@mtcute/core' - +import { ITelegramClient } from '../../client.types.js' import { User } from '../../types/index.js' /** @@ -10,7 +9,7 @@ import { User } from '../../types/index.js' * @param params */ export async function updateProfile( - client: BaseTelegramClient, + client: ITelegramClient, params: { /** * New first name diff --git a/packages/core/src/highlevel/storage/index.ts b/packages/core/src/highlevel/storage/index.ts new file mode 100644 index 00000000..8b3460c4 --- /dev/null +++ b/packages/core/src/highlevel/storage/index.ts @@ -0,0 +1,4 @@ +export * from './provider.js' +export * from './repository/peers.js' +export * from './repository/ref-messages.js' +export * from './storage.js' diff --git a/packages/core/src/highlevel/storage/provider.ts b/packages/core/src/highlevel/storage/provider.ts new file mode 100644 index 00000000..a1ab11d8 --- /dev/null +++ b/packages/core/src/highlevel/storage/provider.ts @@ -0,0 +1,8 @@ +import { IMtStorageProvider } from '../../storage/provider.js' +import { IPeersRepository } from './repository/peers.js' +import { IReferenceMessagesRepository } from './repository/ref-messages.js' + +export interface ITelegramStorageProvider extends IMtStorageProvider { + readonly peers: IPeersRepository + readonly refMessages: IReferenceMessagesRepository +} diff --git a/packages/core/src/highlevel/storage/repository/peers.ts b/packages/core/src/highlevel/storage/repository/peers.ts new file mode 100644 index 00000000..7ec1c598 --- /dev/null +++ b/packages/core/src/highlevel/storage/repository/peers.ts @@ -0,0 +1,37 @@ +import { MaybePromise } from '../../../types/utils.js' + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace IPeersRepository { + /** Information about a cached peer */ + export interface PeerInfo { + /** Peer marked ID */ + id: number + /** Peer access hash, as a fast string representation */ + accessHash: string + /** Peer usernames, if any */ + usernames: string[] + /** Timestamp (in seconds) when the peer was last updated */ + updated: number + /** Peer phone number, if available */ + phone?: string + + /** + * Complete information about the peer, + * serialization of {@link tl.TypeUser} or {@link tl.TypeChat} + */ + complete: Uint8Array + } +} + +export interface IPeersRepository { + /** Store the given peer*/ + store(peer: IPeersRepository.PeerInfo): MaybePromise + /** Find a peer by their `id` */ + getById(id: number): MaybePromise + /** Find a peer by their username (where `usernames` includes `username`) */ + getByUsername(username: string): MaybePromise + /** Find a peer by their `phone` */ + getByPhone(phone: string): MaybePromise + + deleteAll(): MaybePromise +} diff --git a/packages/core/src/highlevel/storage/repository/ref-messages.ts b/packages/core/src/highlevel/storage/repository/ref-messages.ts new file mode 100644 index 00000000..fbf77117 --- /dev/null +++ b/packages/core/src/highlevel/storage/repository/ref-messages.ts @@ -0,0 +1,22 @@ +import { MaybePromise } from '../../../types/utils.js' + +export interface IReferenceMessagesRepository { + /** Store a reference message */ + store(peerId: number, chatId: number, msgId: number): MaybePromise + /** + * Get the reference message for the given `peerId`. + * + * If more than one reference message is stored for the given `peerId`, + * the one with the highest `msgId` should be returned, but this is not + * really important. + */ + getByPeer(peerId: number): MaybePromise<[number, number] | null> + + /** + * Delete reference messages given the `chatId` + * where `msgId` is one of `msgIds` + */ + delete(chatId: number, msgIds: number[]): MaybePromise + deleteByPeer(peerId: number): MaybePromise + deleteAll(): MaybePromise +} diff --git a/packages/core/src/highlevel/storage/service/current-user.ts b/packages/core/src/highlevel/storage/service/current-user.ts new file mode 100644 index 00000000..6b9a9a32 --- /dev/null +++ b/packages/core/src/highlevel/storage/service/current-user.ts @@ -0,0 +1,163 @@ +import { tl } from '@mtcute/tl' +import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime' + +import { IKeyValueRepository } from '../../../storage/repository/key-value.js' +import { BaseService, ServiceOptions } from '../../../storage/service/base.js' +import { MtArgumentError } from '../../../types/index.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { extractUsernames } from '../../utils/peer-utils.js' + +export interface CurrentUserInfo { + userId: number + isBot: boolean + isPremium: boolean + usernames: string[] +} + +const KV_CURRENT_USER = 'current_user' + +function serialize(info: CurrentUserInfo | null): Uint8Array { + if (!info) return new Uint8Array(0) + + const hasUsernames = info.usernames.length > 0 + + let usernamesOverhead = hasUsernames ? 4 : 0 + + for (const username of info.usernames) { + // since usernames are always ASCII, string length is the same as byte length + usernamesOverhead += TlSerializationCounter.countBytesOverhead(username.length) + username.length + } + + const writer = TlBinaryWriter.manual(16 + usernamesOverhead) + writer.int(1) // version + + let flags = 0 + if (info.isBot) flags |= 1 + if (hasUsernames) flags |= 2 + if (info.isPremium) flags |= 4 + + writer.int(flags) + writer.int53(info.userId) + + if (hasUsernames) { + writer.int(info.usernames.length) + + for (const username of info.usernames) { + writer.string(username) + } + } + + return writer.result() +} + +function parse(data: Uint8Array): CurrentUserInfo | null { + if (data.length === 0) return null + + const reader = TlBinaryReader.manual(data) + if (reader.int() !== 1) return null + + const flags = reader.int() + const userId = reader.int53() + + let usernames: string[] = [] + + if (flags & 2) { + const len = reader.int() + usernames = new Array(len) + + for (let i = 0; i < len; i++) { + usernames[i] = reader.string() + } + } + + return { + userId, + isBot: (flags & 1) !== 0, + isPremium: (flags & 4) !== 0, + usernames, + } +} + +// todo: add testMode here + +export class CurrentUserService extends BaseService { + constructor( + private _kv: IKeyValueRepository, + opts: ServiceOptions, + ) { + super(opts) + } + + private _cached?: CurrentUserInfo | null + + async store(info: CurrentUserInfo | null): Promise { + if (info && this._cached) { + // update the existing object so the references to it are still valid + if (this._cached.userId === info.userId) { + return + } + + this._cached.userId = info.userId + this._cached.isBot = info.isBot + } else { + this._cached = info + } + + await this._kv.set(KV_CURRENT_USER, serialize(info)) + await this._driver.save?.() + } + + async storeFrom(user: tl.TypeUser): Promise { + assertTypeIs('storeFrom', user, 'user') + + const obj: CurrentUserInfo = { + userId: user.id, + isBot: user.bot!, + isPremium: user.premium!, + usernames: extractUsernames(user), + } + await this.store(obj) + + return obj + } + + async fetch(): Promise { + if (this._cached) return this._cached + + const data = await this._kv.get(KV_CURRENT_USER) + if (!data) return null + + const info = parse(data) + this._cached = info + + return info + } + + getCached(safe = false): CurrentUserInfo | null { + if (this._cached === undefined) { + if (safe) return null + + throw new MtArgumentError('User info is not cached yet') + } + + return this._cached + } + + async update(params: { username?: string; usernames?: string[]; isPremium?: boolean }): Promise { + const info = await this.fetch() + if (!info) return + + const { username, usernames, isPremium } = params + + if (isPremium !== undefined) info.isPremium = isPremium + + if (username !== undefined) { + // "main" username is always the first one + info.usernames[0] = username + } else if (usernames !== undefined) { + info.usernames = usernames + } + + return this.store(info) + } +} diff --git a/packages/core/src/highlevel/storage/service/peers.ts b/packages/core/src/highlevel/storage/service/peers.ts new file mode 100644 index 00000000..0a3b9273 --- /dev/null +++ b/packages/core/src/highlevel/storage/service/peers.ts @@ -0,0 +1,265 @@ +import Long from 'long' + +import { tl } from '@mtcute/tl' + +import { BaseService, ServiceOptions } from '../../../storage/service/base.js' +import { longFromFastString, longToFastString } from '../../../utils/long-utils.js' +import { LruMap } from '../../../utils/lru-map.js' +import { getAllPeersFrom, parseMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils.js' +import { extractUsernames } from '../../utils/peer-utils.js' +import { IPeersRepository } from '../repository/peers.js' +import { RefMessagesService } from './ref-messages.js' + +interface CacheItem { + peer: tl.TypeInputPeer + complete: tl.TypeUser | tl.TypeChat | null +} + +export interface PeersServiceOptions { + cacheSize?: number + updatesWriteInterval?: number +} + +const USERNAME_TTL = 24 * 60 * 60 * 1000 // 1 day + +function getInputPeer(dto: IPeersRepository.PeerInfo): tl.TypeInputPeer { + const [type, id] = parseMarkedPeerId(dto.id) + + switch (type) { + case 'user': + return { + _: 'inputPeerUser', + userId: id, + accessHash: longFromFastString(dto.accessHash), + } + case 'chat': + return { + _: 'inputPeerChat', + chatId: id, + } + case 'channel': + return { + _: 'inputPeerChannel', + channelId: id, + accessHash: longFromFastString(dto.accessHash), + } + } +} + +export class PeersService extends BaseService { + private _cache: LruMap + private _pendingWrites = new Map() + + constructor( + private options: PeersServiceOptions, + private _peers: IPeersRepository, + private _refs: RefMessagesService, + common: ServiceOptions, + ) { + super(common) + + this._cache = new LruMap(options.cacheSize ?? 100) + } + + async updatePeersFrom(obj: tl.TlObject | tl.TlObject[]) { + let count = 0 + + for (const peer of getAllPeersFrom(obj)) { + // no point in caching min peers as we can't use them + if ((peer as Extract).min) continue + + count += 1 + + await this.store(peer) + } + + if (count > 0) { + await this._driver.save?.() + this._log.debug('cached %d peers', count) + + return true + } + + return false + } + + async store(peer: tl.TypeUser | tl.TypeChat): Promise { + let dto: IPeersRepository.PeerInfo + let accessHash: tl.Long + + switch (peer._) { + case 'user': { + if (!peer.accessHash) { + this._log.warn('received user without access hash: %j', peer) + + return + } + + dto = { + id: peer.id, + accessHash: longToFastString(peer.accessHash), + phone: peer.phone, + usernames: extractUsernames(peer), + updated: Date.now(), + complete: this._serializeTl(peer), + } + accessHash = peer.accessHash + break + } + case 'chat': + case 'chatForbidden': { + dto = { + id: -peer.id, + accessHash: '', + updated: Date.now(), + complete: this._serializeTl(peer), + usernames: [], + } + accessHash = Long.ZERO + break + } + case 'channel': + case 'channelForbidden': { + if (!peer.accessHash) { + this._log.warn('received channel without access hash: %j', peer) + + return + } + + dto = { + id: toggleChannelIdMark(peer.id), + accessHash: longToFastString(peer.accessHash), + usernames: extractUsernames(peer as tl.RawChannel), + updated: Date.now(), + complete: this._serializeTl(peer), + } + accessHash = peer.accessHash + break + } + default: + return + } + + const cached = this._cache.get(peer.id) + + if (cached && this.options.updatesWriteInterval !== 0) { + const oldAccessHash = (cached.peer as Extract).accessHash + + if (oldAccessHash?.eq(accessHash)) { + // when entity is cached and hash is the same, an update query is needed, + // since some field in the full entity might have changed, or the username/phone + // + // to avoid too many DB calls, and since these updates are pretty common, + // they are grouped and applied in batches no more than once every 30sec (or user-defined). + // + // until then, they are either served from in-memory cache, + // or an older version is fetched from DB + + this._pendingWrites.set(peer.id, dto) + cached.complete = peer + + return + } + } + + // entity is not cached in memory, or the access hash has changed + // we need to update it in the DB asap, and also update the in-memory cache + await this._peers.store(dto) + this._cache.set(peer.id, { + peer: getInputPeer(dto), + complete: peer, + }) + + // todo: if (!this._cachedSelf?.isBot) { + // we have the full peer, we no longer need the references + // we can skip this in the other branch, since in that case it would've already been deleted + await this._refs.deleteByPeer(peer.id) + } + + private _returnCaching(id: number, dto: IPeersRepository.PeerInfo) { + const peer = getInputPeer(dto) + const complete = this._deserializeTl(dto.complete) + + this._cache.set(id, { + peer, + complete: complete as tl.TypeUser | tl.TypeChat | null, + }) + + return peer + } + + async getById(id: number, allowRefs = true): Promise { + const cached = this._cache.get(id) + if (cached) return cached.peer + + const dto = await this._peers.getById(id) + + if (dto) { + return this._returnCaching(id, dto) + } + + if (allowRefs) { + const ref = await this._refs.getForPeer(id) + if (!ref) return null + + const [chatId, msgId] = ref + const chat = await this.getById(chatId, false) + if (!chat) return null + + if (id > 0) { + // user + return { + _: 'inputPeerUserFromMessage', + peer: chat, + msgId, + userId: id, + } + } + + // channel + return { + _: 'inputPeerChannelFromMessage', + peer: chat, + msgId, + channelId: toggleChannelIdMark(id), + } + } + + return null + } + + async getByPhone(phone: string): Promise { + const dto = await this._peers.getByPhone(phone) + if (!dto) return null + + return this._returnCaching(dto.id, dto) + } + + async getByUsername(username: string): Promise { + const dto = await this._peers.getByUsername(username.toLowerCase()) + if (!dto) return null + + if (Date.now() - dto.updated > USERNAME_TTL) { + // username is too old, we can't trust it. ask the client to re-fetch it + return null + } + + return this._returnCaching(dto.id, dto) + } + + async getCompleteById(id: number): Promise { + const cached = this._cache.get(id) + if (cached) return cached.complete + + const dto = await this._peers.getById(id) + if (!dto) return null + + const cacheItem: CacheItem = { + peer: getInputPeer(dto), + complete: this._deserializeTl(dto.complete) as tl.TypeUser | tl.TypeChat | null, + } + this._cache.set(id, cacheItem) + + return cacheItem.complete + } +} diff --git a/packages/core/src/highlevel/storage/service/ref-messages.ts b/packages/core/src/highlevel/storage/service/ref-messages.ts new file mode 100644 index 00000000..67d32d08 --- /dev/null +++ b/packages/core/src/highlevel/storage/service/ref-messages.ts @@ -0,0 +1,51 @@ +import { BaseService, ServiceOptions } from '../../../storage/service/base.js' +import { LruMap } from '../../../utils/lru-map.js' +import { IReferenceMessagesRepository } from '../repository/ref-messages.js' + +export interface RefMessagesServiceOptions { + cacheSize?: number +} + +// todo: move inside updates manager? +// todo: chatId -> channelId + +export class RefMessagesService extends BaseService { + private _cache: LruMap + + constructor( + readonly options: RefMessagesServiceOptions, + readonly _refs: IReferenceMessagesRepository, + common: ServiceOptions, + ) { + super(common) + + this._cache = new LruMap(options.cacheSize ?? 1000) + } + + async store(peerId: number, chatId: number, msgId: number): Promise { + await this._refs.store(peerId, chatId, msgId) + this._cache.set(peerId, [chatId, msgId]) + } + + async getForPeer(peerId: number): Promise<[number, number] | null> { + const cached = this._cache.get(peerId) + if (cached) return cached + + const ref = await this._refs.getByPeer(peerId) + if (ref) this._cache.set(peerId, ref) + + return ref + } + + async delete(chatId: number, msgIds: number[]): Promise { + await this._refs.delete(chatId, msgIds) + // it's too expensive to invalidate the cache, + // so we just clear it completely instead + this._cache.clear() + } + + async deleteByPeer(peerId: number): Promise { + await this._refs.deleteByPeer(peerId) + this._cache.delete(peerId) + } +} diff --git a/packages/core/src/highlevel/storage/service/updates.ts b/packages/core/src/highlevel/storage/service/updates.ts new file mode 100644 index 00000000..621339ca --- /dev/null +++ b/packages/core/src/highlevel/storage/service/updates.ts @@ -0,0 +1,89 @@ +import { IKeyValueRepository } from '../../../storage/repository/key-value.js' +import { BaseService, ServiceOptions } from '../../../storage/service/base.js' +import { dataViewFromBuffer } from '../../../utils/buffer-utils.js' + +const KV_PTS = 'updates_pts' +const KV_QTS = 'updates_qts' +const KV_DATE = 'updates_date' +const KV_SEQ = 'updates_seq' +const KV_CHANNEL_PREFIX = 'updates_channel:' + +// todo: move inside updates manager? + +export class UpdatesStateService extends BaseService { + constructor( + readonly _kv: IKeyValueRepository, + opts: ServiceOptions, + ) { + super(opts) + } + + private async _getInt(key: string): Promise { + const val = await this._kv.get(key) + if (!val) return null + + return dataViewFromBuffer(val).getInt32(0, true) + } + + private async _setInt(key: string, val: number): Promise { + const buf = new Uint8Array(4) + dataViewFromBuffer(buf).setInt32(0, val, true) + + await this._kv.set(key, buf) + } + + async getState(): Promise<[number, number, number, number] | null> { + const [pts, qts, date, seq] = await Promise.all([ + this._getInt(KV_PTS), + this._getInt(KV_QTS), + this._getInt(KV_DATE), + this._getInt(KV_SEQ), + ]) + + if (pts === null || qts === null || date === null || seq === null) { + return null + } + + return [pts, qts, date, seq] + } + + async setPts(pts: number): Promise { + await this._setInt(KV_PTS, pts) + } + + async setQts(qts: number): Promise { + await this._setInt(KV_QTS, qts) + } + + async setDate(date: number): Promise { + await this._setInt(KV_DATE, date) + } + + async setSeq(seq: number): Promise { + await this._setInt(KV_SEQ, seq) + } + + async getChannelPts(channelId: number): Promise { + const val = await this._kv.get(KV_CHANNEL_PREFIX + channelId) + if (!val) return null + + return dataViewFromBuffer(val).getUint32(0, true) + } + + async setChannelPts(channelId: number, pts: number): Promise { + const buf = new Uint8Array(4) + dataViewFromBuffer(buf).setUint32(0, pts, true) + + await this._kv.set(KV_CHANNEL_PREFIX + channelId, buf) + } + + async setManyChannelPts(cpts: Map): Promise { + const promises: Promise[] = [] + + for (const [channelId, pts] of cpts.entries()) { + promises.push(this.setChannelPts(channelId, pts)) + } + + await Promise.all(promises) + } +} diff --git a/packages/core/src/highlevel/storage/storage.ts b/packages/core/src/highlevel/storage/storage.ts new file mode 100644 index 00000000..0f32baab --- /dev/null +++ b/packages/core/src/highlevel/storage/storage.ts @@ -0,0 +1,46 @@ +import { StorageManager } from '../../storage/storage.js' +import { PublicPart } from '../../types/utils.js' +import { ITelegramStorageProvider } from './provider.js' +import { CurrentUserService } from './service/current-user.js' +import { PeersService, PeersServiceOptions } from './service/peers.js' +import { RefMessagesService, RefMessagesServiceOptions } from './service/ref-messages.js' +import { UpdatesStateService } from './service/updates.js' + +interface TelegramStorageManagerOptions { + provider: ITelegramStorageProvider +} + +/** @internal */ +export interface TelegramStorageManagerExtraOptions { + refMessages?: RefMessagesServiceOptions + peers?: PeersServiceOptions +} + +export class TelegramStorageManager { + constructor( + private mt: StorageManager, + private options: TelegramStorageManagerOptions & TelegramStorageManagerExtraOptions, + ) {} + + private provider = this.options.provider + + readonly updates = new UpdatesStateService(this.provider.kv, this.mt._serviceOptions) + readonly self: PublicPart = new CurrentUserService(this.provider.kv, this.mt._serviceOptions) + readonly refMsgs = new RefMessagesService( + this.options.refMessages ?? {}, + this.provider.refMessages, + this.mt._serviceOptions, + ) + readonly peers: PublicPart = new PeersService( + this.options.peers ?? {}, + this.provider.peers, + this.refMsgs, + this.mt._serviceOptions, + ) + + async clear(withAuthKeys = false) { + await this.provider.peers.deleteAll() + await this.provider.refMessages.deleteAll() + await this.mt.clear(withAuthKeys) + } +} diff --git a/packages/client/src/types/auth/index.ts b/packages/core/src/highlevel/types/auth/index.ts similarity index 100% rename from packages/client/src/types/auth/index.ts rename to packages/core/src/highlevel/types/auth/index.ts diff --git a/packages/client/src/types/auth/sent-code.ts b/packages/core/src/highlevel/types/auth/sent-code.ts similarity index 98% rename from packages/client/src/types/auth/sent-code.ts rename to packages/core/src/highlevel/types/auth/sent-code.ts index 900f20d2..1c67d75e 100644 --- a/packages/client/src/types/auth/sent-code.ts +++ b/packages/core/src/highlevel/types/auth/sent-code.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/bots/command-scope.ts b/packages/core/src/highlevel/types/bots/command-scope.ts similarity index 98% rename from packages/client/src/types/bots/command-scope.ts rename to packages/core/src/highlevel/types/bots/command-scope.ts index 01ba9b69..3e840668 100644 --- a/packages/client/src/types/bots/command-scope.ts +++ b/packages/core/src/highlevel/types/bots/command-scope.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { InputPeerLike } from '../peers/index.js' diff --git a/packages/client/src/types/bots/game-high-score.ts b/packages/core/src/highlevel/types/bots/game-high-score.ts similarity index 95% rename from packages/client/src/types/bots/game-high-score.ts rename to packages/core/src/highlevel/types/bots/game-high-score.ts index 0a9cd53f..89a21611 100644 --- a/packages/client/src/types/bots/game-high-score.ts +++ b/packages/core/src/highlevel/types/bots/game-high-score.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/bots/index.ts b/packages/core/src/highlevel/types/bots/index.ts similarity index 100% rename from packages/client/src/types/bots/index.ts rename to packages/core/src/highlevel/types/bots/index.ts diff --git a/packages/client/src/types/bots/input/index.ts b/packages/core/src/highlevel/types/bots/input/index.ts similarity index 100% rename from packages/client/src/types/bots/input/index.ts rename to packages/core/src/highlevel/types/bots/input/index.ts diff --git a/packages/client/src/types/bots/input/input-inline-message.ts b/packages/core/src/highlevel/types/bots/input/input-inline-message.ts similarity index 98% rename from packages/client/src/types/bots/input/input-inline-message.ts rename to packages/core/src/highlevel/types/bots/input/input-inline-message.ts index 5913d480..e60d7128 100644 --- a/packages/client/src/types/bots/input/input-inline-message.ts +++ b/packages/core/src/highlevel/types/bots/input/input-inline-message.ts @@ -1,5 +1,7 @@ -import { assertNever, BaseTelegramClient, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { assertNever } from '../../../../types/utils.js' +import { ITelegramClient } from '../../../client.types.js' import { _normalizeInputText } from '../../../methods/misc/normalize-text.js' import { InputText } from '../../../types/misc/entities.js' import { @@ -246,7 +248,7 @@ export namespace BotInlineMessage { /** @internal */ export async function _convertToTl( - client: BaseTelegramClient, + client: ITelegramClient, obj: InputInlineMessage, ): Promise { switch (obj.type) { diff --git a/packages/client/src/types/bots/input/input-inline-result.ts b/packages/core/src/highlevel/types/bots/input/input-inline-result.ts similarity index 99% rename from packages/client/src/types/bots/input/input-inline-result.ts rename to packages/core/src/highlevel/types/bots/input/input-inline-result.ts index a4421fd4..023dcc40 100644 --- a/packages/client/src/types/bots/input/input-inline-result.ts +++ b/packages/core/src/highlevel/types/bots/input/input-inline-result.ts @@ -1,6 +1,8 @@ -import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' -import { fileIdToInputDocument, fileIdToInputPhoto } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../../types/errors.js' +import { ITelegramClient } from '../../../client.types.js' +import { fileIdToInputDocument, fileIdToInputPhoto } from '../../../utils/convert-file-id.js' import { extractFileName } from '../../../utils/file-utils.js' import { BotInlineMessage, InputInlineMessage } from './input-inline-message.js' @@ -723,7 +725,7 @@ export namespace BotInline { /** @internal */ export async function _convertToTl( - client: BaseTelegramClient, + client: ITelegramClient, results: InputInlineResult[], ): Promise<[boolean, tl.TypeInputBotInlineResult[]]> { const normalizeThumb = (obj: InputInlineResult, fallback?: string): tl.RawInputWebDocument | undefined => { diff --git a/packages/client/src/types/bots/keyboard-builder.test.ts b/packages/core/src/highlevel/types/bots/keyboard-builder.test.ts similarity index 100% rename from packages/client/src/types/bots/keyboard-builder.test.ts rename to packages/core/src/highlevel/types/bots/keyboard-builder.test.ts diff --git a/packages/client/src/types/bots/keyboard-builder.ts b/packages/core/src/highlevel/types/bots/keyboard-builder.ts similarity index 98% rename from packages/client/src/types/bots/keyboard-builder.ts rename to packages/core/src/highlevel/types/bots/keyboard-builder.ts index f46b8b3d..fe1c00e5 100644 --- a/packages/client/src/types/bots/keyboard-builder.ts +++ b/packages/core/src/highlevel/types/bots/keyboard-builder.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import type { InlineKeyboardMarkup, ReplyKeyboardMarkup } from './keyboards.js' diff --git a/packages/client/src/types/bots/keyboards.test.ts b/packages/core/src/highlevel/types/bots/keyboards.test.ts similarity index 99% rename from packages/client/src/types/bots/keyboards.test.ts rename to packages/core/src/highlevel/types/bots/keyboards.test.ts index be1f75fe..bffecd6f 100644 --- a/packages/client/src/types/bots/keyboards.test.ts +++ b/packages/core/src/highlevel/types/bots/keyboards.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { BotKeyboard } from './keyboards.js' diff --git a/packages/client/src/types/bots/keyboards.ts b/packages/core/src/highlevel/types/bots/keyboards.ts similarity index 98% rename from packages/client/src/types/bots/keyboards.ts rename to packages/core/src/highlevel/types/bots/keyboards.ts index 62fc7818..f5943232 100644 --- a/packages/client/src/types/bots/keyboards.ts +++ b/packages/core/src/highlevel/types/bots/keyboards.ts @@ -1,6 +1,7 @@ -import { assertNever, tl } from '@mtcute/core' -import { utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { assertNever } from '../../../types/utils.js' import { toInputUser } from '../../utils/peer-utils.js' import { BotKeyboardBuilder } from './keyboard-builder.js' diff --git a/packages/client/src/types/calls/discard-reason.ts b/packages/core/src/highlevel/types/calls/discard-reason.ts similarity index 93% rename from packages/client/src/types/calls/discard-reason.ts rename to packages/core/src/highlevel/types/calls/discard-reason.ts index 83dc9a66..839e7b8a 100644 --- a/packages/client/src/types/calls/discard-reason.ts +++ b/packages/core/src/highlevel/types/calls/discard-reason.ts @@ -1,4 +1,6 @@ -import { assertNever, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' + +import { assertNever } from '../../../types/utils.js' /** * Phone call discard reason. Can be: diff --git a/packages/client/src/types/calls/index.ts b/packages/core/src/highlevel/types/calls/index.ts similarity index 100% rename from packages/client/src/types/calls/index.ts rename to packages/core/src/highlevel/types/calls/index.ts diff --git a/packages/client/src/types/conversation.ts b/packages/core/src/highlevel/types/conversation.ts similarity index 93% rename from packages/client/src/types/conversation.ts rename to packages/core/src/highlevel/types/conversation.ts index d001a7a5..0dd89ccc 100644 --- a/packages/client/src/types/conversation.ts +++ b/packages/core/src/highlevel/types/conversation.ts @@ -1,6 +1,12 @@ -import { BaseTelegramClient, getMarkedPeerId, MaybeAsync, MtArgumentError, MtTimeoutError, tl } from '@mtcute/core' -import { AsyncLock, ControllablePromise, createControllablePromise, Deque } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError, MtTimeoutError } from '../../types/errors.js' +import { MaybePromise } from '../../types/utils.js' +import { AsyncLock } from '../../utils/async-lock.js' +import { ControllablePromise, createControllablePromise } from '../../utils/controllable-promise.js' +import { Deque } from '../../utils/deque.js' +import { getMarkedPeerId } from '../../utils/peer-utils.js' +import { ITelegramClient } from '../client.types.js' import { getPeerDialogs } from '../methods/dialogs/get-peer-dialogs.js' import { readHistory } from '../methods/messages/read-history.js' import { sendMedia } from '../methods/messages/send-media.js' @@ -14,7 +20,7 @@ import { ParametersSkip2 } from './utils.js' interface QueuedHandler { promise: ControllablePromise - check?: (update: T) => MaybeAsync + check?: (update: T) => MaybePromise timeout?: NodeJS.Timeout } @@ -54,7 +60,7 @@ export class Conversation { private _pendingRead: Map> = new Map() constructor( - readonly client: BaseTelegramClient, + readonly client: ITelegramClient, readonly chat: InputPeerLike, ) { if (!(CONVERSATION_SYMBOL in client)) { @@ -66,7 +72,7 @@ export class Conversation { } } - private static _getState(client: BaseTelegramClient): ConversationsState { + private static _getState(client: ITelegramClient): ConversationsState { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (client as any)[CONVERSATION_SYMBOL] as ConversationsState } @@ -77,7 +83,7 @@ export class Conversation { * * @returns `true` if the update was handled by some conversation */ - static handleUpdate(client: BaseTelegramClient, update: ParsedUpdate): boolean { + static handleUpdate(client: ITelegramClient, update: ParsedUpdate): boolean { const state = Conversation._getState(client) if (!state?.hasConversations) return false @@ -172,9 +178,6 @@ export class Conversation { } else { this._lastMessage = this._lastReceivedMessage = 0 } - this.client.on('new_message', this._onNewMessage) - this.client.on('edit_message', this._onEditMessage) - this.client.on('history_read', this._onHistoryRead) const state = Conversation._getState(this.client) @@ -191,10 +194,6 @@ export class Conversation { stop(): void { if (!this._started) return - this.client.off('new_message', this._onNewMessage) - this.client.off('edit_message', this._onEditMessage) - this.client.off('history_read', this._onHistoryRead) - const state = Conversation._getState(this.client) const pending = state.pendingConversations.get(this._chatId) @@ -305,7 +304,7 @@ export class Conversation { * * @param handler */ - async with(handler: () => MaybeAsync): Promise { + async with(handler: () => MaybePromise): Promise { await this.start() let err: unknown @@ -333,7 +332,7 @@ export class Conversation { * When the timeout is reached, `MtTimeoutError` is thrown. */ waitForNewMessage( - filter?: (msg: Message) => MaybeAsync, + filter?: (msg: Message) => MaybePromise, timeout: number | null = 15000, ): Promise { if (!this._started) { @@ -373,7 +372,7 @@ export class Conversation { * @param params */ waitForResponse( - filter?: (msg: Message) => MaybeAsync, + filter?: (msg: Message) => MaybePromise, params?: { /** * Message for which to wait for response for. @@ -409,7 +408,7 @@ export class Conversation { * @param params */ waitForReply( - filter?: (msg: Message) => MaybeAsync, + filter?: (msg: Message) => MaybePromise, params?: { /** * Message for which to wait for reply for. @@ -452,7 +451,7 @@ export class Conversation { * @param params */ async waitForEdit( - filter?: (msg: Message) => MaybeAsync, + filter?: (msg: Message) => MaybePromise, params?: { /** * Message for which to wait for reply for. @@ -568,7 +567,7 @@ export class Conversation { this._queuedNewMessage.popFront() } } catch (e: unknown) { - this.client._emitError(e) + this.client.emitError(e) } this._lastMessage = this._lastReceivedMessage = msg.id @@ -598,7 +597,7 @@ export class Conversation { this._pendingEditMessage.delete(msg.id) } })().catch((e) => { - this.client._emitError(e) + this.client.emitError(e) }) } diff --git a/packages/client/src/types/errors.ts b/packages/core/src/highlevel/types/errors.ts similarity index 95% rename from packages/client/src/types/errors.ts rename to packages/core/src/highlevel/types/errors.ts index 2f5e32f4..f2e93fe6 100644 --- a/packages/client/src/types/errors.ts +++ b/packages/core/src/highlevel/types/errors.ts @@ -1,5 +1,4 @@ -import { MtcuteError } from '@mtcute/core' - +import { MtcuteError } from '../../types/errors.js' import { InputPeerLike } from './peers/index.js' /** diff --git a/packages/client/src/types/files/file-location.ts b/packages/core/src/highlevel/types/files/file-location.ts similarity index 97% rename from packages/client/src/types/files/file-location.ts rename to packages/core/src/highlevel/types/files/file-location.ts index c31fd28b..e3745137 100644 --- a/packages/client/src/types/files/file-location.ts +++ b/packages/core/src/highlevel/types/files/file-location.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/files/index.ts b/packages/core/src/highlevel/types/files/index.ts similarity index 100% rename from packages/client/src/types/files/index.ts rename to packages/core/src/highlevel/types/files/index.ts diff --git a/packages/client/src/types/files/uploaded-file.ts b/packages/core/src/highlevel/types/files/uploaded-file.ts similarity index 95% rename from packages/client/src/types/files/uploaded-file.ts rename to packages/core/src/highlevel/types/files/uploaded-file.ts index a8118cb2..3e10a053 100644 --- a/packages/client/src/types/files/uploaded-file.ts +++ b/packages/core/src/highlevel/types/files/uploaded-file.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' /** * Describes a file uploaded to the Telegram servers diff --git a/packages/client/src/types/files/utils.ts b/packages/core/src/highlevel/types/files/utils.ts similarity index 99% rename from packages/client/src/types/files/utils.ts rename to packages/core/src/highlevel/types/files/utils.ts index f90ef73c..44042905 100644 --- a/packages/client/src/types/files/utils.ts +++ b/packages/core/src/highlevel/types/files/utils.ts @@ -1,8 +1,8 @@ /* eslint-disable no-restricted-imports */ import type { ReadStream } from 'fs' -import { tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' import { FileLocation } from './file-location.js' import { UploadedFile } from './uploaded-file.js' diff --git a/packages/client/src/types/files/web-document.ts b/packages/core/src/highlevel/types/files/web-document.ts similarity index 94% rename from packages/client/src/types/files/web-document.ts rename to packages/core/src/highlevel/types/files/web-document.ts index b24e6958..786c7467 100644 --- a/packages/client/src/types/files/web-document.ts +++ b/packages/core/src/highlevel/types/files/web-document.ts @@ -1,5 +1,6 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { FileLocation } from './file-location.js' diff --git a/packages/client/src/types/index.ts b/packages/core/src/highlevel/types/index.ts similarity index 100% rename from packages/client/src/types/index.ts rename to packages/core/src/highlevel/types/index.ts diff --git a/packages/client/src/types/media/audio.ts b/packages/core/src/highlevel/types/media/audio.ts similarity index 97% rename from packages/client/src/types/media/audio.ts rename to packages/core/src/highlevel/types/media/audio.ts index 1b201fb7..d2dd8764 100644 --- a/packages/client/src/types/media/audio.ts +++ b/packages/core/src/highlevel/types/media/audio.ts @@ -1,5 +1,5 @@ -import { tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/contact.ts b/packages/core/src/highlevel/types/media/contact.ts similarity index 96% rename from packages/client/src/types/media/contact.ts rename to packages/core/src/highlevel/types/media/contact.ts index 885e06d4..be15062b 100644 --- a/packages/client/src/types/media/contact.ts +++ b/packages/core/src/highlevel/types/media/contact.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/media/dice.ts b/packages/core/src/highlevel/types/media/dice.ts similarity index 99% rename from packages/client/src/types/media/dice.ts rename to packages/core/src/highlevel/types/media/dice.ts index 59b4180b..a1e86238 100644 --- a/packages/client/src/types/media/dice.ts +++ b/packages/core/src/highlevel/types/media/dice.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/media/document-utils.ts b/packages/core/src/highlevel/types/media/document-utils.ts similarity index 97% rename from packages/client/src/types/media/document-utils.ts rename to packages/core/src/highlevel/types/media/document-utils.ts index 644fca76..b1c88b0f 100644 --- a/packages/client/src/types/media/document-utils.ts +++ b/packages/core/src/highlevel/types/media/document-utils.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { Audio } from './audio.js' import { Document } from './document.js' diff --git a/packages/client/src/types/media/document.ts b/packages/core/src/highlevel/types/media/document.ts similarity index 99% rename from packages/client/src/types/media/document.ts rename to packages/core/src/highlevel/types/media/document.ts index 14a3431f..0597d3dd 100644 --- a/packages/client/src/types/media/document.ts +++ b/packages/core/src/highlevel/types/media/document.ts @@ -1,5 +1,5 @@ -import { tl } from '@mtcute/core' import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/game.ts b/packages/core/src/highlevel/types/media/game.ts similarity index 98% rename from packages/client/src/types/media/game.ts rename to packages/core/src/highlevel/types/media/game.ts index f6a1702b..3da8867e 100644 --- a/packages/client/src/types/media/game.ts +++ b/packages/core/src/highlevel/types/media/game.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/index.ts b/packages/core/src/highlevel/types/media/index.ts similarity index 100% rename from packages/client/src/types/media/index.ts rename to packages/core/src/highlevel/types/media/index.ts diff --git a/packages/client/src/types/media/input-media.ts b/packages/core/src/highlevel/types/media/input-media.ts similarity index 99% rename from packages/client/src/types/media/input-media.ts rename to packages/core/src/highlevel/types/media/input-media.ts index 03343eb0..d4e81c85 100644 --- a/packages/client/src/types/media/input-media.ts +++ b/packages/core/src/highlevel/types/media/input-media.ts @@ -1,5 +1,6 @@ -import { MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' import { InputText } from '../../types/misc/entities.js' import { InputFileLike } from '../files/index.js' import { InputPeerLike } from '../peers/index.js' diff --git a/packages/client/src/types/media/invoice.ts b/packages/core/src/highlevel/types/media/invoice.ts similarity index 98% rename from packages/client/src/types/media/invoice.ts rename to packages/core/src/highlevel/types/media/invoice.ts index 9e7d5155..92e3623d 100644 --- a/packages/client/src/types/media/invoice.ts +++ b/packages/core/src/highlevel/types/media/invoice.ts @@ -1,5 +1,6 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { WebDocument } from '../files/web-document.js' diff --git a/packages/client/src/types/media/location.ts b/packages/core/src/highlevel/types/media/location.ts similarity index 99% rename from packages/client/src/types/media/location.ts rename to packages/core/src/highlevel/types/media/location.ts index b64cf13b..b09ed6d1 100644 --- a/packages/client/src/types/media/location.ts +++ b/packages/core/src/highlevel/types/media/location.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { FileLocation } from '../files/index.js' diff --git a/packages/client/src/types/media/photo.ts b/packages/core/src/highlevel/types/media/photo.ts similarity index 98% rename from packages/client/src/types/media/photo.ts rename to packages/core/src/highlevel/types/media/photo.ts index df65c177..7e7d411a 100644 --- a/packages/client/src/types/media/photo.ts +++ b/packages/core/src/highlevel/types/media/photo.ts @@ -1,5 +1,6 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { FileLocation } from '../files/index.js' diff --git a/packages/client/src/types/media/poll.ts b/packages/core/src/highlevel/types/media/poll.ts similarity index 98% rename from packages/client/src/types/media/poll.ts rename to packages/core/src/highlevel/types/media/poll.ts index 0c0b1c21..7b8833dd 100644 --- a/packages/client/src/types/media/poll.ts +++ b/packages/core/src/highlevel/types/media/poll.ts @@ -1,4 +1,6 @@ -import { Long, tl } from '@mtcute/core' +import Long from 'long' + +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/sticker.ts b/packages/core/src/highlevel/types/media/sticker.ts similarity index 98% rename from packages/client/src/types/media/sticker.ts rename to packages/core/src/highlevel/types/media/sticker.ts index 9dcb7878..8a5f2604 100644 --- a/packages/client/src/types/media/sticker.ts +++ b/packages/core/src/highlevel/types/media/sticker.ts @@ -1,6 +1,7 @@ -import { MtArgumentError, tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { RawDocument } from './document.js' diff --git a/packages/client/src/types/media/story.ts b/packages/core/src/highlevel/types/media/story.ts similarity index 97% rename from packages/client/src/types/media/story.ts rename to packages/core/src/highlevel/types/media/story.ts index 57749d0e..845f5613 100644 --- a/packages/client/src/types/media/story.ts +++ b/packages/core/src/highlevel/types/media/story.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/inspectable.js' import { parsePeer, Peer } from '../peers/peer.js' diff --git a/packages/client/src/types/media/thumbnail.ts b/packages/core/src/highlevel/types/media/thumbnail.ts similarity index 97% rename from packages/client/src/types/media/thumbnail.ts rename to packages/core/src/highlevel/types/media/thumbnail.ts index c7b29ea5..a9d18421 100644 --- a/packages/client/src/types/media/thumbnail.ts +++ b/packages/core/src/highlevel/types/media/thumbnail.ts @@ -1,7 +1,10 @@ -import { Long, MtArgumentError, MtTypeAssertionError, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' -import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' +import Long from 'long' +import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { inflateSvgPath, strippedPhotoToJpg, svgPathToFile } from '../../utils/file-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/venue.ts b/packages/core/src/highlevel/types/media/venue.ts similarity index 96% rename from packages/client/src/types/media/venue.ts rename to packages/core/src/highlevel/types/media/venue.ts index 6efb74ad..374e57ad 100644 --- a/packages/client/src/types/media/venue.ts +++ b/packages/core/src/highlevel/types/media/venue.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { Location } from './location.js' diff --git a/packages/client/src/types/media/video.ts b/packages/core/src/highlevel/types/media/video.ts similarity index 98% rename from packages/client/src/types/media/video.ts rename to packages/core/src/highlevel/types/media/video.ts index 5fac890a..ba4274f9 100644 --- a/packages/client/src/types/media/video.ts +++ b/packages/core/src/highlevel/types/media/video.ts @@ -1,5 +1,5 @@ -import { tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/voice.ts b/packages/core/src/highlevel/types/media/voice.ts similarity index 97% rename from packages/client/src/types/media/voice.ts rename to packages/core/src/highlevel/types/media/voice.ts index 4e019a4c..006995bd 100644 --- a/packages/client/src/types/media/voice.ts +++ b/packages/core/src/highlevel/types/media/voice.ts @@ -1,5 +1,5 @@ -import { tl } from '@mtcute/core' import { tdFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/media/web-page.ts b/packages/core/src/highlevel/types/media/web-page.ts similarity index 98% rename from packages/client/src/types/media/web-page.ts rename to packages/core/src/highlevel/types/media/web-page.ts index 5ea177f9..456fee51 100644 --- a/packages/client/src/types/media/web-page.ts +++ b/packages/core/src/highlevel/types/media/web-page.ts @@ -1,5 +1,6 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { RawDocument } from './document.js' diff --git a/packages/client/src/types/messages/dialog.ts b/packages/core/src/highlevel/types/messages/dialog.ts similarity index 97% rename from packages/client/src/types/messages/dialog.ts rename to packages/core/src/highlevel/types/messages/dialog.ts index 50c40ef5..6b873dde 100644 --- a/packages/client/src/types/messages/dialog.ts +++ b/packages/core/src/highlevel/types/messages/dialog.ts @@ -1,6 +1,8 @@ -import { getMarkedPeerId, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { assertTypeIsNot, hasValueAtKey, makeInspectable } from '../../utils/index.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { assertTypeIsNot, hasValueAtKey } from '../../../utils/type-assertions.js' +import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { MtMessageNotFoundError } from '../errors.js' import { Chat } from '../peers/chat.js' diff --git a/packages/client/src/types/messages/draft-message.ts b/packages/core/src/highlevel/types/messages/draft-message.ts similarity index 97% rename from packages/client/src/types/messages/draft-message.ts rename to packages/core/src/highlevel/types/messages/draft-message.ts index 5de6b437..c5c36bec 100644 --- a/packages/client/src/types/messages/draft-message.ts +++ b/packages/core/src/highlevel/types/messages/draft-message.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/messages/index.ts b/packages/core/src/highlevel/types/messages/index.ts similarity index 100% rename from packages/client/src/types/messages/index.ts rename to packages/core/src/highlevel/types/messages/index.ts diff --git a/packages/client/src/types/messages/input-message-id.ts b/packages/core/src/highlevel/types/messages/input-message-id.ts similarity index 100% rename from packages/client/src/types/messages/input-message-id.ts rename to packages/core/src/highlevel/types/messages/input-message-id.ts diff --git a/packages/client/src/types/messages/message-action.ts b/packages/core/src/highlevel/types/messages/message-action.ts similarity index 99% rename from packages/client/src/types/messages/message-action.ts rename to packages/core/src/highlevel/types/messages/message-action.ts index a09d14c5..6fad2179 100644 --- a/packages/client/src/types/messages/message-action.ts +++ b/packages/core/src/highlevel/types/messages/message-action.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { _callDiscardReasonFromTl, CallDiscardReason } from '../calls/index.js' import { Photo } from '../media/photo.js' diff --git a/packages/client/src/types/messages/message-entity.ts b/packages/core/src/highlevel/types/messages/message-entity.ts similarity index 99% rename from packages/client/src/types/messages/message-entity.ts rename to packages/core/src/highlevel/types/messages/message-entity.ts index 7135e2dc..3d31679b 100644 --- a/packages/client/src/types/messages/message-entity.ts +++ b/packages/core/src/highlevel/types/messages/message-entity.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/messages/message-forward.ts b/packages/core/src/highlevel/types/messages/message-forward.ts similarity index 95% rename from packages/client/src/types/messages/message-forward.ts rename to packages/core/src/highlevel/types/messages/message-forward.ts index e79cbb23..ad30dfce 100644 --- a/packages/client/src/types/messages/message-forward.ts +++ b/packages/core/src/highlevel/types/messages/message-forward.ts @@ -1,5 +1,6 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat } from '../peers/chat.js' diff --git a/packages/client/src/types/messages/message-media.ts b/packages/core/src/highlevel/types/messages/message-media.ts similarity index 97% rename from packages/client/src/types/messages/message-media.ts rename to packages/core/src/highlevel/types/messages/message-media.ts index a07ee094..17ee96a2 100644 --- a/packages/client/src/types/messages/message-media.ts +++ b/packages/core/src/highlevel/types/messages/message-media.ts @@ -1,5 +1,6 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' import { Audio } from '../media/audio.js' import { Contact } from '../media/contact.js' import { Dice } from '../media/dice.js' diff --git a/packages/client/src/types/messages/message-reactions.ts b/packages/core/src/highlevel/types/messages/message-reactions.ts similarity index 97% rename from packages/client/src/types/messages/message-reactions.ts rename to packages/core/src/highlevel/types/messages/message-reactions.ts index 371f21d0..a8438e71 100644 --- a/packages/client/src/types/messages/message-reactions.ts +++ b/packages/core/src/highlevel/types/messages/message-reactions.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/messages/message-replies.ts b/packages/core/src/highlevel/types/messages/message-replies.ts similarity index 95% rename from packages/client/src/types/messages/message-replies.ts rename to packages/core/src/highlevel/types/messages/message-replies.ts index 652385be..7f7b6547 100644 --- a/packages/client/src/types/messages/message-replies.ts +++ b/packages/core/src/highlevel/types/messages/message-replies.ts @@ -1,5 +1,6 @@ -import { getMarkedPeerId, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' import { parsePeer, Peer } from '../peers/peer.js' diff --git a/packages/client/src/types/messages/message.ts b/packages/core/src/highlevel/types/messages/message.ts similarity index 97% rename from packages/client/src/types/messages/message.ts rename to packages/core/src/highlevel/types/messages/message.ts index fa23ee3b..38d0e516 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/core/src/highlevel/types/messages/message.ts @@ -1,6 +1,9 @@ -import { assertNever, getMarkedPeerId, MtArgumentError, tl, toggleChannelIdMark } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { assertNever } from '../../../types/utils.js' +import { getMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils.js' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { BotKeyboard, ReplyMarkup } from '../bots/keyboards.js' diff --git a/packages/client/src/types/messages/replied-message.ts b/packages/core/src/highlevel/types/messages/replied-message.ts similarity index 98% rename from packages/client/src/types/messages/replied-message.ts rename to packages/core/src/highlevel/types/messages/replied-message.ts index 13b3b517..dfa45d85 100644 --- a/packages/client/src/types/messages/replied-message.ts +++ b/packages/core/src/highlevel/types/messages/replied-message.ts @@ -1,5 +1,6 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat } from '../peers/chat.js' diff --git a/packages/client/src/types/messages/search-filters.ts b/packages/core/src/highlevel/types/messages/search-filters.ts similarity index 100% rename from packages/client/src/types/messages/search-filters.ts rename to packages/core/src/highlevel/types/messages/search-filters.ts diff --git a/packages/client/src/types/misc/entities.ts b/packages/core/src/highlevel/types/misc/entities.ts similarity index 91% rename from packages/client/src/types/misc/entities.ts rename to packages/core/src/highlevel/types/misc/entities.ts index 363a695a..ae9800e2 100644 --- a/packages/client/src/types/misc/entities.ts +++ b/packages/core/src/highlevel/types/misc/entities.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' /** * Formatted text with entities diff --git a/packages/client/src/types/misc/index.ts b/packages/core/src/highlevel/types/misc/index.ts similarity index 100% rename from packages/client/src/types/misc/index.ts rename to packages/core/src/highlevel/types/misc/index.ts diff --git a/packages/client/src/types/misc/input-privacy-rule.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule.ts similarity index 97% rename from packages/client/src/types/misc/input-privacy-rule.ts rename to packages/core/src/highlevel/types/misc/input-privacy-rule.ts index a1ffb5a2..952fe85f 100644 --- a/packages/client/src/types/misc/input-privacy-rule.ts +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MaybeArray } from '../../../types/utils.js' import { InputPeerLike } from '../peers/index.js' export interface InputPrivacyRuleUsers { diff --git a/packages/client/src/types/misc/sticker-set.ts b/packages/core/src/highlevel/types/misc/sticker-set.ts similarity index 98% rename from packages/client/src/types/misc/sticker-set.ts rename to packages/core/src/highlevel/types/misc/sticker-set.ts index 55de608c..4b249dff 100644 --- a/packages/client/src/types/misc/sticker-set.ts +++ b/packages/core/src/highlevel/types/misc/sticker-set.ts @@ -1,6 +1,7 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' -import { LongMap } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' +import { LongMap } from '../../../utils/long-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { MtEmptyError } from '../errors.js' diff --git a/packages/client/src/types/misc/takeout-session.ts b/packages/core/src/highlevel/types/misc/takeout-session.ts similarity index 86% rename from packages/client/src/types/misc/takeout-session.ts rename to packages/core/src/highlevel/types/misc/takeout-session.ts index b29caf21..137d2cd9 100644 --- a/packages/client/src/types/misc/takeout-session.ts +++ b/packages/core/src/highlevel/types/misc/takeout-session.ts @@ -1,6 +1,10 @@ -import { BaseTelegramClient, MustEqual, RpcCallOptions, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { assertTrue, makeInspectable } from '../../utils/index.js' +import { RpcCallOptions } from '../../../network/network-manager.js' +import { MustEqual } from '../../../types/utils.js' +import { assertTrue } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { makeInspectable } from '../../utils/index.js' /** * Account takeout session @@ -12,7 +16,7 @@ export class TakeoutSession { readonly id: tl.Long constructor( - readonly client: BaseTelegramClient, + readonly client: ITelegramClient, session: tl.account.RawTakeout, ) { this.id = session.id @@ -60,7 +64,7 @@ export class TakeoutSession { * that call should be called via takeout session or not. * Returning `true` will use takeout session, `false` will not. */ - createProxy(predicate?: (obj: tl.TlObject) => boolean): BaseTelegramClient { + createProxy(predicate?: (obj: tl.TlObject) => boolean): ITelegramClient { const boundCall: TakeoutSession['call'] = predicate ? (obj, params) => { if (predicate(obj)) { diff --git a/packages/client/src/types/peers/chat-colors.ts b/packages/core/src/highlevel/types/peers/chat-colors.ts similarity index 96% rename from packages/client/src/types/peers/chat-colors.ts rename to packages/core/src/highlevel/types/peers/chat-colors.ts index db6a1150..66872463 100644 --- a/packages/client/src/types/peers/chat-colors.ts +++ b/packages/core/src/highlevel/types/peers/chat-colors.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/inspectable.js' diff --git a/packages/client/src/types/peers/chat-event/actions.ts b/packages/core/src/highlevel/types/peers/chat-event/actions.ts similarity index 99% rename from packages/client/src/types/peers/chat-event/actions.ts rename to packages/core/src/highlevel/types/peers/chat-event/actions.ts index 854cf03d..cbfced39 100644 --- a/packages/client/src/types/peers/chat-event/actions.ts +++ b/packages/core/src/highlevel/types/peers/chat-event/actions.ts @@ -1,6 +1,7 @@ -import { tl, toggleChannelIdMark } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { toggleChannelIdMark } from '../../../../utils/peer-utils.js' +import { assertTypeIs } from '../../../../utils/type-assertions.js' import { Photo } from '../../media/photo.js' import { Message } from '../../messages/message.js' import { ChatInviteLink } from '../chat-invite-link.js' diff --git a/packages/client/src/types/peers/chat-event/filters.ts b/packages/core/src/highlevel/types/peers/chat-event/filters.ts similarity index 97% rename from packages/client/src/types/peers/chat-event/filters.ts rename to packages/core/src/highlevel/types/peers/chat-event/filters.ts index 336ab2fd..9bb21c98 100644 --- a/packages/client/src/types/peers/chat-event/filters.ts +++ b/packages/core/src/highlevel/types/peers/chat-event/filters.ts @@ -1,5 +1,6 @@ -import { assertNever, MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { assertNever, MaybeArray } from '../../../../types/utils.js' import { ChatAction } from './actions.js' export interface ChatEventFilters { diff --git a/packages/client/src/types/peers/chat-event/index.ts b/packages/core/src/highlevel/types/peers/chat-event/index.ts similarity index 97% rename from packages/client/src/types/peers/chat-event/index.ts rename to packages/core/src/highlevel/types/peers/chat-event/index.ts index 35e53a7b..718cb7e7 100644 --- a/packages/client/src/types/peers/chat-event/index.ts +++ b/packages/core/src/highlevel/types/peers/chat-event/index.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../../utils/index.js' import { memoizeGetters } from '../../../utils/memoize.js' diff --git a/packages/client/src/types/peers/chat-invite-link-member.ts b/packages/core/src/highlevel/types/peers/chat-invite-link-member.ts similarity index 97% rename from packages/client/src/types/peers/chat-invite-link-member.ts rename to packages/core/src/highlevel/types/peers/chat-invite-link-member.ts index 9761e864..e2cac291 100644 --- a/packages/client/src/types/peers/chat-invite-link-member.ts +++ b/packages/core/src/highlevel/types/peers/chat-invite-link-member.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/peers/chat-invite-link.ts b/packages/core/src/highlevel/types/peers/chat-invite-link.ts similarity index 96% rename from packages/client/src/types/peers/chat-invite-link.ts rename to packages/core/src/highlevel/types/peers/chat-invite-link.ts index 52b689bc..1400853b 100644 --- a/packages/client/src/types/peers/chat-invite-link.ts +++ b/packages/core/src/highlevel/types/peers/chat-invite-link.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIsNot } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { PeersIndex } from './index.js' @@ -17,6 +17,7 @@ export class ChatInviteLink { readonly _peers?: PeersIndex, ) { assertTypeIsNot('ChatInviteLink', raw, 'chatInvitePublicJoinRequests') + this.raw = raw } diff --git a/packages/client/src/types/peers/chat-location.ts b/packages/core/src/highlevel/types/peers/chat-location.ts similarity index 94% rename from packages/client/src/types/peers/chat-location.ts rename to packages/core/src/highlevel/types/peers/chat-location.ts index 24e8a27c..9de0f0ac 100644 --- a/packages/client/src/types/peers/chat-location.ts +++ b/packages/core/src/highlevel/types/peers/chat-location.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/peers/chat-member.ts b/packages/core/src/highlevel/types/peers/chat-member.ts similarity index 98% rename from packages/client/src/types/peers/chat-member.ts rename to packages/core/src/highlevel/types/peers/chat-member.ts index b864e139..92269535 100644 --- a/packages/client/src/types/peers/chat-member.ts +++ b/packages/core/src/highlevel/types/peers/chat-member.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { ChatPermissions } from './chat-permissions.js' diff --git a/packages/client/src/types/peers/chat-permissions.ts b/packages/core/src/highlevel/types/peers/chat-permissions.ts similarity index 99% rename from packages/client/src/types/peers/chat-permissions.ts rename to packages/core/src/highlevel/types/peers/chat-permissions.ts index 55182a37..e4c6b252 100644 --- a/packages/client/src/types/peers/chat-permissions.ts +++ b/packages/core/src/highlevel/types/peers/chat-permissions.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/peers/chat-photo.ts b/packages/core/src/highlevel/types/peers/chat-photo.ts similarity index 95% rename from packages/client/src/types/peers/chat-photo.ts rename to packages/core/src/highlevel/types/peers/chat-photo.ts index 3ee3a2f3..eb6d34b1 100644 --- a/packages/client/src/types/peers/chat-photo.ts +++ b/packages/core/src/highlevel/types/peers/chat-photo.ts @@ -1,6 +1,10 @@ -import { Long, MtArgumentError, tl, toggleChannelIdMark } from '@mtcute/core' -import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id' +import Long from 'long' +import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { MtArgumentError } from '../../../types/errors.js' +import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { strippedPhotoToJpg } from '../../utils/file-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/peers/chat-preview.ts b/packages/core/src/highlevel/types/peers/chat-preview.ts similarity index 98% rename from packages/client/src/types/peers/chat-preview.ts rename to packages/core/src/highlevel/types/peers/chat-preview.ts index 0750cbad..b47255fd 100644 --- a/packages/client/src/types/peers/chat-preview.ts +++ b/packages/core/src/highlevel/types/peers/chat-preview.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/peers/chat.ts b/packages/core/src/highlevel/types/peers/chat.ts similarity index 99% rename from packages/client/src/types/peers/chat.ts rename to packages/core/src/highlevel/types/peers/chat.ts index da74966c..e82dba5a 100644 --- a/packages/client/src/types/peers/chat.ts +++ b/packages/core/src/highlevel/types/peers/chat.ts @@ -1,5 +1,7 @@ -import { getMarkedPeerId, MtArgumentError, MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { Photo } from '../media/photo.js' diff --git a/packages/client/src/types/peers/forum-topic.ts b/packages/core/src/highlevel/types/peers/forum-topic.ts similarity index 96% rename from packages/client/src/types/peers/forum-topic.ts rename to packages/core/src/highlevel/types/peers/forum-topic.ts index b9726caf..e5409126 100644 --- a/packages/client/src/types/peers/forum-topic.ts +++ b/packages/core/src/highlevel/types/peers/forum-topic.ts @@ -1,6 +1,7 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { hasValueAtKey, makeInspectable } from '../../utils/index.js' +import { hasValueAtKey } from '../../../utils/type-assertions.js' +import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { MtMessageNotFoundError } from '../errors.js' import { DraftMessage, Message } from '../messages/index.js' diff --git a/packages/client/src/types/peers/index.ts b/packages/core/src/highlevel/types/peers/index.ts similarity index 100% rename from packages/client/src/types/peers/index.ts rename to packages/core/src/highlevel/types/peers/index.ts diff --git a/packages/client/src/types/peers/peer.ts b/packages/core/src/highlevel/types/peers/peer.ts similarity index 98% rename from packages/client/src/types/peers/peer.ts rename to packages/core/src/highlevel/types/peers/peer.ts index f3e1f1b7..1169d33a 100644 --- a/packages/client/src/types/peers/peer.ts +++ b/packages/core/src/highlevel/types/peers/peer.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { Chat } from './chat.js' import { PeersIndex } from './peers-index.js' diff --git a/packages/client/src/types/peers/peers-index.test.ts b/packages/core/src/highlevel/types/peers/peers-index.test.ts similarity index 98% rename from packages/client/src/types/peers/peers-index.test.ts rename to packages/core/src/highlevel/types/peers/peers-index.test.ts index 56749751..238cdb2e 100644 --- a/packages/client/src/types/peers/peers-index.test.ts +++ b/packages/core/src/highlevel/types/peers/peers-index.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' -import { MtArgumentError } from '@mtcute/core/src/index.js' import { createStub } from '@mtcute/test' +import { MtArgumentError } from '../../../types/errors.js' import { PeersIndex } from './peers-index.js' describe('PeersIndex', () => { diff --git a/packages/client/src/types/peers/peers-index.ts b/packages/core/src/highlevel/types/peers/peers-index.ts similarity index 87% rename from packages/client/src/types/peers/peers-index.ts rename to packages/core/src/highlevel/types/peers/peers-index.ts index 14401ff5..898b1891 100644 --- a/packages/client/src/types/peers/peers-index.ts +++ b/packages/core/src/highlevel/types/peers/peers-index.ts @@ -1,10 +1,14 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' + +import { MtArgumentError } from '../../../types/errors.js' const ERROR_MSG = 'Given peer is not available in this index. This is most likely an internal library error.' export class PeersIndex { - readonly users: Map = new Map() - readonly chats: Map = new Map() + constructor( + readonly users: Map = new Map(), + readonly chats: Map = new Map(), + ) {} hasMin = false diff --git a/packages/client/src/types/peers/typing-status.ts b/packages/core/src/highlevel/types/peers/typing-status.ts similarity index 81% rename from packages/client/src/types/peers/typing-status.ts rename to packages/core/src/highlevel/types/peers/typing-status.ts index fb411a11..76ce9051 100644 --- a/packages/client/src/types/peers/typing-status.ts +++ b/packages/core/src/highlevel/types/peers/typing-status.ts @@ -17,9 +17,11 @@ * - `game`: User is playing a game * - `record_round`: User is recording a round video message * - `upload_round`: User is uploading a round video message - * - `speak_call`: *undocumented* User is speaking in a group call - * - `history_import`: *undocumented* User is importing history + * - `speak_call`: User is speaking in a group call + * - `history_import`: User is importing history * - `sticker`: User is choosing a sticker + * - `interaction`: User has sent an emoji interaction + * - `interaction_seen`: User is watching a previously sent emoji interaction */ export type TypingStatus = | 'typing' @@ -38,3 +40,5 @@ export type TypingStatus = | 'speak_call' | 'history_import' | 'sticker' + | 'interaction' + | 'interaction_seen' diff --git a/packages/client/src/types/peers/user.test.ts b/packages/core/src/highlevel/types/peers/user.test.ts similarity index 99% rename from packages/client/src/types/peers/user.test.ts rename to packages/core/src/highlevel/types/peers/user.test.ts index cd62aa67..a83e473a 100644 --- a/packages/client/src/types/peers/user.test.ts +++ b/packages/core/src/highlevel/types/peers/user.test.ts @@ -1,6 +1,6 @@ +import Long from 'long' import { describe, expect, it } from 'vitest' -import { Long } from '@mtcute/core' import { createStub } from '@mtcute/test' import { MessageEntity } from '../messages/index.js' diff --git a/packages/client/src/types/peers/user.ts b/packages/core/src/highlevel/types/peers/user.ts similarity index 98% rename from packages/client/src/types/peers/user.ts rename to packages/core/src/highlevel/types/peers/user.ts index e27d3548..9f3b5364 100644 --- a/packages/client/src/types/peers/user.ts +++ b/packages/core/src/highlevel/types/peers/user.ts @@ -1,6 +1,7 @@ -import { MtArgumentError, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { MtArgumentError } from '../../../types/errors.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { MessageEntity } from '../messages/message-entity.js' diff --git a/packages/client/src/types/premium/boost-slot.ts b/packages/core/src/highlevel/types/premium/boost-slot.ts similarity index 94% rename from packages/client/src/types/premium/boost-slot.ts rename to packages/core/src/highlevel/types/premium/boost-slot.ts index aa8337bb..1a4ea2bb 100644 --- a/packages/client/src/types/premium/boost-slot.ts +++ b/packages/core/src/highlevel/types/premium/boost-slot.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat, PeersIndex } from '../peers/index.js' diff --git a/packages/client/src/types/premium/boost-stats.ts b/packages/core/src/highlevel/types/premium/boost-stats.ts similarity index 98% rename from packages/client/src/types/premium/boost-stats.ts rename to packages/core/src/highlevel/types/premium/boost-stats.ts index 2db47914..74c7aa2d 100644 --- a/packages/client/src/types/premium/boost-stats.ts +++ b/packages/core/src/highlevel/types/premium/boost-stats.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/premium/boost.ts b/packages/core/src/highlevel/types/premium/boost.ts similarity index 96% rename from packages/client/src/types/premium/boost.ts rename to packages/core/src/highlevel/types/premium/boost.ts index 9b0ff52c..8ccccbee 100644 --- a/packages/client/src/types/premium/boost.ts +++ b/packages/core/src/highlevel/types/premium/boost.ts @@ -1,5 +1,6 @@ -import { MtUnsupportedError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtUnsupportedError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { PeersIndex, User } from '../peers/index.js' diff --git a/packages/client/src/types/premium/index.ts b/packages/core/src/highlevel/types/premium/index.ts similarity index 100% rename from packages/client/src/types/premium/index.ts rename to packages/core/src/highlevel/types/premium/index.ts diff --git a/packages/client/src/types/reactions/emoji-status.ts b/packages/core/src/highlevel/types/reactions/emoji-status.ts similarity index 94% rename from packages/client/src/types/reactions/emoji-status.ts rename to packages/core/src/highlevel/types/reactions/emoji-status.ts index a95761c2..cea50467 100644 --- a/packages/client/src/types/reactions/emoji-status.ts +++ b/packages/core/src/highlevel/types/reactions/emoji-status.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/reactions/index.ts b/packages/core/src/highlevel/types/reactions/index.ts similarity index 100% rename from packages/client/src/types/reactions/index.ts rename to packages/core/src/highlevel/types/reactions/index.ts diff --git a/packages/client/src/types/reactions/peer-reaction.ts b/packages/core/src/highlevel/types/reactions/peer-reaction.ts similarity index 88% rename from packages/client/src/types/reactions/peer-reaction.ts rename to packages/core/src/highlevel/types/reactions/peer-reaction.ts index f819fd3e..61753d25 100644 --- a/packages/client/src/types/reactions/peer-reaction.ts +++ b/packages/core/src/highlevel/types/reactions/peer-reaction.ts @@ -1,6 +1,7 @@ -import { getMarkedPeerId, tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { PeersIndex } from '../peers/peers-index.js' diff --git a/packages/client/src/types/reactions/reaction-count.ts b/packages/core/src/highlevel/types/reactions/reaction-count.ts similarity index 95% rename from packages/client/src/types/reactions/reaction-count.ts rename to packages/core/src/highlevel/types/reactions/reaction-count.ts index 3cef298e..09b6e1a4 100644 --- a/packages/client/src/types/reactions/reaction-count.ts +++ b/packages/core/src/highlevel/types/reactions/reaction-count.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { ReactionEmoji, toReactionEmoji } from './types.js' diff --git a/packages/client/src/types/reactions/types.ts b/packages/core/src/highlevel/types/reactions/types.ts similarity index 92% rename from packages/client/src/types/reactions/types.ts rename to packages/core/src/highlevel/types/reactions/types.ts index 96261992..f3ef305d 100644 --- a/packages/client/src/types/reactions/types.ts +++ b/packages/core/src/highlevel/types/reactions/types.ts @@ -1,4 +1,8 @@ -import { Long, MtTypeAssertionError, tl } from '@mtcute/core' +import Long from 'long' + +import { tl } from '@mtcute/tl' + +import { MtTypeAssertionError } from '../../../types/errors.js' /** * Input version of {@link ReactionEmoji}, which also accepts bare TL object diff --git a/packages/client/src/types/stories/all-stories.ts b/packages/core/src/highlevel/types/stories/all-stories.ts similarity index 97% rename from packages/client/src/types/stories/all-stories.ts rename to packages/core/src/highlevel/types/stories/all-stories.ts index 23f3747d..24cf549b 100644 --- a/packages/client/src/types/stories/all-stories.ts +++ b/packages/core/src/highlevel/types/stories/all-stories.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/stories/index.ts b/packages/core/src/highlevel/types/stories/index.ts similarity index 100% rename from packages/client/src/types/stories/index.ts rename to packages/core/src/highlevel/types/stories/index.ts diff --git a/packages/client/src/types/stories/interactive/base.ts b/packages/core/src/highlevel/types/stories/interactive/base.ts similarity index 84% rename from packages/client/src/types/stories/interactive/base.ts rename to packages/core/src/highlevel/types/stories/interactive/base.ts index 823ebbbe..ea96429b 100644 --- a/packages/client/src/types/stories/interactive/base.ts +++ b/packages/core/src/highlevel/types/stories/interactive/base.ts @@ -1,9 +1,9 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' export abstract class StoryInteractiveArea { abstract type: string - constructor(readonly raw: Exclude) { + constructor(readonly raw: tl.TypeMediaArea) { this.raw = raw } diff --git a/packages/client/src/types/stories/interactive/channel-post.ts b/packages/core/src/highlevel/types/stories/interactive/channel-post.ts similarity index 96% rename from packages/client/src/types/stories/interactive/channel-post.ts rename to packages/core/src/highlevel/types/stories/interactive/channel-post.ts index a53c6447..a0c62fd5 100644 --- a/packages/client/src/types/stories/interactive/channel-post.ts +++ b/packages/core/src/highlevel/types/stories/interactive/channel-post.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../../utils/index.js' import { memoizeGetters } from '../../../utils/memoize.js' diff --git a/packages/client/src/types/stories/interactive/index.ts b/packages/core/src/highlevel/types/stories/interactive/index.ts similarity index 92% rename from packages/client/src/types/stories/interactive/index.ts rename to packages/core/src/highlevel/types/stories/interactive/index.ts index e7b7aa9a..d1826192 100644 --- a/packages/client/src/types/stories/interactive/index.ts +++ b/packages/core/src/highlevel/types/stories/interactive/index.ts @@ -1,5 +1,6 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../../types/errors.js' import { PeersIndex } from '../../peers/index.js' import { StoryInteractiveChannelPost } from './channel-post.js' import { StoryInteractiveLocation } from './location.js' diff --git a/packages/client/src/types/stories/interactive/input.ts b/packages/core/src/highlevel/types/stories/interactive/input.ts similarity index 98% rename from packages/client/src/types/stories/interactive/input.ts rename to packages/core/src/highlevel/types/stories/interactive/input.ts index 60000b0c..4bcef23c 100644 --- a/packages/client/src/types/stories/interactive/input.ts +++ b/packages/core/src/highlevel/types/stories/interactive/input.ts @@ -1,4 +1,6 @@ -import { Long, tl } from '@mtcute/core' +import Long from 'long' + +import { tl } from '@mtcute/tl' import { VenueSource } from '../../media/index.js' import { InputReaction, normalizeInputReaction } from '../../reactions/index.js' diff --git a/packages/client/src/types/stories/interactive/location.ts b/packages/core/src/highlevel/types/stories/interactive/location.ts similarity index 88% rename from packages/client/src/types/stories/interactive/location.ts rename to packages/core/src/highlevel/types/stories/interactive/location.ts index ad846dea..2108e3c1 100644 --- a/packages/client/src/types/stories/interactive/location.ts +++ b/packages/core/src/highlevel/types/stories/interactive/location.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../../utils/type-assertions.js' import { makeInspectable } from '../../../utils/index.js' import { memoizeGetters } from '../../../utils/memoize.js' import { Location } from '../../media/location.js' diff --git a/packages/client/src/types/stories/interactive/reaction.ts b/packages/core/src/highlevel/types/stories/interactive/reaction.ts similarity index 96% rename from packages/client/src/types/stories/interactive/reaction.ts rename to packages/core/src/highlevel/types/stories/interactive/reaction.ts index 5ba50ca9..51facf4e 100644 --- a/packages/client/src/types/stories/interactive/reaction.ts +++ b/packages/core/src/highlevel/types/stories/interactive/reaction.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../../utils/index.js' import { ReactionEmoji, toReactionEmoji } from '../../reactions/index.js' diff --git a/packages/client/src/types/stories/interactive/venue.ts b/packages/core/src/highlevel/types/stories/interactive/venue.ts similarity index 93% rename from packages/client/src/types/stories/interactive/venue.ts rename to packages/core/src/highlevel/types/stories/interactive/venue.ts index 0bc8aebb..1845c1b4 100644 --- a/packages/client/src/types/stories/interactive/venue.ts +++ b/packages/core/src/highlevel/types/stories/interactive/venue.ts @@ -1,6 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIs } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../../utils/type-assertions.js' import { makeInspectable } from '../../../utils/index.js' import { memoizeGetters } from '../../../utils/memoize.js' import { Location } from '../../media/location.js' diff --git a/packages/client/src/types/stories/peer-stories.ts b/packages/core/src/highlevel/types/stories/peer-stories.ts similarity index 85% rename from packages/client/src/types/stories/peer-stories.ts rename to packages/core/src/highlevel/types/stories/peer-stories.ts index 11dc6fd9..8216d623 100644 --- a/packages/client/src/types/stories/peer-stories.ts +++ b/packages/core/src/highlevel/types/stories/peer-stories.ts @@ -1,6 +1,7 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' -import { assertTypeIs, makeInspectable } from '../../utils/index.js' +import { assertTypeIs } from '../../../utils/type-assertions.js' +import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { parsePeer, Peer, PeersIndex } from '../peers/index.js' import { Story } from './story.js' diff --git a/packages/client/src/types/stories/stealth-mode.ts b/packages/core/src/highlevel/types/stories/stealth-mode.ts similarity index 94% rename from packages/client/src/types/stories/stealth-mode.ts rename to packages/core/src/highlevel/types/stories/stealth-mode.ts index 49c8fc30..a56cfb1a 100644 --- a/packages/client/src/types/stories/stealth-mode.ts +++ b/packages/core/src/highlevel/types/stories/stealth-mode.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/stories/story-interactions.ts b/packages/core/src/highlevel/types/stories/story-interactions.ts similarity index 98% rename from packages/client/src/types/stories/story-interactions.ts rename to packages/core/src/highlevel/types/stories/story-interactions.ts index cf1fcee3..2ba88aca 100644 --- a/packages/client/src/types/stories/story-interactions.ts +++ b/packages/core/src/highlevel/types/stories/story-interactions.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/stories/story-viewer.ts b/packages/core/src/highlevel/types/stories/story-viewer.ts similarity index 97% rename from packages/client/src/types/stories/story-viewer.ts rename to packages/core/src/highlevel/types/stories/story-viewer.ts index 45a7736e..e20afddf 100644 --- a/packages/client/src/types/stories/story-viewer.ts +++ b/packages/core/src/highlevel/types/stories/story-viewer.ts @@ -1,5 +1,6 @@ -import { MtTypeAssertionError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtTypeAssertionError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { Message } from '../messages/index.js' diff --git a/packages/client/src/types/stories/story.ts b/packages/core/src/highlevel/types/stories/story.ts similarity index 98% rename from packages/client/src/types/stories/story.ts rename to packages/core/src/highlevel/types/stories/story.ts index d0eda855..948e0696 100644 --- a/packages/client/src/types/stories/story.ts +++ b/packages/core/src/highlevel/types/stories/story.ts @@ -1,5 +1,6 @@ -import { MtUnsupportedError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtUnsupportedError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { parseDocument } from '../media/document-utils.js' diff --git a/packages/client/src/types/updates/bot-chat-join-request.ts b/packages/core/src/highlevel/types/updates/bot-chat-join-request.ts similarity index 93% rename from packages/client/src/types/updates/bot-chat-join-request.ts rename to packages/core/src/highlevel/types/updates/bot-chat-join-request.ts index 2f285ae1..0f359199 100644 --- a/packages/client/src/types/updates/bot-chat-join-request.ts +++ b/packages/core/src/highlevel/types/updates/bot-chat-join-request.ts @@ -1,5 +1,6 @@ -import { getBarePeerId, getMarkedPeerId, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { getBarePeerId, getMarkedPeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat, ChatInviteLink, PeersIndex, User } from '../peers/index.js' diff --git a/packages/client/src/types/updates/bot-reaction.ts b/packages/core/src/highlevel/types/updates/bot-reaction.ts similarity index 98% rename from packages/client/src/types/updates/bot-reaction.ts rename to packages/core/src/highlevel/types/updates/bot-reaction.ts index dc76bfd0..56ec38d8 100644 --- a/packages/client/src/types/updates/bot-reaction.ts +++ b/packages/core/src/highlevel/types/updates/bot-reaction.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/bot-stopped.ts b/packages/core/src/highlevel/types/updates/bot-stopped.ts similarity index 96% rename from packages/client/src/types/updates/bot-stopped.ts rename to packages/core/src/highlevel/types/updates/bot-stopped.ts index ae4cc328..3c99fd64 100644 --- a/packages/client/src/types/updates/bot-stopped.ts +++ b/packages/core/src/highlevel/types/updates/bot-stopped.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/callback-query.ts b/packages/core/src/highlevel/types/updates/callback-query.ts similarity index 95% rename from packages/client/src/types/updates/callback-query.ts rename to packages/core/src/highlevel/types/updates/callback-query.ts index a3bd9a3c..e4f165be 100644 --- a/packages/client/src/types/updates/callback-query.ts +++ b/packages/core/src/highlevel/types/updates/callback-query.ts @@ -1,6 +1,8 @@ -import { MtArgumentError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { utf8Decode } from '@mtcute/tl-runtime' -import { makeInspectable, utf8Decode } from '../../utils/index.js' +import { MtArgumentError } from '../../../types/errors.js' +import { makeInspectable } from '../../utils/index.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat } from '../peers/chat.js' diff --git a/packages/client/src/types/updates/chat-join-request.ts b/packages/core/src/highlevel/types/updates/chat-join-request.ts similarity index 93% rename from packages/client/src/types/updates/chat-join-request.ts rename to packages/core/src/highlevel/types/updates/chat-join-request.ts index 49d2904b..fc36c034 100644 --- a/packages/client/src/types/updates/chat-join-request.ts +++ b/packages/core/src/highlevel/types/updates/chat-join-request.ts @@ -1,5 +1,6 @@ -import { getBarePeerId, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { getBarePeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { PeersIndex, User } from '../peers/index.js' diff --git a/packages/client/src/types/updates/chat-member-update.ts b/packages/core/src/highlevel/types/updates/chat-member-update.ts similarity index 98% rename from packages/client/src/types/updates/chat-member-update.ts rename to packages/core/src/highlevel/types/updates/chat-member-update.ts index abe15528..177ae32d 100644 --- a/packages/client/src/types/updates/chat-member-update.ts +++ b/packages/core/src/highlevel/types/updates/chat-member-update.ts @@ -1,5 +1,6 @@ -import { getMarkedPeerId, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat } from '../peers/chat.js' diff --git a/packages/client/src/types/updates/chosen-inline-result.ts b/packages/core/src/highlevel/types/updates/chosen-inline-result.ts similarity index 98% rename from packages/client/src/types/updates/chosen-inline-result.ts rename to packages/core/src/highlevel/types/updates/chosen-inline-result.ts index 36d846cf..5c8e32bd 100644 --- a/packages/client/src/types/updates/chosen-inline-result.ts +++ b/packages/core/src/highlevel/types/updates/chosen-inline-result.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js' diff --git a/packages/client/src/types/updates/delete-message-update.ts b/packages/core/src/highlevel/types/updates/delete-message-update.ts similarity index 86% rename from packages/client/src/types/updates/delete-message-update.ts rename to packages/core/src/highlevel/types/updates/delete-message-update.ts index 9357f4db..86b7a404 100644 --- a/packages/client/src/types/updates/delete-message-update.ts +++ b/packages/core/src/highlevel/types/updates/delete-message-update.ts @@ -1,5 +1,6 @@ -import { tl, toggleChannelIdMark } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' /** diff --git a/packages/client/src/types/updates/delete-story-update.ts b/packages/core/src/highlevel/types/updates/delete-story-update.ts similarity index 95% rename from packages/client/src/types/updates/delete-story-update.ts rename to packages/core/src/highlevel/types/updates/delete-story-update.ts index 1075d663..c163fc6c 100644 --- a/packages/client/src/types/updates/delete-story-update.ts +++ b/packages/core/src/highlevel/types/updates/delete-story-update.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { parsePeer, Peer, PeersIndex } from '../../types/peers/index.js' import { makeInspectable } from '../../utils/index.js' diff --git a/packages/client/src/types/updates/history-read-update.ts b/packages/core/src/highlevel/types/updates/history-read-update.ts similarity index 96% rename from packages/client/src/types/updates/history-read-update.ts rename to packages/core/src/highlevel/types/updates/history-read-update.ts index 434c8c96..53ecaf1d 100644 --- a/packages/client/src/types/updates/history-read-update.ts +++ b/packages/core/src/highlevel/types/updates/history-read-update.ts @@ -1,5 +1,6 @@ -import { getMarkedPeerId, tl, toggleChannelIdMark } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' export class HistoryReadUpdate { diff --git a/packages/client/src/types/updates/index.ts b/packages/core/src/highlevel/types/updates/index.ts similarity index 100% rename from packages/client/src/types/updates/index.ts rename to packages/core/src/highlevel/types/updates/index.ts diff --git a/packages/client/src/types/updates/inline-query.ts b/packages/core/src/highlevel/types/updates/inline-query.ts similarity index 98% rename from packages/client/src/types/updates/inline-query.ts rename to packages/core/src/highlevel/types/updates/inline-query.ts index 5d897d4e..c98a17de 100644 --- a/packages/client/src/types/updates/inline-query.ts +++ b/packages/core/src/highlevel/types/updates/inline-query.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/parse-update.ts b/packages/core/src/highlevel/types/updates/parse-update.ts similarity index 99% rename from packages/client/src/types/updates/parse-update.ts rename to packages/core/src/highlevel/types/updates/parse-update.ts index 77be37e3..ff01145d 100644 --- a/packages/client/src/types/updates/parse-update.ts +++ b/packages/core/src/highlevel/types/updates/parse-update.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-argument */ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { BotChatJoinRequestUpdate, diff --git a/packages/client/src/types/updates/poll-update.ts b/packages/core/src/highlevel/types/updates/poll-update.ts similarity index 98% rename from packages/client/src/types/updates/poll-update.ts rename to packages/core/src/highlevel/types/updates/poll-update.ts index 1c04b653..c9bd652c 100644 --- a/packages/client/src/types/updates/poll-update.ts +++ b/packages/core/src/highlevel/types/updates/poll-update.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/poll-vote.ts b/packages/core/src/highlevel/types/updates/poll-vote.ts similarity index 96% rename from packages/client/src/types/updates/poll-vote.ts rename to packages/core/src/highlevel/types/updates/poll-vote.ts index 476fda3f..182dc143 100644 --- a/packages/client/src/types/updates/poll-vote.ts +++ b/packages/core/src/highlevel/types/updates/poll-vote.ts @@ -1,5 +1,6 @@ -import { MtUnsupportedError, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { MtUnsupportedError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { parsePeer, Peer, PeersIndex } from '../peers/index.js' diff --git a/packages/client/src/types/updates/pre-checkout-query.ts b/packages/core/src/highlevel/types/updates/pre-checkout-query.ts similarity index 97% rename from packages/client/src/types/updates/pre-checkout-query.ts rename to packages/core/src/highlevel/types/updates/pre-checkout-query.ts index f42caa9b..d46c1b58 100644 --- a/packages/client/src/types/updates/pre-checkout-query.ts +++ b/packages/core/src/highlevel/types/updates/pre-checkout-query.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/story-update.ts b/packages/core/src/highlevel/types/updates/story-update.ts similarity index 86% rename from packages/client/src/types/updates/story-update.ts rename to packages/core/src/highlevel/types/updates/story-update.ts index 2d7e4db3..88738273 100644 --- a/packages/client/src/types/updates/story-update.ts +++ b/packages/core/src/highlevel/types/updates/story-update.ts @@ -1,8 +1,9 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { assertTypeIs } from '../../../utils/type-assertions.js' import { parsePeer, Peer, PeersIndex } from '../../types/peers/index.js' import { Story } from '../../types/stories/index.js' -import { assertTypeIs, makeInspectable } from '../../utils/index.js' +import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' /** diff --git a/packages/client/src/types/updates/user-status-update.ts b/packages/core/src/highlevel/types/updates/user-status-update.ts similarity index 97% rename from packages/client/src/types/updates/user-status-update.ts rename to packages/core/src/highlevel/types/updates/user-status-update.ts index 3e64c0c5..9e5bd874 100644 --- a/packages/client/src/types/updates/user-status-update.ts +++ b/packages/core/src/highlevel/types/updates/user-status-update.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' diff --git a/packages/client/src/types/updates/user-typing-update.ts b/packages/core/src/highlevel/types/updates/user-typing-update.ts similarity index 83% rename from packages/client/src/types/updates/user-typing-update.ts rename to packages/core/src/highlevel/types/updates/user-typing-update.ts index 3fb47262..08c237d9 100644 --- a/packages/client/src/types/updates/user-typing-update.ts +++ b/packages/core/src/highlevel/types/updates/user-typing-update.ts @@ -1,5 +1,8 @@ -import { BasicPeerType, getBarePeerId, MtUnsupportedError, tl, toggleChannelIdMark } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { BasicPeerType } from '../../../types/peers.js' +import { assertNever } from '../../../types/utils.js' +import { getBarePeerId, toggleChannelIdMark } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' import { TypingStatus } from '../peers/index.js' @@ -72,6 +75,8 @@ export class UserTypingUpdate { return 'upload_document' case 'sendMessageGeoLocationAction': return 'geo' + case 'sendMessageGamePlayAction': + return 'game' case 'sendMessageChooseContactAction': return 'contact' case 'sendMessageRecordRoundAction': @@ -84,9 +89,13 @@ export class UserTypingUpdate { return 'history_import' case 'sendMessageChooseStickerAction': return 'sticker' + case 'sendMessageEmojiInteraction': + return 'interaction' + case 'sendMessageEmojiInteractionSeen': + return 'interaction_seen' + default: + assertNever(this.raw.action) } - - throw new MtUnsupportedError() } } diff --git a/packages/client/src/types/utils.ts b/packages/core/src/highlevel/types/utils.ts similarity index 79% rename from packages/client/src/types/utils.ts rename to packages/core/src/highlevel/types/utils.ts index 078fb9b4..02d13044 100644 --- a/packages/client/src/types/utils.ts +++ b/packages/core/src/highlevel/types/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MaybeAsync } from '@mtcute/core' +import { MaybePromise } from '../../types/utils.js' -export type MaybeDynamic = MaybeAsync | (() => MaybeAsync) +export type MaybeDynamic = MaybePromise | (() => MaybePromise) export type ArrayWithTotal = T[] & { total: number } export type ArrayPaginated = T[] & { total: number; next?: Offset } diff --git a/packages/client/src/methods/updates/index.ts b/packages/core/src/highlevel/updates/index.ts similarity index 60% rename from packages/client/src/methods/updates/index.ts rename to packages/core/src/highlevel/updates/index.ts index 09bef6a2..9ff9e1ae 100644 --- a/packages/client/src/methods/updates/index.ts +++ b/packages/core/src/highlevel/updates/index.ts @@ -1,3 +1,3 @@ export * from './manager.js' -export * from './parsed.js' +// export * from './parsed.js' todo export * from './types.js' diff --git a/packages/core/src/highlevel/updates/manager.ts b/packages/core/src/highlevel/updates/manager.ts new file mode 100644 index 00000000..5f2771a2 --- /dev/null +++ b/packages/core/src/highlevel/updates/manager.ts @@ -0,0 +1,1856 @@ +/* eslint-disable max-depth,max-params */ +import { tl } from '@mtcute/tl' + +import { MtArgumentError } from '../../types/errors.js' +import { assertNever, MaybePromise } from '../../types/utils.js' +import { + AsyncLock, + ConditionVariable, + Deque, + EarlyTimer, + getBarePeerId, + getMarkedPeerId, + parseMarkedPeerId, + SortedLinkedList, + toggleChannelIdMark, + toInputChannel, +} from '../../utils/index.js' +import { BaseTelegramClient } from '../base.js' +import { CurrentUserInfo } from '../storage/service/current-user.js' +import { PeersIndex } from '../types/peers/peers-index.js' +import { PendingUpdate, PendingUpdateContainer, RawUpdateHandler, UpdatesManagerParams } from './types.js' +import { + createDummyUpdatesContainer, + extractChannelIdFromUpdate, + isMessageEmpty, + messageToUpdate, + toPendingUpdate, +} from './utils.js' + +// code in this file is very bad, thanks to Telegram's awesome updates mechanism + +// todo: maybe move to another class? +// /** +// * Enable RPS meter. +// * +// * > **Note**: This may have negative impact on performance +// * +// * @param size Sampling size +// * @param time Window time +// */ +// export function enableRps(client: ITelegramClient, size?: number, time?: number): void { +// const state = getState(client) +// state.rpsIncoming = new RpsMeter(size, time) +// state.rpsProcessing = new RpsMeter(size, time) +// } + +// /** +// * Get current average incoming RPS +// * +// * Incoming RPS is calculated based on +// * incoming update containers. Normally, +// * they should be around the same, except +// * rare situations when processing rps +// * may peak. +// */ +// export function getCurrentRpsIncoming(client: ITelegramClient): number { +// const state = getState(client) + +// if (!state.rpsIncoming) { +// throw new MtArgumentError('RPS meter is not enabled, use .enableRps() first') +// } + +// return state.rpsIncoming.getRps() +// } + +// /** +// * Get current average processing RPS +// * +// * Processing RPS is calculated based on +// * dispatched updates. Normally, +// * they should be around the same, except +// * rare situations when processing rps +// * may peak. +// */ +// export function getCurrentRpsProcessing(client: ITelegramClient): number { +// const state = getState(client) + +// if (!state.rpsProcessing) { +// throw new MtArgumentError('RPS meter is not enabled, use .enableRps() first') +// } + +// return state.rpsProcessing.getRps() +// } + +const KEEP_ALIVE_INTERVAL = 15 * 60 * 1000 // 15 minutes + +// todo: fix docs +export class UpdatesManager { + updatesLoopActive = false + updatesLoopCv = new ConditionVariable() + + postponedTimer = new EarlyTimer() + hasTimedoutPostponed = false + + pendingUpdateContainers = new SortedLinkedList((a, b) => a.seqStart - b.seqStart) + pendingPtsUpdates = new SortedLinkedList((a, b) => a.ptsBefore! - b.ptsBefore!) + pendingPtsUpdatesPostponed = new SortedLinkedList((a, b) => a.ptsBefore! - b.ptsBefore!) + pendingQtsUpdates = new SortedLinkedList((a, b) => a.qtsBefore! - b.qtsBefore!) + pendingQtsUpdatesPostponed = new SortedLinkedList((a, b) => a.qtsBefore! - b.qtsBefore!) + pendingUnorderedUpdates = new Deque() + + noDispatchEnabled = !this.params.disableNoDispatch + // channel id or 0 => msg id + noDispatchMsg = new Map>() + // channel id or 0 => pts + noDispatchPts = new Map>() + noDispatchQts = new Set() + + lock = new AsyncLock() + // rpsIncoming?: RpsMeter + // rpsProcessing?: RpsMeter + + // accessing storage every time might be expensive, + // so store everything here, and load & save + // every time session is loaded & saved. + pts?: number + qts?: number + date?: number + seq?: number + + // old values of the updates state (i.e. as in DB) + // used to avoid redundant storage calls + oldPts?: number + oldQts?: number + oldDate?: number + oldSeq?: number + selfChanged = false // todo what is this? + + // whether to catch up channels from the locally stored pts + catchUpChannels = false + catchUpOnStart = this.params.catchUp ?? false + + cpts = new Map() + cptsMod = new Map() + channelDiffTimeouts = new Map() + channelsOpened = new Map() + + log = this.client.log.create('updates') + private _handler: RawUpdateHandler = () => {} + + auth?: CurrentUserInfo | null // todo: do we need a local copy? + keepAliveInterval?: NodeJS.Timeout + + constructor( + readonly client: BaseTelegramClient, + readonly params: UpdatesManagerParams = {}, + ) { + if (client.params.disableUpdates) { + throw new MtArgumentError('Updates must be enabled to use updates manager') + } + + this._onKeepAlive = this._onKeepAlive.bind(this) + + this.postponedTimer.onTimeout(() => { + this.hasTimedoutPostponed = true + this.updatesLoopCv.notify() + }) + } + + setHandler(handler: RawUpdateHandler): void { + this._handler = handler + } + + destroy() { + this.stopLoop() + } + + notifyLoggedIn(self: CurrentUserInfo): void { + this.auth = self + this._fetchUpdatesState() + .then(() => this.startLoop()) + .catch((err) => this.client.emitError(err)) + } + + notifyLoggedOut(): void { + this.stopLoop() + this.cpts.clear() + this.cptsMod.clear() + this.pts = this.qts = this.date = this.seq = undefined + } + + async prepare(): Promise { + await this._loadUpdatesStorage() + } + + private _onKeepAlive() { + this.log.debug('no updates for >15 minutes, catching up') + this.handleUpdate({ _: 'updatesTooLong' }) + } + + /** + * Start updates loop. + * + * You must first call {@link enableUpdatesProcessing} to use this method. + * + * It is recommended to use this method in callback to {@link start}, + * or otherwise make sure the user is logged in. + * + * > **Note**: If you are using {@link UpdatesManagerParams.catchUp} option, + * > catching up will be done in background, you can't await it. + */ + async startLoop(): Promise { + if (this.updatesLoopActive) return + + // otherwise we will catch up on the first update + if (!this.catchUpOnStart) { + await this._fetchUpdatesState() + } + + // start updates loop in background + this.updatesLoopActive = true + this.keepAliveInterval = setInterval(this._onKeepAlive, KEEP_ALIVE_INTERVAL) + this._loop().catch((err) => this.client.emitError(err)) + + if (this.catchUpOnStart) { + this.catchUp() + } + } + + /** + * **ADVANCED** + * + * Manually stop updates loop. + * Usually done automatically when stopping the client with {@link close} + */ + stopLoop(): void { + if (!this.updatesLoopActive) return + + clearInterval(this.keepAliveInterval) + + for (const timer of this.channelDiffTimeouts.values()) { + clearTimeout(timer) + } + this.channelDiffTimeouts.clear() + + this.updatesLoopActive = false + this.pendingUpdateContainers.clear() + this.pendingUnorderedUpdates.clear() + this.pendingPtsUpdates.clear() + this.pendingQtsUpdates.clear() + this.pendingPtsUpdatesPostponed.clear() + this.pendingQtsUpdatesPostponed.clear() + this.postponedTimer.reset() + this.updatesLoopCv.notify() + } + + /** + * Catch up with the server by loading missed updates. + * + * > **Note**: In case the storage was not properly + * > closed the last time, "catching up" might + * > result in duplicate updates. + */ + catchUp(): void { + this.log.debug('catch up requested') + + this.catchUpChannels = true + this.handleUpdate({ _: 'updatesTooLong' }) + } + + handleClientUpdate(update: tl.TypeUpdates, noDispatch = true): void { + if (noDispatch && this.noDispatchEnabled) { + this._addToNoDispatchIndex(update) + } + + this.handleUpdate(update) + } + + handleUpdate(update: tl.TypeUpdates): void { + this.log.debug( + 'received %s, queueing for processing. containers queue size: %d', + update._, + this.pendingUpdateContainers.length, + ) + // this.rpsIncoming?.hit() + + switch (update._) { + case 'updatesTooLong': + case 'updateShortMessage': + case 'updateShortChatMessage': + case 'updateShort': + case 'updateShortSentMessage': + this.pendingUpdateContainers.add({ + upd: update, + seqStart: 0, + seqEnd: 0, + }) + break + case 'updates': + case 'updatesCombined': + this.pendingUpdateContainers.add({ + upd: update, + seqStart: update._ === 'updatesCombined' ? update.seqStart : update.seq, + seqEnd: update.seq, + }) + break + default: + assertNever(update) + } + + this.updatesLoopCv.notify() + } + + /** + * **ADVANCED** + * + * Notify the updates manager that some channel was "opened". + * Channel difference for "opened" channels will be fetched on a regular basis. + * This is a low-level method, prefer using {@link openChat} instead. + * + * Channel must be resolve-able with `resolvePeer` method (i.e. be in cache); + * base chat PTS must either be passed (e.g. from {@link Dialog}), or cached in storage. + * + * @param channelId Bare ID of the channel + * @param pts PTS of the channel, if known (e.g. from {@link Dialog}) + * @returns `true` if the channel was opened for the first time, `false` if it is already opened + */ + notifyChannelOpened(channelId: number, pts?: number): boolean { + // this method is intentionally very dumb to avoid making this file even more unreadable + + if (this.channelsOpened.has(channelId)) { + this.log.debug('channel %d opened again', channelId) + this.channelsOpened.set(channelId, this.channelsOpened.get(channelId)! + 1) + + return false + } + + this.channelsOpened.set(channelId, 1) + this.log.debug('channel %d opened (pts=%d)', channelId, pts) + + // force fetch channel difference + this._fetchChannelDifferenceViaUpdate(channelId, pts) + + return true + } + + /** + * **ADVANCED** + * + * Notify the updates manager that some channel was "closed". + * Basically the opposite of {@link notifyChannelOpened}. + * This is a low-level method, prefer using {@link closeChat} instead. + * + * @param channelId Bare channel ID + * @returns `true` if the chat was closed for the last time, `false` otherwise + */ + notifyChannelClosed(channelId: number): boolean { + const opened = this.channelsOpened.get(channelId)! + + if (opened === undefined) { + return false + } + + if (opened > 1) { + this.log.debug('channel %d closed, but is opened %d more times', channelId, opened - 1) + this.channelsOpened.set(channelId, opened - 1) + + return false + } + + this.channelsOpened.delete(channelId) + this.log.debug('channel %d closed', channelId) + + return true + } + + ////////////////////////////////////////////// IMPLEMENTATION ////////////////////////////////////////////// + + async _fetchUpdatesState(): Promise { + const { client, lock, log } = this + + await lock.acquire() + + log.debug('fetching initial state') + + try { + let fetchedState = await client.call({ _: 'updates.getState' }) + + log.debug( + 'updates.getState returned state: pts=%d, qts=%d, date=%d, seq=%d', + fetchedState.pts, + fetchedState.qts, + fetchedState.date, + fetchedState.seq, + ) + + // for some unknown fucking reason getState may return old qts + // call getDifference to get actual values :shrug: + const diff = await client.call({ + _: 'updates.getDifference', + pts: fetchedState.pts, + qts: fetchedState.qts, + date: fetchedState.date, + }) + + switch (diff._) { + case 'updates.differenceEmpty': + break + case 'updates.differenceTooLong': // shouldn't happen, but who knows? + (fetchedState as tl.Mutable).pts = diff.pts + break + case 'updates.differenceSlice': + fetchedState = diff.intermediateState + break + case 'updates.difference': + fetchedState = diff.state + break + default: + assertNever(diff) + } + + this.qts = fetchedState.qts + this.pts = fetchedState.pts + this.date = fetchedState.date + this.seq = fetchedState.seq + + log.debug('loaded initial state: pts=%d, qts=%d, date=%d, seq=%d', this.pts, this.qts, this.date, this.seq) + } catch (e) { + if (this.client.isConnected) { + log.error('failed to fetch updates state: %s', e) + } + + lock.release() + throw e + } + + lock.release() + } + + async _loadUpdatesStorage(): Promise { + const storedState = await this.client.storage.updates.getState() + + if (storedState) { + this.pts = this.oldPts = storedState[0] + this.qts = this.oldQts = storedState[1] + this.date = this.oldDate = storedState[2] + this.seq = this.oldSeq = storedState[3] + + this.log.debug( + 'loaded stored state: pts=%d, qts=%d, date=%d, seq=%d', + storedState[0], + storedState[1], + storedState[2], + storedState[3], + ) + } + // if no state, don't bother initializing properties + // since that means that there is no authorization, + // and thus fetchUpdatesState will be called + } + + async _saveUpdatesStorage(save = false): Promise { + const { client } = this + + // todo: move this to updates state service + // before any authorization pts will be undefined + if (this.pts !== undefined) { + // if old* value is not available, assume it has changed. + if (this.oldPts === undefined || this.oldPts !== this.pts) { + await client.storage.updates.setPts(this.pts) + } + if (this.oldQts === undefined || this.oldQts !== this.qts) { + await client.storage.updates.setQts(this.qts!) + } + if (this.oldDate === undefined || this.oldDate !== this.date) { + await client.storage.updates.setDate(this.date!) + } + if (this.oldSeq === undefined || this.oldSeq !== this.seq) { + await client.storage.updates.setSeq(this.seq!) + } + + // update old* values + this.oldPts = this.pts + this.oldQts = this.qts + this.oldDate = this.date + this.oldSeq = this.seq + + await client.storage.updates.setManyChannelPts(this.cptsMod) + this.cptsMod.clear() + + if (save) { + await client.mt.storage.save() + } + } + } + + _addToNoDispatchIndex(updates?: tl.TypeUpdates): void { + if (!updates) return + + const { noDispatchMsg, noDispatchPts, noDispatchQts } = this + + const addUpdate = (upd: tl.TypeUpdate) => { + const channelId = extractChannelIdFromUpdate(upd) ?? 0 + const pts = 'pts' in upd ? upd.pts : undefined + + if (pts) { + const set = noDispatchPts.get(channelId) + if (!set) noDispatchPts.set(channelId, new Set([pts])) + else set.add(pts) + } + + const qts = 'qts' in upd ? upd.qts : undefined + + if (qts) { + noDispatchQts.add(qts) + } + + switch (upd._) { + case 'updateNewMessage': + case 'updateNewChannelMessage': { + const channelId = upd.message.peerId?._ === 'peerChannel' ? upd.message.peerId.channelId : 0 + + const set = noDispatchMsg.get(channelId) + if (!set) noDispatchMsg.set(channelId, new Set([upd.message.id])) + else set.add(upd.message.id) + + break + } + } + } + + switch (updates._) { + case 'updates': + case 'updatesCombined': + updates.updates.forEach(addUpdate) + break + case 'updateShortMessage': + case 'updateShortChatMessage': + case 'updateShortSentMessage': { + // these updates are only used for non-channel messages, so we use 0 + let set = noDispatchMsg.get(0) + if (!set) noDispatchMsg.set(0, new Set([updates.id])) + else set.add(updates.id) + + set = noDispatchPts.get(0) + if (!set) noDispatchPts.set(0, new Set([updates.pts])) + else set.add(updates.pts) + break + } + case 'updateShort': + addUpdate(updates.update) + break + case 'updatesTooLong': + break + default: + assertNever(updates) + } + } + + async _fetchMissingPeers(upd: tl.TypeUpdate, peers: PeersIndex, allowMissing = false): Promise> { + const { client } = this + + const missing = new Set() + + async function fetchPeer(peer?: tl.TypePeer | number) { + if (!peer) return true + + const bare = typeof peer === 'number' ? parseMarkedPeerId(peer)[1] : getBarePeerId(peer) + + const marked = typeof peer === 'number' ? peer : getMarkedPeerId(peer) + const index = marked > 0 ? peers.users : peers.chats + + if (index.has(bare)) return true + if (missing.has(marked)) return false + + const cached = await client.storage.peers.getCompleteById(marked) + + if (!cached) { + missing.add(marked) + + return allowMissing + } + + // whatever, ts is not smart enough to understand + (index as Map).set(bare, cached) + + return true + } + + switch (upd._) { + case 'updateNewMessage': + case 'updateNewChannelMessage': + case 'updateEditMessage': + case 'updateEditChannelMessage': { + const msg = upd.message + if (msg._ === 'messageEmpty') return missing + + // ref: https://github.com/tdlib/td/blob/master/td/telegram/UpdatesManager.cpp + // (search by UpdatesManager::is_acceptable_update) + if (!(await fetchPeer(msg.peerId))) return missing + if (!(await fetchPeer(msg.fromId))) return missing + + if (msg.replyTo) { + if (msg.replyTo._ === 'messageReplyHeader' && !(await fetchPeer(msg.replyTo.replyToPeerId))) { + return missing + } + if (msg.replyTo._ === 'messageReplyStoryHeader' && !(await fetchPeer(msg.replyTo.userId))) { + return missing + } + } + + if (msg._ !== 'messageService') { + if ( + msg.fwdFrom && + (!(await fetchPeer(msg.fwdFrom.fromId)) || !(await fetchPeer(msg.fwdFrom.savedFromPeer))) + ) { + return missing + } + if (!(await fetchPeer(msg.viaBotId))) return missing + + if (msg.entities) { + for (const ent of msg.entities) { + if (ent._ === 'messageEntityMentionName') { + if (!(await fetchPeer(ent.userId))) return missing + } + } + } + + if (msg.media) { + switch (msg.media._) { + case 'messageMediaContact': + if (msg.media.userId && !(await fetchPeer(msg.media.userId))) { + return missing + } + } + } + } else { + switch (msg.action._) { + case 'messageActionChatCreate': + case 'messageActionChatAddUser': + case 'messageActionInviteToGroupCall': + for (const user of msg.action.users) { + if (!(await fetchPeer(user))) return missing + } + break + case 'messageActionChatJoinedByLink': + if (!(await fetchPeer(msg.action.inviterId))) { + return missing + } + break + case 'messageActionChatDeleteUser': + if (!(await fetchPeer(msg.action.userId))) return missing + break + case 'messageActionChatMigrateTo': + if (!(await fetchPeer(toggleChannelIdMark(msg.action.channelId)))) { + return missing + } + break + case 'messageActionChannelMigrateFrom': + if (!(await fetchPeer(-msg.action.chatId))) return missing + break + case 'messageActionGeoProximityReached': + if (!(await fetchPeer(msg.action.fromId))) return missing + if (!(await fetchPeer(msg.action.toId))) return missing + break + } + } + break + } + case 'updateDraftMessage': + if ('entities' in upd.draft && upd.draft.entities) { + for (const ent of upd.draft.entities) { + if (ent._ === 'messageEntityMentionName') { + if (!(await fetchPeer(ent.userId))) return missing + } + } + } + } + + return missing + } + + async _storeMessageReferences(msg: tl.TypeMessage): Promise { + if (msg._ === 'messageEmpty') return + + const { client } = this + + const peerId = msg.peerId + if (peerId._ !== 'peerChannel') return + + const channelId = toggleChannelIdMark(peerId.channelId) + + const promises: MaybePromise[] = [] + + function store(peer?: tl.TypePeer | number | number[]): void { + if (!peer) return + + if (Array.isArray(peer)) { + peer.forEach(store) + + return + } + + const marked = typeof peer === 'number' ? peer : getMarkedPeerId(peer) + + promises.push(client.storage.refMsgs.store(marked, channelId, msg.id)) + } + + // reference: https://github.com/tdlib/td/blob/master/td/telegram/MessagesManager.cpp + // (search by get_message_user_ids, get_message_channel_ids) + store(msg.fromId) + + if (msg._ === 'message') { + store(msg.viaBotId) + store(msg.fwdFrom?.fromId) + + if (msg.media) { + switch (msg.media._) { + case 'messageMediaWebPage': + if (msg.media.webpage._ === 'webPage' && msg.media.webpage.attributes) { + for (const attr of msg.media.webpage.attributes) { + if (attr._ === 'webPageAttributeStory') { + store(attr.peer) + } + } + } + break + case 'messageMediaContact': + store(msg.media.userId) + break + case 'messageMediaStory': + store(msg.media.peer) + break + case 'messageMediaGiveaway': + store(msg.media.channels.map(toggleChannelIdMark)) + break + } + } + } else { + switch (msg.action._) { + case 'messageActionChatCreate': + case 'messageActionChatAddUser': + case 'messageActionInviteToGroupCall': + store(msg.action.users) + break + case 'messageActionChatDeleteUser': + store(msg.action.userId) + break + } + } + + if (msg.replyTo) { + switch (msg.replyTo._) { + case 'messageReplyHeader': + store(msg.replyTo.replyToPeerId) + store(msg.replyTo.replyFrom?.fromId) + break + case 'messageReplyStoryHeader': + store(msg.replyTo.userId) + break + } + // in fact, we can also use peers contained in the replied-to message, + // but we don't fetch it automatically, so we can't know which peers are there + } + + await Promise.all(promises) + } + + async _fetchChannelDifference(channelId: number, fallbackPts?: number): Promise { + const { channelDiffTimeouts, cpts, cptsMod, channelsOpened, client, log, pendingUnorderedUpdates } = this + + // clear timeout if any + if (channelDiffTimeouts.has(channelId)) { + clearTimeout(channelDiffTimeouts.get(channelId)) + channelDiffTimeouts.delete(channelId) + } + + let _pts: number | null | undefined = cpts.get(channelId) + + if (!_pts && this.catchUpChannels) { + _pts = await client.storage.updates.getChannelPts(channelId) + } + if (!_pts) _pts = fallbackPts + + if (!_pts) { + log.debug('fetchChannelDifference failed for channel %d: base pts not available', channelId) + + return false + } + + const channelPeer = await client.storage.peers.getById(toggleChannelIdMark(channelId)) + + if (!channelPeer) { + log.debug('fetchChannelDifference failed for channel %d: input peer not found', channelId) + + return false + } + + const channel = toInputChannel(channelPeer) + + // to make TS happy + let pts = _pts + let limit = this.auth?.isBot ? 100000 : 100 + + if (pts <= 0) { + pts = 1 + limit = 1 + } + + let lastTimeout = 0 + + for (;;) { + const diff = await client.call({ + _: 'updates.getChannelDifference', + force: true, // Set to true to skip some possibly unneeded updates and reduce server-side load + channel, + pts, + limit, + filter: { _: 'channelMessagesFilterEmpty' }, + }) + + if (diff.timeout) lastTimeout = diff.timeout + + if (diff._ === 'updates.channelDifferenceEmpty') { + log.debug('getChannelDifference (cid = %d) returned channelDifferenceEmpty', channelId) + break + } + + const peers = PeersIndex.from(diff) + + if (diff._ === 'updates.channelDifferenceTooLong') { + if (diff.dialog._ === 'dialog') { + pts = diff.dialog.pts! + } + + log.warn( + 'getChannelDifference (cid = %d) returned channelDifferenceTooLong. new pts: %d, recent msgs: %d', + channelId, + pts, + diff.messages.length, + ) + + diff.messages.forEach((message) => { + log.debug( + 'processing message %d (%s) from TooLong diff for channel %d', + message.id, + message._, + channelId, + ) + + if (message._ === 'messageEmpty') return + + pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) + }) + break + } + + log.debug( + 'getChannelDifference (cid = %d) returned %d messages, %d updates. new pts: %d, final: %b', + channelId, + diff.newMessages.length, + diff.otherUpdates.length, + diff.pts, + diff.final, + ) + + diff.newMessages.forEach((message) => { + log.debug('processing message %d (%s) from diff for channel %d', message.id, message._, channelId) + + if (message._ === 'messageEmpty') return + + pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) + }) + + diff.otherUpdates.forEach((upd) => { + const parsed = toPendingUpdate(upd, peers, true) + + log.debug( + 'processing %s from diff for channel %d, pts_before: %d, pts: %d', + upd._, + channelId, + parsed.ptsBefore, + parsed.pts, + ) + + if (isMessageEmpty(upd)) return + + pendingUnorderedUpdates.pushBack(parsed) + }) + + pts = diff.pts + + if (diff.final) break + } + + cpts.set(channelId, pts) + cptsMod.set(channelId, pts) + + // schedule next fetch + if (lastTimeout !== 0 && channelsOpened.has(channelId)) { + log.debug('scheduling next fetch for channel %d in %d seconds', channelId, lastTimeout) + channelDiffTimeouts.set( + channelId, + setTimeout(() => this._fetchChannelDifferenceViaUpdate(channelId), lastTimeout * 1000), + ) + } + + return true + } + + _fetchChannelDifferenceLater( + requestedDiff: Map>, + channelId: number, + fallbackPts?: number, + ): void { + if (!requestedDiff.has(channelId)) { + requestedDiff.set( + channelId, + this._fetchChannelDifference(channelId, fallbackPts) + .catch((err) => { + this.log.warn('error fetching difference for %d: %s', channelId, err) + }) + .then((ok) => { + requestedDiff.delete(channelId) + + if (!ok) { + this.log.debug('channel difference for %d failed, falling back to common diff', channelId) + this._fetchDifferenceLater(requestedDiff) + } + }), + ) + } + } + + _fetchChannelDifferenceViaUpdate(channelId: number, pts?: number): void { + this.handleUpdate( + createDummyUpdatesContainer([ + { + _: 'updateChannelTooLong', + channelId, + pts, + }, + ]), + ) + } + + async _fetchDifference(requestedDiff: Map>): Promise { + const { client, log, pendingPtsUpdates, pendingUnorderedUpdates } = this + + for (;;) { + const diff = await client.call({ + _: 'updates.getDifference', + pts: this.pts!, + date: this.date!, + qts: this.qts!, + }) + + switch (diff._) { + case 'updates.differenceEmpty': + log.debug('updates.getDifference returned updates.differenceEmpty') + + return + case 'updates.differenceTooLong': + this.pts = diff.pts + log.debug('updates.getDifference returned updates.differenceTooLong') + + return + } + + const fetchedState = diff._ === 'updates.difference' ? diff.state : diff.intermediateState + + log.debug( + 'updates.getDifference returned %d messages, %d updates. new pts: %d, qts: %d, seq: %d, final: %b', + diff.newMessages.length, + diff.otherUpdates.length, + fetchedState.pts, + fetchedState.qts, + fetchedState.seq, + diff._ === 'updates.difference', + ) + + const peers = PeersIndex.from(diff) + + diff.newMessages.forEach((message) => { + log.debug('processing message %d in %j (%s) from common diff', message.id, message.peerId, message._) + + if (message._ === 'messageEmpty') return + + // pts does not need to be checked for them + pendingUnorderedUpdates.pushBack(toPendingUpdate(messageToUpdate(message), peers, true)) + }) + + diff.otherUpdates.forEach((upd) => { + if (upd._ === 'updateChannelTooLong') { + log.debug( + 'received updateChannelTooLong for channel %d in common diff (pts = %d), fetching diff', + upd.channelId, + upd.pts, + ) + + this._fetchChannelDifferenceLater(requestedDiff, upd.channelId, upd.pts) + + return + } + + if (isMessageEmpty(upd)) return + + const parsed = toPendingUpdate(upd, peers, true) + + if (parsed.channelId && parsed.ptsBefore) { + // we need to check pts for these updates, put into pts queue + pendingPtsUpdates.add(parsed) + } else { + // the updates are in order already, we can treat them as unordered + pendingUnorderedUpdates.pushBack(parsed) + } + + log.debug( + 'received %s from common diff, cid: %d, pts_before: %d, pts: %d, qts_before: %d', + upd._, + parsed.channelId, + parsed.ptsBefore, + parsed.pts, + parsed.qtsBefore, + ) + }) + + this.pts = fetchedState.pts + this.qts = fetchedState.qts + this.seq = fetchedState.seq + this.date = fetchedState.date + + if (diff._ === 'updates.difference') { + return + } + } + } + + _fetchDifferenceLater(requestedDiff: Map>): void { + if (!requestedDiff.has(0)) { + requestedDiff.set( + 0, + this._fetchDifference(requestedDiff) + .catch((err) => { + if (tl.RpcError.is(err, 'AUTH_KEY_UNREGISTERED')) { + // for some reason, when logging out telegram may send updatesTooLong + // in any case, we need to stop updates loop + this.stopLoop() + + return + } + + this.log.warn('error fetching common difference: %s', err) + + if (tl.RpcError.is(err, 'PERSISTENT_TIMESTAMP_INVALID')) { + // this function never throws + return this._fetchUpdatesState() + } + }) + .then(() => { + requestedDiff.delete(0) + }), + ) + } + } + + async _onUpdate( + pending: PendingUpdate, + requestedDiff: Map>, + postponed = false, + unordered = false, + ): Promise { + const { client, log } = this + const upd = pending.update + + let missing: Set | undefined = undefined + + // it is important to do this before updating pts + if (pending.peers.hasMin || pending.peers.empty) { + // even if we have min peers in difference, we can't do anything about them. + // we still want to collect them, so we can fetch them in the background. + // we won't wait for them, since that would block the updates loop + + log.debug('loading missing peers for %s (pts = %d, cid = %d)', upd._, pending.pts, pending.channelId) + missing = await this._fetchMissingPeers(upd, pending.peers, pending.fromDifference) + + if (!pending.fromDifference && missing.size) { + log.debug( + 'fetching difference because some peers were min (%J) and not cached for %s (pts = %d, cid = %d)', + missing, + upd._, + pending.pts, + pending.channelId, + ) + + if (pending.channelId && !(upd._ === 'updateNewChannelMessage' && upd.message._ === 'messageService')) { + // don't replace service messages, because they can be about bot's kicking + this._fetchChannelDifferenceLater(requestedDiff, pending.channelId, pending.ptsBefore) + } else { + this._fetchDifferenceLater(requestedDiff) + } + + return + } + + if (missing.size) { + log.debug( + 'peers still missing after fetching difference: %J for %s (pts = %d, cid = %d)', + missing, + upd._, + pending.pts, + pending.channelId, + ) + } + } + + // apply new pts/qts, if applicable + if (!unordered) { + // because unordered may contain pts/qts values when received from diff + + if (pending.pts) { + const localPts = pending.channelId ? this.cpts.get(pending.channelId) : this.pts + + if (localPts && pending.ptsBefore !== localPts) { + log.warn( + 'pts_before does not match local_pts for %s (cid = %d, pts_before = %d, pts = %d, local_pts = %d)', + upd._, + pending.channelId, + pending.ptsBefore, + pending.pts, + localPts, + ) + } + + log.debug( + 'applying new pts (cid = %d) because received %s: %d -> %d (before: %d, count: %d) (postponed = %s)', + pending.channelId, + upd._, + localPts, + pending.pts, + pending.ptsBefore, + pending.pts - pending.ptsBefore!, + postponed, + ) + + if (pending.channelId) { + this.cpts.set(pending.channelId, pending.pts) + this.cptsMod.set(pending.channelId, pending.pts) + } else { + this.pts = pending.pts + } + } + + if (pending.qtsBefore) { + log.debug( + 'applying new qts because received %s: %d -> %d (postponed = %s)', + upd._, + this.qts, + pending.qtsBefore + 1, + postponed, + ) + + this.qts = pending.qts + } + } + + if (isMessageEmpty(upd)) return + + // this.rpsProcessing?.hit() + + // updates that are also used internally + switch (upd._) { + case 'mtcute.dummyUpdate': + // we just needed to apply new pts values + return + case 'updateDcOptions': { + const config = client.mt.network.config.getNow() + + if (config) { + client.mt.network.config.setConfig({ + ...config, + dcOptions: upd.dcOptions, + }) + } else { + client.mt.network.config.update(true).catch((err) => client.emitError(err)) + } + break + } + case 'updateConfig': + client.mt.network.config.update(true).catch((err) => client.emitError(err)) + break + case 'updateUserName': + // todo + // if (upd.userId === state.auth?.userId) { + // state.auth.selfUsername = upd.usernames.find((it) => it.active)?.username ?? null + // } + break + case 'updateDeleteChannelMessages': + if (!this.auth?.isBot) { + await client.storage.refMsgs.delete(toggleChannelIdMark(upd.channelId), upd.messages) + } + break + case 'updateNewMessage': + case 'updateEditMessage': + case 'updateNewChannelMessage': + case 'updateEditChannelMessage': + if (!this.auth?.isBot) { + await this._storeMessageReferences(upd.message) + } + break + } + + if (missing?.size) { + if (this.auth?.isBot) { + this.log.warn( + 'missing peers (%J) after getDifference for %s (pts = %d, cid = %d)', + missing, + upd._, + pending.pts, + pending.channelId, + ) + } else { + // force save storage so the min peers are stored + await client.mt.storage.save() + + for (const id of missing) { + Promise.resolve(client.storage.peers.getById(id)) + .then((peer): unknown => { + if (!peer) { + this.log.warn('cannot fetch full peer %d - getPeerById returned null', id) + + return + } + + // the peer will be automatically cached by the `.call()`, we don't have to do anything + // todo + // if (isInputPeerChannel(peer)) { + // return _getChannelsBatched(client, toInputChannel(peer)) + // } else if (isInputPeerUser(peer)) { + // return _getUsersBatched(client, toInputUser(peer)) + // } + + log.warn('cannot fetch full peer %d - unknown peer type %s', id, peer._) + }) + .catch((err) => { + log.warn('error fetching full peer %d: %s', id, err) + }) + } + } + } + + // dispatch the update + if (this.noDispatchEnabled) { + const channelId = pending.channelId ?? 0 + const msgId = + upd._ === 'updateNewMessage' || upd._ === 'updateNewChannelMessage' ? upd.message.id : undefined + + // we first need to remove it from each index, and then check if it was there + const foundByMsgId = msgId && this.noDispatchMsg.get(channelId)?.delete(msgId) + const foundByPts = this.noDispatchPts.get(channelId)?.delete(pending.pts!) + const foundByQts = this.noDispatchQts.delete(pending.qts!) + + if (foundByMsgId || foundByPts || foundByQts) { + log.debug('not dispatching %s because it is in no_dispatch index', upd._) + + return + } + } + + log.debug('dispatching %s (postponed = %s)', upd._, postponed) + this._handler(upd, pending.peers) + } + + async _loop(): Promise { + const { + log, + client, + cpts, + cptsMod, + pendingUpdateContainers, + pendingPtsUpdates, + pendingPtsUpdatesPostponed, + pendingQtsUpdates, + pendingQtsUpdatesPostponed, + pendingUnorderedUpdates, + updatesLoopCv, + postponedTimer, + } = this + + log.debug('updates loop started, state available? %b', this.pts) + + try { + if (!this.pts) { + await this._fetchUpdatesState() + } + + while (this.updatesLoopActive) { + if ( + !( + pendingUpdateContainers.length || + pendingPtsUpdates.length || + pendingQtsUpdates.length || + pendingUnorderedUpdates.length || + this.hasTimedoutPostponed + ) + ) { + await updatesLoopCv.wait() + } + if (!this.updatesLoopActive) break + + log.debug( + 'updates loop tick. pending containers: %d, pts: %d, pts_postponed: %d, qts: %d, qts_postponed: %d, unordered: %d', + pendingUpdateContainers.length, + pendingPtsUpdates.length, + pendingPtsUpdatesPostponed.length, + pendingQtsUpdates.length, + pendingQtsUpdatesPostponed.length, + pendingUnorderedUpdates.length, + ) + + const requestedDiff = new Map>() + + // first process pending containers + while (pendingUpdateContainers.length) { + const { upd, seqStart, seqEnd } = pendingUpdateContainers.popFront()! + + switch (upd._) { + case 'updatesTooLong': + log.debug('received updatesTooLong, fetching difference') + this._fetchDifferenceLater(requestedDiff) + break + case 'updatesCombined': + case 'updates': { + if (seqStart !== 0) { + // https://t.me/tdlibchat/5843 + const nextLocalSeq = this.seq! + 1 + log.debug( + 'received seq-ordered %s (seq_start = %d, seq_end = %d, size = %d)', + upd._, + seqStart, + seqEnd, + upd.updates.length, + ) + + if (nextLocalSeq > seqStart) { + log.debug( + 'ignoring updates group because already applied (by seq: exp %d, got %d)', + nextLocalSeq, + seqStart, + ) + // "the updates were already applied, and must be ignored" + continue + } + + if (nextLocalSeq < seqStart) { + log.debug( + 'fetching difference because gap detected (by seq: exp %d, got %d)', + nextLocalSeq, + seqStart, + ) + // "there's an updates gap that must be filled" + this._fetchDifferenceLater(requestedDiff) + } + } else { + log.debug('received %s (size = %d)', upd._, upd.updates.length) + } + + await client.storage.peers.updatePeersFrom(upd) + + const peers = PeersIndex.from(upd) + + for (const update of upd.updates) { + switch (update._) { + case 'updateChannelTooLong': + log.debug( + 'received updateChannelTooLong for channel %d (pts = %d) in container, fetching diff', + update.channelId, + update.pts, + ) + this._fetchChannelDifferenceLater(requestedDiff, update.channelId, update.pts) + continue + case 'updatePtsChanged': + // see https://github.com/tdlib/td/blob/07c1d53a6d3cb1fad58d2822e55eef6d57363581/td/telegram/UpdatesManager.cpp#L4051 + if (client.mt.network.getPoolSize('main') > 1) { + // highload bot + log.debug( + 'updatePtsChanged received, resetting pts to 1 and fetching difference', + ) + this.pts = 1 + this._fetchDifferenceLater(requestedDiff) + } else { + log.debug('updatePtsChanged received, fetching updates state') + await this._fetchUpdatesState() + } + continue + } + + const parsed = toPendingUpdate(update, peers) + + if (parsed.ptsBefore !== undefined) { + pendingPtsUpdates.add(parsed) + } else if (parsed.qtsBefore !== undefined) { + pendingQtsUpdates.add(parsed) + } else { + pendingUnorderedUpdates.pushBack(parsed) + } + } + + if (seqEnd !== 0 && seqEnd > this.seq!) { + this.seq = seqEnd + this.date = upd.date + } + + break + } + case 'updateShort': { + log.debug('received short %s', upd._) + + const parsed = toPendingUpdate(upd.update, new PeersIndex()) + + if (parsed.ptsBefore !== undefined) { + pendingPtsUpdates.add(parsed) + } else if (parsed.qtsBefore !== undefined) { + pendingQtsUpdates.add(parsed) + } else { + pendingUnorderedUpdates.pushBack(parsed) + } + + break + } + case 'updateShortMessage': { + log.debug('received updateShortMessage') + + const message: tl.RawMessage = { + _: 'message', + out: upd.out, + mentioned: upd.mentioned, + mediaUnread: upd.mediaUnread, + silent: upd.silent, + id: upd.id, + fromId: { + _: 'peerUser', + userId: upd.out ? this.auth!.userId : upd.userId, + }, + peerId: { + _: 'peerUser', + userId: upd.userId, + }, + fwdFrom: upd.fwdFrom, + viaBotId: upd.viaBotId, + replyTo: upd.replyTo, + date: upd.date, + message: upd.message, + entities: upd.entities, + ttlPeriod: upd.ttlPeriod, + } + + const update: tl.RawUpdateNewMessage = { + _: 'updateNewMessage', + message, + pts: upd.pts, + ptsCount: upd.ptsCount, + } + + pendingPtsUpdates.add({ + update, + ptsBefore: upd.pts - upd.ptsCount, + pts: upd.pts, + peers: new PeersIndex(), + fromDifference: false, + }) + + break + } + case 'updateShortChatMessage': { + log.debug('received updateShortChatMessage') + + const message: tl.RawMessage = { + _: 'message', + out: upd.out, + mentioned: upd.mentioned, + mediaUnread: upd.mediaUnread, + silent: upd.silent, + id: upd.id, + fromId: { + _: 'peerUser', + userId: upd.fromId, + }, + peerId: { + _: 'peerChat', + chatId: upd.chatId, + }, + fwdFrom: upd.fwdFrom, + viaBotId: upd.viaBotId, + replyTo: upd.replyTo, + date: upd.date, + message: upd.message, + entities: upd.entities, + ttlPeriod: upd.ttlPeriod, + } + + const update: tl.RawUpdateNewMessage = { + _: 'updateNewMessage', + message, + pts: upd.pts, + ptsCount: upd.ptsCount, + } + + pendingPtsUpdates.add({ + update, + ptsBefore: upd.pts - upd.ptsCount, + pts: upd.pts, + peers: new PeersIndex(), + fromDifference: false, + }) + + break + } + case 'updateShortSentMessage': { + // should not happen + log.warn('received updateShortSentMessage') + break + } + default: + assertNever(upd) + } + } + + // process pts-ordered updates + while (pendingPtsUpdates.length) { + const pending = pendingPtsUpdates.popFront()! + const upd = pending.update + + // check pts + + let localPts: number | null = null + + if (!pending.channelId) localPts = this.pts! + else if (cpts.has(pending.channelId)) { + localPts = cpts.get(pending.channelId)! + } else if (this.catchUpChannels) { + // only load stored channel pts in case + // the user has enabled catching up. + // not loading stored pts effectively disables + // catching up, but doesn't interfere with further + // update gaps (i.e. first update received is considered + // to be the base state) + + const saved = await client.storage.updates.getChannelPts(pending.channelId) + + if (saved) { + cpts.set(pending.channelId, saved) + localPts = saved + } + } + + if (localPts) { + const diff = localPts - pending.ptsBefore! + // PTS can only go up or drop cardinally + const isPtsDrop = diff > 1000009 + + if (diff > 0 && !isPtsDrop) { + // "the update was already applied, and must be ignored" + log.debug( + 'ignoring %s (cid = %d) because already applied (by pts: exp %d, got %d)', + upd._, + pending.channelId, + localPts, + pending.ptsBefore, + ) + continue + } + if (diff < 0) { + // "there's an update gap that must be filled" + // if the gap is less than 3, put the update into postponed queue + // otherwise, call getDifference + if (diff > -3) { + log.debug( + 'postponing %s for 0.5s (cid = %d) because small gap detected (by pts: exp %d, got %d, diff=%d)', + upd._, + pending.channelId, + localPts, + pending.ptsBefore, + diff, + ) + pending.timeout = Date.now() + 500 + pendingPtsUpdatesPostponed.add(pending) + postponedTimer.emitBefore(pending.timeout) + } else if (diff > -1000000) { + log.debug( + 'fetching difference after %s (cid = %d) because pts gap detected (by pts: exp %d, got %d, diff=%d)', + upd._, + pending.channelId, + localPts, + pending.ptsBefore, + diff, + ) + + if (pending.channelId) { + this._fetchChannelDifferenceLater(requestedDiff, pending.channelId) + } else { + this._fetchDifferenceLater(requestedDiff) + } + } else { + log.debug( + 'skipping all updates because pts gap is too big (by pts: exp %d, got %d, diff=%d)', + localPts, + pending.ptsBefore, + diff, + ) + + if (pending.channelId) { + cpts.set(pending.channelId, 0) + cptsMod.set(pending.channelId, 0) + } else { + await this._fetchUpdatesState() + } + } + continue + } + + if (isPtsDrop) { + log.debug('pts drop detected (%d -> %d)', localPts, pending.ptsBefore) + } + } + + await this._onUpdate(pending, requestedDiff) + } + + // process postponed pts-ordered updates + for (let item = pendingPtsUpdatesPostponed._first; item; item = item.n) { + // awesome fucking iteration because i'm so fucking tired and wanna kms + const pending = item.v + + const upd = pending.update + + let localPts + + if (!pending.channelId) localPts = this.pts! + else if (cpts.has(pending.channelId)) { + localPts = cpts.get(pending.channelId) + } + + // channel pts from storage will be available because we loaded it earlier + if (!localPts) { + log.warn( + 'local pts not available for postponed %s (cid = %d), skipping', + upd._, + pending.channelId, + ) + continue + } + + // check the pts to see if the gap was filled + if (localPts > pending.ptsBefore!) { + // "the update was already applied, and must be ignored" + log.debug( + 'ignoring postponed %s (cid = %d) because already applied (by pts: exp %d, got %d)', + upd._, + pending.channelId, + localPts, + pending.ptsBefore, + ) + pendingPtsUpdatesPostponed._remove(item) + continue + } + if (localPts < pending.ptsBefore!) { + // "there's an update gap that must be filled" + // if the timeout has not expired yet, keep the update in the queue + // otherwise, fetch diff + const now = Date.now() + + if (now < pending.timeout!) { + log.debug( + 'postponed %s (cid = %d) is still waiting (%dms left) (current pts %d, need %d)', + upd._, + pending.channelId, + pending.timeout! - now, + localPts, + pending.ptsBefore, + ) + } else { + log.debug( + "gap for postponed %s (cid = %d) wasn't filled, fetching diff (current pts %d, need %d)", + upd._, + pending.channelId, + localPts, + pending.ptsBefore, + ) + pendingPtsUpdatesPostponed._remove(item) + + if (pending.channelId) { + this._fetchChannelDifferenceLater(requestedDiff, pending.channelId) + } else { + this._fetchDifferenceLater(requestedDiff) + } + } + continue + } + + await this._onUpdate(pending, requestedDiff, true) + pendingPtsUpdatesPostponed._remove(item) + } + + // process qts-ordered updates + while (pendingQtsUpdates.length) { + const pending = pendingQtsUpdates.popFront()! + const upd = pending.update + + // check qts + const diff = this.qts! - pending.qtsBefore! + const isQtsDrop = diff > 1000009 + + if (diff > 0 && !isQtsDrop) { + // "the update was already applied, and must be ignored" + log.debug( + 'ignoring %s because already applied (by qts: exp %d, got %d)', + upd._, + this.qts!, + pending.qtsBefore, + ) + continue + } + if (this.qts! < pending.qtsBefore!) { + // "there's an update gap that must be filled" + // if the gap is less than 3, put the update into postponed queue + // otherwise, call getDifference + if (diff > -3) { + log.debug( + 'postponing %s for 0.5s because small gap detected (by qts: exp %d, got %d, diff=%d)', + upd._, + this.qts!, + pending.qtsBefore, + diff, + ) + pending.timeout = Date.now() + 500 + pendingQtsUpdatesPostponed.add(pending) + postponedTimer.emitBefore(pending.timeout) + } else { + log.debug( + 'fetching difference after %s because qts gap detected (by qts: exp %d, got %d, diff=%d)', + upd._, + this.qts!, + pending.qtsBefore, + diff, + ) + this._fetchDifferenceLater(requestedDiff) + } + continue + } + + if (isQtsDrop) { + log.debug('qts drop detected (%d -> %d)', this.qts, pending.qtsBefore) + } + + await this._onUpdate(pending, requestedDiff) + } + + // process postponed qts-ordered updates + for (let item = pendingQtsUpdatesPostponed._first; item; item = item.n) { + // awesome fucking iteration because i'm so fucking tired and wanna kms + const pending = item.v + const upd = pending.update + + // check the pts to see if the gap was filled + if (this.qts! > pending.qtsBefore!) { + // "the update was already applied, and must be ignored" + log.debug( + 'ignoring postponed %s because already applied (by qts: exp %d, got %d)', + upd._, + this.qts!, + pending.qtsBefore, + ) + continue + } + if (this.qts! < pending.qtsBefore!) { + // "there's an update gap that must be filled" + // if the timeout has not expired yet, keep the update in the queue + // otherwise, fetch diff + const now = Date.now() + + if (now < pending.timeout!) { + log.debug( + 'postponed %s is still waiting (%dms left) (current qts %d, need %d)', + upd._, + pending.timeout! - now, + this.qts!, + pending.qtsBefore, + ) + } else { + log.debug( + "gap for postponed %s wasn't filled, fetching diff (current qts %d, need %d)", + upd._, + this.qts!, + pending.qtsBefore, + ) + pendingQtsUpdatesPostponed._remove(item) + this._fetchDifferenceLater(requestedDiff) + } + continue + } + + // gap was filled, and the update can be applied + await this._onUpdate(pending, requestedDiff, true) + pendingQtsUpdatesPostponed._remove(item) + } + + this.hasTimedoutPostponed = false + + // wait for all pending diffs to load + while (requestedDiff.size) { + log.debug( + 'waiting for %d pending diffs before processing unordered: %J', + requestedDiff.size, + requestedDiff.keys(), + ) + + await Promise.all([...requestedDiff.values()]) + + // diff results may as well contain new diffs to be requested + log.debug( + 'pending diffs awaited, new diffs requested: %d (%J)', + requestedDiff.size, + requestedDiff.keys(), + ) + } + + // process unordered updates (or updates received from diff) + while (pendingUnorderedUpdates.length) { + const pending = pendingUnorderedUpdates.popFront()! + + await this._onUpdate(pending, requestedDiff, false, true) + } + + // onUpdate may also call getDiff in some cases, so we also need to check + // diff may also contain new updates, which will be processed in the next tick, + // but we don't want to postpone diff fetching + while (requestedDiff.size) { + log.debug( + 'waiting for %d pending diffs after processing unordered: %J', + requestedDiff.size, + requestedDiff.keys(), + ) + + await Promise.all([...requestedDiff.values()]) + + // diff results may as well contain new diffs to be requested + log.debug( + 'pending diffs awaited, new diffs requested: %d (%j)', + requestedDiff.size, + requestedDiff.keys(), + ) + } + + // save new update state + await this._saveUpdatesStorage(true) + } + + log.debug('updates loop stopped') + } catch (e) { + log.error('updates loop encountered error, restarting: %s', e) + + return this._loop() + } + } +} diff --git a/packages/client/src/methods/updates/parsed.ts b/packages/core/src/highlevel/updates/parsed.ts similarity index 93% rename from packages/client/src/methods/updates/parsed.ts rename to packages/core/src/highlevel/updates/parsed.ts index 5934e791..df97d7b2 100644 --- a/packages/client/src/methods/updates/parsed.ts +++ b/packages/core/src/highlevel/updates/parsed.ts @@ -1,6 +1,6 @@ -import { Message } from '../../types/messages/index.js' -import { ParsedUpdate } from '../../types/updates/index.js' -import { _parseUpdate } from '../../types/updates/parse-update.js' +import { Message } from '../types/messages/index.js' +import { ParsedUpdate } from '../types/updates/index.js' +import { _parseUpdate } from '../types/updates/parse-update.js' import { RawUpdateHandler } from './types.js' export interface ParsedUpdateHandlerParams { diff --git a/packages/client/src/methods/updates/types.ts b/packages/core/src/highlevel/updates/types.ts similarity index 56% rename from packages/client/src/methods/updates/types.ts rename to packages/core/src/highlevel/updates/types.ts index 3c7cc3f6..95b688d2 100644 --- a/packages/client/src/methods/updates/types.ts +++ b/packages/core/src/highlevel/updates/types.ts @@ -1,10 +1,8 @@ -import { BaseTelegramClient, tl } from '@mtcute/core' -import { AsyncLock, ConditionVariable, Deque, EarlyTimer, Logger, SortedLinkedList } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' -import { PeersIndex } from '../../types/index.js' -import { RpsMeter } from '../../utils/index.js' -import { AuthState } from '../auth/_state.js' -import { extractChannelIdFromUpdate } from './utils.js' +import { AsyncLock, ConditionVariable, Deque, EarlyTimer, Logger, SortedLinkedList } from '../../utils/index.js' +import { CurrentUserInfo } from '../storage/service/current-user.js' +import { PeersIndex } from '../types/peers/peers-index.js' /** * Function to be called for each update. @@ -53,11 +51,6 @@ export interface UpdatesManagerParams { * > "catching up" might result in duplicate updates. */ catchUp?: boolean - - /** - * Handler for raw updates. - */ - onUpdate: RawUpdateHandler } /** @internal */ @@ -103,8 +96,8 @@ export interface UpdatesState { noDispatchQts: Set lock: AsyncLock - rpsIncoming?: RpsMeter - rpsProcessing?: RpsMeter + // rpsIncoming?: RpsMeter + // rpsProcessing?: RpsMeter // accessing storage every time might be expensive, // so store everything here, and load & save @@ -134,73 +127,5 @@ export interface UpdatesState { log: Logger stop: () => void handler: RawUpdateHandler - auth: AuthState -} - -/** - * @internal - * @noemit - */ -export function createUpdatesState( - client: BaseTelegramClient, - authState: AuthState, - opts: UpdatesManagerParams, -): UpdatesState { - return { - updatesLoopActive: false, - updatesLoopCv: new ConditionVariable(), - postponedTimer: new EarlyTimer(), - hasTimedoutPostponed: false, - pendingUpdateContainers: new SortedLinkedList((a, b) => a.seqStart - b.seqStart), - pendingPtsUpdates: new SortedLinkedList((a, b) => a.ptsBefore! - b.ptsBefore!), - pendingPtsUpdatesPostponed: new SortedLinkedList((a, b) => a.ptsBefore! - b.ptsBefore!), - pendingQtsUpdates: new SortedLinkedList((a, b) => a.qtsBefore! - b.qtsBefore!), - pendingQtsUpdatesPostponed: new SortedLinkedList((a, b) => a.qtsBefore! - b.qtsBefore!), - pendingUnorderedUpdates: new Deque(), - noDispatchEnabled: !opts.disableNoDispatch, - noDispatchMsg: new Map(), - noDispatchPts: new Map(), - noDispatchQts: new Set(), - lock: new AsyncLock(), - pts: undefined, - qts: undefined, - date: undefined, - seq: undefined, - oldPts: undefined, - oldQts: undefined, - oldDate: undefined, - oldSeq: undefined, - rpsIncoming: undefined, - rpsProcessing: undefined, - selfChanged: false, - catchUpChannels: false, - catchUpOnStart: opts.catchUp ?? false, - cpts: new Map(), - cptsMod: new Map(), - channelDiffTimeouts: new Map(), - channelsOpened: new Map(), - log: client.log.create('updates'), - stop: () => {}, // will be set later - handler: opts.onUpdate, - auth: authState, - } -} - -export function toPendingUpdate(upd: tl.TypeUpdate, peers: PeersIndex, fromDifference = false): PendingUpdate { - const channelId = extractChannelIdFromUpdate(upd) || 0 - const pts = 'pts' in upd ? upd.pts : undefined - // eslint-disable-next-line no-nested-ternary - const ptsCount = 'ptsCount' in upd ? upd.ptsCount : pts ? 0 : undefined - const qts = 'qts' in upd ? upd.qts : undefined - - return { - update: upd, - channelId, - pts, - ptsBefore: pts ? pts - ptsCount! : undefined, - qts, - qtsBefore: qts ? qts - 1 : undefined, - peers, - fromDifference, - } + auth: CurrentUserInfo | null } diff --git a/packages/core/src/highlevel/updates/utils.ts b/packages/core/src/highlevel/updates/utils.ts new file mode 100644 index 00000000..117317c9 --- /dev/null +++ b/packages/core/src/highlevel/updates/utils.ts @@ -0,0 +1,117 @@ +import { tl } from '@mtcute/tl' + +import { MtTypeAssertionError } from '../../types/errors.js' +import { PeersIndex } from '../types/peers/peers-index.js' +import type { PendingUpdate } from './types.js' + +export function messageToUpdate(message: tl.TypeMessage): tl.TypeUpdate { + switch (message.peerId!._) { + case 'peerUser': + case 'peerChat': + return { + _: 'updateNewMessage', + message, + pts: 0, + ptsCount: 0, + } + case 'peerChannel': + return { + _: 'updateNewChannelMessage', + message, + pts: 0, + ptsCount: 0, + } + } +} + +export function extractChannelIdFromUpdate(upd: tl.TypeUpdate): number | undefined { + // holy shit + let res = 0 + + if ('channelId' in upd) { + res = upd.channelId + } else if ( + 'message' in upd && + typeof upd.message !== 'string' && + 'peerId' in upd.message && + upd.message.peerId && + 'channelId' in upd.message.peerId + ) { + res = upd.message.peerId.channelId + } + + if (res === 0) return undefined + + return res +} + +export function toPendingUpdate(upd: tl.TypeUpdate, peers: PeersIndex, fromDifference = false): PendingUpdate { + const channelId = extractChannelIdFromUpdate(upd) || 0 + const pts = 'pts' in upd ? upd.pts : undefined + // eslint-disable-next-line no-nested-ternary + const ptsCount = 'ptsCount' in upd ? upd.ptsCount : pts ? 0 : undefined + const qts = 'qts' in upd ? upd.qts : undefined + + return { + update: upd, + channelId, + pts, + ptsBefore: pts ? pts - ptsCount! : undefined, + qts, + qtsBefore: qts ? qts - 1 : undefined, + peers, + fromDifference, + } +} + +export function isMessageEmpty(upd: tl.TypeUpdate): boolean { + return (upd as Extract).message?._ === 'messageEmpty' +} + +// dummy updates which are used for methods that return messages.affectedHistory. +// that is not an update, but it carries info about pts, and we need to handle it + +/** + * Create a dummy `updates` container with given updates. + */ +export function createDummyUpdatesContainer(updates: tl.TypeUpdate[], seq = 0): tl.TypeUpdates { + return { + _: 'updates', + seq, + date: 0, + chats: [], + users: [], + updates, + } +} + +/** + * Create a dummy update from PTS and PTS count. + * + * @param pts PTS + * @param ptsCount PTS count + * @param channelId Channel ID (bare), if applicable + */ +export function createDummyUpdate(pts: number, ptsCount: number, channelId = 0): tl.TypeUpdates { + return createDummyUpdatesContainer([ + { + _: 'mtcute.dummyUpdate', + channelId, + pts, + ptsCount, + }, + ]) +} + +/** @internal */ +export function assertIsUpdatesGroup( + ctx: string, + upd: tl.TypeUpdates, +): asserts upd is tl.RawUpdates | tl.RawUpdatesCombined { + switch (upd._) { + case 'updates': + case 'updatesCombined': + return + } + throw new MtTypeAssertionError(ctx, 'updates | updatesCombined', upd._) +} diff --git a/packages/file-id/src/convert.ts b/packages/core/src/highlevel/utils/convert-file-id.ts similarity index 97% rename from packages/file-id/src/convert.ts rename to packages/core/src/highlevel/utils/convert-file-id.ts index b5a7fe97..0d7276f3 100644 --- a/packages/file-id/src/convert.ts +++ b/packages/core/src/highlevel/utils/convert-file-id.ts @@ -1,8 +1,12 @@ -import { assertNever, getBasicPeerType, Long, markedPeerIdToBare, tl } from '@mtcute/core' +import Long from 'long' + +import { parseFileId, tdFileId as td } from '@mtcute/file-id' +import { tl } from '@mtcute/tl' + +import { parseMarkedPeerId } from '../../utils/peer-utils.js' -import { parseFileId } from './parse.js' -import { tdFileId as td } from './types.js' import FileType = td.FileType +import { assertNever } from '../../types/utils.js' const EMPTY_BUFFER = new Uint8Array(0) @@ -12,8 +16,7 @@ function dialogPhotoToInputPeer( dialog: td.RawPhotoSizeSourceDialogPhoto | td.RawPhotoSizeSourceDialogPhotoLegacy, ): tl.TypeInputPeer { const markedPeerId = dialog.id - const peerType = getBasicPeerType(markedPeerId) - const peerId = markedPeerIdToBare(markedPeerId) + const [peerType, peerId] = parseMarkedPeerId(markedPeerId) if (peerType === 'user') { return { diff --git a/packages/client/src/utils/file-type.test.ts b/packages/core/src/highlevel/utils/file-type.test.ts similarity index 98% rename from packages/client/src/utils/file-type.test.ts rename to packages/core/src/highlevel/utils/file-type.test.ts index f0fa9d5e..8549c554 100644 --- a/packages/client/src/utils/file-type.test.ts +++ b/packages/core/src/highlevel/utils/file-type.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' +import { hexDecodeToBuffer } from '@mtcute/tl-runtime' + import { guessFileMime } from './file-type.js' -import { hexDecodeToBuffer } from './index.js' describe('guessFileMime', () => { it.each([ diff --git a/packages/client/src/utils/file-type.ts b/packages/core/src/highlevel/utils/file-type.ts similarity index 100% rename from packages/client/src/utils/file-type.ts rename to packages/core/src/highlevel/utils/file-type.ts diff --git a/packages/client/src/utils/file-utils.test.ts b/packages/core/src/highlevel/utils/file-utils.test.ts similarity index 99% rename from packages/client/src/utils/file-utils.test.ts rename to packages/core/src/highlevel/utils/file-utils.test.ts index 47b27cc3..aeb31ad2 100644 --- a/packages/client/src/utils/file-utils.test.ts +++ b/packages/core/src/highlevel/utils/file-utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { extractFileName, diff --git a/packages/client/src/utils/file-utils.ts b/packages/core/src/highlevel/utils/file-utils.ts similarity index 95% rename from packages/client/src/utils/file-utils.ts rename to packages/core/src/highlevel/utils/file-utils.ts index ed6b8fdb..345843dd 100644 --- a/packages/client/src/utils/file-utils.ts +++ b/packages/core/src/highlevel/utils/file-utils.ts @@ -1,5 +1,7 @@ -import { MtArgumentError } from '@mtcute/core' -import { concatBuffers, hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/tl-runtime' + +import { MtArgumentError } from '../../types/errors.js' +import { concatBuffers } from '../../utils/buffer-utils.js' /** * Given file size, determine the appropriate chunk size (in KB) diff --git a/packages/client/src/utils/index.ts b/packages/core/src/highlevel/utils/index.ts similarity index 78% rename from packages/client/src/utils/index.ts rename to packages/core/src/highlevel/utils/index.ts index 2c595af5..661c2378 100644 --- a/packages/client/src/utils/index.ts +++ b/packages/core/src/highlevel/utils/index.ts @@ -1,3 +1,5 @@ +// todo: merge this with the main utils dir + export * from './file-utils.js' export * from './inline-utils.js' export * from './inspectable.js' @@ -5,6 +7,4 @@ export * from './misc-utils.js' export * from './peer-utils.js' export * from './rps-meter.js' export * from './stream-utils.js' -export * from './updates-utils.js' export * from './voice-utils.js' -export * from '@mtcute/core/utils.js' diff --git a/packages/client/src/utils/inline-utils.test.ts b/packages/core/src/highlevel/utils/inline-utils.test.ts similarity index 96% rename from packages/client/src/utils/inline-utils.test.ts rename to packages/core/src/highlevel/utils/inline-utils.test.ts index 4defb711..c628e7c3 100644 --- a/packages/client/src/utils/inline-utils.test.ts +++ b/packages/core/src/highlevel/utils/inline-utils.test.ts @@ -1,6 +1,7 @@ +import Long from 'long' import { describe, expect, it } from 'vitest' -import { Long, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' import { encodeInlineMessageId, normalizeInlineId, parseInlineMessageId } from './inline-utils.js' diff --git a/packages/client/src/utils/inline-utils.ts b/packages/core/src/highlevel/utils/inline-utils.ts similarity index 93% rename from packages/client/src/utils/inline-utils.ts rename to packages/core/src/highlevel/utils/inline-utils.ts index 22240acb..61b782ca 100644 --- a/packages/client/src/utils/inline-utils.ts +++ b/packages/core/src/highlevel/utils/inline-utils.ts @@ -1,5 +1,7 @@ -import { assertNever, tl } from '@mtcute/core' -import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter } from '@mtcute/core/utils.js' +import { tl } from '@mtcute/tl' +import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' + +import { assertNever } from '../../types/utils.js' /** * Parse TDLib style inline message ID diff --git a/packages/client/src/utils/inspectable.test.ts b/packages/core/src/highlevel/utils/inspectable.test.ts similarity index 100% rename from packages/client/src/utils/inspectable.test.ts rename to packages/core/src/highlevel/utils/inspectable.test.ts diff --git a/packages/client/src/utils/inspectable.ts b/packages/core/src/highlevel/utils/inspectable.ts similarity index 97% rename from packages/client/src/utils/inspectable.ts rename to packages/core/src/highlevel/utils/inspectable.ts index dc9ef932..6aa86749 100644 --- a/packages/client/src/utils/inspectable.ts +++ b/packages/core/src/highlevel/utils/inspectable.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */ -import { base64Encode } from '@mtcute/core/utils.js' +import { base64Encode } from '@mtcute/tl-runtime' const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') diff --git a/packages/client/src/utils/memoize.test.ts b/packages/core/src/highlevel/utils/memoize.test.ts similarity index 100% rename from packages/client/src/utils/memoize.test.ts rename to packages/core/src/highlevel/utils/memoize.test.ts diff --git a/packages/client/src/utils/memoize.ts b/packages/core/src/highlevel/utils/memoize.ts similarity index 100% rename from packages/client/src/utils/memoize.ts rename to packages/core/src/highlevel/utils/memoize.ts diff --git a/packages/client/src/utils/misc-utils.ts b/packages/core/src/highlevel/utils/misc-utils.ts similarity index 96% rename from packages/client/src/utils/misc-utils.ts rename to packages/core/src/highlevel/utils/misc-utils.ts index b54a0422..f7cddf25 100644 --- a/packages/client/src/utils/misc-utils.ts +++ b/packages/core/src/highlevel/utils/misc-utils.ts @@ -1,5 +1,4 @@ -import { MtArgumentError } from '@mtcute/core' - +import { MtArgumentError } from '../../types/errors.js' import { ArrayPaginated, ArrayWithTotal, MaybeDynamic, Message } from '../types/index.js' /** diff --git a/packages/client/src/utils/peer-utils.test.ts b/packages/core/src/highlevel/utils/peer-utils.test.ts similarity index 100% rename from packages/client/src/utils/peer-utils.test.ts rename to packages/core/src/highlevel/utils/peer-utils.test.ts diff --git a/packages/client/src/utils/peer-utils.ts b/packages/core/src/highlevel/utils/peer-utils.ts similarity index 93% rename from packages/client/src/utils/peer-utils.ts rename to packages/core/src/highlevel/utils/peer-utils.ts index 02adc632..407b2b4f 100644 --- a/packages/client/src/utils/peer-utils.ts +++ b/packages/core/src/highlevel/utils/peer-utils.ts @@ -1,5 +1,6 @@ -import { assertNever, tl } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { assertNever } from '../../types/utils.js' import { MtInvalidPeerTypeError } from '../types/errors.js' import { InputPeerLike } from '../types/peers/index.js' @@ -143,3 +144,10 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer { throw new MtInvalidPeerTypeError(inp, `Cannot convert ${inp._} to peer`) } } + +export function extractUsernames(obj: tl.RawUser | tl.RawChannel) { + if (obj.usernames?.length) return obj.usernames.map((x) => x.username.toLowerCase()) + if (obj.username) return [obj.username.toLowerCase()] + + return [] +} diff --git a/packages/core/src/highlevel/utils/platform/storage.ts b/packages/core/src/highlevel/utils/platform/storage.ts new file mode 100644 index 00000000..d65566e3 --- /dev/null +++ b/packages/core/src/highlevel/utils/platform/storage.ts @@ -0,0 +1,7 @@ +import { MtUnsupportedError } from '../../../types/errors.js' +import { ITelegramStorageProvider } from '../../storage/provider.js' + +/** @internal */ +export const _defaultStorageFactory = (_name: string): ITelegramStorageProvider => { + throw new MtUnsupportedError('Please provide a storage explicitly (e.g. @mtcute/sqlite)') +} diff --git a/packages/client/src/utils/platform/storage.web.ts b/packages/core/src/highlevel/utils/platform/storage.web.ts similarity index 65% rename from packages/client/src/utils/platform/storage.web.ts rename to packages/core/src/highlevel/utils/platform/storage.web.ts index 135f0b8c..faed84bd 100644 --- a/packages/client/src/utils/platform/storage.web.ts +++ b/packages/core/src/highlevel/utils/platform/storage.web.ts @@ -1,6 +1,5 @@ -import { IdbStorage } from '@mtcute/core/src/storage/idb.js' - -import { MtUnsupportedError } from '../../index.js' +import { IdbStorage } from '../../../storage/index.js' +import { MtUnsupportedError } from '../../../types/errors.js' /** @internal */ export const _defaultStorageFactory = (name: string) => { diff --git a/packages/client/src/utils/query-batcher.test.ts b/packages/core/src/highlevel/utils/query-batcher.test.ts similarity index 96% rename from packages/client/src/utils/query-batcher.test.ts rename to packages/core/src/highlevel/utils/query-batcher.test.ts index f92f1450..3e269230 100644 --- a/packages/client/src/utils/query-batcher.test.ts +++ b/packages/core/src/highlevel/utils/query-batcher.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest' -import { BaseTelegramClient } from '@mtcute/core' -import { sleep } from '@mtcute/core/utils.js' import { StubTelegramClient } from '@mtcute/test' +import { sleep } from '../../utils/misc-utils.js' +import { ITelegramClient } from '../client.types.js' import { batchedQuery } from './query-batcher.js' describe('batchedQuery', () => { @@ -13,7 +13,7 @@ describe('batchedQuery', () => { it('should correctly batch requests', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -83,7 +83,7 @@ describe('batchedQuery', () => { it('should correctly limit batch size', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -164,7 +164,7 @@ describe('batchedQuery', () => { it('should correctly do concurrent requests', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -248,7 +248,7 @@ describe('batchedQuery', () => { it('should correctly handle errors', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -314,7 +314,7 @@ describe('batchedQuery', () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] ${client.log.prefix} fetch() ${items.join(', ')}`) await sleep(10) @@ -397,7 +397,7 @@ describe('batchedQuery', () => { it('should correctly handle fetcher omitting some items', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -473,7 +473,7 @@ describe('batchedQuery', () => { it('should correctly retry failed batches one by one entirely', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) @@ -540,7 +540,7 @@ describe('batchedQuery', () => { it('should correctly retry failed batches one by one partially', async () => { const log: string[] = [] - const fetch = async (client: BaseTelegramClient, items: number[]) => { + const fetch = async (client: ITelegramClient, items: number[]) => { log.push(`[start] fetch() ${items.join(', ')}`) await sleep(10) diff --git a/packages/client/src/utils/query-batcher.ts b/packages/core/src/highlevel/utils/query-batcher.ts similarity index 90% rename from packages/client/src/utils/query-batcher.ts rename to packages/core/src/highlevel/utils/query-batcher.ts index dc39ed45..abc5e811 100644 --- a/packages/client/src/utils/query-batcher.ts +++ b/packages/core/src/highlevel/utils/query-batcher.ts @@ -1,5 +1,5 @@ -import { BaseTelegramClient } from '@mtcute/core' -import { Deque } from '@mtcute/core/utils.js' +import { Deque } from '../../utils/deque.js' +import { ITelegramClient } from '../client.types.js' type Resolve = (value: T | PromiseLike) => void type Reject = (err?: unknown) => void @@ -12,6 +12,8 @@ interface InternalState { numRunning: number } +// todo: should it be MtClient? + /** * Helper function for building batched queries. * @@ -28,12 +30,12 @@ export function batchedQuery(params: { * If some item is not found, it should be omitted from the result array, * this way the corresponding request will be resolved with `null`. */ - fetch: (client: BaseTelegramClient, items: T[]) => Promise + fetch: (client: ITelegramClient, items: T[]) => Promise /** Key derivation function for input items */ - inputKey: (item: T, client: BaseTelegramClient) => K + inputKey: (item: T, client: ITelegramClient) => K /** Key derivation function for output items */ - outputKey: (item: U, client: BaseTelegramClient) => K + outputKey: (item: U, client: ITelegramClient) => K /** * Maximum number of items to be passed to the `fetcher` function at once. @@ -62,12 +64,12 @@ export function batchedQuery(params: { * or an array of items for which the query should be retried (waiters for other items will throw `err`). */ retrySingleOnError?: (items: T[], err: unknown) => boolean | T[] -}): (client: BaseTelegramClient, item: T) => Promise { +}): (client: ITelegramClient, item: T) => Promise { const { inputKey, outputKey, fetch, maxBatchSize = Infinity, maxConcurrent = 1, retrySingleOnError } = params const symbol = Symbol('batchedQueryState') - function getState(client_: BaseTelegramClient) { + function getState(client_: ITelegramClient) { const client = client_ as { [symbol]?: InternalState } if (!client[symbol]) { @@ -82,7 +84,7 @@ export function batchedQuery(params: { return client[symbol] } - function addWaiter(client: BaseTelegramClient, waiters: WaitersMap, item: T) { + function addWaiter(client: ITelegramClient, waiters: WaitersMap, item: T) { const key = inputKey(item, client) let arr = waiters.get(key) @@ -106,13 +108,13 @@ export function batchedQuery(params: { return arr } - function startLoops(client: BaseTelegramClient, state: InternalState) { + function startLoops(client: ITelegramClient, state: InternalState) { for (let i = state.numRunning; i <= maxConcurrent; i++) { processPending(client, state) } } - function processPending(client: BaseTelegramClient, state: InternalState) { + function processPending(client: ITelegramClient, state: InternalState) { const { waiters, fetchingKeys, retryQueue } = state if (state.numRunning >= maxConcurrent) return diff --git a/packages/client/src/utils/rps-meter.ts b/packages/core/src/highlevel/utils/rps-meter.ts similarity index 96% rename from packages/client/src/utils/rps-meter.ts rename to packages/core/src/highlevel/utils/rps-meter.ts index 28525053..8fdf2a9c 100644 --- a/packages/client/src/utils/rps-meter.ts +++ b/packages/core/src/highlevel/utils/rps-meter.ts @@ -1,4 +1,4 @@ -import { Deque } from '@mtcute/core/utils.js' +import { Deque } from '../../utils/deque.js' export class RpsMeter { _hits: Deque diff --git a/packages/client/src/utils/stream-utils.test.ts b/packages/core/src/highlevel/utils/stream-utils.test.ts similarity index 100% rename from packages/client/src/utils/stream-utils.test.ts rename to packages/core/src/highlevel/utils/stream-utils.test.ts diff --git a/packages/client/src/utils/stream-utils.ts b/packages/core/src/highlevel/utils/stream-utils.ts similarity index 97% rename from packages/client/src/utils/stream-utils.ts rename to packages/core/src/highlevel/utils/stream-utils.ts index 7355dde1..a600adc5 100644 --- a/packages/client/src/utils/stream-utils.ts +++ b/packages/core/src/highlevel/utils/stream-utils.ts @@ -1,4 +1,5 @@ -import { AsyncLock, concatBuffers } from '@mtcute/core/utils.js' +import { AsyncLock } from '../../utils/async-lock.js' +import { concatBuffers } from '../../utils/buffer-utils.js' export function bufferToStream(buf: Uint8Array): ReadableStream { return new ReadableStream({ diff --git a/packages/client/src/utils/voice-utils.test.ts b/packages/core/src/highlevel/utils/voice-utils.test.ts similarity index 96% rename from packages/client/src/utils/voice-utils.test.ts rename to packages/core/src/highlevel/utils/voice-utils.test.ts index 78414a74..60036d4a 100644 --- a/packages/client/src/utils/voice-utils.test.ts +++ b/packages/core/src/highlevel/utils/voice-utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' import { decodeWaveform, encodeWaveform } from './voice-utils.js' diff --git a/packages/client/src/utils/voice-utils.ts b/packages/core/src/highlevel/utils/voice-utils.ts similarity index 97% rename from packages/client/src/utils/voice-utils.ts rename to packages/core/src/highlevel/utils/voice-utils.ts index e6f2a677..ee8a83b9 100644 --- a/packages/client/src/utils/voice-utils.ts +++ b/packages/core/src/highlevel/utils/voice-utils.ts @@ -1,4 +1,4 @@ -import { dataViewFromBuffer } from '@mtcute/core/utils.js' +import { dataViewFromBuffer } from '../../utils/buffer-utils.js' /** * Decode 5-bit encoded voice message waveform into diff --git a/packages/core/src/highlevel/worker/errors.ts b/packages/core/src/highlevel/worker/errors.ts new file mode 100644 index 00000000..288f15d0 --- /dev/null +++ b/packages/core/src/highlevel/worker/errors.ts @@ -0,0 +1,130 @@ +import { tl } from '@mtcute/tl' + +import { + MtArgumentError, + MtcuteError, + MtSecurityError, + MtTimeoutError, + MtTypeAssertionError, + MtUnsupportedError, +} from '../../types/errors.js' +import { MtEmptyError, MtInvalidPeerTypeError, MtMessageNotFoundError, MtPeerNotFoundError } from '../types/errors.js' + +export interface SerializedError { + name: string + message: string + stack?: string + custom?: Record +} + +export function serializeError(error: unknown): SerializedError { + if (!(error instanceof Error)) return { name: 'Error', message: String(error) } + + const res: SerializedError = { + name: 'Error', + message: error.message, + stack: error.stack, + } + + const ctor = error.constructor + + if (ctor === MtTypeAssertionError) { + const _error = error as MtTypeAssertionError + res.name = 'MtTypeAssertionError' + res.custom = { + context: _error.context, + expected: _error.expected, + actual: _error.actual, + } + } else if (ctor === MtTimeoutError) { + const _error = error as MtTimeoutError + res.name = 'MtTimeoutError' + res.custom = { + timeout: _error.timeout, + } + } else if (ctor === MtMessageNotFoundError) { + const _error = error as MtMessageNotFoundError + res.name = 'MtMessageNotFoundError' + res.custom = { + peerId: _error.peerId, + messageId: _error.messageId, + context: _error.context, + } + } else if (ctor === tl.RpcError) { + res.name = 'RpcError' + res.custom = { ...error } + } else if (ctor === MtArgumentError) res.name = 'MtArgumentError' + else if (ctor === MtSecurityError) res.name = 'MtSecurityError' + else if (ctor === MtUnsupportedError) res.name = 'MtUnsupportedError' + else if (ctor === MtPeerNotFoundError) res.name = 'MtPeerNotFoundError' + else if (ctor === MtInvalidPeerTypeError) res.name = 'MtInvalidPeerTypeError' + else if (ctor === MtEmptyError) res.name = 'MtEmptyError' + else if (ctor instanceof MtcuteError) res.name = 'MtcuteError' + + return res +} + +export function deserializeError(error: SerializedError): Error { + let err2: Error + + switch (error.name) { + case 'MtTypeAssertionError': { + const custom = error.custom as { context: string; expected: string; actual: string } + + err2 = new MtTypeAssertionError(custom.context, custom.expected, custom.actual) + break + } + case 'MtTimeoutError': { + const custom = error.custom as { timeout?: number } + + err2 = new MtTimeoutError(custom.timeout) + break + } + case 'MtMessageNotFoundError': { + const custom = error.custom as { peerId: number; messageId: number; context?: string } + + err2 = new MtMessageNotFoundError(custom.peerId, custom.messageId, custom.context) + break + } + case 'RpcError': { + const custom = error.custom as unknown as tl.RpcError + err2 = new tl.RpcError(custom.code, custom.text) + err2.message = error.message // may have been formatted + + for (const key in custom) { + if (key === 'code' || key === 'text') continue + // @ts-expect-error lol + err2[key] = custom[key] // eslint-disable-line + } + break + } + case 'MtArgumentError': + err2 = new MtArgumentError() + break + case 'MtSecurityError': + err2 = new MtSecurityError() + break + case 'MtUnsupportedError': + err2 = new MtUnsupportedError() + break + case 'MtPeerNotFoundError': + err2 = new MtPeerNotFoundError() + break + case 'MtInvalidPeerTypeError': + err2 = new MtInvalidPeerTypeError('', '') + err2.message = error.message // lol + break + case 'MtEmptyError': + err2 = new MtEmptyError() + break + case 'MtcuteError': + err2 = new MtcuteError() + break + default: + err2 = new Error(error.message) + } + + err2.stack = error.stack + + return err2 +} diff --git a/packages/core/src/highlevel/worker/invoker.ts b/packages/core/src/highlevel/worker/invoker.ts new file mode 100644 index 00000000..424a5d89 --- /dev/null +++ b/packages/core/src/highlevel/worker/invoker.ts @@ -0,0 +1,67 @@ +import { ControllablePromise, createControllablePromise } from '../../utils/controllable-promise.js' +import { deserializeError } from './errors.js' +import { SendFn, WorkerInboundMessage, WorkerOutboundMessage } from './protocol.js' + +export type InvokeTarget = Extract['target'] + +export class WorkerInvoker { + constructor(private send: SendFn) {} + + private _nextId = 0 + private _pending = new Map() + + private _invoke(target: InvokeTarget, method: string, args: unknown[], isVoid: boolean) { + const id = this._nextId++ + + this.send({ + type: 'invoke', + id, + target, + method, + args, + void: isVoid, + }) + + if (!isVoid) { + const promise = createControllablePromise() + + this._pending.set(id, promise) + + return promise + } + } + + invoke(target: InvokeTarget, method: string, args: unknown[]): Promise { + return this._invoke(target, method, args, false) as Promise + } + + invokeVoid(target: InvokeTarget, method: string, args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._invoke(target, method, args, true) + } + + handleResult(msg: Extract) { + const promise = this._pending.get(msg.id) + if (!promise) return + + if (msg.error) { + promise.reject(deserializeError(msg.error)) + } else { + promise.resolve(msg.result) + } + } + + makeBinder(target: InvokeTarget) { + return (method: K, isVoid = false) => { + let fn + + if (isVoid) { + fn = (...args: unknown[]) => this.invokeVoid(target, method as string, args) + } else { + fn = (...args: unknown[]) => this.invoke(target, method as string, args) + } + + return fn as T[K] + } + } +} diff --git a/packages/core/src/highlevel/worker/platform/connect.ts b/packages/core/src/highlevel/worker/platform/connect.ts new file mode 100644 index 00000000..cfeafbdd --- /dev/null +++ b/packages/core/src/highlevel/worker/platform/connect.ts @@ -0,0 +1,20 @@ +import { Worker } from 'worker_threads' + +import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js' + +export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] { + if (!(worker instanceof Worker)) { + throw new Error('Only worker_threads are supported') + } + + const send: SendFn = worker.postMessage.bind(worker) + + worker.on('message', handler) + + return [ + send, + () => { + worker.off('message', handler) + }, + ] +} diff --git a/packages/core/src/highlevel/worker/platform/connect.web.ts b/packages/core/src/highlevel/worker/platform/connect.web.ts new file mode 100644 index 00000000..b22db3b0 --- /dev/null +++ b/packages/core/src/highlevel/worker/platform/connect.web.ts @@ -0,0 +1,61 @@ +import { beforeExit } from '../../../utils/platform/exit-hook.js' +import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js' + +export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] { + if (worker instanceof Worker) { + const send: SendFn = worker.postMessage.bind(worker) + + const messageHandler = (ev: MessageEvent) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + handler(ev.data) + } + + worker.addEventListener('message', messageHandler) + + return [ + send, + () => { + worker.removeEventListener('message', messageHandler) + }, + ] + } + + if (worker instanceof SharedWorker) { + const send: SendFn = worker.port.postMessage.bind(worker.port) + + const pingInterval = setInterval(() => { + worker.port.postMessage({ __type__: 'ping' }) + }, 10000) + + const messageHandler = (ev: MessageEvent) => { + if (ev.data.__type__ === 'timeout') { + // we got disconnected from the worker due to timeout + // if the page is still alive (which is unlikely), we should reconnect + // however it's not really possible with SharedWorker API without re-creating the worker + // so we just reload the page for now + location.reload() + + return + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + handler(ev.data) + } + + worker.port.addEventListener('message', messageHandler) + worker.port.start() + + const close = () => { + clearInterval(pingInterval) + worker.port.postMessage({ __type__: 'close' }) + worker.port.removeEventListener('message', messageHandler) + worker.port.close() + } + + beforeExit(close) + + return [send, close] + } + + throw new Error('Only workers and shared workers are supported') +} diff --git a/packages/core/src/highlevel/worker/platform/register.ts b/packages/core/src/highlevel/worker/platform/register.ts new file mode 100644 index 00000000..f680a46e --- /dev/null +++ b/packages/core/src/highlevel/worker/platform/register.ts @@ -0,0 +1,23 @@ +import { parentPort } from 'worker_threads' + +import { RespondFn, WorkerMessageHandler } from '../protocol.js' + +const registered = false + +export function registerWorker(handler: WorkerMessageHandler): RespondFn { + if (!parentPort) { + throw new Error('registerWorker() must be called from a worker thread') + } + if (registered) { + throw new Error('registerWorker() must be called only once') + } + + const port = parentPort + + const respond: RespondFn = port.postMessage.bind(port) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + parentPort.on('message', (message) => handler(message, respond)) + + return respond +} diff --git a/packages/core/src/highlevel/worker/platform/register.web.ts b/packages/core/src/highlevel/worker/platform/register.web.ts new file mode 100644 index 00000000..3e55208c --- /dev/null +++ b/packages/core/src/highlevel/worker/platform/register.web.ts @@ -0,0 +1,80 @@ +import { RespondFn, WorkerMessageHandler } from '../protocol.js' + +const registered = false + +export function registerWorker(handler: WorkerMessageHandler): RespondFn { + if (registered) { + throw new Error('registerWorker() must be called only once') + } + + if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { + const respond: RespondFn = self.postMessage.bind(self) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + self.addEventListener('message', (message) => handler(message.data, respond)) + + return respond + } + + if (typeof SharedWorkerGlobalScope !== 'undefined' && self instanceof SharedWorkerGlobalScope) { + const connections: MessagePort[] = [] + + const broadcast = (message: unknown) => { + for (const port of connections) { + port.postMessage(message) + } + } + + self.onconnect = (event) => { + const port = event.ports[0] + connections.push(port) + + const respond = port.postMessage.bind(port) + + // not very reliable, but better than nothing + // SharedWorker API doesn't provide a way to detect when the client closes the connection + // so we just assume that the client is done when it sends a 'close' message + // and keep a timeout for the case when the client closes without sending a 'close' message + const onClose = () => { + port.close() + const idx = connections.indexOf(port) + + if (idx >= 0) { + connections.splice(connections.indexOf(port), 1) + } + } + + const onTimeout = () => { + console.warn('some connection timed out!') + respond({ __type__: 'timeout' }) + onClose() + } + + // 60s should be a reasonable timeout considering that the client should send a ping every 10s + // so even if the browser has suspended the timers, we should still get a ping within a minute + let timeout = setTimeout(onTimeout, 60000) + + port.addEventListener('message', (message) => { + if (message.data.__type__ === 'close') { + onClose() + + return + } + + if (message.data.__type__ === 'ping') { + clearTimeout(timeout) + timeout = setTimeout(onTimeout, 60000) + + return + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + handler(message.data, respond) + }) + } + + return broadcast + } + + throw new Error('registerWorker() must be called from a worker') +} diff --git a/packages/core/src/highlevel/worker/port.ts b/packages/core/src/highlevel/worker/port.ts new file mode 100644 index 00000000..22986bf2 --- /dev/null +++ b/packages/core/src/highlevel/worker/port.ts @@ -0,0 +1,105 @@ +import { tl } from '@mtcute/tl' + +import { LogManager } from '../../utils/logger.js' +import { ITelegramClient } from '../client.types.js' +import { PeersIndex } from '../types/peers/peers-index.js' +import { RawUpdateHandler } from '../updates/types.js' +import { WorkerInvoker } from './invoker.js' +import { connectToWorker } from './platform/connect.js' +import { ClientMessageHandler, SomeWorker, WorkerCustomMethods } from './protocol.js' +import { TelegramStorageProxy } from './storage.js' + +export interface TelegramWorkerPortOptions { + worker: SomeWorker +} + +export class TelegramWorkerPort implements ITelegramClient { + constructor(readonly options: TelegramWorkerPortOptions) {} + + readonly log = new LogManager('worker') + + private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {} + onServerUpdate(handler: (updates: tl.TypeUpdates) => void): void { + this._serverUpdatesHandler = handler + } + + private _errorHandler: (err: unknown) => void = () => {} + onError(handler: (err: unknown) => void): void { + this._errorHandler = handler + } + emitError(err: unknown): void { + this._errorHandler(err) + } + + private _updateHandler: RawUpdateHandler = () => {} + onUpdate(handler: RawUpdateHandler): void { + this._updateHandler = handler + } + + private _onMessage: ClientMessageHandler = (message) => { + switch (message.type) { + case 'log': + this.log.handler(message.color, message.level, message.tag, message.fmt, message.args) + break + case 'server_update': + this._serverUpdatesHandler(message.update) + break + case 'update': { + const peers = new PeersIndex(message.users, message.chats) + peers.hasMin = message.hasMin + this._updateHandler(message.update, peers) + break + } + case 'result': + this._invoker.handleResult(message) + break + case 'error': + this.emitError(message.error) + break + } + } + + private _connection = connectToWorker(this.options.worker, this._onMessage) + private _invoker = new WorkerInvoker(this._connection[0]) + private _bind = this._invoker.makeBinder('client') + + readonly storage = new TelegramStorageProxy(this._invoker) + + private _destroyed = false + destroy(terminate = false): void { + if (this._destroyed) return + this._connection[1]() + this._destroyed = true + + if (terminate && 'terminate' in this.options.worker) { + Promise.resolve(this.options.worker.terminate()).catch(() => {}) + } + } + + invokeCustom(method: T, ...args: Parameters): Promise> { + return this._invoker.invoke('custom', method as string, args) as Promise> + } + + readonly prepare = this._bind('prepare') + private _connect = this._bind('connect') + async connect(): Promise { + await this._connect() + await this.storage.self.fetch() // force cache self locally + } + readonly close = this._bind('close') + readonly notifyLoggedIn = this._bind('notifyLoggedIn') + readonly notifyLoggedOut = this._bind('notifyLoggedOut') + readonly notifyChannelOpened = this._bind('notifyChannelOpened') + readonly notifyChannelClosed = this._bind('notifyChannelClosed') + readonly call = this._bind('call') + readonly importSession = this._bind('importSession') + readonly exportSession = this._bind('exportSession') + readonly handleClientUpdate = this._bind('handleClientUpdate', true) + readonly getApiCrenetials = this._bind('getApiCrenetials') + readonly getPoolSize = this._bind('getPoolSize') + readonly getPrimaryDcId = this._bind('getPrimaryDcId') + readonly computeSrpParams = this._bind('computeSrpParams') + readonly computeNewPasswordHash = this._bind('computeNewPasswordHash') + readonly startUpdatesLoop = this._bind('startUpdatesLoop') + readonly stopUpdatesLoop = this._bind('stopUpdatesLoop') +} diff --git a/packages/core/src/highlevel/worker/protocol.ts b/packages/core/src/highlevel/worker/protocol.ts new file mode 100644 index 00000000..b607f0d7 --- /dev/null +++ b/packages/core/src/highlevel/worker/protocol.ts @@ -0,0 +1,55 @@ +import type { Worker as NodeWorker } from 'worker_threads' + +import { tl } from '@mtcute/tl' + +import { SerializedError } from './errors.js' + +export type WorkerInboundMessage = + | { + type: 'invoke' + id: number + target: + | 'custom' + | 'client' + | 'storage' + | 'storage-self' + | 'storage-peers' + method: string + args: unknown[] + void: boolean + } + +export type WorkerOutboundMessage = + | { type: 'server_update'; update: tl.TypeUpdates } + | { + type: 'update' + update: tl.TypeUpdate + users: Map + chats: Map + hasMin: boolean + } + | { type: 'error'; error: unknown } + | { + type: 'log' + color: number + level: number + tag: string + fmt: string + args: unknown[] + } + | { + type: 'result' + id: number + result?: unknown + error?: SerializedError + } + +export type SomeWorker = NodeWorker | Worker | SharedWorker + +export type SendFn = (message: WorkerInboundMessage) => void +export type ClientMessageHandler = (message: WorkerOutboundMessage) => void + +export type RespondFn = (message: WorkerOutboundMessage) => void +export type WorkerMessageHandler = (message: WorkerInboundMessage, respond: RespondFn) => void + +export type WorkerCustomMethods = Record Promise> diff --git a/packages/core/src/highlevel/worker/storage.ts b/packages/core/src/highlevel/worker/storage.ts new file mode 100644 index 00000000..49252410 --- /dev/null +++ b/packages/core/src/highlevel/worker/storage.ts @@ -0,0 +1,80 @@ +import { tl } from '@mtcute/tl' + +import { MtArgumentError } from '../../types/errors.js' +import { PublicPart } from '../../types/utils.js' +import type { CurrentUserInfo, CurrentUserService } from '../storage/service/current-user.js' +import type { PeersService } from '../storage/service/peers.js' +import { TelegramStorageManager } from '../storage/storage.js' +import { WorkerInvoker } from './invoker.js' + +class CurrentUserServiceProxy implements PublicPart { + constructor(private _invoker: WorkerInvoker) {} + private _bind = this._invoker.makeBinder('storage-self') + + private _cached?: CurrentUserInfo | null + + private _store = this._bind('store') + async store(info: CurrentUserInfo | null): Promise { + await this._store(info) + this._cached = info + } + + private _storeFrom = this._bind('storeFrom') + async storeFrom(user: tl.TypeUser): Promise { + this._cached = await this._storeFrom(user) + + return this._cached + } + + private _fetch = this._bind('fetch') + async fetch(): Promise { + if (this._cached) return this._cached + + this._cached = await this._fetch() + + return this._cached + } + + getCached(safe?: boolean): CurrentUserInfo | null { + if (this._cached === undefined) { + if (safe) return null + + throw new MtArgumentError('User info is not cached yet') + } + + return this._cached + } + + private _update = this._bind('update') + async update(params: Parameters[0]): Promise { + await this._update(params) + this._cached = await this._fetch() + } +} + +class PeersServiceProxy implements PublicPart { + constructor(private _invoker: WorkerInvoker) {} + private _bind = this._invoker.makeBinder('storage-peers') + + readonly updatePeersFrom = this._bind('updatePeersFrom') + readonly store = this._bind('store') + readonly getById = this._bind('getById') + readonly getByPhone = this._bind('getByPhone') + readonly getByUsername = this._bind('getByUsername') + readonly getCompleteById = this._bind('getCompleteById') +} + +export class TelegramStorageProxy implements PublicPart { + constructor(private _invoker: WorkerInvoker) {} + + private _bind = this._invoker.makeBinder('storage') + + // todo - remove once we move these to updates manager + readonly updates = null as never + readonly refMsgs = null as never + + readonly self = new CurrentUserServiceProxy(this._invoker) + readonly peers = new PeersServiceProxy(this._invoker) + + readonly clear = this._bind('clear') +} diff --git a/packages/core/src/highlevel/worker/worker.ts b/packages/core/src/highlevel/worker/worker.ts new file mode 100644 index 00000000..33513946 --- /dev/null +++ b/packages/core/src/highlevel/worker/worker.ts @@ -0,0 +1,125 @@ +import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' +import { serializeError } from './errors.js' +import { registerWorker } from './platform/register.js' +import { RespondFn, WorkerCustomMethods, WorkerInboundMessage, WorkerMessageHandler } from './protocol.js' + +export interface TelegramWorkerOptions { + client: BaseTelegramClient | BaseTelegramClientOptions + customMethods?: T +} + +export function makeTelegramWorker(params: TelegramWorkerOptions) { + const { client: client_, customMethods } = params + + const client = client_ instanceof BaseTelegramClient ? client_ : new BaseTelegramClient(client_) + + const onInvoke = (msg: Extract, respond: RespondFn) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let target: any + + switch (msg.target) { + case 'custom': + target = customMethods + break + case 'client': + target = client + break + case 'storage': + target = client.storage + break + case 'storage-self': + target = client.storage.self + break + case 'storage-peers': + target = client.storage.peers + break + + default: { + respond({ + type: 'result', + id: msg.id, + error: new Error(`Unknown target ${msg.target}`), + }) + + return + } + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const method = target[msg.method] + + if (!method) { + respond({ + type: 'result', + id: msg.id, + error: new Error(`Method ${msg.method} not found on ${msg.target}`), + }) + + return + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + Promise.resolve(method.apply(target, msg.args)) + .then((res) => { + if (msg.void) return + + respond({ + type: 'result', + id: msg.id, + result: res, + }) + }) + .catch((err) => { + respond({ + type: 'result', + id: msg.id, + error: serializeError(err), + }) + }) + } + + const onMessage: WorkerMessageHandler = (message, respond) => { + switch (message.type) { + case 'invoke': + onInvoke(message, respond) + break + } + } + + const broadcast = registerWorker(onMessage) + + client.log.mgr.handler = (color, level, tag, fmt, args) => + broadcast({ + type: 'log', + color, + level, + tag, + fmt, + args, + }) + client.onError((err) => + broadcast({ + type: 'error', + error: err, + }), + ) + + if (client.updates) { + client.onUpdate((update, peers) => + broadcast({ + type: 'update', + update, + users: peers.users, + chats: peers.chats, + hasMin: peers.hasMin, + }), + ) + } else { + client.onServerUpdate((update) => + broadcast({ + type: 'server_update', + update, + }), + ) + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a5a1e4fe..564cca66 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,4 @@ -export * from './base-client.js' -export * from './base-client.types.js' +export * from './highlevel/index.js' export * from './network/index.js' export * from './storage/index.js' export * from './types/index.js' diff --git a/packages/core/src/network/client.ts b/packages/core/src/network/client.ts new file mode 100644 index 00000000..9e1c087f --- /dev/null +++ b/packages/core/src/network/client.ts @@ -0,0 +1,393 @@ +/* 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, + CryptoProviderFactory, + DcOptions, + defaultCryptoProviderFactory, + 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 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. + * + * @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 ?? defaultCryptoProviderFactory)() + 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 + } +} diff --git a/packages/core/src/network/index.ts b/packages/core/src/network/index.ts index 7057a4f0..a1dba3f2 100644 --- a/packages/core/src/network/index.ts +++ b/packages/core/src/network/index.ts @@ -1,3 +1,4 @@ +export * from './client.js' export type { ConnectionKind, NetworkManagerExtraParams, RpcCallOptions } from './network-manager.js' export * from './reconnection.js' export * from './session-connection.js' diff --git a/packages/core/src/network/mtproto-session.ts b/packages/core/src/network/mtproto-session.ts index 2aa5db53..9d4342bf 100644 --- a/packages/core/src/network/mtproto-session.ts +++ b/packages/core/src/network/mtproto-session.ts @@ -155,16 +155,20 @@ export class MtprotoSession { */ reset(withAuthKey = false): void { if (withAuthKey) { - this._authKey.reset() - this._authKeyTemp.reset() - this._authKeyTempSecondary.reset() + this.resetAuthKey() } clearTimeout(this.current429Timeout) - this.resetState() + this.resetState(withAuthKey) this.resetLastPing(true) } + resetAuthKey(): void { + this._authKey.reset() + this._authKeyTemp.reset() + this._authKeyTempSecondary.reset() + } + /** * Reset session state and generate a new session ID. * diff --git a/packages/core/src/network/network-manager.ts b/packages/core/src/network/network-manager.ts index 2d5e879b..ad68e6d1 100644 --- a/packages/core/src/network/network-manager.ts +++ b/packages/core/src/network/network-manager.ts @@ -1,9 +1,9 @@ import { mtp, tl } from '@mtcute/tl' import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' -import { ITelegramStorage } from '../storage/index.js' -import { MtArgumentError, MtcuteError, MtTimeoutError } from '../types/index.js' -import { ControllablePromise, createControllablePromise, ICryptoProvider, Logger, sleep } from '../utils/index.js' +import { StorageManager } from '../storage/storage.js' +import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js' +import { ControllablePromise, createControllablePromise, DcOptions, ICryptoProvider, Logger, sleep } from '../utils/index.js' import { assertTypeIs } from '../utils/type-assertions.js' import { ConfigManager } from './config-manager.js' import { MultiSessionConnection } from './multi-session-connection.js' @@ -30,7 +30,7 @@ const CLIENT_ERRORS = { * This type is intended for internal usage only. */ export interface NetworkManagerParams { - storage: ITelegramStorage + storage: StorageManager crypto: ICryptoProvider log: Logger @@ -48,8 +48,8 @@ export interface NetworkManagerParams { readerMap: TlReaderMap writerMap: TlWriterMap isPremium: boolean - _emitError: (err: Error, connection?: SessionConnection) => void - keepAliveAction: () => void + emitError: (err: Error, connection?: SessionConnection) => void + onUpdate: (upd: tl.TypeUpdates) => void onUsable: () => void } @@ -238,7 +238,7 @@ export class DcConnectionManager { /** DC ID */ readonly dcId: number, /** DC options to use */ - readonly _dcs: ITelegramStorage.DcOptions, + readonly _dcs: DcOptions, /** Whether this DC is the primary one */ public isPrimary = false, ) { @@ -278,13 +278,13 @@ export class DcConnectionManager { this.upload.setAuthKey(key) this.download.setAuthKey(key) this.downloadSmall.setAuthKey(key) - Promise.resolve(this.manager._storage.setAuthKeyFor(this.dcId, key)) + Promise.resolve(this.manager._storage.provider.authKeys.set(this.dcId, key)) .then(() => { this.upload.notifyKeyChange() this.download.notifyKeyChange() this.downloadSmall.notifyKeyChange() }) - .catch((e: Error) => this.manager.params._emitError(e)) + .catch((e: Error) => this.manager.params.emitError(e)) }) connection.on('tmp-key-change', (idx: number, key: Uint8Array | null, expires: number) => { if (kind !== 'main') { @@ -300,17 +300,17 @@ export class DcConnectionManager { this.download.setAuthKey(key, true) this.downloadSmall.setAuthKey(key, true) - Promise.resolve(this.manager._storage.setTempAuthKeyFor(this.dcId, idx, key, expires * 1000)) + Promise.resolve(this.manager._storage.provider.authKeys.setTemp(this.dcId, idx, key, expires * 1000)) .then(() => { this.upload.notifyKeyChange() this.download.notifyKeyChange() this.downloadSmall.notifyKeyChange() }) - .catch((e: Error) => this.manager.params._emitError(e)) + .catch((e: Error) => this.manager.params.emitError(e)) }) connection.on('future-salts', (salts: mtp.RawMt_future_salt[]) => { - Promise.resolve(this.manager._storage.setFutureSalts(this.dcId, salts)).catch((e: Error) => - this.manager.params._emitError(e), + Promise.resolve(this.manager._storage.salts.store(this.dcId, salts)).catch((e: Error) => + this.manager.params.emitError(e), ) }) @@ -342,7 +342,7 @@ export class DcConnectionManager { }) connection.on('error', (err: Error, conn: SessionConnection) => { - this.manager.params._emitError(err, conn) + this.manager.params.emitError(err, conn) }) } @@ -366,8 +366,8 @@ export class DcConnectionManager { async loadKeys(forcePfs = false): Promise { const [permanent, salts] = await Promise.all([ - this.manager._storage.getAuthKeyFor(this.dcId), - this.manager._storage.getFutureSalts(this.dcId), + this.manager._storage.provider.authKeys.get(this.dcId), + this.manager._storage.salts.fetch(this.dcId), ]) this.main.setAuthKey(permanent) @@ -384,9 +384,10 @@ export class DcConnectionManager { } if (this.manager.params.usePfs || forcePfs) { + const now = Date.now() await Promise.all( this.main._sessions.map(async (_, i) => { - const temp = await this.manager._storage.getAuthKeyFor(this.dcId, i) + const temp = await this.manager._storage.provider.authKeys.getTemp(this.dcId, i, now) this.main.setAuthKey(temp, true, i) if (i === 0) { @@ -425,9 +426,7 @@ export class NetworkManager { protected readonly _dcConnections = new Map() protected _primaryDc?: DcConnectionManager - private _keepAliveInterval?: NodeJS.Timeout - private _lastUpdateTime = 0 - private _updateHandler: (upd: tl.TypeUpdates, fromClient: boolean) => void = () => {} + private _updateHandler: (upd: tl.TypeUpdates, fromClient: boolean) => void constructor( readonly params: NetworkManagerParams & NetworkManagerExtraParams, @@ -460,12 +459,13 @@ export class NetworkManager { this._transportFactory = params.transport ?? defaultTransportFactory this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate + this._updateHandler = params.onUpdate this._onConfigChanged = this._onConfigChanged.bind(this) config.onConfigUpdate(this._onConfigChanged) } - private async _findDcOptions(dcId: number): Promise { + private async _findDcOptions(dcId: number): Promise { const main = await this.config.findOption({ dcId, allowIpv6: this.params.useIpv6, @@ -499,31 +499,9 @@ export class NetworkManager { dc.setIsPrimary(true) dc.main.on('usable', () => { - this._lastUpdateTime = Date.now() this.params.onUsable() - - if (this._keepAliveInterval) clearInterval(this._keepAliveInterval) - this._keepAliveInterval = setInterval(() => { - if (Date.now() - this._lastUpdateTime > 900_000) { - // telegram asks to fetch pending updates if there are no updates for 15 minutes. - // it is up to the user to decide whether to do it or not - - this.params.keepAliveAction() - this._lastUpdateTime = Date.now() - } - }, 60_000) - - Promise.resolve(this._storage.getSelf()) - .then((self) => { - if (self?.isBot) { - // bots may receive tmpSessions, which we should respect - return this.config.update(true) - } - }) - .catch((e: Error) => this.params._emitError(e)) }) dc.main.on('update', (update: tl.TypeUpdates) => { - this._lastUpdateTime = Date.now() this._updateHandler(update, false) }) @@ -569,7 +547,7 @@ export class NetworkManager { * * @param defaultDcs Default DCs to connect to */ - async connect(defaultDcs: ITelegramStorage.DcOptions): Promise { + async connect(defaultDcs: DcOptions): Promise { if (defaultDcs.main.id !== defaultDcs.media.id) { throw new MtArgumentError('Default DCs must be the same') } @@ -631,16 +609,44 @@ export class NetworkManager { } } - notifyLoggedIn(auth: tl.auth.TypeAuthorization): void { - if (auth._ === 'auth.authorizationSignUpRequired' || auth.user._ === 'userEmpty') { - return + notifyLoggedIn(auth: tl.auth.TypeAuthorization | tl.RawUser): tl.RawUser { + if (auth._ === 'auth.authorizationSignUpRequired') { + throw new MtUnsupportedError( + 'Signup is no longer supported by Telegram for non-official clients. Please use your mobile device to sign up.', + ) } - if (auth.tmpSessions) { - this._primaryDc?.main.setCount(auth.tmpSessions) + let user: tl.RawUser + + if (auth._ === 'auth.authorization') { + if (auth.tmpSessions) { + this._primaryDc?.main.setCount(auth.tmpSessions) + } + + user = auth.user as tl.RawUser + } else { + if (auth.bot) { + // bots may receive tmpSessions, which we should respect + this.config.update(true) + .catch((e: Error) => this.params.emitError(e)) + } + + user = auth } - this.setIsPremium(auth.user.premium!) + this.setIsPremium(user.premium!) + + // telegram ignores invokeWithoutUpdates for auth methods + if (auth._ === 'auth.authorization' && this.params.disableUpdates) { + this.resetSessions() + } + + return user + } + + notifyLoggedOut(): void { + this.setIsPremium(false) + this.resetSessions() } resetSessions(): void { @@ -668,7 +674,7 @@ export class NetworkManager { this._dcConnections.set(newDc, new DcConnectionManager(this, newDc, options, true)) } - await this._storage.setDefaultDcs(options) + await this._storage.dcs.store(options) await this._switchPrimaryDc(this._dcConnections.get(newDc)!) } @@ -723,10 +729,6 @@ export class NetworkManager { try { const res = await multi.sendRpc(message, stack, params?.timeout, params?.abortSignal, params?.chainId) - if (kind === 'main') { - this._lastUpdateTime = Date.now() - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return res // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -800,14 +802,6 @@ export class NetworkManager { throw lastError! } - setUpdateHandler(handler: (upd: tl.TypeUpdates, fromClient: boolean) => void): void { - this._updateHandler = handler - } - - handleUpdate(update: tl.TypeUpdates, fromClient = true): void { - this._updateHandler(update, fromClient) - } - changeTransport(factory: TransportFactory): void { for (const dc of this._dcConnections.values()) { dc.main.changeTransport(factory) @@ -843,7 +837,6 @@ export class NetworkManager { for (const dc of this._dcConnections.values()) { dc.destroy() } - if (this._keepAliveInterval) clearInterval(this._keepAliveInterval) this.config.offConfigUpdate(this._onConfigChanged) } } diff --git a/packages/core/src/network/persistent-connection.ts b/packages/core/src/network/persistent-connection.ts index e1be85bf..fec0510e 100644 --- a/packages/core/src/network/persistent-connection.ts +++ b/packages/core/src/network/persistent-connection.ts @@ -1,16 +1,14 @@ import EventEmitter from 'events' -import { tl } from '@mtcute/tl' - import { MtcuteError } from '../types/index.js' -import { ICryptoProvider, Logger } from '../utils/index.js' +import { BasicDcOption, ICryptoProvider, Logger } from '../utils/index.js' import { ReconnectionStrategy } from './reconnection.js' import { ITelegramTransport, TransportFactory, TransportState } from './transports/index.js' export interface PersistentConnectionParams { crypto: ICryptoProvider transportFactory: TransportFactory - dc: tl.RawDcOption + dc: BasicDcOption testMode: boolean reconnectionStrategy: ReconnectionStrategy inactivityTimeout?: number diff --git a/packages/core/src/network/session-connection.ts b/packages/core/src/network/session-connection.ts index 421acf7f..753a68de 100644 --- a/packages/core/src/network/session-connection.ts +++ b/packages/core/src/network/session-connection.ts @@ -218,13 +218,13 @@ export class SessionConnection extends PersistentConnection { } // otherwise, 404 must be referencing the perm_key - this.log.info('transport error 404, reauthorizing') } // there happened a little trolling - this._session.reset(true) + this.log.info('transport error 404, reauthorizing') + this._session.resetAuthKey() + this._resetSession() this.emit('key-change', null) - this._authorize() return } @@ -250,6 +250,7 @@ export class SessionConnection extends PersistentConnection { // we must send some user-related rpc to the server to make sure that // it will send us updates this.sendRpc({ _: 'updates.getState' }).catch((err: any) => { + if (this._destroyed) return // silently fail this.log.warn('failed to send updates.getState: %s', err.text || err.message) }) } @@ -1289,6 +1290,7 @@ export class SessionConnection extends PersistentConnection { this._queuedDestroySession.push(this._session._sessionId) this._session.resetState(true) + this._onAllFailed('session reset') this.reconnect() // once we receive new_session_created, all pending messages will be resent. diff --git a/packages/core/src/network/transports/abstract.ts b/packages/core/src/network/transports/abstract.ts index a78b63da..eaf70218 100644 --- a/packages/core/src/network/transports/abstract.ts +++ b/packages/core/src/network/transports/abstract.ts @@ -2,8 +2,8 @@ import EventEmitter from 'events' import { tl } from '@mtcute/tl' -import { MaybeAsync } from '../../types/index.js' -import { ICryptoProvider, Logger } from '../../utils/index.js' +import { MaybePromise } from '../../types/index.js' +import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js' /** Current state of the transport */ export enum TransportState { @@ -40,13 +40,13 @@ export interface ITelegramTransport extends EventEmitter { /** returns current state */ state(): TransportState /** returns current DC. should return null if state == IDLE */ - currentDc(): tl.RawDcOption | null + currentDc(): BasicDcOption | null /** * Start trying to connect to a specified DC. * Will throw an error if state != IDLE */ - connect(dc: tl.RawDcOption, testMode: boolean): void + connect(dc: BasicDcOption, testMode: boolean): void /** call to close existing connection to some DC */ close(): void /** send a message */ @@ -73,10 +73,10 @@ export type TransportFactory = () => ITelegramTransport */ export interface IPacketCodec { /** Initial tag of the codec. Will be sent immediately once connected. */ - tag(): MaybeAsync + tag(): MaybePromise /** Encodes and frames a single packet */ - encode(packet: Uint8Array): MaybeAsync + encode(packet: Uint8Array): MaybePromise /** Feed packet to the codec. Once packet is processed, codec is supposed to emit `packet` or `error` */ feed(data: Uint8Array): void diff --git a/packages/core/src/network/transports/tcp.ts b/packages/core/src/network/transports/tcp.ts index 7d7ed217..bd191ea7 100644 --- a/packages/core/src/network/transports/tcp.ts +++ b/packages/core/src/network/transports/tcp.ts @@ -1,10 +1,8 @@ import EventEmitter from 'events' import { connect, Socket } from 'net' -import { tl } from '@mtcute/tl' - import { MtcuteError } from '../../types/errors.js' -import { ICryptoProvider, Logger } from '../../utils/index.js' +import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js' import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js' import { IntermediatePacketCodec } from './intermediate.js' @@ -13,7 +11,7 @@ import { IntermediatePacketCodec } from './intermediate.js' * Subclasses must provide packet codec in `_packetCodec` property */ export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { - protected _currentDc: tl.RawDcOption | null = null + protected _currentDc: BasicDcOption | null = null protected _state: TransportState = TransportState.Idle protected _socket: Socket | null = null @@ -41,12 +39,12 @@ export abstract class BaseTcpTransport extends EventEmitter implements ITelegram return this._state } - currentDc(): tl.RawDcOption | null { + currentDc(): BasicDcOption | null { return this._currentDc } // eslint-disable-next-line @typescript-eslint/no-unused-vars - connect(dc: tl.RawDcOption, testMode: boolean): void { + connect(dc: BasicDcOption, testMode: boolean): void { if (this._state !== TransportState.Idle) { throw new MtcuteError('Transport is not IDLE') } diff --git a/packages/core/src/network/transports/websocket.ts b/packages/core/src/network/transports/websocket.ts index 72876ab5..3523e753 100644 --- a/packages/core/src/network/transports/websocket.ts +++ b/packages/core/src/network/transports/websocket.ts @@ -1,9 +1,7 @@ import EventEmitter from 'events' -import { tl } from '@mtcute/tl' - import { MtcuteError, MtUnsupportedError } from '../../types/errors.js' -import { ICryptoProvider, Logger } from '../../utils/index.js' +import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js' import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js' import { IntermediatePacketCodec } from './intermediate.js' import { ObfuscatedPacketCodec } from './obfuscated.js' @@ -25,7 +23,7 @@ const subdomainsMap: Record = { * Subclasses must provide packet codec in `_packetCodec` property */ export abstract class BaseWebSocketTransport extends EventEmitter implements ITelegramTransport { - private _currentDc: tl.RawDcOption | null = null + private _currentDc: BasicDcOption | null = null private _state: TransportState = TransportState.Idle private _socket: WebSocket | null = null private _crypto!: ICryptoProvider @@ -87,11 +85,11 @@ export abstract class BaseWebSocketTransport extends EventEmitter implements ITe return this._state } - currentDc(): tl.RawDcOption | null { + currentDc(): BasicDcOption | null { return this._currentDc } - connect(dc: tl.RawDcOption, testMode: boolean): void { + connect(dc: BasicDcOption, testMode: boolean): void { if (this._state !== TransportState.Idle) { throw new MtcuteError('Transport is not IDLE') } diff --git a/packages/core/src/storage/abstract.ts b/packages/core/src/storage/abstract.ts deleted file mode 100644 index 8585315e..00000000 --- a/packages/core/src/storage/abstract.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { mtp, tl } from '@mtcute/tl' -import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' - -import { BasicPeerType, MaybeAsync } from '../types/index.js' -import { Logger } from '../utils/index.js' - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace ITelegramStorage { - /** Information about a cached peer */ - export interface PeerInfo { - /** Peer marked ID */ - id: number - /** Peer access hash */ - accessHash: tl.Long - /** Peer type */ - type: BasicPeerType - /** Peer username, if any */ - username?: string - /** Peer phone number, if available */ - phone?: string - - /** Full TL object with the cached entity */ - full: tl.TypeUser | tl.TypeChat - } - - /** Information about currently logged in user */ - export interface SelfInfo { - /** Whether this is a bot */ - isBot: boolean - /** Current user's ID */ - userId: number - } - - /** Information about preferred DC-s for the user */ - export interface DcOptions { - /** Main DC */ - main: tl.RawDcOption - /** Media DC. Can be the same as main */ - media: tl.RawDcOption - } -} - -/** - * Abstract interface for persistent storage. - * - * In some cases you may want to extend existing MemorySession - * and override save()/load()/destroy() methods, but you are also free - * to implement your own session (be sure to refer to MemorySession - * source code to avoid shooting your leg though) - * - * Note that even though set methods *can* be async, you should only - * write updates to the disk when `save()` is called. - */ -export interface ITelegramStorage { - /** - * This method is called before any other. - * For storages that use logging, logger instance. - * For storages that use binary storage, binary maps - */ - setup?(log: Logger, readerMap: TlReaderMap, writerMap: TlWriterMap): void - - /** - * Load session from some external storage. - * Should be used either to load session content from file/network/etc - * to memory, or to open required connections to fetch session content later - */ - load?(): MaybeAsync - /** - * Save session to some external storage. - * Should be used to commit pending changes in the session. - * For example, saving session content to file/network/etc, - * or committing a database transaction - */ - save?(): MaybeAsync - /** - * Cleanup session and release all used resources. - */ - destroy?(): MaybeAsync - - /** - * Reset session to its default state, optionally resetting auth keys - * - * @param [withAuthKeys=false] Whether to also reset auth keys - */ - reset(withAuthKeys?: boolean): MaybeAsync - - /** - * Set default datacenter to use with this session. - */ - setDefaultDcs(dcs: ITelegramStorage.DcOptions | null): MaybeAsync - /** - * Get default datacenter for this session - * (by default should return null) - */ - getDefaultDcs(): MaybeAsync - - /** - * Store information about future salts for a given DC - */ - setFutureSalts(dcId: number, salts: mtp.RawMt_future_salt[]): MaybeAsync - /** - * Get information about future salts for a given DC (if available) - * - * You don't need to implement any checks, they will be done by the library. - * It is enough to just return the same array that was passed to `setFutureSalts`. - */ - getFutureSalts(dcId: number): MaybeAsync - - /** - * Get auth_key for a given DC - * (returning null will start authorization) - * For temp keys: should also return null if the key has expired - * - * @param dcId DC ID - * @param tempIndex Index of the temporary key (usually 0, used for multi-connections) - */ - getAuthKeyFor(dcId: number, tempIndex?: number): MaybeAsync - /** - * Set auth_key for a given DC - */ - setAuthKeyFor(dcId: number, key: Uint8Array | null): MaybeAsync - /** - * Set temp_auth_key for a given DC - * expiresAt is unix time in ms - */ - setTempAuthKeyFor(dcId: number, index: number, key: Uint8Array | null, expiresAt: number): MaybeAsync - /** - * Remove all saved auth keys (both temp and perm) - * for the given DC. Used when perm_key becomes invalid, - * meaning all temp_keys also become invalid - */ - dropAuthKeysFor(dcId: number): MaybeAsync - - /** - * Get information about currently logged in user (if available) - */ - getSelf(): MaybeAsync - /** - * Save information about currently logged in user - */ - setSelf(self: ITelegramStorage.SelfInfo | null): MaybeAsync - - /** - * Update local database of input peers from the peer info list - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - updatePeers(peers: ITelegramStorage.PeerInfo[]): MaybeAsync - - /** - * Find a peer in local database by its marked ID - * - * If no peer was found, the storage should try searching its - * reference messages database. If a reference message is found, - * a `inputPeer*FromMessage` constructor should be returned - */ - getPeerById(peerId: number): MaybeAsync - - /** - * Find a peer in local database by its username - */ - getPeerByUsername(username: string): MaybeAsync - - /** - * Find a peer in local database by its phone number - */ - getPeerByPhone(phone: string): MaybeAsync - - /** - * For `*FromMessage` constructors: store a reference to a `peerId` - - * it was seen in message `messageId` in chat `chatId`. - * - * `peerId` and `chatId` are marked peer IDs. - * - * Learn more: https://core.telegram.org/api/min - */ - saveReferenceMessage(peerId: number, chatId: number, messageId: number): MaybeAsync - - /** - * For `*FromMessage` constructors: messages `messageIds` in chat `chatId` were deleted, - * so remove any stored peer references to them. - */ - deleteReferenceMessages(chatId: number, messageIds: number[]): MaybeAsync - - /** - * Get updates state (if available), represented as a tuple - * containing: `pts, qts, date, seq` - */ - getUpdatesState(): MaybeAsync<[number, number, number, number] | null> - - /** - * Set common `pts` value - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - setUpdatesPts(val: number): MaybeAsync - /** - * Set common `qts` value - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - setUpdatesQts(val: number): MaybeAsync - /** - * Set updates `date` value - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - setUpdatesDate(val: number): MaybeAsync - /** - * Set updates `seq` value - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - setUpdatesSeq(val: number): MaybeAsync - - /** - * Get channel `pts` value - */ - getChannelPts(entityId: number): MaybeAsync - /** - * Set channels `pts` values in batch. - * - * Storage is supposed to replace stored channel `pts` values - * with given in the object (key is unmarked peer id, value is the `pts`) - * - * Client will call `.save()` after all updates-related methods - * are called, so you can safely batch these updates - */ - setManyChannelPts(values: Map): MaybeAsync - - /** - * Get cached peer information by their marked ID. - * Return `null` if caching is not supported, or the entity - * is not cached (yet). - * - * This is primarily used when a `min` entity is encountered - * in an update, or when a *short* update is encountered. - * Returning `null` will require re-fetching that - * update with the peers added, which might not be very efficient. - */ - getFullPeerById(id: number): MaybeAsync -} diff --git a/packages/core/src/storage/driver.ts b/packages/core/src/storage/driver.ts new file mode 100644 index 00000000..2358e01a --- /dev/null +++ b/packages/core/src/storage/driver.ts @@ -0,0 +1,90 @@ +import { MaybePromise } from '../types/utils.js' +import { Logger } from '../utils/logger.js' + +/** + * Basic storage driver interface, + * describing the lifecycle of a storage driver + */ +export interface IStorageDriver { + /** + * Load session from some external storage. + * Should be used either to load data from file/network/etc + * to memory, or to open required connections to fetch data on demand + * + * May be called more than once, handle this with care + * (or use {@link BaseStorageDriver} that handles this for you) + */ + load?(): MaybePromise + /** + * Save session to some external storage. + * Should be used to commit pending changes in the session. + * For example, saving session content to file/network/etc, + * or committing a database transaction + * + * It is safe to batch all changes and only commit them here, + * unless stated otherwise in the method description + */ + save?(): MaybePromise + /** + * Cleanup session and release all used resources. + * + * May be called more than once, handle this with care + * (or use {@link BaseStorageDriver} that handles this for you) + */ + destroy?(): MaybePromise + + /** + * Setup the driver, passing the logger instance, + * in case your driver needs it + */ + setup?(log: Logger): void +} + +/** + * Base storage driver class, implementing {@link IStorageDriver} + * and handling the lifecycle for you + */ +export abstract class BaseStorageDriver implements IStorageDriver { + abstract _load(): MaybePromise + abstract _destroy(): MaybePromise + abstract _save?(): MaybePromise + + private _loadedTimes = 0 + private _destroyed = false + + protected _log!: Logger + + setup(log: Logger): void { + this._log = log + } + + protected get loaded(): boolean { + return this._loadedTimes > 0 + } + + async load(): Promise { + if (this._loadedTimes === 0) { + await this._load() + this._destroyed = false + } + + this._loadedTimes++ + } + + async destroy(): Promise { + if (this._destroyed) { + return + } + + this._loadedTimes-- + + if (this._loadedTimes === 0) { + await this._destroy() + this._destroyed = true + } + } + + save(): MaybePromise { + return this._save?.() + } +} diff --git a/packages/core/src/storage/idb.test.ts b/packages/core/src/storage/idb.test.ts deleted file mode 100644 index 4108d903..00000000 --- a/packages/core/src/storage/idb.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { afterAll, describe } from 'vitest' - -import { testStateStorage, testStorage } from '@mtcute/test' - -import { IdbStorage } from './idb.js' - -describe.skipIf(import.meta.env.TEST_ENV !== 'browser')('IdbStorage', () => { - const idbName = 'mtcute_test_' + Math.random().toString(36).slice(2) - - const storage = new IdbStorage(idbName) - testStorage(storage) - testStateStorage(storage) - - afterAll(async () => { - storage.destroy() - - const req = indexedDB.deleteDatabase(idbName) - await new Promise((resolve, reject) => { - req.onerror = () => reject(req.error) - req.onsuccess = () => resolve() - req.onblocked = () => resolve() - }) - }) -}) diff --git a/packages/core/src/storage/idb.ts b/packages/core/src/storage/idb.ts deleted file mode 100644 index bb52f434..00000000 --- a/packages/core/src/storage/idb.ts +++ /dev/null @@ -1,685 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { mtp, tl } from '@mtcute/tl' -import { TlBinaryReader, TlBinaryWriter, TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' - -import { Logger } from '../utils/logger.js' -import { longFromFastString, longToFastString } from '../utils/long-utils.js' -import { LruMap } from '../utils/lru-map.js' -import { toggleChannelIdMark } from '../utils/peer-utils.js' -import { ITelegramStorage } from './abstract.js' - -const CURRENT_VERSION = 1 - -const TABLES = { - kv: 'kv', - state: 'state', - authKeys: 'auth_keys', - tempAuthKeys: 'temp_auth_keys', - pts: 'pts', - entities: 'entities', - messageRefs: 'message_refs', -} as const -const EMPTY_BUFFER = new Uint8Array(0) - -interface AuthKeyDto { - dc: number - key: Uint8Array - expiresAt?: number -} - -interface EntityDto { - id: number - hash: string - type: string - username?: string - phone?: string - updated: number - full: Uint8Array -} - -interface MessageRefDto { - peerId: number - chatId: number - msgId: number -} - -interface FsmItemDto { - key: string - value: string - expires?: number -} - -function txToPromise(tx: IDBTransaction): Promise { - return new Promise((resolve, reject) => { - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error) - }) -} - -function reqToPromise(req: IDBRequest): Promise { - return new Promise((resolve, reject) => { - req.onsuccess = () => resolve(req.result) - req.onerror = () => reject(req.error) - }) -} - -function getInputPeer(row: EntityDto | ITelegramStorage.PeerInfo): tl.TypeInputPeer { - const id = row.id - - switch (row.type) { - case 'user': - return { - _: 'inputPeerUser', - userId: id, - accessHash: 'accessHash' in row ? row.accessHash : longFromFastString(row.hash), - } - case 'chat': - return { - _: 'inputPeerChat', - chatId: -id, - } - case 'channel': - return { - _: 'inputPeerChannel', - channelId: toggleChannelIdMark(id), - accessHash: 'accessHash' in row ? row.accessHash : longFromFastString(row.hash), - } - } - - throw new Error(`Invalid peer type: ${row.type}`) -} - -interface CachedEntity { - peer: tl.TypeInputPeer - full: tl.TypeUser | tl.TypeChat | null -} - -/** - * mtcute storage that uses IndexedDB as a backend. - * - * This storage is the default one for browsers, and is generally - * recommended over local storage based one. - */ -export class IdbStorage implements ITelegramStorage { - private _cache?: LruMap - - private _vacuumTimeout?: NodeJS.Timeout - private _vacuumInterval: number - - constructor( - readonly _dbName: string, - params?: { - /** - * Entities cache size, in number of entities. - * - * Recently encountered entities are cached in memory, - * to avoid redundant database calls. Set to 0 to - * disable caching (not recommended) - * - * Note that by design in-memory cached is only - * used when finding peer by ID, since other - * kinds of lookups (phone, username) may get stale quickly - * - * @default `100` - */ - cacheSize?: number - - /** - * Updates to already cached in-memory entities are only - * applied in DB once in a while, to avoid redundant - * DB calls. - * - * If you are having issues with this, you can set this to `0` - * - * @default `30000` (30 sec) - */ - unimportantSavesDelay?: number - - /** - * Interval in milliseconds for vacuuming the storage. - * - * When vacuuming, the storage will remove expired FSM - * states to reduce disk and memory usage. - * - * @default `300_000` (5 minutes) - */ - vacuumInterval?: number - }, - ) { - if (params?.cacheSize !== 0) { - this._cache = new LruMap(params?.cacheSize ?? 100) - } - - this._vacuumInterval = params?.vacuumInterval ?? 300_000 - } - - db!: IDBDatabase - - private _upgradeDb(db: IDBDatabase, oldVer: number, newVer: number): void { - while (oldVer < newVer) { - switch (oldVer) { - case 0: { - db.createObjectStore(TABLES.kv, { keyPath: 'key' }) - db.createObjectStore(TABLES.authKeys, { keyPath: 'dc' }) - db.createObjectStore(TABLES.tempAuthKeys, { keyPath: ['dc', 'idx'] }) - db.createObjectStore(TABLES.pts, { keyPath: 'channelId' }) - - const stateOs = db.createObjectStore(TABLES.state, { keyPath: 'key' }) - stateOs.createIndex('by_expires', 'expires') - - const entitiesOs = db.createObjectStore(TABLES.entities, { keyPath: 'id' }) - entitiesOs.createIndex('by_username', 'username') - entitiesOs.createIndex('by_phone', 'phone') - - const msgRefsOs = db.createObjectStore(TABLES.messageRefs, { keyPath: 'peerId' }) - msgRefsOs.createIndex('by_msg', ['chatId', 'msgId']) - - oldVer++ - } - } - } - - if (newVer !== CURRENT_VERSION) throw new Error(`Invalid db version: ${newVer}`) - } - - private log!: Logger - private readerMap!: TlReaderMap - private writerMap!: TlWriterMap - private _reader!: TlBinaryReader - - setup(log: Logger, readerMap: TlReaderMap, writerMap: TlWriterMap): void { - this.log = log.create('idb') - this.readerMap = readerMap - this.writerMap = writerMap - this._reader = new TlBinaryReader(readerMap, EMPTY_BUFFER) - } - - private _pendingWrites: [string, unknown][] = [] - private _pendingWritesOses = new Set() - - private _writeLater(table: string, obj: unknown): void { - this._pendingWrites.push([table, obj]) - this._pendingWritesOses.add(table) - } - - private _readFullPeer(data: Uint8Array): tl.TypeUser | tl.TypeChat | null { - this._reader = new TlBinaryReader(this.readerMap, data) - let obj - - try { - obj = this._reader.object() - } catch (e) { - // object might be from an older tl layer, in which case it will be ignored - obj = null - } - - return obj as tl.TypeUser | tl.TypeChat | null - } - - async load(): Promise { - this.db = await new Promise((resolve, reject) => { - const req = indexedDB.open(this._dbName, CURRENT_VERSION) - - req.onerror = () => reject(req.error) - req.onsuccess = () => resolve(req.result) - req.onupgradeneeded = (event) => - this._upgradeDb(req.result, event.oldVersion, event.newVersion || CURRENT_VERSION) - }) - - this._vacuumTimeout = setInterval(() => { - this._vacuum().catch((e) => { - this.log.warn('Failed to vacuum database: %s', e) - }) - }, this._vacuumInterval) - } - - async save() { - if (this._pendingWritesOses.size === 0) return - - const writes = this._pendingWrites - const oses = this._pendingWritesOses - this._pendingWrites = [] - this._pendingWritesOses = new Set() - - const tx = this.db.transaction(oses, 'readwrite') - - const osMap = new Map() - - for (const table of oses) { - osMap.set(table, tx.objectStore(table)) - } - - for (const [table, obj] of writes) { - const os = osMap.get(table)! - - if (obj === null) { - os.delete(table) - } else { - os.put(obj) - } - } - - await txToPromise(tx) - } - - private async _vacuum(): Promise { - const tx = this.db.transaction(TABLES.state, 'readwrite') - const os = tx.objectStore(TABLES.state) - - const keys = await reqToPromise(os.index('by_expires').getAllKeys(IDBKeyRange.upperBound(Date.now()))) - - for (const key of keys) { - os.delete(key) - } - - return txToPromise(tx) - } - - destroy(): void { - this.db.close() - clearInterval(this._vacuumTimeout) - } - - async reset(withAuthKeys?: boolean | undefined): Promise { - this._cache?.clear() - const tx = this.db.transaction(Object.values(TABLES), 'readwrite') - - for (const table of Object.values(TABLES)) { - if (table === TABLES.authKeys && !withAuthKeys) continue - if (table === TABLES.tempAuthKeys && !withAuthKeys) continue - - tx.objectStore(table).clear() - } - - return txToPromise(tx) - } - - private async _getFromKv(key: string): Promise { - const tx = this.db.transaction(TABLES.kv) - const store = tx.objectStore(TABLES.kv) - - const res = await reqToPromise<{ value: string }>(store.get(key)) - - if (res === undefined) return null - - return JSON.parse(res.value) as T - } - - private async _setToKv(key: string, value: T, now = false): Promise { - const dto = { key, value: JSON.stringify(value) } - - if (!now) { - this._writeLater(TABLES.kv, dto) - - return - } - - const tx = this.db.transaction(TABLES.kv, 'readwrite') - const store = tx.objectStore(TABLES.kv) - - await reqToPromise(store.put(dto)) - } - - setDefaultDcs(dcs: ITelegramStorage.DcOptions | null): Promise { - return this._setToKv('dcs', dcs, true) - } - - getDefaultDcs(): Promise { - return this._getFromKv('dcs') - } - - async getFutureSalts(dcId: number): Promise { - const res = await this._getFromKv(`futureSalts:${dcId}`) - if (!res) return null - - return res.map((it) => { - const [salt, validSince, validUntil] = it.split(',') - - return { - _: 'mt_future_salt', - validSince: Number(validSince), - validUntil: Number(validUntil), - salt: longFromFastString(salt), - } - }) - } - - setFutureSalts(dcId: number, salts: mtp.RawMt_future_salt[]): Promise { - return this._setToKv( - `futureSalts:${dcId}`, - salts.map((salt) => `${longToFastString(salt.salt)},${salt.validSince},${salt.validUntil}`), - true, - ) - } - - async getAuthKeyFor(dcId: number, tempIndex?: number | undefined): Promise { - let row: AuthKeyDto - - if (tempIndex !== undefined) { - const os = this.db.transaction(TABLES.tempAuthKeys).objectStore(TABLES.tempAuthKeys) - row = await reqToPromise(os.get([dcId, tempIndex])) - if (row === undefined || row.expiresAt! < Date.now()) return null - } else { - const os = this.db.transaction(TABLES.authKeys).objectStore(TABLES.authKeys) - row = await reqToPromise(os.get(dcId)) - if (row === undefined) return null - } - - return row.key - } - - async setAuthKeyFor(dcId: number, key: Uint8Array | null): Promise { - const os = this.db.transaction(TABLES.authKeys, 'readwrite').objectStore(TABLES.authKeys) - - if (key === null) { - return reqToPromise(os.delete(dcId)) - } - - await reqToPromise(os.put({ dc: dcId, key })) - } - - async setTempAuthKeyFor(dcId: number, index: number, key: Uint8Array | null, expiresAt: number): Promise { - const os = this.db.transaction(TABLES.tempAuthKeys, 'readwrite').objectStore(TABLES.tempAuthKeys) - - if (key === null) { - return reqToPromise(os.delete([dcId, index])) - } - - await reqToPromise(os.put({ dc: dcId, idx: index, key, expiresAt })) - } - - async dropAuthKeysFor(dcId: number): Promise { - const tx = this.db.transaction([TABLES.authKeys, TABLES.tempAuthKeys], 'readwrite') - - tx.objectStore(TABLES.authKeys).delete(dcId) - - // IndexedDB sucks - const tempOs = tx.objectStore(TABLES.tempAuthKeys) - const keys = await reqToPromise(tempOs.getAllKeys()) - - for (const key of keys) { - if ((key as [number, number])[0] === dcId) { - tempOs.delete(key) - } - } - } - - private _cachedSelf?: ITelegramStorage.SelfInfo | null - async getSelf(): Promise { - if (this._cachedSelf !== undefined) return this._cachedSelf - - const self = await this._getFromKv('self') - this._cachedSelf = self - - return self - } - - async setSelf(self: ITelegramStorage.SelfInfo | null): Promise { - this._cachedSelf = self - - return this._setToKv('self', self, true) - } - - async getUpdatesState(): Promise<[number, number, number, number] | null> { - const os = this.db.transaction(TABLES.kv).objectStore(TABLES.kv) - - const [pts, qts, date, seq] = await Promise.all([ - reqToPromise<{ value: number }>(os.get('pts')), - reqToPromise<{ value: number }>(os.get('qts')), - reqToPromise<{ value: number }>(os.get('date')), - reqToPromise<{ value: number }>(os.get('seq')), - ]) - - if (pts === undefined || qts === undefined || date === undefined || seq === undefined) return null - - return [Number(pts.value), Number(qts.value), Number(date.value), Number(seq.value)] - } - - setUpdatesPts(val: number): Promise { - return this._setToKv('pts', val) - } - - setUpdatesQts(val: number): Promise { - return this._setToKv('qts', val) - } - - setUpdatesDate(val: number): Promise { - return this._setToKv('date', val) - } - - setUpdatesSeq(val: number): Promise { - return this._setToKv('seq', val) - } - - async getChannelPts(entityId: number): Promise { - const os = this.db.transaction(TABLES.pts).objectStore(TABLES.pts) - const row = await reqToPromise<{ pts: number }>(os.get(entityId)) - - if (row === undefined) return null - - return row.pts - } - - async setManyChannelPts(values: Map): Promise { - const tx = this.db.transaction(TABLES.pts, 'readwrite') - const os = tx.objectStore(TABLES.pts) - - for (const [id, pts] of values) { - os.put({ channelId: id, pts }) - } - - return txToPromise(tx) - } - - updatePeers(peers: ITelegramStorage.PeerInfo[]): void { - for (const peer of peers) { - const dto: EntityDto = { - id: peer.id, - hash: longToFastString(peer.accessHash), - type: peer.type, - username: peer.username, - phone: peer.phone, - updated: Date.now(), - full: TlBinaryWriter.serializeObject(this.writerMap, peer.full), - } - - this._writeLater(TABLES.entities, dto) - - if (!this._cachedSelf?.isBot) { - this._writeLater(TABLES.messageRefs, null) - } - - this._cache?.set(peer.id, { - peer: getInputPeer(peer), - full: peer.full, - }) - } - } - - private async _findPeerByReference(os: IDBObjectStore, peerId: number): Promise { - const row = await reqToPromise(os.get(peerId)) - if (row === undefined) return null - - const chat = await this.getPeerById(row.chatId, false) - if (chat === null) return null - - if (peerId > 0) { - return { - _: 'inputPeerUserFromMessage', - userId: peerId, - peer: chat, - msgId: row.msgId, - } - } - - return { - _: 'inputPeerChannelFromMessage', - channelId: toggleChannelIdMark(peerId), - peer: chat, - msgId: row.msgId, - } - } - - async getPeerById(peerId: number, allowRefs = true): Promise { - const cached = this._cache?.get(peerId) - if (cached) return cached.peer - - const tx = this.db.transaction([TABLES.entities, TABLES.messageRefs]) - const entOs = tx.objectStore(TABLES.entities) - - const row = await reqToPromise(entOs.get(peerId)) - - if (row) { - return getInputPeer(row) - } - - if (allowRefs) { - return this._findPeerByReference(tx.objectStore(TABLES.messageRefs), peerId) - } - - return null - } - - async getPeerByUsername(username: string): Promise { - const tx = this.db.transaction(TABLES.entities) - const os = tx.objectStore(TABLES.entities) - - const row = await reqToPromise(os.index('by_username').get(username)) - - if (row === undefined) return null - - return getInputPeer(row) - } - - async getPeerByPhone(phone: string): Promise { - const tx = this.db.transaction(TABLES.entities) - const os = tx.objectStore(TABLES.entities) - - const row = await reqToPromise(os.index('by_phone').get(phone)) - - if (row === undefined) return null - - return getInputPeer(row) - } - - async getFullPeerById(peerId: number): Promise { - const cached = this._cache?.get(peerId) - if (cached) return cached.full - - const tx = this.db.transaction(TABLES.entities) - const os = tx.objectStore(TABLES.entities) - - const row = await reqToPromise(os.get(peerId)) - - if (row === undefined) return null - - return this._readFullPeer(row.full) - } - - async saveReferenceMessage(peerId: number, chatId: number, messageId: number): Promise { - const os = this.db.transaction(TABLES.messageRefs, 'readwrite').objectStore(TABLES.messageRefs) - - await reqToPromise(os.put({ peerId, chatId, msgId: messageId } satisfies MessageRefDto)) - } - - async deleteReferenceMessages(chatId: number, messageIds: number[]): Promise { - const tx = this.db.transaction(TABLES.messageRefs, 'readwrite') - const os = tx.objectStore(TABLES.messageRefs) - const index = os.index('by_msg') - - for (const msgId of messageIds) { - const key = await reqToPromise(index.getKey([chatId, msgId])) - if (key === undefined) continue - - os.delete(key) - } - - return txToPromise(tx) - } - - // IStateStorage implementation - - async getState(key: string): Promise { - const tx = this.db.transaction(TABLES.state, 'readwrite') - const os = tx.objectStore(TABLES.state) - - const row = await reqToPromise(os.get(key)) - if (!row) return null - - if (row.expires && row.expires < Date.now()) { - await reqToPromise(os.delete(key)) - - return null - } - - return JSON.parse(row.value) as unknown - } - - async setState(key: string, state: unknown, ttl?: number): Promise { - const tx = this.db.transaction(TABLES.state, 'readwrite') - const os = tx.objectStore(TABLES.state) - - const dto: FsmItemDto = { - key, - value: JSON.stringify(state), - expires: ttl ? Date.now() + ttl * 1000 : undefined, - } - - await reqToPromise(os.put(dto)) - } - - async deleteState(key: string): Promise { - const tx = this.db.transaction(TABLES.state, 'readwrite') - const os = tx.objectStore(TABLES.state) - - await reqToPromise(os.delete(key)) - } - - getCurrentScene(key: string): Promise { - return this.getState(`$current_scene_${key}`) as Promise - } - - setCurrentScene(key: string, scene: string, ttl?: number): Promise { - return this.setState(`$current_scene_${key}`, scene, ttl) - } - - deleteCurrentScene(key: string): Promise { - return this.deleteState(`$current_scene_${key}`) - } - - async getRateLimit(key: string, limit: number, window: number): Promise<[number, number]> { - // leaky bucket - const now = Date.now() - - const tx = this.db.transaction(TABLES.state, 'readwrite') - const os = tx.objectStore(TABLES.state) - - const row = await reqToPromise(os.get(`$rate_limit_${key}`)) - - if (!row || row.expires! < now) { - // expired or does not exist - const dto: FsmItemDto = { - key: `$rate_limit_${key}`, - value: limit.toString(), - expires: now + window * 1000, - } - await reqToPromise(os.put(dto)) - - return [limit, dto.expires!] - } - - let value = Number(row.value) - - if (value > 0) { - value -= 1 - row.value = value.toString() - await reqToPromise(os.put(row)) - } - - return [value, row.expires!] - } - - resetRateLimit(key: string): Promise { - return this.deleteState(`$rate_limit_${key}`) - } -} diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts index fb36a4c5..ddf40746 100644 --- a/packages/core/src/storage/index.ts +++ b/packages/core/src/storage/index.ts @@ -1,2 +1,6 @@ -export * from './abstract.js' -export * from './memory.js' +export * from './driver.js' +export * from './provider.js' +export * from './providers/idb/index.js' +export * from './providers/memory/index.js' +export * from './repository/index.js' +export * from './storage.js' diff --git a/packages/core/src/storage/json-file.ts b/packages/core/src/storage/json-file.ts deleted file mode 100644 index 04b0bc23..00000000 --- a/packages/core/src/storage/json-file.ts +++ /dev/null @@ -1,98 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import * as fs from 'fs' - -import { beforeExit } from '../utils/index.js' -import { JsonMemoryStorage } from './json.js' - -/** - * mtcute storage that stores data in a JSON file. - * - * > **Note**: This storage is **not fully persistent**, meaning that - * > some data *will* be lost on restart, including entities cache, - * > FSM and rate limiter states, because JSON file would be too large otherwise. - * > - * > This storage should only be used for testing purposes, - * > and should not be used in production. Use e.g. `@mtcute/sqlite` instead. - * - * @deprecated - */ -export class JsonFileStorage extends JsonMemoryStorage { - private readonly _filename: string - private readonly _safe: boolean - private readonly _cleanupUnregister?: () => void - - constructor( - filename: string, - params?: { - /** - * Whether to save file "safely", meaning that the file will first be saved - * to `${filename}.tmp`, and then renamed to `filename`, - * instead of writing directly to `filename`. - * - * This solves the issue with the storage being saved as - * a blank file because of the app being stopped while - * the storage is being written. - * - * @default `true` - */ - safe?: boolean - - /** - * Whether to save file on process exit. - * - * @default `true` - */ - cleanup?: boolean - }, - ) { - super() - - this._filename = filename - this._safe = params?.safe ?? true - - if (params?.cleanup !== false) { - this._cleanupUnregister = beforeExit(() => this._onProcessExit()) - } - } - - async load(): Promise { - try { - this._loadJson( - await new Promise((res, rej) => - fs.readFile(this._filename, 'utf-8', (err, data) => (err ? rej(err) : res(data))), - ), - ) - } catch (e) {} - } - - save(): Promise { - return new Promise((resolve, reject) => { - fs.writeFile(this._safe ? this._filename + '.tmp' : this._filename, this._saveJson(), (err) => { - if (err) reject(err) - else if (this._safe) { - fs.rename(this._filename + '.tmp', this._filename, (err) => { - if (err && err.code !== 'ENOENT') reject(err) - else resolve() - }) - } else resolve() - }) - }) - } - - private _onProcessExit(): void { - // on exit handler must be synchronous, thus we use sync methods here - try { - fs.writeFileSync(this._filename, this._saveJson()) - } catch (e) {} - - if (this._safe) { - try { - fs.unlinkSync(this._filename + '.tmp') - } catch (e) {} - } - } - - destroy(): void { - this._cleanupUnregister?.() - } -} diff --git a/packages/core/src/storage/json.test.ts b/packages/core/src/storage/json.test.ts deleted file mode 100644 index 362b36fe..00000000 --- a/packages/core/src/storage/json.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { JsonMemoryStorage } from './json.js' - -// eslint-disable-next-line no-restricted-globals -const createBuffer = import.meta.env.TEST_ENV === 'node' ? Buffer.from : (d: number[]) => new Uint8Array(d) - -describe('JsonMemoryStorage', () => { - class ExtJsonMemoryStorage extends JsonMemoryStorage { - loadJson(json: string): void { - this._loadJson(json) - } - - saveJson(): string { - return this._saveJson() - } - - getInternalState() { - return this._state - } - } - - it('should allow importing and exporting to json', () => { - const s = new ExtJsonMemoryStorage() - - s.setUpdatesPts(123) - s.setUpdatesQts(456) - // eslint-disable-next-line no-restricted-globals - s.setAuthKeyFor(1, createBuffer([1, 2, 3])) - // eslint-disable-next-line no-restricted-globals - s.setTempAuthKeyFor(2, 0, createBuffer([4, 5, 6]), 1234567890) - - const json = s.saveJson() - const s2 = new ExtJsonMemoryStorage() - s2.loadJson(json) - - expect(s2.getInternalState()).toEqual({ - ...s.getInternalState(), - entities: new Map(), // entities are not saved - }) - }) -}) diff --git a/packages/core/src/storage/json.ts b/packages/core/src/storage/json.ts deleted file mode 100644 index dbe9039a..00000000 --- a/packages/core/src/storage/json.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import { base64DecodeToBuffer, base64Encode } from '@mtcute/tl-runtime' - -import { MemorySessionState, MemoryStorage } from './memory.js' - -/** - * Helper class that provides json serialization functions - * to the session. - */ -export class JsonMemoryStorage extends MemoryStorage { - protected _loadJson(json: string): void { - this._setStateFrom( - JSON.parse(json, (key, value) => { - switch (key) { - case 'authKeys': - case 'authKeysTemp': { - const ret = new Map() - - ;(value as string).split('|').forEach((pair: string) => { - const [dcId, b64] = pair.split(',') - const mapKey = key === 'authKeysTemp' ? dcId : parseInt(dcId) - - ret.set(mapKey, base64DecodeToBuffer(b64)) - }) - - return ret - } - case 'authKeysTempExpiry': - case 'pts': - case 'futureSalts': - return new Map(Object.entries(value as Record)) - case 'phoneIndex': - case 'usernameIndex': - case 'fsm': - case 'rl': - case 'refs': - case 'entities': - return new Map() - } - - return value - }) as MemorySessionState, - ) - } - - protected _saveJson(): string { - return JSON.stringify(this._state, (key, value) => { - switch (key) { - case 'authKeys': - case 'authKeysTemp': { - const value_ = value as Map - - return [...value_.entries()] - .filter((it): it is [string, Uint8Array] => it[1] !== null) - .map(([dcId, key]) => dcId + ',' + base64Encode(key)) - .join('|') - } - case 'authKeysTempExpiry': - case 'phoneIndex': - case 'usernameIndex': - case 'pts': - case 'fsm': - case 'rl': - return Object.fromEntries([...(value as Map).entries()]) - case 'entities': - return {} - } - - return value - }) - } -} diff --git a/packages/core/src/storage/localstorage.test.ts b/packages/core/src/storage/localstorage.test.ts deleted file mode 100644 index 3e041277..00000000 --- a/packages/core/src/storage/localstorage.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' - -import { LocalstorageStorage } from './localstorage.js' - -const localStorageStub = { - getItem: vi.fn().mockImplementation(() => null), - setItem: vi.fn(), -} -describe('LocalstorageStorage', () => { - beforeAll(() => void vi.stubGlobal('localStorage', localStorageStub)) - afterAll(() => void vi.unstubAllGlobals()) - - it('should load from localstorage', () => { - const s = new LocalstorageStorage('test') - s.load() - - expect(localStorageStub.getItem).toHaveBeenCalledWith('test') - }) - - it('should save to localstorage', () => { - const s = new LocalstorageStorage('test') - s.save() - - expect(localStorageStub.setItem).toHaveBeenCalledWith('test', expect.any(String)) - }) -}) diff --git a/packages/core/src/storage/localstorage.ts b/packages/core/src/storage/localstorage.ts deleted file mode 100644 index 6a1e8f23..00000000 --- a/packages/core/src/storage/localstorage.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MtUnsupportedError } from '../types/index.js' -import { JsonMemoryStorage } from './json.js' - -/** - * mtcute storage that stores data in a `localStorage` key. - * - * > **Note**: This storage is **not fully persistent**, meaning that - * > some data *will* be lost on restart, including entities cache, - * > FSM and rate limiter states, because the JSON would be too large otherwise. - * > - * > This storage should only be used for testing purposes, - * > and should not be used in production. Use e.g. {@link IdbStorage} instead. - * - * @deprecated - */ -export class LocalstorageStorage extends JsonMemoryStorage { - private readonly _key: string - - constructor(key: string) { - super() - - if (typeof localStorage === 'undefined') { - throw new MtUnsupportedError('localStorage is not available!') - } - - this._key = key - } - - load(): void { - try { - const val = localStorage.getItem(this._key) - if (val === null) return - - this._loadJson(val) - } catch (e) {} - } - - save(): void { - localStorage.setItem(this._key, this._saveJson()) - } -} diff --git a/packages/core/src/storage/memory.test.ts b/packages/core/src/storage/memory.test.ts deleted file mode 100644 index 6ab8f664..00000000 --- a/packages/core/src/storage/memory.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { testStateStorage, testStorage } from '@mtcute/test' - -import { MemoryStorage } from './memory.js' - -describe('MemoryStorage', () => { - testStorage(new MemoryStorage()) - testStateStorage(new MemoryStorage()) - - describe('extending', () => { - it('should allow populating from an object', () => { - class ExtendedMemoryStorage extends MemoryStorage { - constructor() { - super() - this._setStateFrom({ - $version: 3, - defaultDcs: null, - authKeys: new Map(), - authKeysTemp: new Map(), - authKeysTempExpiry: new Map(), - entities: new Map(), - phoneIndex: new Map(), - usernameIndex: new Map(), - gpts: [1, 2, 3, 4], - pts: new Map(), - fsm: new Map(), - rl: new Map(), - refs: new Map(), - self: null, - futureSalts: new Map(), - }) - } - } - - const s = new ExtendedMemoryStorage() - - expect(s.getUpdatesState()).toEqual([1, 2, 3, 4]) - }) - - it('should silently fail if version is wrong', () => { - class ExtendedMemoryStorage extends MemoryStorage { - constructor() { - super() - // eslint-disable-next-line - this._setStateFrom({ $version: 0 } as any) - } - } - - const s = new ExtendedMemoryStorage() - - expect(s.getUpdatesState()).toEqual(null) - }) - }) -}) diff --git a/packages/core/src/storage/memory.ts b/packages/core/src/storage/memory.ts deleted file mode 100644 index da5637d9..00000000 --- a/packages/core/src/storage/memory.ts +++ /dev/null @@ -1,524 +0,0 @@ -import { mtp, tl } from '@mtcute/tl' - -import { LruMap, toggleChannelIdMark } from '../utils/index.js' -import { ITelegramStorage } from './abstract.js' - -const CURRENT_VERSION = 3 - -type PeerInfoWithUpdated = ITelegramStorage.PeerInfo & { updated: number } - -export interface MemorySessionState { - // forwards compatibility for persistent storages - $version: typeof CURRENT_VERSION - - defaultDcs: ITelegramStorage.DcOptions | null - authKeys: Map - authKeysTemp: Map - authKeysTempExpiry: Map - - // marked peer id -> entity info - entities: Map - // phone number -> peer id - phoneIndex: Map - // username -> peer id - usernameIndex: Map - - // reference messages. peer id -> `${chat id}:${msg id}][] - refs: Map> - - // common pts, date, seq, qts - gpts: [number, number, number, number] | null - // channel pts - pts: Map - - // state for fsm - fsm: Map< - string, - { - // value - v: unknown - // expires - e?: number - } - > - - // state for rate limiter - rl: Map< - string, - { - // reset - res: number - // remaining - rem: number - } - > - - self: ITelegramStorage.SelfInfo | null - futureSalts: Map -} - -const USERNAME_TTL = 86400000 // 24 hours - -/** - * In-memory storage implementation for mtcute. - * - * This storage is **not persistent**, meaning that all data - * **will** be lost on restart. Only use this storage for testing, - * or if you know what you're doing. - */ -export class MemoryStorage implements ITelegramStorage { - protected _state!: MemorySessionState - private _cachedInputPeers: LruMap = new LruMap(100) - - private _cachedFull: LruMap - - private _vacuumTimeout?: NodeJS.Timeout - private _vacuumInterval: number - - constructor(params?: { - /** - * Maximum number of cached full entities. - * - * Note that full entities are **NOT** persisted - * to the disk (in case this storage is backed - * by a local storage), and only available within - * the current runtime. - * - * @default `100`, use `0` to disable - */ - cacheSize?: number - - /** - * Interval in milliseconds for vacuuming the storage. - * - * When vacuuming, the storage will remove expired FSM - * states to reduce memory usage. - * - * @default `300_000` (5 minutes) - */ - vacuumInterval?: number - }) { - this.reset(true) - this._cachedFull = new LruMap(params?.cacheSize ?? 100) - this._vacuumInterval = params?.vacuumInterval ?? 300_000 - } - - load(): void { - this._vacuumTimeout = setInterval(this._vacuum.bind(this), this._vacuumInterval) - } - - destroy(): void { - clearInterval(this._vacuumTimeout) - } - - reset(withAuthKeys = false): void { - this._state = { - $version: CURRENT_VERSION, - defaultDcs: null, - authKeys: withAuthKeys ? new Map() : this._state.authKeys, - authKeysTemp: withAuthKeys ? new Map() : this._state.authKeysTemp, - authKeysTempExpiry: withAuthKeys ? new Map() : this._state.authKeysTempExpiry, - entities: new Map(), - phoneIndex: new Map(), - usernameIndex: new Map(), - refs: new Map(), - gpts: null, - pts: new Map(), - fsm: new Map(), - rl: new Map(), - self: null, - futureSalts: new Map(), - } - this._cachedInputPeers?.clear() - this._cachedFull?.clear() - } - - /** - * Set a given object as an underlying state. - * - * Note that this object will be used as-is, so if - * you plan on using it somewhere else, be sure to copy it beforehand. - */ - protected _setStateFrom(obj: MemorySessionState): void { - let ver = obj.$version as number - - if (ver === 1) { - // v2: introduced message references - obj.refs = new Map() - obj.$version = ver = 2 as any // eslint-disable-line - } - if (ver === 2) { - // v3: introduced future salts - obj.futureSalts = new Map() - obj.$version = ver = 3 - } - if (ver !== CURRENT_VERSION) return - - // populate indexes if needed - let populate = false - - if (!obj.phoneIndex?.size) { - obj.phoneIndex = new Map() - populate = true - } - if (!obj.usernameIndex?.size) { - obj.usernameIndex = new Map() - populate = true - } - - if (populate) { - Object.values(obj.entities).forEach((ent: ITelegramStorage.PeerInfo) => { - if (ent.phone) obj.phoneIndex.set(ent.phone, ent.id) - - if (ent.username) { - obj.usernameIndex.set(ent.username, ent.id) - } - }) - } - - this._state = obj - } - - private _vacuum(): void { - // remove expired entities from fsm and rate limit storages - - const now = Date.now() - - // make references in advance to avoid lookups - const state = this._state - const fsm = state.fsm - const rl = state.rl - - for (const [key, item] of fsm) { - if (item.e && item.e < now) { - fsm.delete(key) - } - } - - for (const [key, item] of rl) { - if (item.res < now) { - rl.delete(key) - } - } - } - - getDefaultDcs(): ITelegramStorage.DcOptions | null { - return this._state.defaultDcs - } - - setDefaultDcs(dcs: ITelegramStorage.DcOptions | null): void { - this._state.defaultDcs = dcs - } - - setFutureSalts(dcId: number, salts: mtp.RawMt_future_salt[]): void { - this._state.futureSalts.set(dcId, salts) - } - - getFutureSalts(dcId: number): mtp.RawMt_future_salt[] | null { - return this._state.futureSalts.get(dcId) ?? null - } - - setTempAuthKeyFor(dcId: number, index: number, key: Uint8Array | null, expiresAt: number): void { - const k = `${dcId}:${index}` - - if (key) { - this._state.authKeysTemp.set(k, key) - this._state.authKeysTempExpiry.set(k, expiresAt) - } else { - this._state.authKeysTemp.delete(k) - this._state.authKeysTempExpiry.delete(k) - } - } - - setAuthKeyFor(dcId: number, key: Uint8Array | null): void { - if (key) { - this._state.authKeys.set(dcId, key) - } else { - this._state.authKeys.delete(dcId) - } - } - - getAuthKeyFor(dcId: number, tempIndex?: number): Uint8Array | null { - if (tempIndex !== undefined) { - const k = `${dcId}:${tempIndex}` - - if (Date.now() > (this._state.authKeysTempExpiry.get(k) ?? 0)) { - return null - } - - return this._state.authKeysTemp.get(k) ?? null - } - - return this._state.authKeys.get(dcId) ?? null - } - - dropAuthKeysFor(dcId: number): void { - this._state.authKeys.delete(dcId) - - for (const key of this._state.authKeysTemp.keys()) { - if (key.startsWith(`${dcId}:`)) { - this._state.authKeysTemp.delete(key) - this._state.authKeysTempExpiry.delete(key) - } - } - - // future salts are linked to auth keys - this._state.futureSalts.delete(dcId) - } - - updatePeers(peers: PeerInfoWithUpdated[]): void { - for (const peer of peers) { - this._cachedFull.set(peer.id, peer.full) - - peer.updated = Date.now() - const old = this._state.entities.get(peer.id) - - if (old) { - // delete old index entries if needed - if (old.username && peer.username !== old.username) { - this._state.usernameIndex.delete(old.username) - } - if (old.phone && old.phone !== peer.phone) { - this._state.phoneIndex.delete(old.phone) - } - } - - if (peer.username) { - this._state.usernameIndex.set(peer.username, peer.id) - } - - if (peer.phone) this._state.phoneIndex.set(peer.phone, peer.id) - - this._state.entities.set(peer.id, peer) - - // no point in storing references anymore, since we have the full peer - if (this._state.refs.has(peer.id)) { - this._state.refs.delete(peer.id) - } - } - } - - protected _getInputPeer(peerInfo?: ITelegramStorage.PeerInfo): tl.TypeInputPeer | null { - if (!peerInfo) return null - - switch (peerInfo.type) { - case 'user': - return { - _: 'inputPeerUser', - userId: peerInfo.id, - accessHash: peerInfo.accessHash, - } - case 'chat': - return { - _: 'inputPeerChat', - chatId: -peerInfo.id, - } - case 'channel': - return { - _: 'inputPeerChannel', - channelId: toggleChannelIdMark(peerInfo.id), - accessHash: peerInfo.accessHash, - } - } - } - - private _findPeerByRef(peerId: number): tl.TypeInputPeer | null { - const refs = this._state.refs.get(peerId) - if (!refs || refs.size === 0) return null - - const [ref] = refs.values() - const [chatId, msgId] = ref.split(':').map(Number) - - const chatPeer = this._getInputPeer(this._state.entities.get(chatId)) - if (!chatPeer) return null - - if (peerId > 0) { - // user - return { - _: 'inputPeerUserFromMessage', - msgId, - userId: peerId, - peer: chatPeer, - } - } - - // channel - return { - _: 'inputPeerChannelFromMessage', - msgId, - channelId: toggleChannelIdMark(peerId), - peer: chatPeer, - } - } - - getPeerById(peerId: number): tl.TypeInputPeer | null { - if (this._cachedInputPeers.has(peerId)) { - return this._cachedInputPeers.get(peerId)! - } - - let peer = this._getInputPeer(this._state.entities.get(peerId)) - if (!peer) peer = this._findPeerByRef(peerId) - if (peer) this._cachedInputPeers.set(peerId, peer) - - return peer - } - - getPeerByPhone(phone: string): tl.TypeInputPeer | null { - const peerId = this._state.phoneIndex.get(phone) - if (!peerId) return null - - return this._getInputPeer(this._state.entities.get(peerId)) - } - - getPeerByUsername(username: string): tl.TypeInputPeer | null { - const id = this._state.usernameIndex.get(username.toLowerCase()) - if (!id) return null - const peer = this._state.entities.get(id) - if (!peer) return null - - if (Date.now() - peer.updated > USERNAME_TTL) return null - - return this._getInputPeer(peer) - } - - saveReferenceMessage(peerId: number, chatId: number, messageId: number): void { - if (!this._state.refs.has(peerId)) { - this._state.refs.set(peerId, new Set()) - } - - this._state.refs.get(peerId)!.add(`${chatId}:${messageId}`) - } - - deleteReferenceMessages(chatId: number, messageIds: number[]): void { - // not the most efficient way, but it's fine - for (const refs of this._state.refs.values()) { - for (const msg of messageIds) { - refs.delete(`${chatId}:${msg}`) - } - } - } - - getSelf(): ITelegramStorage.SelfInfo | null { - return this._state.self - } - - setSelf(self: ITelegramStorage.SelfInfo | null): void { - this._state.self = self - } - - setManyChannelPts(values: Map): void { - for (const [id, pts] of values) { - this._state.pts.set(id, pts) - } - } - - getChannelPts(entityId: number): number | null { - return this._state.pts.get(entityId) ?? null - } - - getUpdatesState(): [number, number, number, number] | null { - return this._state.gpts ?? null - } - - setUpdatesPts(val: number): void { - if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0] - this._state.gpts[0] = val - } - - setUpdatesQts(val: number): void { - if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0] - this._state.gpts[1] = val - } - - setUpdatesDate(val: number): void { - if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0] - this._state.gpts[2] = val - } - - setUpdatesSeq(val: number): void { - if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0] - this._state.gpts[3] = val - } - - getFullPeerById(id: number): tl.TypeUser | tl.TypeChat | null { - return this._cachedFull.get(id) ?? null - } - - // IStateStorage implementation - - getState(key: string): unknown { - const val = this._state.fsm.get(key) - if (!val) return null - - if (val.e && val.e < Date.now()) { - // expired - this._state.fsm.delete(key) - - return null - } - - return val.v - } - - setState(key: string, state: unknown, ttl?: number): void { - this._state.fsm.set(key, { - v: state, - e: ttl ? Date.now() + ttl * 1000 : undefined, - }) - } - - deleteState(key: string): void { - this._state.fsm.delete(key) - } - - getCurrentScene(key: string): string | null { - return this.getState(`$current_scene_${key}`) as string | null - } - - setCurrentScene(key: string, scene: string, ttl?: number): void { - return this.setState(`$current_scene_${key}`, scene, ttl) - } - - deleteCurrentScene(key: string): void { - this._state.fsm.delete(`$current_scene_${key}`) - } - - getRateLimit(key: string, limit: number, window: number): [number, number] { - // leaky bucket - const now = Date.now() - - const item = this._state.rl.get(key) - - if (!item) { - const state = { - res: now + window * 1000, - rem: limit, - } - - this._state.rl.set(key, state) - - return [state.rem, state.res] - } - - if (item.res < now) { - // expired - - const state = { - res: now + window * 1000, - rem: limit, - } - - this._state.rl.set(key, state) - - return [state.rem, state.res] - } - - item.rem = item.rem > 0 ? item.rem - 1 : 0 - - return [item.rem, item.res] - } - - resetRateLimit(key: string): void { - this._state.rl.delete(key) - } -} diff --git a/packages/core/src/storage/provider.ts b/packages/core/src/storage/provider.ts new file mode 100644 index 00000000..acd2e783 --- /dev/null +++ b/packages/core/src/storage/provider.ts @@ -0,0 +1,12 @@ +import { IStorageDriver } from './driver.js' +import { IAuthKeysRepository } from './repository/auth-keys.js' +import { IKeyValueRepository } from './repository/key-value.js' + +export type IStorageProvider = T & { + readonly driver: IStorageDriver +} + +export type IMtStorageProvider = IStorageProvider<{ + readonly kv: IKeyValueRepository + readonly authKeys: IAuthKeysRepository +}> diff --git a/packages/core/src/storage/providers/idb/driver.ts b/packages/core/src/storage/providers/idb/driver.ts new file mode 100644 index 00000000..90b706fb --- /dev/null +++ b/packages/core/src/storage/providers/idb/driver.ts @@ -0,0 +1,169 @@ +import { MtUnsupportedError } from '../../../types/errors.js' +import { BaseStorageDriver } from '../../driver.js' +import { txToPromise } from './utils.js' + +export type PostMigrationFunction = (db: IDBDatabase) => Promise +type MigrationFunction = (db: IDBDatabase) => void | PostMigrationFunction + +const REPO_VERSION_PREFIX = '__version:' + +export class IdbStorageDriver extends BaseStorageDriver { + db!: IDBDatabase + + constructor(readonly _dbName: string) { + super() + + if (typeof indexedDB === 'undefined') { + throw new MtUnsupportedError('IndexedDB is not available') + } + } + + private _pendingWrites: [string, unknown][] = [] + private _pendingWritesOses = new Set() + private _migrations: Map> = new Map() + private _maxVersion: Map = new Map() + + registerMigration(repo: string, version: number, migration: MigrationFunction): void { + if (this.loaded) { + throw new Error('Cannot register migrations after loading') + } + + let map = this._migrations.get(repo) + + if (!map) { + map = new Map() + this._migrations.set(repo, map) + } + + if (map.has(version)) { + throw new Error(`Migration for ${repo} version ${version} is already registered`) + } + + map.set(version, migration) + + const prevMax = this._maxVersion.get(repo) ?? 0 + + if (version > prevMax) { + this._maxVersion.set(repo, version) + } + } + + writeLater(os: string, obj: unknown): void { + this._pendingWrites.push([os, obj]) + this._pendingWritesOses.add(os) + } + + async _load(): Promise { + this.db = await new Promise((resolve, reject) => { + // indexed db fucking sucks - we can't create tables once we have loaded + // and making an ever-incrementing version number is pretty hard + // since migrations are added dynamically. + // + // force the database to always emit `upgradeneeded` by passing current time + const req = indexedDB.open(this._dbName, Date.now()) + + req.onerror = () => reject(req.error) + + const postUpgrade: PostMigrationFunction[] = [] + + req.onsuccess = async () => { + try { + for (const cb of postUpgrade) { + await cb(req.result) + } + resolve(req.result) + } catch (e) { + reject(e) + } + } + req.onupgradeneeded = () => { + // indexed db still fucking sucks. we can't fetch anything from here, + // since migrations must be sync, and any fetch is inherently async + // what we do have, however, is the list of object stores. + // we can abuse them to store the current migrations status as plain strings + const db = req.result + + const didUpgrade = new Set() + + const doUpgrade = (repo: string, fromVersion: number) => { + const migrations = this._migrations.get(repo) + if (!migrations) return + + const targetVer = this._maxVersion.get(repo)! + + while (fromVersion < targetVer) { + const nextVersion = fromVersion + 1 + const migration = migrations.get(nextVersion) + + if (!migration) { + throw new Error(`No migration for ${repo} to version ${nextVersion}`) + } + + const result = migration(db) + + if (result) { + // guess what? IDB still. fucking. sucks! + // if we want to do something except creating/removing + // databases, we should do this outside of migration + postUpgrade.push(result) + } + + fromVersion = nextVersion + } + + didUpgrade.add(repo) + db.createObjectStore(`${REPO_VERSION_PREFIX}${repo}:${targetVer}`) + } + + for (const key of db.objectStoreNames) { + if (!key.startsWith(REPO_VERSION_PREFIX)) continue + const [, repo, version] = key.split(':') + + const currentVer = Number(version) + db.deleteObjectStore(key) + doUpgrade(repo, currentVer) + didUpgrade.add(repo) + } + + for (const repo of this._migrations.keys()) { + if (didUpgrade.has(repo)) continue + + doUpgrade(repo, 0) + } + } + }) + } + + async _save() { + if (this._pendingWritesOses.size === 0) return + + const writes = this._pendingWrites + const oses = this._pendingWritesOses + this._pendingWrites = [] + this._pendingWritesOses = new Set() + + const tx = this.db.transaction(oses, 'readwrite') + + const osMap = new Map() + + for (const table of oses) { + osMap.set(table, tx.objectStore(table)) + } + + for (const [table, obj] of writes) { + const os = osMap.get(table)! + + if (obj === null) { + os.delete(table) + } else { + os.put(obj) + } + } + + await txToPromise(tx) + } + + _destroy(): void { + this.db.close() + } +} diff --git a/packages/core/src/storage/providers/idb/idb.test.ts b/packages/core/src/storage/providers/idb/idb.test.ts new file mode 100644 index 00000000..a05ddde3 --- /dev/null +++ b/packages/core/src/storage/providers/idb/idb.test.ts @@ -0,0 +1,38 @@ +import { afterAll, beforeAll, describe } from 'vitest' + +import { + testAuthKeysRepository, + testKeyValueRepository, + testPeersRepository, + testRefMessagesRepository, +} from '@mtcute/test' + +import { IdbStorage } from './index.js' + +if (import.meta.env.TEST_ENV === 'browser') { + describe('idb storage', () => { + const idbName = 'mtcute_test_' + Math.random().toString(36).slice(2) + + const storage = new IdbStorage(idbName) + + beforeAll(() => storage.driver.load()) + + testAuthKeysRepository(storage.authKeys) + testKeyValueRepository(storage.kv, storage.driver) + testPeersRepository(storage.peers, storage.driver) + testRefMessagesRepository(storage.refMessages, storage.driver) + + afterAll(async () => { + await storage.driver.destroy() + + const req = indexedDB.deleteDatabase(idbName) + await new Promise((resolve, reject) => { + req.onerror = () => reject(req.error) + req.onsuccess = () => resolve() + req.onblocked = () => resolve() + }) + }) + }) +} else { + describe.skip('idb storage', () => {}) +} diff --git a/packages/core/src/storage/providers/idb/index.ts b/packages/core/src/storage/providers/idb/index.ts new file mode 100644 index 00000000..a0d36e8e --- /dev/null +++ b/packages/core/src/storage/providers/idb/index.ts @@ -0,0 +1,24 @@ +import { IMtStorageProvider } from '../../provider.js' +import { IdbStorageDriver } from './driver.js' +import { IdbAuthKeysRepository } from './repository/auth-keys.js' +import { IdbKvRepository } from './repository/kv.js' +import { IdbPeersRepository } from './repository/peers.js' +import { IdbRefMsgRepository } from './repository/ref-messages.js' + +export { IdbStorageDriver } from './driver.js' + +/** + * mtcute storage that uses IndexedDB as a backend. + * + * This storage is the default one for browsers, and is generally + * recommended over local storage based one. + */ +export class IdbStorage implements IMtStorageProvider { + constructor(readonly dbName: string) {} + + readonly driver = new IdbStorageDriver(this.dbName) + readonly kv = new IdbKvRepository(this.driver) + readonly authKeys = new IdbAuthKeysRepository(this.driver) + readonly peers = new IdbPeersRepository(this.driver) + readonly refMessages = new IdbRefMsgRepository(this.driver) +} diff --git a/packages/core/src/storage/providers/idb/repository/auth-keys.ts b/packages/core/src/storage/providers/idb/repository/auth-keys.ts new file mode 100644 index 00000000..a2fb7474 --- /dev/null +++ b/packages/core/src/storage/providers/idb/repository/auth-keys.ts @@ -0,0 +1,96 @@ +import { IAuthKeysRepository } from '../../../repository/auth-keys.js' +import { IdbStorageDriver } from '../driver.js' +import { reqToPromise, txToPromise } from '../utils.js' + +const TABLE_AUTH_KEYS = 'authKeys' +const TABLE_TEMP_AUTH_KEYS = 'tempAuthKeys' + +interface AuthKeyDto { + dc: number + key: Uint8Array +} +interface TempAuthKeyDto extends AuthKeyDto { + expiresAt?: number + idx?: number +} + +export class IdbAuthKeysRepository implements IAuthKeysRepository { + constructor(readonly _driver: IdbStorageDriver) { + _driver.registerMigration(TABLE_AUTH_KEYS, 1, (db) => { + db.createObjectStore(TABLE_AUTH_KEYS, { keyPath: 'dc' }) + db.createObjectStore(TABLE_TEMP_AUTH_KEYS, { keyPath: ['dc', 'idx'] }) + }) + } + + private os(mode?: IDBTransactionMode): IDBObjectStore { + return this._driver.db.transaction(TABLE_AUTH_KEYS, mode).objectStore(TABLE_AUTH_KEYS) + } + + async set(dc: number, key: Uint8Array | null): Promise { + const os = this.os('readwrite') + + if (key === null) { + return reqToPromise(os.delete(dc)) + } + + await reqToPromise(os.put({ dc, key } satisfies AuthKeyDto)) + } + + async get(dc: number): Promise { + const os = this.os() + + const it = await reqToPromise(os.get(dc) as IDBRequest) + if (it === undefined) return null + + return it.key + } + + private osTemp(mode?: IDBTransactionMode): IDBObjectStore { + return this._driver.db.transaction(TABLE_TEMP_AUTH_KEYS, mode).objectStore(TABLE_TEMP_AUTH_KEYS) + } + + async setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): Promise { + const os = this.osTemp('readwrite') + + if (!key) { + return reqToPromise(os.delete([dc, idx])) + } + + await reqToPromise(os.put({ dc, idx, key, expiresAt: expires } satisfies TempAuthKeyDto)) + } + + async getTemp(dc: number, idx: number, now: number): Promise { + const os = this.osTemp() + const row = await reqToPromise(os.get([dc, idx]) as IDBRequest) + + if (row === undefined || row.expiresAt! < now) return null + + return row.key + } + + async deleteByDc(dc: number): Promise { + const tx = this._driver.db.transaction([TABLE_AUTH_KEYS, TABLE_TEMP_AUTH_KEYS], 'readwrite') + + tx.objectStore(TABLE_AUTH_KEYS).delete(dc) + + // IndexedDB sucks + const tempOs = tx.objectStore(TABLE_TEMP_AUTH_KEYS) + const keys = await reqToPromise(tempOs.getAllKeys()) + + for (const key of keys) { + if ((key as [number, number])[0] === dc) { + tempOs.delete(key) + } + } + + await txToPromise(tx) + } + + deleteAll(): Promise { + const tx = this._driver.db.transaction([TABLE_AUTH_KEYS, TABLE_TEMP_AUTH_KEYS], 'readwrite') + tx.objectStore(TABLE_AUTH_KEYS).clear() + tx.objectStore(TABLE_TEMP_AUTH_KEYS).clear() + + return txToPromise(tx) + } +} diff --git a/packages/core/src/storage/providers/idb/repository/kv.ts b/packages/core/src/storage/providers/idb/repository/kv.ts new file mode 100644 index 00000000..94e58b5f --- /dev/null +++ b/packages/core/src/storage/providers/idb/repository/kv.ts @@ -0,0 +1,41 @@ +import { IKeyValueRepository } from '../../../repository/key-value.js' +import { IdbStorageDriver } from '../driver.js' +import { reqToPromise } from '../utils.js' + +const KV_TABLE = 'kv' +interface KeyValueDto { + key: string + value: Uint8Array +} + +export class IdbKvRepository implements IKeyValueRepository { + constructor(readonly _driver: IdbStorageDriver) { + _driver.registerMigration(KV_TABLE, 1, (db) => { + db.createObjectStore(KV_TABLE, { keyPath: 'key' }) + }) + } + + set(key: string, value: Uint8Array): void { + this._driver.writeLater(KV_TABLE, { key, value } satisfies KeyValueDto) + } + + private os(mode?: IDBTransactionMode): IDBObjectStore { + return this._driver.db.transaction(KV_TABLE, mode).objectStore(KV_TABLE) + } + + async get(key: string): Promise { + const os = this.os() + const res = await reqToPromise(os.get(key) as IDBRequest) + if (res === undefined) return null + + return res.value + } + + async delete(key: string): Promise { + await reqToPromise(this.os('readwrite').delete(key)) + } + + async deleteAll(): Promise { + await reqToPromise(this.os('readwrite').clear()) + } +} diff --git a/packages/core/src/storage/providers/idb/repository/peers.ts b/packages/core/src/storage/providers/idb/repository/peers.ts new file mode 100644 index 00000000..010a27d1 --- /dev/null +++ b/packages/core/src/storage/providers/idb/repository/peers.ts @@ -0,0 +1,47 @@ +import { IPeersRepository } from '../../../../highlevel/storage/repository/peers.js' +import { IdbStorageDriver } from '../driver.js' +import { reqToPromise } from '../utils.js' + +const TABLE = 'peers' + +export class IdbPeersRepository implements IPeersRepository { + constructor(readonly _driver: IdbStorageDriver) { + _driver.registerMigration(TABLE, 1, (db) => { + const os = db.createObjectStore(TABLE, { keyPath: 'id' }) + os.createIndex('by_username', 'usernames', { unique: true, multiEntry: true }) + os.createIndex('by_phone', 'phone', { unique: true }) + }) + } + + store(peer: IPeersRepository.PeerInfo): void { + this._driver.writeLater(TABLE, peer) + } + + private os(mode?: IDBTransactionMode): IDBObjectStore { + return this._driver.db.transaction(TABLE, mode).objectStore(TABLE) + } + + async getById(id: number): Promise { + const it = await reqToPromise(this.os().get(id) as IDBRequest) + + return it ?? null + } + + async getByUsername(username: string): Promise { + const it = await reqToPromise( + this.os().index('by_username').get(username) as IDBRequest, + ) + + return it ?? null + } + + async getByPhone(phone: string): Promise { + const it = await reqToPromise(this.os().index('by_phone').get(phone) as IDBRequest) + + return it ?? null + } + + deleteAll(): Promise { + return reqToPromise(this.os('readwrite').clear()) + } +} diff --git a/packages/core/src/storage/providers/idb/repository/ref-messages.ts b/packages/core/src/storage/providers/idb/repository/ref-messages.ts new file mode 100644 index 00000000..b7c18a17 --- /dev/null +++ b/packages/core/src/storage/providers/idb/repository/ref-messages.ts @@ -0,0 +1,74 @@ +import { IReferenceMessagesRepository } from '../../../../highlevel/storage/repository/ref-messages.js' +import { IdbStorageDriver } from '../driver.js' +import { cursorToIterator, reqToPromise, txToPromise } from '../utils.js' + +const TABLE = 'messageRefs' + +interface MessageRefDto { + peerId: number + chatId: number + msgId: number +} + +export class IdbRefMsgRepository implements IReferenceMessagesRepository { + constructor(readonly _driver: IdbStorageDriver) { + _driver.registerMigration(TABLE, 1, (db) => { + const os = db.createObjectStore(TABLE, { keyPath: ['peerId', 'chatId', 'msgId'] }) + os.createIndex('by_peer', 'peerId') + os.createIndex('by_msg', ['chatId', 'msgId']) + }) + } + + private os(mode?: IDBTransactionMode): IDBObjectStore { + return this._driver.db.transaction(TABLE, mode).objectStore(TABLE) + } + + async store(peerId: number, chatId: number, msgId: number): Promise { + const os = this.os('readwrite') + + await reqToPromise(os.put({ peerId, chatId, msgId } satisfies MessageRefDto)) + } + + async getByPeer(peerId: number): Promise<[number, number] | null> { + const os = this.os() + const index = os.index('by_peer') + + const it = await reqToPromise(index.get(peerId) as IDBRequest) + if (!it) return null + + return [it.chatId, it.msgId] + } + + async delete(chatId: number, msgIds: number[]): Promise { + const tx = this._driver.db.transaction(TABLE, 'readwrite') + const os = tx.objectStore(TABLE) + const index = os.index('by_msg') + + for (const msgId of msgIds) { + const keys = await reqToPromise(index.getAllKeys([chatId, msgId])) + + // there are never that many keys, so we can avoid using cursor + for (const key of keys) { + os.delete(key) + } + } + + return txToPromise(tx) + } + + async deleteByPeer(peerId: number): Promise { + const tx = this._driver.db.transaction(TABLE, 'readwrite') + const os = tx.objectStore(TABLE) + const index = os.index('by_peer') + + for await (const cursor of cursorToIterator(index.openCursor(peerId))) { + cursor.delete() + } + + return txToPromise(tx) + } + + async deleteAll(): Promise { + await reqToPromise(this.os('readwrite').clear()) + } +} diff --git a/packages/core/src/storage/providers/idb/utils.ts b/packages/core/src/storage/providers/idb/utils.ts new file mode 100644 index 00000000..e30b866f --- /dev/null +++ b/packages/core/src/storage/providers/idb/utils.ts @@ -0,0 +1,25 @@ +export function txToPromise(tx: IDBTransaction): Promise { + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error) + }) +} + +export function reqToPromise(req: IDBRequest): Promise { + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result) + req.onerror = () => reject(req.error) + }) +} + +export async function* cursorToIterator( + req: IDBRequest, +): AsyncIterableIterator { + let cursor = await reqToPromise(req) + + while (cursor) { + yield cursor + cursor.continue() + cursor = await reqToPromise(req) + } +} diff --git a/packages/core/src/storage/providers/memory/driver.ts b/packages/core/src/storage/providers/memory/driver.ts new file mode 100644 index 00000000..36037cb8 --- /dev/null +++ b/packages/core/src/storage/providers/memory/driver.ts @@ -0,0 +1,15 @@ +import { IStorageDriver } from '../../driver.js' + +export class MemoryStorageDriver implements IStorageDriver { + readonly states: Map = new Map() + + getState(repo: string, def: () => T) { + if (!this.states.has(repo)) { + this.states.set(repo, def()) + } + + return this.states.get(repo) as T + } + + load() {} +} diff --git a/packages/core/src/storage/providers/memory/index.ts b/packages/core/src/storage/providers/memory/index.ts new file mode 100644 index 00000000..c6b1f0b4 --- /dev/null +++ b/packages/core/src/storage/providers/memory/index.ts @@ -0,0 +1,24 @@ +import { ITelegramStorageProvider } from '../../../highlevel/storage/provider.js' +import { IMtStorageProvider } from '../../provider.js' +import { MemoryStorageDriver } from './driver.js' +import { MemoryAuthKeysRepository } from './repository/auth-keys.js' +import { MemoryKeyValueRepository } from './repository/kv.js' +import { MemoryPeersRepository } from './repository/peers.js' +import { MemoryRefMessagesRepository } from './repository/ref-messages.js' + +export { MemoryStorageDriver } from './driver.js' + +/** + * In-memory storage driver implementation for mtcute. + * + * This storage is **not persistent**, meaning that all data + * **will** be lost on restart. Only use this storage for testing, + * or if you know exactly what you're doing. + */ +export class MemoryStorage implements IMtStorageProvider, ITelegramStorageProvider { + readonly driver = new MemoryStorageDriver() + readonly kv = new MemoryKeyValueRepository(this.driver) + readonly authKeys = new MemoryAuthKeysRepository(this.driver) + readonly peers = new MemoryPeersRepository(this.driver) + readonly refMessages = new MemoryRefMessagesRepository(this.driver) +} diff --git a/packages/core/src/storage/providers/memory/memory.test.ts b/packages/core/src/storage/providers/memory/memory.test.ts new file mode 100644 index 00000000..03b6a2b8 --- /dev/null +++ b/packages/core/src/storage/providers/memory/memory.test.ts @@ -0,0 +1,19 @@ +import { describe } from 'vitest' + +import { + testAuthKeysRepository, + testKeyValueRepository, + testPeersRepository, + testRefMessagesRepository, +} from '@mtcute/test' + +import { MemoryStorage } from './index.js' + +describe('memory storage', () => { + const storage = new MemoryStorage() + + testAuthKeysRepository(storage.authKeys) + testKeyValueRepository(storage.kv, storage.driver) + testPeersRepository(storage.peers, storage.driver) + testRefMessagesRepository(storage.refMessages, storage.driver) +}) diff --git a/packages/core/src/storage/providers/memory/repository/auth-keys.ts b/packages/core/src/storage/providers/memory/repository/auth-keys.ts new file mode 100644 index 00000000..a43ec9da --- /dev/null +++ b/packages/core/src/storage/providers/memory/repository/auth-keys.ts @@ -0,0 +1,69 @@ +import { IAuthKeysRepository } from '../../../repository/auth-keys.js' +import { MemoryStorageDriver } from '../driver.js' + +interface AuthKeysState { + authKeys: Map + authKeysTemp: Map + authKeysTempExpiry: Map +} + +export class MemoryAuthKeysRepository implements IAuthKeysRepository { + constructor(readonly _driver: MemoryStorageDriver) {} + + readonly state = this._driver.getState('authKeys', () => ({ + authKeys: new Map(), + authKeysTemp: new Map(), + authKeysTempExpiry: new Map(), + })) + + set(dc: number, key: Uint8Array | null): void { + if (key) { + this.state.authKeys.set(dc, key) + } else { + this.state.authKeys.delete(dc) + } + } + + get(dc: number): Uint8Array | null { + return this.state.authKeys.get(dc) ?? null + } + + setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void { + const k = `${dc}:${idx}` + + if (key) { + this.state.authKeysTemp.set(k, key) + this.state.authKeysTempExpiry.set(k, expires) + } else { + this.state.authKeysTemp.delete(k) + this.state.authKeysTempExpiry.delete(k) + } + } + + getTemp(dc: number, idx: number, now: number): Uint8Array | null { + const k = `${dc}:${idx}` + + if (now > (this.state.authKeysTempExpiry.get(k) ?? 0)) { + return null + } + + return this.state.authKeysTemp.get(k) ?? null + } + + deleteByDc(dc: number): void { + this.state.authKeys.delete(dc) + + for (const key of this.state.authKeysTemp.keys()) { + if (key.startsWith(`${dc}:`)) { + this.state.authKeysTemp.delete(key) + this.state.authKeysTempExpiry.delete(key) + } + } + } + + deleteAll(): void { + this.state.authKeys.clear() + this.state.authKeysTemp.clear() + this.state.authKeysTempExpiry.clear() + } +} diff --git a/packages/core/src/storage/providers/memory/repository/kv.ts b/packages/core/src/storage/providers/memory/repository/kv.ts new file mode 100644 index 00000000..0e56f912 --- /dev/null +++ b/packages/core/src/storage/providers/memory/repository/kv.ts @@ -0,0 +1,24 @@ +import { IKeyValueRepository } from '../../../repository/key-value.js' +import { MemoryStorageDriver } from '../driver.js' + +export class MemoryKeyValueRepository implements IKeyValueRepository { + constructor(readonly _driver: MemoryStorageDriver) {} + + readonly state = this._driver.getState>('kv', () => new Map()) + + set(key: string, value: Uint8Array): void { + this.state.set(key, value) + } + + get(key: string): Uint8Array | null { + return this.state.get(key) ?? null + } + + delete(key: string): void { + this.state.delete(key) + } + + deleteAll(): void { + this.state.clear() + } +} diff --git a/packages/core/src/storage/providers/memory/repository/peers.ts b/packages/core/src/storage/providers/memory/repository/peers.ts new file mode 100644 index 00000000..6c5b1ba9 --- /dev/null +++ b/packages/core/src/storage/providers/memory/repository/peers.ts @@ -0,0 +1,67 @@ +import { IPeersRepository } from '../../../../highlevel/storage/repository/peers.js' +import { MemoryStorageDriver } from '../driver.js' + +interface PeersState { + entities: Map + usernameIndex: Map + phoneIndex: Map +} + +export class MemoryPeersRepository implements IPeersRepository { + constructor(readonly _driver: MemoryStorageDriver) {} + + readonly state = this._driver.getState('peers', () => ({ + entities: new Map(), + usernameIndex: new Map(), + phoneIndex: new Map(), + })) + + store(peer: IPeersRepository.PeerInfo): void { + const old = this.state.entities.get(peer.id) + + if (old) { + // delete old index entries if needed + old.usernames.forEach((username) => { + this.state.usernameIndex.delete(username) + }) + + if (old.phone) { + this.state.phoneIndex.delete(old.phone) + } + } + + if (peer.usernames) { + for (const username of peer.usernames) { + this.state.usernameIndex.set(username, peer.id) + } + } + + if (peer.phone) this.state.phoneIndex.set(peer.phone, peer.id) + + this.state.entities.set(peer.id, peer) + } + + getById(id: number): IPeersRepository.PeerInfo | null { + return this.state.entities.get(id) ?? null + } + + getByUsername(username: string): IPeersRepository.PeerInfo | null { + const id = this.state.usernameIndex.get(username.toLowerCase()) + if (!id) return null + + return this.state.entities.get(id) ?? null + } + + getByPhone(phone: string): IPeersRepository.PeerInfo | null { + const id = this.state.phoneIndex.get(phone) + if (!id) return null + + return this.state.entities.get(id) ?? null + } + + deleteAll(): void { + this.state.entities.clear() + this.state.phoneIndex.clear() + this.state.usernameIndex.clear() + } +} diff --git a/packages/core/src/storage/providers/memory/repository/ref-messages.ts b/packages/core/src/storage/providers/memory/repository/ref-messages.ts new file mode 100644 index 00000000..d85be0c7 --- /dev/null +++ b/packages/core/src/storage/providers/memory/repository/ref-messages.ts @@ -0,0 +1,49 @@ +import { IReferenceMessagesRepository } from '../../../../highlevel/storage/repository/ref-messages.js' +import { MemoryStorageDriver } from '../driver.js' + +interface RefMessagesState { + refs: Map> +} + +export class MemoryRefMessagesRepository implements IReferenceMessagesRepository { + constructor(readonly _driver: MemoryStorageDriver) {} + + readonly state = this._driver.getState('refMessages', () => ({ + refs: new Map(), + })) + + store(peerId: number, chatId: number, msgId: number): void { + if (!this.state.refs.has(peerId)) { + this.state.refs.set(peerId, new Set()) + } + + this.state.refs.get(peerId)!.add(`${chatId}:${msgId}`) + } + + getByPeer(peerId: number): [number, number] | null { + const refs = this.state.refs.get(peerId) + if (!refs?.size) return null + const [ref] = refs + + const [chatId, msgId] = ref.split(':') + + return [Number(chatId), Number(msgId)] + } + + delete(chatId: number, msgIds: number[]): void { + // not the most efficient way, but it's fine + for (const refs of this.state.refs.values()) { + for (const msg of msgIds) { + refs.delete(`${chatId}:${msg}`) + } + } + } + + deleteByPeer(peerId: number): void { + this.state.refs.delete(peerId) + } + + deleteAll(): void { + this.state.refs.clear() + } +} diff --git a/packages/core/src/storage/repository/auth-keys.ts b/packages/core/src/storage/repository/auth-keys.ts new file mode 100644 index 00000000..d8b9ea8a --- /dev/null +++ b/packages/core/src/storage/repository/auth-keys.ts @@ -0,0 +1,45 @@ +import { MaybePromise } from '../../types/utils.js' + +export interface IAuthKeysRepository { + /** + * Store auth_key for the given DC + * + * If `key` is `null`, the key should be deleted instead + * + * **MUST** be applied immediately, without batching + */ + set(dc: number, key: Uint8Array | null): MaybePromise + /** Get auth_key for the given DC */ + get(dc: number): MaybePromise + + /** + * Store temp_auth_key for the given DC and idx, + * along with its expiration date (in seconds) + * + * If `key` is `null`, the key should be deleted instead + * + * **MUST** be applied immediately, without batching + */ + setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): MaybePromise + /** + * Given the DC id, idx and point in time (in seconds), + * return the temp_auth_key that should be used for the next request + * (such that `now < key.expires`), or `null` if no such key exists + */ + getTemp(dc: number, idx: number, now: number): MaybePromise + + /** + * Delete all stored auth keys for the given DC, including + * both permanent and temp keys + * + * **MUST** be applied immediately, without batching + */ + deleteByDc(dc: number): MaybePromise + + /** + * Delete all stored auth keys, including both permanent and temp keys + * + * **MUST** be applied immediately, without batching + */ + deleteAll(): MaybePromise +} diff --git a/packages/core/src/storage/repository/index.ts b/packages/core/src/storage/repository/index.ts new file mode 100644 index 00000000..e21bb0df --- /dev/null +++ b/packages/core/src/storage/repository/index.ts @@ -0,0 +1,2 @@ +export * from './auth-keys.js' +export * from './key-value.js' diff --git a/packages/core/src/storage/repository/key-value.ts b/packages/core/src/storage/repository/key-value.ts new file mode 100644 index 00000000..78d3421b --- /dev/null +++ b/packages/core/src/storage/repository/key-value.ts @@ -0,0 +1,12 @@ +import { MaybePromise } from '../../types/utils.js' + +export interface IKeyValueRepository { + /** Set a key-value pair */ + set(key: string, value: Uint8Array): MaybePromise + /** Get a key-value pair */ + get(key: string): MaybePromise + /** Delete a key-value pair */ + delete(key: string): MaybePromise + + deleteAll(): MaybePromise +} diff --git a/packages/core/src/storage/service/auth-keys.test.ts b/packages/core/src/storage/service/auth-keys.test.ts new file mode 100644 index 00000000..f8d8d22d --- /dev/null +++ b/packages/core/src/storage/service/auth-keys.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest' + +import { fakeAuthKeysRepository, fakeKeyValueRepository } from '@mtcute/test' + +import { AuthKeysService } from './auth-keys.js' +import { FutureSaltsService } from './future-salts.js' +import { testServiceOptions } from './utils.test-utils.js' + +describe('auth keys service', () => { + const fakeKeys = fakeAuthKeysRepository() + const fakeKv = fakeKeyValueRepository() + + describe('deleteByDc', () => { + it('should delete keys and salts for given DC', async () => { + const saltsService = new FutureSaltsService(fakeKv, testServiceOptions()) + const service = new AuthKeysService(fakeKeys, saltsService, testServiceOptions()) + + vi.spyOn(saltsService, 'delete') + + await service.deleteByDc(2) + + expect(fakeKeys.deleteByDc).toHaveBeenCalledWith(2) + expect(saltsService.delete).toHaveBeenCalledWith(2) + }) + }) +}) diff --git a/packages/core/src/storage/service/auth-keys.ts b/packages/core/src/storage/service/auth-keys.ts new file mode 100644 index 00000000..fe743c50 --- /dev/null +++ b/packages/core/src/storage/service/auth-keys.ts @@ -0,0 +1,18 @@ +import { IAuthKeysRepository } from '../repository/auth-keys.js' +import { BaseService, ServiceOptions } from './base.js' +import { FutureSaltsService } from './future-salts.js' + +export class AuthKeysService extends BaseService { + constructor( + readonly _keys: IAuthKeysRepository, + readonly _salts: FutureSaltsService, + opts: ServiceOptions, + ) { + super(opts) + } + + async deleteByDc(dc: number): Promise { + await this._keys.deleteByDc(dc) + await this._salts.delete(dc) + } +} diff --git a/packages/core/src/storage/service/base.ts b/packages/core/src/storage/service/base.ts new file mode 100644 index 00000000..0a04a217 --- /dev/null +++ b/packages/core/src/storage/service/base.ts @@ -0,0 +1,38 @@ +import { tl } from '@mtcute/tl' +import { TlBinaryReader, TlBinaryWriter, TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' + +import { Logger } from '../../utils/logger.js' +import { IStorageDriver } from '../driver.js' + +export interface ServiceOptions { + driver: IStorageDriver + readerMap: TlReaderMap + writerMap: TlWriterMap + log: Logger +} + +export class BaseService { + protected _driver: IStorageDriver + protected _readerMap: TlReaderMap + protected _writerMap: TlWriterMap + protected _log: Logger + + constructor(opts: ServiceOptions) { + this._driver = opts.driver + this._readerMap = opts.readerMap + this._writerMap = opts.writerMap + this._log = opts.log + } + + protected _serializeTl(obj: tl.TlObject): Uint8Array { + return TlBinaryWriter.serializeObject(this._writerMap, obj) + } + + protected _deserializeTl(data: Uint8Array): tl.TlObject | null { + try { + return TlBinaryReader.deserializeObject(this._readerMap, data) + } catch (e) { + return null + } + } +} diff --git a/packages/core/src/storage/service/default-dcs.ts b/packages/core/src/storage/service/default-dcs.ts new file mode 100644 index 00000000..c204b436 --- /dev/null +++ b/packages/core/src/storage/service/default-dcs.ts @@ -0,0 +1,67 @@ +import { DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js' +import { IKeyValueRepository } from '../repository/key-value.js' +import { BaseService, ServiceOptions } from './base.js' + +const KV_MAIN = 'dc_main' +const KV_MEDIA = 'dc_media' + +export class DefaultDcsService extends BaseService { + constructor( + readonly _kv: IKeyValueRepository, + opts: ServiceOptions, + ) { + super(opts) + } + + private _cached?: DcOptions + + async store(dcs: DcOptions): Promise { + if (this._cached) { + if ( + this._cached.main === dcs.main && + this._cached.media === dcs.media + ) return + } + + this._cached = dcs + + const { main, media } = dcs + const mainData = serializeBasicDcOption(main) + await this._kv.set(KV_MAIN, mainData) + + if (media !== main) { + const mediaData = serializeBasicDcOption(media) + await this._kv.set(KV_MEDIA, mediaData) + } else { + await this._kv.delete(KV_MEDIA) + } + } + + async fetch(): Promise { + if (this._cached) return this._cached + + const [mainData, mediaData] = await Promise.all([ + this._kv.get(KV_MAIN), + this._kv.get(KV_MEDIA), + ]) + + if (!mainData) return null + + const main = parseBasicDcOption(mainData) + if (!main) return null + + const dcs: DcOptions = { main, media: main } + + if (mediaData) { + const media = parseBasicDcOption(mediaData) + + if (media) { + dcs.media = media + } + } + + this._cached = dcs + + return dcs + } +} diff --git a/packages/core/src/storage/service/future-salts.ts b/packages/core/src/storage/service/future-salts.ts new file mode 100644 index 00000000..0eec7202 --- /dev/null +++ b/packages/core/src/storage/service/future-salts.ts @@ -0,0 +1,52 @@ +import { mtp } from '@mtcute/tl' +import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' + +import { IKeyValueRepository } from '../repository/key-value.js' +import { BaseService, ServiceOptions } from './base.js' + +const KV_PREFIX = 'salts:' + +export class FutureSaltsService extends BaseService { + constructor( + readonly _kv: IKeyValueRepository, + opts: ServiceOptions, + ) { + super(opts) + } + + private _cached = new Map() + + async store(dcId: number, salts: mtp.RawMt_future_salt[]): Promise { + if (this._cached.get(dcId) === salts) return + + const writer = TlBinaryWriter.alloc(this._writerMap, 8 + 20 * salts.length) + writer.vector(writer.object, salts) + + await this._kv.set(KV_PREFIX + dcId, writer.result()) + } + + async fetch(dcId: number): Promise { + const cached = this._cached.get(dcId) + if (cached) return cached + + const data = await this._kv.get(KV_PREFIX + dcId) + if (!data) return null + + const reader = new TlBinaryReader(this._readerMap, data) + const salts = reader.vector() + + for (const salt of salts) { + if ((salt as { _: string })._ !== 'mt_future_salt') return null + } + + const salts_ = salts as mtp.RawMt_future_salt[] + this._cached.set(dcId, salts_) + + return salts_ + } + + async delete(dcId: number): Promise { + this._cached.delete(dcId) + await this._kv.delete(KV_PREFIX + dcId) + } +} diff --git a/packages/core/src/storage/service/updates.test.ts b/packages/core/src/storage/service/updates.test.ts new file mode 100644 index 00000000..ad8c9bcf --- /dev/null +++ b/packages/core/src/storage/service/updates.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from 'vitest' + +import { fakeKeyValueRepository } from '@mtcute/test' + +import { UpdatesStateService } from '../../highlevel/storage/service/updates.js' +import { testServiceOptions } from './utils.test-utils.js' + +describe('updates state service', () => { + const kv = fakeKeyValueRepository() + const service = new UpdatesStateService(kv, testServiceOptions()) + + it('should write pts to updates_pts key', async () => { + await service.setPts(123) + + expect(kv.set).toHaveBeenCalledWith('updates_pts', new Uint8Array([123, 0, 0, 0])) + }) + + it('should write qts to updates_qts key', async () => { + await service.setQts(123) + + expect(kv.set).toHaveBeenCalledWith('updates_qts', new Uint8Array([123, 0, 0, 0])) + }) + + it('should write date to updates_date key', async () => { + await service.setDate(123) + + expect(kv.set).toHaveBeenCalledWith('updates_date', new Uint8Array([123, 0, 0, 0])) + }) + + it('should write seq to updates_seq key', async () => { + await service.setSeq(123) + + expect(kv.set).toHaveBeenCalledWith('updates_seq', new Uint8Array([123, 0, 0, 0])) + }) + + describe('getState', () => { + it('should read from updates_* keys', async () => { + await service.getState() + + expect(kv.get).toHaveBeenCalledWith('updates_pts') + expect(kv.get).toHaveBeenCalledWith('updates_qts') + expect(kv.get).toHaveBeenCalledWith('updates_date') + expect(kv.get).toHaveBeenCalledWith('updates_seq') + }) + + it('should return null if no state is stored', async () => { + vi.mocked(kv.get).mockResolvedValueOnce(null) + + expect(await service.getState()).toEqual(null) + }) + }) + + describe('getChannelPts', () => { + it('should read from updates_channel:xxx key', async () => { + await service.getChannelPts(123) + + expect(kv.get).toHaveBeenCalledWith('updates_channel:123') + }) + + it('should return null if no value is stored', async () => { + vi.mocked(kv.get).mockResolvedValueOnce(null) + + expect(await service.getChannelPts(123)).toEqual(null) + }) + + it('should return the value if it is stored', async () => { + vi.mocked(kv.get).mockResolvedValueOnce(new Uint8Array([1, 2, 3, 4])) + + expect(await service.getChannelPts(123)).toEqual(0x04030201) + }) + }) + + describe('setChannelPts', () => { + it('should write to updates_channel:xxx key', async () => { + await service.setChannelPts(123, 0x04030201) + + expect(kv.set).toHaveBeenCalledWith('updates_channel:123', new Uint8Array([1, 2, 3, 4])) + }) + }) +}) diff --git a/packages/core/src/storage/service/utils.test-utils.ts b/packages/core/src/storage/service/utils.test-utils.ts new file mode 100644 index 00000000..9bf74bea --- /dev/null +++ b/packages/core/src/storage/service/utils.test-utils.ts @@ -0,0 +1,18 @@ +import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' +import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' + +import { LogManager } from '../../utils/logger.js' +import { MemoryStorageDriver } from '../providers/memory/driver.js' +import { ServiceOptions } from './base.js' + +export function testServiceOptions(): ServiceOptions { + const logger = new LogManager() + logger.level = 0 + + return { + driver: new MemoryStorageDriver(), + readerMap: __tlReaderMap, + writerMap: __tlWriterMap, + log: logger, + } +} diff --git a/packages/core/src/storage/storage.ts b/packages/core/src/storage/storage.ts new file mode 100644 index 00000000..ad6f96fb --- /dev/null +++ b/packages/core/src/storage/storage.ts @@ -0,0 +1,104 @@ +import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' + +import { asyncResettable, beforeExit } from '../utils/index.js' +import { Logger } from '../utils/logger.js' +import { IMtStorageProvider } from './provider.js' +import { AuthKeysService } from './service/auth-keys.js' +import { ServiceOptions } from './service/base.js' +import { DefaultDcsService } from './service/default-dcs.js' +import { FutureSaltsService } from './service/future-salts.js' + +interface StorageManagerOptions { + provider: IMtStorageProvider + log: Logger + readerMap: TlReaderMap + writerMap: TlWriterMap +} + +/** + * Additional options for {@link StorageManager}, that + * can be customized by the user. + * + * @internal + */ +export interface StorageManagerExtraOptions { + /** + * Interval in milliseconds for saving the storage. + * + * When saving, the storage is expected to persist + * all changes to disk, so that they are not lost. + */ + saveInterval?: number + + /** + * Whether to finalize database before exiting. + * + * @default `true` + */ + cleanup?: boolean +} + +export class StorageManager { + constructor(readonly options: StorageManagerOptions & StorageManagerExtraOptions) {} + + readonly provider = this.options.provider + readonly driver = this.provider.driver + readonly log = this.options.log.create('storage') + + readonly _serviceOptions: ServiceOptions = { + driver: this.driver, + readerMap: this.options.readerMap, + writerMap: this.options.writerMap, + log: this.log, + } + + readonly dcs = new DefaultDcsService(this.provider.kv, this._serviceOptions) + readonly salts = new FutureSaltsService(this.provider.kv, this._serviceOptions) + readonly keys = new AuthKeysService(this.provider.authKeys, this.salts, this._serviceOptions) + + private _cleanupRestore?: () => void + + private _load = asyncResettable(async () => { + this.driver.setup?.(this.log) + + if (this.options.cleanup ?? true) { + this._cleanupRestore = beforeExit(() => { + this._destroy().catch((err) => this.log.error('cleanup error: %s', err)) + }) + } + + await this.driver.load?.() + }) + load(): Promise { + return this._load.run() + } + + async save(): Promise { + await this.driver.save?.() + } + + async clear(withAuthKeys = false) { + if (withAuthKeys) { + await this.provider.authKeys.deleteAll() + } + await this.provider.kv.deleteAll() + await this.save() + } + + private async _destroy(): Promise { + if (!this._load.finished()) return + await this._load.wait() + + await this.driver.destroy?.() + this._load.reset() + } + + async destroy(): Promise { + if (this._cleanupRestore) { + this._cleanupRestore() + this._cleanupRestore = undefined + } + + await this._destroy() + } +} diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts index 083d8e8c..c831d251 100644 --- a/packages/core/src/types/utils.ts +++ b/packages/core/src/types/utils.ts @@ -1,4 +1,4 @@ -export type MaybeAsync = T | Promise +export type MaybePromise = T | Promise export type PartialExcept = Partial> & Pick export type PartialOnly = Partial> & Omit @@ -6,6 +6,8 @@ export type MaybeArray = T | T[] export type MustEqual = T extends V ? (V extends T ? T : V) : V +export type PublicPart = { [K in keyof T]: T[K] } + // eslint-disable-next-line @typescript-eslint/no-unused-vars export function assertNever(x: never): never { throw new Error('Illegal state') diff --git a/packages/core/src/utils/crypto/abstract.ts b/packages/core/src/utils/crypto/abstract.ts index 19af66f8..f0c4142a 100644 --- a/packages/core/src/utils/crypto/abstract.ts +++ b/packages/core/src/utils/crypto/abstract.ts @@ -1,4 +1,4 @@ -import { MaybeAsync } from '../../types/index.js' +import { MaybePromise } from '../../types/index.js' import { factorizePQSync } from './factorization.js' export interface IEncryptionScheme { @@ -12,7 +12,7 @@ export interface IAesCtr { } export interface ICryptoProvider { - initialize?(): MaybeAsync + initialize?(): MaybePromise sha1(data: Uint8Array): Uint8Array @@ -24,15 +24,15 @@ export interface ICryptoProvider { iterations: number, keylen?: number, // = 64 algo?: string, // sha1 or sha512 (default sha512) - ): MaybeAsync + ): MaybePromise - hmacSha256(data: Uint8Array, key: Uint8Array): MaybeAsync + hmacSha256(data: Uint8Array, key: Uint8Array): MaybePromise createAesCtr(key: Uint8Array, iv: Uint8Array, encrypt: boolean): IAesCtr createAesIge(key: Uint8Array, iv: Uint8Array): IEncryptionScheme - factorizePQ(pq: Uint8Array): MaybeAsync<[Uint8Array, Uint8Array]> + factorizePQ(pq: Uint8Array): MaybePromise<[Uint8Array, Uint8Array]> gzip(data: Uint8Array, maxSize: number): Uint8Array | null gunzip(data: Uint8Array): Uint8Array diff --git a/packages/core/src/utils/crypto/node.ts b/packages/core/src/utils/crypto/node.ts index f71e00a2..f63cf7fb 100644 --- a/packages/core/src/utils/crypto/node.ts +++ b/packages/core/src/utils/crypto/node.ts @@ -4,7 +4,7 @@ import { deflateSync, gunzipSync } from 'zlib' import { ige256Decrypt, ige256Encrypt, initAsync, InitInput } from '@mtcute/wasm' -import { MaybeAsync } from '../../types/index.js' +import { MaybePromise } from '../../types/index.js' import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from './abstract.js' export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { @@ -24,7 +24,7 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { iterations: number, keylen = 64, algo = 'sha512', - ): MaybeAsync { + ): MaybePromise { return new Promise((resolve, reject) => pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) => err !== null ? reject(err) : resolve(buf), diff --git a/packages/core/src/utils/crypto/password.ts b/packages/core/src/utils/crypto/password.ts index 6f37d729..6662d628 100644 --- a/packages/core/src/utils/crypto/password.ts +++ b/packages/core/src/utils/crypto/password.ts @@ -4,6 +4,7 @@ import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js' import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' import { concatBuffers } from '../buffer-utils.js' +import { assertTypeIs } from '../type-assertions.js' import { ICryptoProvider } from './abstract.js' import { xorBuffer } from './utils.js' @@ -38,9 +39,11 @@ export async function computePasswordHash( */ export async function computeNewPasswordHash( crypto: ICryptoProvider, - algo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, + algo: tl.TypePasswordKdfAlgo, password: string, ): Promise { + assertTypeIs('account.getPassword', algo, 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow') + const salt1 = new Uint8Array(algo.salt1.length + 32) salt1.set(algo.salt1) crypto.randomFill(salt1.subarray(algo.salt1.length)) diff --git a/packages/core/src/utils/dcs.ts b/packages/core/src/utils/dcs.ts new file mode 100644 index 00000000..34a71f81 --- /dev/null +++ b/packages/core/src/utils/dcs.ts @@ -0,0 +1,114 @@ +import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' + +export interface BasicDcOption { + ipAddress: string + port: number + id: number + ipv6?: boolean + mediaOnly?: boolean +} + +export function serializeBasicDcOption(dc: BasicDcOption): Uint8Array { + const writer = TlBinaryWriter.manual(64) + + const flags = (dc.ipv6 ? 1 : 0) | (dc.mediaOnly ? 2 : 0) + writer.raw( + new Uint8Array([ + 1, // version + dc.id, + flags, + ]), + ) + + writer.string(dc.ipAddress) + writer.int(dc.port) + + return writer.result() +} + +export function parseBasicDcOption(data: Uint8Array): BasicDcOption | null { + const reader = TlBinaryReader.manual(data) + + const [version, id, flags] = reader.raw(3) + if (version !== 1) return null + + const ipAddress = reader.string() + const port = reader.int() + + return { + id, + ipAddress, + port, + ipv6: (flags & 1) !== 0, + mediaOnly: (flags & 2) !== 0, + } +} + +export interface DcOptions { + main: BasicDcOption + media: BasicDcOption +} + +/** @internal */ +export const defaultProductionDc: DcOptions = { + main: { + ipAddress: '149.154.167.50', + port: 443, + id: 2, + }, + media: { + ipAddress: '149.154.167.222', + port: 443, + id: 2, + mediaOnly: true, + }, +} + +/** @internal */ +export const defaultProductionIpv6Dc: DcOptions = { + main: { + ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000a', + ipv6: true, + port: 443, + id: 2, + }, + media: { + ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000b', + ipv6: true, + port: 443, + id: 2, + mediaOnly: true, + }, +} + +/** @internal */ +export const defaultTestDc: DcOptions = { + main: { + ipAddress: '149.154.167.40', + port: 443, + id: 2, + }, + media: { + ipAddress: '149.154.167.40', + port: 443, + id: 2, + mediaOnly: true, + }, +} + +/** @internal */ +export const defaultTestIpv6Dc: DcOptions = { + main: { + ipAddress: '2001:67c:4e8:f002::e', + port: 443, + ipv6: true, + id: 2, + }, + media: { + ipAddress: '2001:67c:4e8:f002::e', + port: 443, + ipv6: true, + id: 2, + mediaOnly: true, + }, +} diff --git a/packages/core/src/utils/default-dcs.ts b/packages/core/src/utils/default-dcs.ts deleted file mode 100644 index 97924b14..00000000 --- a/packages/core/src/utils/default-dcs.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ITelegramStorage } from '../storage/abstract.js' - -/** @internal */ -export const defaultProductionDc: ITelegramStorage.DcOptions = { - main: { - _: 'dcOption', - ipAddress: '149.154.167.50', - port: 443, - id: 2, - }, - media: { - _: 'dcOption', - ipAddress: '149.154.167.222', - port: 443, - id: 2, - mediaOnly: true, - }, -} - -/** @internal */ -export const defaultProductionIpv6Dc: ITelegramStorage.DcOptions = { - main: { - _: 'dcOption', - ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000a', - ipv6: true, - port: 443, - id: 2, - }, - media: { - _: 'dcOption', - ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000b', - ipv6: true, - mediaOnly: true, - port: 443, - id: 2, - }, -} - -/** @internal */ -export const defaultTestDc: ITelegramStorage.DcOptions = { - main: { - _: 'dcOption', - ipAddress: '149.154.167.40', - port: 443, - id: 2, - }, - media: { - _: 'dcOption', - ipAddress: '149.154.167.40', - port: 443, - id: 2, - }, -} - -/** @internal */ -export const defaultTestIpv6Dc: ITelegramStorage.DcOptions = { - main: { - _: 'dcOption', - ipAddress: '2001:67c:4e8:f002::e', - port: 443, - ipv6: true, - id: 2, - }, - media: { - _: 'dcOption', - ipAddress: '2001:67c:4e8:f002::e', - port: 443, - ipv6: true, - id: 2, - }, -} - -export const defaultDcs = { - defaultTestDc, - defaultTestIpv6Dc, - defaultProductionDc, - defaultProductionIpv6Dc, -} as const diff --git a/packages/core/src/utils/function-utils.ts b/packages/core/src/utils/function-utils.ts index 8a7fc3e2..9844ec5d 100644 --- a/packages/core/src/utils/function-utils.ts +++ b/packages/core/src/utils/function-utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export type ThrottledFunction = (() => void) & { reset: () => void } @@ -38,3 +39,34 @@ export function throttle(func: () => void, delay: number): ThrottledFunction { return res } + +export function asyncResettable Promise>(func: T) { + let runningPromise: Promise | null = null + let finished = false + + const run = function (...args: any[]) { + if (finished) return Promise.resolve() + + if (runningPromise) { + return runningPromise + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + runningPromise = func(...args) + void runningPromise.then(() => { + runningPromise = null + finished = true + }) + + return runningPromise + } as T + + return { + run, + finished: () => finished, + wait: () => runningPromise, + reset: () => { + finished = false + }, + } +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 20b68940..87b0e002 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,10 +1,11 @@ +export * from '../highlevel/utils/index.js' export * from './async-lock.js' export * from './bigint-utils.js' export * from './buffer-utils.js' export * from './condition-variable.js' export * from './controllable-promise.js' export * from './crypto/index.js' -export * from './default-dcs.js' +export * from './dcs.js' export * from './deque.js' export * from './early-timer.js' export * from './function-utils.js' diff --git a/packages/core/src/utils/peer-utils.test.ts b/packages/core/src/utils/peer-utils.test.ts index 5816ad22..bddc86f6 100644 --- a/packages/core/src/utils/peer-utils.test.ts +++ b/packages/core/src/utils/peer-utils.test.ts @@ -6,9 +6,8 @@ import { createStub } from '@mtcute/test' import { getAllPeersFrom, getBarePeerId, - getBasicPeerType, getMarkedPeerId, - markedPeerIdToBare, + parseMarkedPeerId, toggleChannelIdMark, } from './peer-utils.js' @@ -68,32 +67,18 @@ describe('getMarkedPeerId', () => { }) }) -describe('getBasicPeerType', () => { - it('should return basic peer type from Peer', () => { - expect(getBasicPeerType({ _: 'peerUser', userId: 123 })).toEqual('user') - expect(getBasicPeerType({ _: 'peerChat', chatId: 456 })).toEqual('chat') - expect(getBasicPeerType({ _: 'peerChannel', channelId: SOME_CHANNEL_ID })).toEqual('channel') - }) - - it('should return basic peer type from marked id', () => { - expect(getBasicPeerType(123)).toEqual('user') - expect(getBasicPeerType(-456)).toEqual('chat') - expect(getBasicPeerType(SOME_CHANNEL_ID_MARKED)).toEqual('channel') +describe('parseMarkedPeerId', () => { + it('should correctly parse marked ids', () => { + expect(parseMarkedPeerId(123)).toEqual(['user', 123]) + expect(parseMarkedPeerId(-456)).toEqual(['chat', 456]) + expect(parseMarkedPeerId(SOME_CHANNEL_ID_MARKED)).toEqual(['channel', SOME_CHANNEL_ID]) }) it('should throw for invalid marked ids', () => { - expect(() => getBasicPeerType(0)).toThrow('Invalid marked peer id') + expect(() => parseMarkedPeerId(0)).toThrow('Invalid marked peer id') // secret chats are not supported yet - expect(() => getBasicPeerType(-1997852516400)).toThrow('Secret chats are not supported') - }) -}) - -describe('markedPeerIdToBare', () => { - it('should return bare peer id from marked id', () => { - expect(markedPeerIdToBare(123)).toEqual(123) - expect(markedPeerIdToBare(-456)).toEqual(456) - expect(markedPeerIdToBare(SOME_CHANNEL_ID_MARKED)).toEqual(SOME_CHANNEL_ID) + expect(() => parseMarkedPeerId(-1997852516400)).toThrow('Secret chats are not supported') }) }) diff --git a/packages/core/src/utils/peer-utils.ts b/packages/core/src/utils/peer-utils.ts index a4fd03c6..be896a2c 100644 --- a/packages/core/src/utils/peer-utils.ts +++ b/packages/core/src/utils/peer-utils.ts @@ -93,56 +93,27 @@ export function getMarkedPeerId( } /** - * Extract basic peer type from {@link tl.TypePeer} or its marked ID. + * Parse a marked ID into a {@link BasicPeerType} and a bare ID */ -export function getBasicPeerType(peer: tl.TypePeer | number): BasicPeerType { - if (typeof peer !== 'number') { - switch (peer._) { - case 'peerUser': - return 'user' - case 'peerChat': - return 'chat' - case 'peerChannel': - return 'channel' - } - } - - if (peer < 0) { - if (MIN_MARKED_CHAT_ID <= peer) { - return 'chat' +export function parseMarkedPeerId(id: number): [BasicPeerType, number] { + if (id < 0) { + if (MIN_MARKED_CHAT_ID <= id) { + return ['chat', -id] } - if (MIN_MARKED_CHANNEL_ID <= peer && peer !== ZERO_CHANNEL_ID) { - return 'channel' + if (MIN_MARKED_CHANNEL_ID <= id && id !== ZERO_CHANNEL_ID) { + return ['channel', ZERO_CHANNEL_ID - id] } - if (MAX_SECRET_CHAT_ID >= peer && peer !== ZERO_SECRET_CHAT_ID) { + if (MAX_SECRET_CHAT_ID >= id && id !== ZERO_SECRET_CHAT_ID) { // return 'secret' throw new MtUnsupportedError('Secret chats are not supported') } - } else if (peer > 0 && peer <= MAX_USER_ID) { - return 'user' + } else if (id > 0 && id <= MAX_USER_ID) { + return ['user', id] } - throw new MtArgumentError(`Invalid marked peer id: ${peer}`) -} - -/** - * Extract bare peer ID from marked ID. - * - * @param peerId Marked peer ID - */ -export function markedPeerIdToBare(peerId: number): number { - const type = getBasicPeerType(peerId) - - switch (type) { - case 'user': - return peerId - case 'chat': - return -peerId - case 'channel': - return toggleChannelIdMark(peerId) - } + throw new MtArgumentError(`Invalid marked peer id: ${id}`) } /** diff --git a/packages/core/src/utils/string-session.test.ts b/packages/core/src/utils/string-session.test.ts index 4036822d..b2f7faca 100644 --- a/packages/core/src/utils/string-session.test.ts +++ b/packages/core/src/utils/string-session.test.ts @@ -4,7 +4,7 @@ import { createStub } from '@mtcute/test' import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' -import { defaultProductionDc } from './default-dcs.js' +import { defaultProductionDc } from './dcs.js' import { readStringSession, writeStringSession } from './string-session.js' const stubAuthKey = new Uint8Array(32) @@ -50,7 +50,12 @@ describe('writeStringSession', () => { testMode: false, primaryDcs: stubDcs, authKey: stubAuthKey, - self: { userId: 12345, isBot: false }, + self: { + userId: 12345, + isBot: false, + isPremium: false, + usernames: [], + }, }), ).toMatchInlineSnapshot( '"AgUAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"', @@ -64,7 +69,12 @@ describe('writeStringSession', () => { testMode: true, primaryDcs: stubDcs, authKey: stubAuthKey, - self: { userId: 12345, isBot: false }, + self: { + userId: 12345, + isBot: false, + isPremium: false, + usernames: [], + }, }), ).toMatchInlineSnapshot( '"AgcAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"', @@ -115,7 +125,12 @@ describe('readStringSession', () => { testMode: false, primaryDcs: stubDcs, authKey: stubAuthKey, - self: { userId: 12345, isBot: false }, + self: { + userId: 12345, + isBot: false, + isPremium: false, + usernames: [], + }, }) }) @@ -130,7 +145,12 @@ describe('readStringSession', () => { testMode: true, primaryDcs: stubDcs, authKey: stubAuthKey, - self: { userId: 12345, isBot: false }, + self: { + userId: 12345, + isBot: false, + isPremium: false, + usernames: [], + }, }) }) }) @@ -148,7 +168,12 @@ describe('readStringSession', () => { // v1 didn't have separate media dc primaryDcs: stubDcsSameMedia, authKey: stubAuthKey, - self: { userId: 12345, isBot: false }, + self: { + userId: 12345, + isBot: false, + isPremium: false, + usernames: [], + }, }) }) }) diff --git a/packages/core/src/utils/string-session.ts b/packages/core/src/utils/string-session.ts index e4d7d89b..614b7edb 100644 --- a/packages/core/src/utils/string-session.ts +++ b/packages/core/src/utils/string-session.ts @@ -1,3 +1,4 @@ +// todo move to highlevel/ import { tl } from '@mtcute/tl' import { base64DecodeToBuffer, @@ -8,14 +9,15 @@ import { TlWriterMap, } from '@mtcute/tl-runtime' -import { ITelegramStorage } from '../storage/index.js' +import { CurrentUserInfo } from '../highlevel/storage/service/current-user.js' import { MtArgumentError } from '../types/index.js' +import { DcOptions } from './dcs.js' export interface StringSessionData { version: number testMode: boolean - primaryDcs: ITelegramStorage.DcOptions - self?: ITelegramStorage.SelfInfo | null + primaryDcs: DcOptions + self?: CurrentUserInfo | null authKey: Uint8Array } @@ -67,7 +69,7 @@ export function readStringSession(readerMap: TlReaderMap, data: string): StringS const version = buf[0] - if (version !== 1 && version !== 2) { + if (version !== 1 && version !== 2 && version !== 3) { throw new Error(`Invalid session string (version = ${version})`) } @@ -85,7 +87,7 @@ export function readStringSession(readerMap: TlReaderMap, data: string): StringS throw new MtArgumentError(`Invalid session string (dc._ = ${primaryDc._})`) } - let self: ITelegramStorage.SelfInfo | null = null + let self: CurrentUserInfo | null = null if (hasSelf) { const selfId = reader.int53() @@ -94,6 +96,9 @@ export function readStringSession(readerMap: TlReaderMap, data: string): StringS self = { userId: selfId, isBot: selfBot, + // todo: we should make sure we fetch this from the server at first start + isPremium: false, + usernames: [], } } diff --git a/packages/dispatcher/package.json b/packages/dispatcher/package.json index 4b6dad91..56e4a3f2 100644 --- a/packages/dispatcher/package.json +++ b/packages/dispatcher/package.json @@ -20,8 +20,18 @@ "gen-updates": "node ./scripts/generate.cjs" }, "dependencies": { - "@mtcute/client": "workspace:^", - "@mtcute/test": "workspace:^", + "@mtcute/core": "workspace:^", "events": "3.2.0" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" + }, + "peerDependencies": { + "@mtcute/sqlite": "workspace:^" + }, + "peerDependenciesMeta": { + "@mtcute/sqlite": { + "optional": true + } } } diff --git a/packages/dispatcher/scripts/generate.cjs b/packages/dispatcher/scripts/generate.cjs index e2620e17..59b6f58c 100644 --- a/packages/dispatcher/scripts/generate.cjs +++ b/packages/dispatcher/scripts/generate.cjs @@ -1,4 +1,4 @@ -const { types, toSentence, replaceSections, formatFile } = require('../../client/scripts/generate-updates.cjs') +const { types, toSentence, replaceSections, formatFile } = require('../../mtcute/scripts/generate-updates.cjs') function generateHandler() { const lines = [] diff --git a/packages/dispatcher/src/callback-data-builder.test.ts b/packages/dispatcher/src/callback-data-builder.test.ts index a3e156f6..5275515e 100644 --- a/packages/dispatcher/src/callback-data-builder.test.ts +++ b/packages/dispatcher/src/callback-data-builder.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { CallbackQuery, MtArgumentError, PeersIndex } from '@mtcute/client' -import { utf8EncodeToBuffer } from '@mtcute/client/utils.js' +import { CallbackQuery, MtArgumentError, PeersIndex } from '@mtcute/core' +import { utf8EncodeToBuffer } from '@mtcute/core/utils.js' import { createStub } from '@mtcute/test' import { CallbackDataBuilder } from './callback-data-builder.js' diff --git a/packages/dispatcher/src/callback-data-builder.ts b/packages/dispatcher/src/callback-data-builder.ts index 3c639261..098c10e8 100644 --- a/packages/dispatcher/src/callback-data-builder.ts +++ b/packages/dispatcher/src/callback-data-builder.ts @@ -1,4 +1,4 @@ -import { CallbackQuery, MaybeArray, MtArgumentError } from '@mtcute/client' +import { CallbackQuery, MaybeArray, MtArgumentError } from '@mtcute/core' import { UpdateFilter } from './filters/types.js' diff --git a/packages/dispatcher/src/context/base.ts b/packages/dispatcher/src/context/base.ts index d5f9d6c0..c99ce7a2 100644 --- a/packages/dispatcher/src/context/base.ts +++ b/packages/dispatcher/src/context/base.ts @@ -1,4 +1,4 @@ -import { ParsedUpdate, TelegramClient } from '@mtcute/client' +import { ParsedUpdate, TelegramClient } from '@mtcute/core' export type UpdateContext = T & { client: TelegramClient diff --git a/packages/dispatcher/src/context/callback-query.ts b/packages/dispatcher/src/context/callback-query.ts index ba5bf468..b305a51d 100644 --- a/packages/dispatcher/src/context/callback-query.ts +++ b/packages/dispatcher/src/context/callback-query.ts @@ -1,4 +1,4 @@ -import { CallbackQuery, InlineCallbackQuery, MaybeAsync, Message, TelegramClient } from '@mtcute/client' +import { CallbackQuery, InlineCallbackQuery, MaybePromise, Message, TelegramClient } from '@mtcute/core' import { UpdateContext } from './base.js' @@ -46,7 +46,7 @@ export class CallbackQueryContext extends CallbackQuery implements UpdateContext /** * Shortcut for getting the message and editing it. */ - async editMessageWith(handler: (msg: Message) => MaybeAsync[0]>) { + async editMessageWith(handler: (msg: Message) => MaybePromise[0]>) { const msg = await this.getMessage() if (!msg) return diff --git a/packages/dispatcher/src/context/chat-join-request.ts b/packages/dispatcher/src/context/chat-join-request.ts index 0bad1eda..bed8159c 100644 --- a/packages/dispatcher/src/context/chat-join-request.ts +++ b/packages/dispatcher/src/context/chat-join-request.ts @@ -1,4 +1,4 @@ -import { BotChatJoinRequestUpdate, TelegramClient } from '@mtcute/client' +import { BotChatJoinRequestUpdate, TelegramClient } from '@mtcute/core' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/chosen-inline-result.ts b/packages/dispatcher/src/context/chosen-inline-result.ts index 575d9b0e..c6c7b810 100644 --- a/packages/dispatcher/src/context/chosen-inline-result.ts +++ b/packages/dispatcher/src/context/chosen-inline-result.ts @@ -1,4 +1,4 @@ -import { ChosenInlineResult, MtArgumentError, TelegramClient } from '@mtcute/client' +import { ChosenInlineResult, MtArgumentError, TelegramClient } from '@mtcute/core' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/inline-query.ts b/packages/dispatcher/src/context/inline-query.ts index e9594f59..707770d7 100644 --- a/packages/dispatcher/src/context/inline-query.ts +++ b/packages/dispatcher/src/context/inline-query.ts @@ -1,4 +1,4 @@ -import { InlineQuery, ParametersSkip1, TelegramClient } from '@mtcute/client' +import { InlineQuery, ParametersSkip1, TelegramClient } from '@mtcute/core' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/message.ts b/packages/dispatcher/src/context/message.ts index bac6b13c..fda723f3 100644 --- a/packages/dispatcher/src/context/message.ts +++ b/packages/dispatcher/src/context/message.ts @@ -1,8 +1,9 @@ -import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer, TelegramClient } from '@mtcute/client' -import { DeleteMessagesParams } from '@mtcute/client/src/methods/messages/delete-messages.js' -import { ForwardMessageOptions } from '@mtcute/client/src/methods/messages/forward-messages.js' -import { SendCopyParams } from '@mtcute/client/src/methods/messages/send-copy.js' -import { SendCopyGroupParams } from '@mtcute/client/src/methods/messages/send-copy-group.js' +import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer, TelegramClient } from '@mtcute/core' +// todo: fix these imports when packaging +import { DeleteMessagesParams } from '@mtcute/core/src/highlevel/methods/messages/delete-messages.js' +import { ForwardMessageOptions } from '@mtcute/core/src/highlevel/methods/messages/forward-messages.js' +import { SendCopyParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy.js' +import { SendCopyGroupParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy-group.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/parse.ts b/packages/dispatcher/src/context/parse.ts index 416e9754..a63e2ba1 100644 --- a/packages/dispatcher/src/context/parse.ts +++ b/packages/dispatcher/src/context/parse.ts @@ -1,4 +1,4 @@ -import { ParsedUpdate, TelegramClient } from '@mtcute/client' +import { ParsedUpdate, TelegramClient } from '@mtcute/core' import { UpdateContextDistributed } from './base.js' import { CallbackQueryContext } from './callback-query.js' diff --git a/packages/dispatcher/src/context/pre-checkout-query.ts b/packages/dispatcher/src/context/pre-checkout-query.ts index da5f6077..05699cbb 100644 --- a/packages/dispatcher/src/context/pre-checkout-query.ts +++ b/packages/dispatcher/src/context/pre-checkout-query.ts @@ -1,4 +1,4 @@ -import { PreCheckoutQuery, TelegramClient } from '@mtcute/client' +import { PreCheckoutQuery, TelegramClient } from '@mtcute/core' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 6ac13bf7..cc773ee5 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -12,7 +12,7 @@ import { DeleteMessageUpdate, DeleteStoryUpdate, HistoryReadUpdate, - MaybeAsync, + MaybePromise, MtArgumentError, ParsedUpdate, PeersIndex, @@ -23,7 +23,7 @@ import { tl, UserStatusUpdate, UserTypingUpdate, -} from '@mtcute/client' +} from '@mtcute/core' import { UpdateContext } from './context/base.js' import { @@ -66,13 +66,8 @@ import { } from './handler.js' // end-codegen-imports import { PropagationAction } from './propagation.js' -import { - defaultStateKeyDelegate, - isCompatibleStorage, - IStateStorage, - StateKeyDelegate, - UpdateState, -} from './state/index.js' +import { defaultStateKeyDelegate, IStateStorageProvider, StateKeyDelegate, UpdateState } from './state/index.js' +import { StateService } from './state/service.js' export interface DispatcherParams { /** @@ -87,7 +82,7 @@ export interface DispatcherParams { * * @default Client's storage */ - storage?: IStateStorage + storage?: IStateStorageProvider /** * Custom key delegate for the dispatcher. @@ -111,34 +106,32 @@ export class Dispatcher { private _scene?: string private _sceneScoped?: boolean - private _storage?: State extends never ? undefined : IStateStorage - private _stateKeyDelegate?: State extends never ? undefined : StateKeyDelegate + private _storage?: StateService + private _stateKeyDelegate?: StateKeyDelegate private _customStateKeyDelegate?: StateKeyDelegate - private _customStorage?: IStateStorage + private _customStorage?: StateService private _errorHandler?: ( err: Error, update: ParsedUpdate & T, state?: UpdateState, - ) => MaybeAsync + ) => MaybePromise private _preUpdateHandler?: ( update: ParsedUpdate & T, state?: UpdateState, - ) => MaybeAsync + ) => MaybePromise private _postUpdateHandler?: ( handled: boolean, update: ParsedUpdate & T, state?: UpdateState, - ) => MaybeAsync + ) => MaybePromise protected constructor(client?: TelegramClient, params?: DispatcherParams) { this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this) this.dispatchUpdate = this.dispatchUpdate.bind(this) - this._onClientBeforeConnect = this._onClientBeforeConnect.bind(this) - this._onClientBeforeClose = this._onClientBeforeClose.bind(this) // eslint-disable-next-line prefer-const let { storage, key, sceneName } = params ?? {} @@ -146,31 +139,19 @@ export class Dispatcher { if (client) { this.bindToClient(client) - if (!storage) { - const _storage = client.storage - - if (!isCompatibleStorage(_storage)) { - throw new MtArgumentError( - 'Storage used by the client is not compatible with the dispatcher. Please provide a compatible storage manually', - ) - } - - storage = _storage - } - if (storage) { - this._storage = storage as any - this._stateKeyDelegate = (key ?? defaultStateKeyDelegate) as any + this._storage = new StateService(storage) + this._stateKeyDelegate = key ?? defaultStateKeyDelegate } } else { // child dispatcher without client if (storage) { - this._customStorage = storage as any + this._customStorage = new StateService(storage) } if (key) { - this._customStateKeyDelegate = key as any + this._customStateKeyDelegate = key } if (sceneName) { @@ -212,64 +193,6 @@ export class Dispatcher { return this._scene } - private _onClientBeforeConnect() { - (async () => { - if ( - !this._parent && - this._storage && - this._storage !== (this._client!.storage as unknown as IStateStorage) - ) { - // this is a root dispatcher with custom storage - await this._storage.load?.() - } - - if (this._parent && this._customStorage) { - // this is a child dispatcher with custom storage - await this._customStorage.load?.() - } - - for (const child of this._children) { - child._onClientBeforeConnect() - } - - if (this._scenes) { - for (const scene of this._scenes.values()) { - scene._onClientBeforeConnect() - } - } - })().catch((err) => this._client!._emitError(err)) - } - - private _onClientBeforeClose() { - (async () => { - if ( - !this._parent && - this._storage && - this._storage !== (this._client!.storage as unknown as IStateStorage) - ) { - // this is a root dispatcher with custom storage - await this._storage.save?.() - await this._storage.destroy?.() - } - - if (this._parent && this._customStorage) { - // this is a child dispatcher with custom storage - await this._customStorage.save?.() - await this._customStorage.destroy?.() - } - - for (const child of this._children) { - child._onClientBeforeClose() - } - - if (this._scenes) { - for (const scene of this._scenes.values()) { - scene._onClientBeforeClose() - } - } - })().catch((err) => this._client!._emitError(err)) - } - /** * Bind the dispatcher to the client. * Called by the constructor automatically if @@ -280,8 +203,6 @@ export class Dispatcher { bindToClient(client: TelegramClient): void { client.on('update', this.dispatchUpdate) client.on('raw_update', this.dispatchRawUpdate) - client.on('before_connect', this._onClientBeforeConnect) - client.on('before_close', this._onClientBeforeClose) this._client = client } @@ -293,13 +214,35 @@ export class Dispatcher { if (this._client) { this._client.off('update', this.dispatchUpdate) this._client.off('raw_update', this.dispatchRawUpdate) - this._client.off('before_connect', this._onClientBeforeConnect) - this._client.off('before_close', this._onClientBeforeClose) this._client = undefined } } + /** + * Destroy the dispatcher and all its children. + * + * When destroying, all the registered handlers are removed, + * and the underlying storage is freed. + */ + async destroy(): Promise { + if (this._parent && this._customStorage) { + await this._customStorage.destroy() + } else if (!this._parent && this._storage) { + await this._storage.destroy() + } + + this.removeUpdateHandler('all') + + for (const child of this._children) { + await child.destroy() + } + + for (const scene of this._scenes?.values() ?? []) { + await scene.destroy() + } + } + /** * Process a raw update with this dispatcher. * Calling this method without bound client will not work. @@ -315,7 +258,7 @@ export class Dispatcher { // order does not matter in the dispatcher, // so we can handle each update in its own task - this.dispatchRawUpdateNow(update, peers).catch((err) => this._client!._emitError(err)) + this.dispatchRawUpdateNow(update, peers).catch((err) => this._client!.emitError(err)) } /** @@ -385,7 +328,7 @@ export class Dispatcher { // order does not matter in the dispatcher, // so we can handle each update in its own task - this.dispatchUpdateNow(update).catch((err) => this._client!._emitError(err)) + this.dispatchUpdateNow(update).catch((err) => this._client!.emitError(err)) } /** @@ -649,7 +592,7 @@ export class Dispatcher { * @param handler Error handler */ onError( - handler: ((err: Error, update: ParsedUpdate & T, state?: UpdateState) => MaybeAsync) | null, + handler: ((err: Error, update: ParsedUpdate & T, state?: UpdateState) => MaybePromise) | null, ): void { if (handler) this._errorHandler = handler else this._errorHandler = undefined @@ -669,7 +612,7 @@ export class Dispatcher { */ onPreUpdate( handler: - | ((update: ParsedUpdate & T, state?: UpdateState) => MaybeAsync) + | ((update: ParsedUpdate & T, state?: UpdateState) => MaybePromise) | null, ): void { if (handler) this._preUpdateHandler = handler @@ -689,7 +632,9 @@ export class Dispatcher { * @param handler Pre-update middleware */ onPostUpdate( - handler: ((handled: boolean, update: ParsedUpdate & T, state?: UpdateState) => MaybeAsync) | null, + handler: + | ((handled: boolean, update: ParsedUpdate & T, state?: UpdateState) => MaybePromise) + | null, ): void { if (handler) this._postUpdateHandler = handler else this._postUpdateHandler = undefined @@ -699,7 +644,7 @@ export class Dispatcher { * Set error handler that will propagate * the error to the parent dispatcher */ - propagateErrorToParent(err: Error, update: ParsedUpdate, state?: UpdateState): MaybeAsync { + propagateErrorToParent(err: Error, update: ParsedUpdate, state?: UpdateState): MaybePromise { if (!this.parent) { throw new MtArgumentError('This dispatcher is not a child') } @@ -964,7 +909,7 @@ export class Dispatcher { * @template S State type, defaults to dispatcher's state type. Only checked at compile-time */ getState(object: Parameters[0]): Promise> - getState(object: string | Parameters[0]): MaybeAsync> { + getState(object: string | Parameters[0]): MaybePromise> { if (!this._storage) { throw new MtArgumentError('Cannot use getUpdateState() filter without state storage') } diff --git a/packages/dispatcher/src/filters/bots.test.ts b/packages/dispatcher/src/filters/bots.test.ts index 163e67a7..6982af5a 100644 --- a/packages/dispatcher/src/filters/bots.test.ts +++ b/packages/dispatcher/src/filters/bots.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { Message, PeersIndex, tl } from '@mtcute/client' +import { Message, PeersIndex, tl } from '@mtcute/core' import { createStub, StubTelegramClient } from '@mtcute/test' import { MessageContext } from '../index.js' @@ -20,10 +20,11 @@ describe('filters.command', () => { const ctx = createMessageContext({ message: text, }) - ctx.client.getAuthState = () => ({ + void ctx.client.storage.self.store({ isBot: true, + isPremium: false, userId: 0, - selfUsername: 'testbot', + usernames: ['testbot'], }) // eslint-disable-next-line diff --git a/packages/dispatcher/src/filters/bots.ts b/packages/dispatcher/src/filters/bots.ts index c1fd539c..bf6f3d9a 100644 --- a/packages/dispatcher/src/filters/bots.ts +++ b/packages/dispatcher/src/filters/bots.ts @@ -1,4 +1,4 @@ -import { MaybeArray, MaybeAsync, Message } from '@mtcute/client' +import { MaybeArray, MaybePromise, Message } from '@mtcute/core' import { MessageContext } from '../context/message.js' import { chat } from './chat.js' @@ -51,7 +51,7 @@ export const command = ( const _prefixes = prefixes - const check = (msg: MessageContext): MaybeAsync => { + const check = (msg: MessageContext): MaybePromise => { if (msg.isMessageGroup) return check(msg.messages[0]) for (const pref of _prefixes) { @@ -66,9 +66,9 @@ export const command = ( const lastGroup = m[m.length - 1] if (lastGroup) { - const state = msg.client.getAuthState() + const self = msg.client.storage.self.getCached() - if (state.isBot && lastGroup !== state.selfUsername) { + if (self && self.isBot && !self.usernames.includes(lastGroup)) { return false } } diff --git a/packages/dispatcher/src/filters/chat.ts b/packages/dispatcher/src/filters/chat.ts index f3e716e9..d482c38f 100644 --- a/packages/dispatcher/src/filters/chat.ts +++ b/packages/dispatcher/src/filters/chat.ts @@ -9,7 +9,7 @@ import { PollVoteUpdate, User, UserTypingUpdate, -} from '@mtcute/client' +} from '@mtcute/core' import { UpdateContextDistributed } from '../context/base.js' import { EmptyObject, Modify, UpdateFilter } from './types.js' @@ -89,7 +89,7 @@ export const chatId: { case 'user_typing': { const id = upd.chatId - return (matchSelf && id === upd.client.getAuthState().userId) || indexId.has(id) + return (matchSelf && id === upd.client.storage.self.getCached()?.userId) || indexId.has(id) } } diff --git a/packages/dispatcher/src/filters/group.ts b/packages/dispatcher/src/filters/group.ts index 6ad76ce2..06774ff4 100644 --- a/packages/dispatcher/src/filters/group.ts +++ b/packages/dispatcher/src/filters/group.ts @@ -1,4 +1,4 @@ -import { MaybeAsync, Message } from '@mtcute/client' +import { MaybePromise, Message } from '@mtcute/core' import { MessageContext } from '../context/message.js' import { Modify, UpdateFilter } from './types.js' @@ -26,7 +26,7 @@ export function every( const upds = ctx.messages const max = upds.length - const next = (): MaybeAsync => { + const next = (): MaybePromise => { if (i === max) return true const res = filter(upds[i++], state) @@ -67,7 +67,7 @@ export function some( const upds = ctx.messages const max = upds.length - const next = (): MaybeAsync => { + const next = (): MaybePromise => { if (i === max) return false const res = filter(upds[i++], state) diff --git a/packages/dispatcher/src/filters/logic.ts b/packages/dispatcher/src/filters/logic.ts index dc8c63af..f06c91f0 100644 --- a/packages/dispatcher/src/filters/logic.ts +++ b/packages/dispatcher/src/filters/logic.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // ^^ will be looked into in MTQ-29 -import { MaybeAsync } from '@mtcute/client' +import { MaybePromise } from '@mtcute/core' import { ExtractBaseMany, ExtractMod, Invert, UnionToIntersection, UpdateFilter } from './types.js' @@ -160,7 +160,7 @@ export function and(...fns: UpdateFilter[]): UpdateFilter => { + const next = (): MaybePromise => { if (i === max) return true const res = fns[i++](upd, state) @@ -306,7 +306,7 @@ export function or(...fns: UpdateFilter[]): UpdateFilter => { + const next = (): MaybePromise => { if (i === max) return false const res = fns[i++](upd, state) diff --git a/packages/dispatcher/src/filters/message.ts b/packages/dispatcher/src/filters/message.ts index b374bf9b..14f700ec 100644 --- a/packages/dispatcher/src/filters/message.ts +++ b/packages/dispatcher/src/filters/message.ts @@ -16,7 +16,7 @@ import { StickerType, User, Video, -} from '@mtcute/client' +} from '@mtcute/core' import { MessageContext } from '../index.js' import { Modify, UpdateFilter } from './types.js' diff --git a/packages/dispatcher/src/filters/state.ts b/packages/dispatcher/src/filters/state.ts index 8892eea5..6cce030a 100644 --- a/packages/dispatcher/src/filters/state.ts +++ b/packages/dispatcher/src/filters/state.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MaybeAsync } from '@mtcute/client' +import { MaybePromise } from '@mtcute/core' import { UpdateFilter } from './types.js' @@ -21,7 +21,7 @@ export const stateEmpty: UpdateFilter = async (upd, state) => { * @param predicate State predicate */ export const state = ( - predicate: (state: T) => MaybeAsync, + predicate: (state: T) => MaybePromise, // eslint-disable-next-line @typescript-eslint/ban-types ): UpdateFilter => { return async (upd, state) => { diff --git a/packages/dispatcher/src/filters/text.ts b/packages/dispatcher/src/filters/text.ts index 2b8f3983..507d0423 100644 --- a/packages/dispatcher/src/filters/text.ts +++ b/packages/dispatcher/src/filters/text.ts @@ -1,4 +1,4 @@ -import { CallbackQuery, ChosenInlineResult, InlineCallbackQuery, InlineQuery, Message } from '@mtcute/client' +import { CallbackQuery, ChosenInlineResult, InlineCallbackQuery, InlineQuery, Message } from '@mtcute/core' import { UpdateContextDistributed } from '../context/base.js' import { UpdateFilter } from './types.js' diff --git a/packages/dispatcher/src/filters/types.ts b/packages/dispatcher/src/filters/types.ts index 7832bdf7..85c723a7 100644 --- a/packages/dispatcher/src/filters/types.ts +++ b/packages/dispatcher/src/filters/types.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // ^^ will be looked into in MTQ-29 -import { MaybeAsync } from '@mtcute/client' +import { MaybePromise } from '@mtcute/core' import { UpdateState } from '../state/update-state.js' /** @@ -78,7 +78,7 @@ import { UpdateState } from '../state/update-state.js' export type UpdateFilter = ( update: Base, state?: UpdateState, -) => MaybeAsync +) => MaybePromise export type Modify = Omit & Mod export type Invert = { diff --git a/packages/dispatcher/src/filters/updates.ts b/packages/dispatcher/src/filters/updates.ts index 2360c19f..9fef1534 100644 --- a/packages/dispatcher/src/filters/updates.ts +++ b/packages/dispatcher/src/filters/updates.ts @@ -1,4 +1,4 @@ -import { ChatMemberUpdate, ChatMemberUpdateType, MaybeArray, UserStatus, UserStatusUpdate } from '@mtcute/client' +import { ChatMemberUpdate, ChatMemberUpdateType, MaybeArray, UserStatus, UserStatusUpdate } from '@mtcute/core' import { UpdateFilter } from './types.js' diff --git a/packages/dispatcher/src/filters/user.ts b/packages/dispatcher/src/filters/user.ts index e291ebc4..4030e63e 100644 --- a/packages/dispatcher/src/filters/user.ts +++ b/packages/dispatcher/src/filters/user.ts @@ -14,7 +14,7 @@ import { User, UserStatusUpdate, UserTypingUpdate, -} from '@mtcute/client' +} from '@mtcute/core' import { UpdateContextDistributed } from '../context/base.js' import { UpdateFilter } from './types.js' @@ -94,7 +94,7 @@ export const userId: { case 'user_typing': { const id = upd.userId - return (matchSelf && id === upd.client.getAuthState().userId) || + return (matchSelf && id === upd.client.storage.self.getCached()?.userId) || indexId.has(id) } case 'poll_vote': @@ -110,7 +110,7 @@ export const userId: { case 'history_read': { const id = upd.chatId - return (matchSelf && id === upd.client.getAuthState().userId) || + return (matchSelf && id === upd.client.storage.self.getCached()?.userId) || indexId.has(id) } } diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index 6f71ef64..a1229d4f 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -7,7 +7,7 @@ import { DeleteMessageUpdate, DeleteStoryUpdate, HistoryReadUpdate, - MaybeAsync, + MaybePromise, PeersIndex, PollUpdate, PollVoteUpdate, @@ -16,7 +16,7 @@ import { tl, UserStatusUpdate, UserTypingUpdate, -} from '@mtcute/client' +} from '@mtcute/core' import { UpdateContext } from './context/base.js' import { @@ -39,8 +39,8 @@ export interface BaseUpdateHandler { export type ParsedUpdateHandler = BaseUpdateHandler< Name, - (update: Update, state: State) => MaybeAsync, - (update: Update, state: State) => MaybeAsync + (update: Update, state: State) => MaybePromise, + (update: Update, state: State) => MaybePromise > export type RawUpdateHandler = BaseUpdateHandler< @@ -49,8 +49,8 @@ export type RawUpdateHandler = BaseUpdateHandler< client: TelegramClient, update: tl.TypeUpdate | tl.TypeMessage, peers: PeersIndex, - ) => MaybeAsync, - (client: TelegramClient, update: tl.TypeUpdate | tl.TypeMessage, peers: PeersIndex) => MaybeAsync + ) => MaybePromise, + (client: TelegramClient, update: tl.TypeUpdate | tl.TypeMessage, peers: PeersIndex) => MaybePromise > // begin-codegen diff --git a/packages/dispatcher/src/state/index.ts b/packages/dispatcher/src/state/index.ts index f44fb9f0..8a615236 100644 --- a/packages/dispatcher/src/state/index.ts +++ b/packages/dispatcher/src/state/index.ts @@ -1,3 +1,5 @@ export * from './key.js' -export * from './storage.js' +export * from './provider.js' +export * from './providers/index.js' +export * from './repository.js' export * from './update-state.js' diff --git a/packages/dispatcher/src/state/key.ts b/packages/dispatcher/src/state/key.ts index 9524ef53..713c51d9 100644 --- a/packages/dispatcher/src/state/key.ts +++ b/packages/dispatcher/src/state/key.ts @@ -1,4 +1,4 @@ -import { assertNever, MaybeAsync, Peer } from '@mtcute/client' +import { assertNever, MaybePromise, Peer } from '@mtcute/core' import { CallbackQueryContext, MessageContext } from '../context/index.js' @@ -10,7 +10,7 @@ import { CallbackQueryContext, MessageContext } from '../context/index.js' * @param msg Message or callback from which to derive the key * @param scene Current scene UID, or `null` if none */ -export type StateKeyDelegate = (upd: MessageContext | CallbackQueryContext | Peer) => MaybeAsync +export type StateKeyDelegate = (upd: MessageContext | CallbackQueryContext | Peer) => MaybePromise /** * Default state key delegate. diff --git a/packages/dispatcher/src/state/provider.ts b/packages/dispatcher/src/state/provider.ts new file mode 100644 index 00000000..7ec35aac --- /dev/null +++ b/packages/dispatcher/src/state/provider.ts @@ -0,0 +1,7 @@ +import { IStorageProvider } from '@mtcute/core' + +import { IStateRepository } from './repository.js' + +export type IStateStorageProvider = IStorageProvider<{ + state: IStateRepository +}> diff --git a/packages/dispatcher/src/state/providers/index.ts b/packages/dispatcher/src/state/providers/index.ts new file mode 100644 index 00000000..05bd34a6 --- /dev/null +++ b/packages/dispatcher/src/state/providers/index.ts @@ -0,0 +1,2 @@ +export * from './memory.js' +export * from './sqlite.js' diff --git a/packages/dispatcher/src/state/providers/memory.ts b/packages/dispatcher/src/state/providers/memory.ts new file mode 100644 index 00000000..65dd3928 --- /dev/null +++ b/packages/dispatcher/src/state/providers/memory.ts @@ -0,0 +1,103 @@ +import { MaybePromise, MemoryStorageDriver } from '@mtcute/core' + +import { IStateStorageProvider } from '../provider.js' +import { IStateRepository } from '../repository.js' + +interface StateDto { + value: string + expiresAt?: number +} + +interface RateLimitDto { + reset: number + remaining: number +} + +class MemoryStateRepository implements IStateRepository { + constructor(readonly _driver: MemoryStorageDriver) {} + + readonly state = this._driver.getState>('dispatcher_fsm', () => new Map()) + + setState(key: string, state: string, ttl?: number | undefined): void { + this.state.set(key, { + value: state, + expiresAt: ttl ? Date.now() + ttl * 1000 : undefined, + }) + } + + getState(key: string, now: number): string | null { + const state = this.state.get(key) + if (!state) return null + + if (state.expiresAt && state.expiresAt < now) { + this.state.delete(key) + + return null + } + + return state.value + } + + deleteState(key: string): void { + this.state.delete(key) + } + + vacuum(now: number): void { + for (const [key, state] of this.state.entries()) { + if (state.expiresAt && state.expiresAt < now) { + this.state.delete(key) + } + } + + for (const [key, state] of this.rl.entries()) { + if (state.reset < now) { + this.rl.delete(key) + } + } + } + + readonly rl = this._driver.getState>('rl', () => new Map()) + + getRateLimit(key: string, now: number, limit: number, window: number): [number, number] { + // leaky bucket + const item = this.rl.get(key) + + if (!item) { + const state: RateLimitDto = { + reset: now + window * 1000, + remaining: limit, + } + + this.rl.set(key, state) + + return [state.remaining, state.reset] + } + + if (item.reset < now) { + // expired + + const state: RateLimitDto = { + reset: now + window * 1000, + remaining: limit, + } + + this.rl.set(key, state) + + return [state.remaining, state.reset] + } + + item.remaining = item.remaining > 0 ? item.remaining - 1 : 0 + + return [item.remaining, item.reset] + } + + resetRateLimit(key: string): MaybePromise { + this.rl.delete(key) + } +} + +export class MemoryStateStorage implements IStateStorageProvider { + constructor(readonly driver: MemoryStorageDriver = new MemoryStorageDriver()) {} + + readonly state = new MemoryStateRepository(this.driver) +} diff --git a/packages/dispatcher/src/state/providers/sqlite.ts b/packages/dispatcher/src/state/providers/sqlite.ts new file mode 100644 index 00000000..e7361893 --- /dev/null +++ b/packages/dispatcher/src/state/providers/sqlite.ts @@ -0,0 +1,123 @@ +import { MaybePromise } from '@mtcute/core' +import type { SqliteStorage, SqliteStorageDriver, Statement } from '@mtcute/sqlite' + +import { IStateStorageProvider } from '../provider.js' +import { IStateRepository } from '../repository.js' + +interface StateDto { + value: string + expires_at: number | null +} + +interface RateLimitDto { + reset: number + remaining: number +} + +class SqliteStateRepository implements IStateRepository { + constructor(readonly _driver: SqliteStorageDriver) { + _driver.registerMigration('state', 1, (db) => { + db.exec(` + create table fsm_state ( + key text primary key, + value text not null, + expires_at integer + ); + create table rl_state ( + key text primary key, + reset integer not null, + remaining integer not null + ); + `) + }) + _driver.onLoad(() => { + this._setState = _driver.db.prepare( + 'insert or replace into fsm_state (key, value, expires_at) values (?, ?, ?)', + ) + this._getState = _driver.db.prepare('select value, expires_at from fsm_state where key = ?') + this._deleteState = _driver.db.prepare('delete from fsm_state where key = ?') + this._deleteOldState = _driver.db.prepare('delete from fsm_state where expires_at < ?') + + this._setRl = _driver.db.prepare('insert or replace into rl_state (key, reset, remaining) values (?, ?, ?)') + this._getRl = _driver.db.prepare('select reset, remaining from rl_state where key = ?') + this._deleteRl = _driver.db.prepare('delete from rl_state where key = ?') + this._deleteOldRl = _driver.db.prepare('delete from rl_state where reset < ?') + }) + } + + private _setState!: Statement + setState(key: string, state: string, ttl?: number | undefined): MaybePromise { + this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined) + } + + private _getState!: Statement + getState(key: string, now: number): MaybePromise { + const res_ = this._getState.get(key) + if (!res_) return null + const res = res_ as StateDto + + if (res.expires_at && res.expires_at < now) { + this._deleteState.run(key) + + return null + } + + return res.value + } + + private _deleteState!: Statement + deleteState(key: string): MaybePromise { + this._deleteState.run(key) + } + + private _deleteOldState!: Statement + private _deleteOldRl!: Statement + vacuum(now: number): MaybePromise { + this._deleteOldState.run(now) + this._deleteOldRl.run(now) + } + + private _setRl!: Statement + private _getRl!: Statement + private _deleteRl!: Statement + + getRateLimit(key: string, now: number, limit: number, window: number): [number, number] { + const val = this._getRl.get(key) as RateLimitDto | undefined + + // hot path. rate limit fsm entries always have an expiration date + + if (!val || val.reset < now) { + // expired or does not exist + const item: RateLimitDto = { + reset: now + window * 1000, + remaining: limit, + } + + this._setRl.run(key, item.reset, item.remaining) + + return [item.remaining, item.reset] + } + + if (val.remaining > 0) { + val.remaining -= 1 + + this._setRl.run(key, val.reset, val.remaining) + } + + return [val.remaining, val.reset] + } + + resetRateLimit(key: string): MaybePromise { + this._deleteRl.run(key) + } +} + +export class SqliteStateStorage implements IStateStorageProvider { + constructor(readonly driver: SqliteStorageDriver) {} + + static from(provider: SqliteStorage) { + return new SqliteStateStorage(provider.driver) + } + + readonly state = new SqliteStateRepository(this.driver) +} diff --git a/packages/dispatcher/src/state/repository.ts b/packages/dispatcher/src/state/repository.ts new file mode 100644 index 00000000..d4b395f4 --- /dev/null +++ b/packages/dispatcher/src/state/repository.ts @@ -0,0 +1,68 @@ +import { MaybePromise } from '@mtcute/core' + +/** + * Interface for FSM storage for the dispatcher. + * + * All of the officially supported storages already implement + * this interface, so you can just re-use it. + * + * Current scene is a special case of a `string` state, + * Most of the time you can just store it the same way + * as normal state, prefixing with something like `$current_state_` + * (scene name can't start with `$`). + * Alternatively, you can store them as simple strings + */ +export interface IStateRepository { + /** + * Retrieve state from the storage + * If state is not found or has expired, return `null` + * + * @param key Key of the state, as defined by {@link StateKeyDelegate} + */ + getState(key: string, now: number): MaybePromise + + /** + * Save state to the storage + * + * @param key Key of the state, as defined by {@link StateKeyDelegate} + * @param state String representing the state + * @param ttl TTL for the state, in seconds + */ + setState(key: string, state: string, ttl?: number): MaybePromise + + /** + * Delete state from the storage + * + * @param key Key of the state, as defined by {@link StateKeyDelegate} + */ + deleteState(key: string): MaybePromise + + /** + * Clean up expired states and rate limits. + * + * @param now Current unix time in ms + */ + vacuum(now: number): MaybePromise + + /** + * Get information about a rate limit. + * + * It is recommended that you use sliding window or leaky bucket + * to implement rate limiting ([learn more](https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm/)), + * + * @param key Key of the rate limit + * @param now Current unix time in ms + * @param limit Maximum number of requests in `window` + * @param window Window size in seconds + * @returns Tuple containing the number of remaining and + * unix time in ms when the user can try again + */ + getRateLimit(key: string, now: number, limit: number, window: number): MaybePromise<[number, number]> + + /** + * Reset a rate limit. + * + * @param key Key of the rate limit + */ + resetRateLimit(key: string): MaybePromise +} diff --git a/packages/dispatcher/src/state/service.ts b/packages/dispatcher/src/state/service.ts new file mode 100644 index 00000000..da74b227 --- /dev/null +++ b/packages/dispatcher/src/state/service.ts @@ -0,0 +1,77 @@ +import { asyncResettable, LruMap } from '@mtcute/core/utils.js' + +import { IStateStorageProvider } from './provider.js' + +const makeCurrentSceneKey = (key: string) => `$current_scene_${key}` + +export class StateService { + constructor(readonly provider: IStateStorageProvider) {} + + private _cache: LruMap = new LruMap(100) + private _vacuumTimer?: NodeJS.Timeout + + private _loaded = false + private _load = asyncResettable(async () => { + await this.provider.driver.load?.() + this._loaded = true + }) + async load() { + await this._load.run() + this._vacuumTimer = setInterval(() => { + Promise.resolve(this.provider.state.vacuum(Date.now())).catch(() => {}) + }, 300_000) + } + + async destroy() { + await this.provider.driver.save?.() + await this.provider.driver.destroy?.() + clearInterval(this._vacuumTimer) + this._loaded = false + } + + async getState(key: string): Promise { + if (!this._loaded) await this.load() + + const cached = this._cache.get(key) + if (cached) return cached as T + + const state = await this.provider.state.getState(key, Date.now()) + if (!state) return null + + return JSON.parse(state) as T + } + + async setState(key: string, state: T, ttl?: number): Promise { + if (!this._loaded) await this.load() + + this._cache.set(key, state) + await this.provider.state.setState(key, JSON.stringify(state), ttl) + } + + async deleteState(key: string): Promise { + if (!this._loaded) await this.load() + + this._cache.delete(key) + await this.provider.state.deleteState(key) + } + + getCurrentScene(key: string): Promise { + return this.getState(makeCurrentSceneKey(key)) + } + + setCurrentScene(key: string, scene: string, ttl?: number): Promise { + return this.setState(makeCurrentSceneKey(key), scene, ttl) + } + + deleteCurrentScene(key: string): Promise { + return this.deleteState(makeCurrentSceneKey(key)) + } + + getRateLimit(key: string, limit: number, window: number) { + return this.provider.state.getRateLimit(key, Date.now(), limit, window) + } + + resetRateLimit(key: string) { + return this.provider.state.resetRateLimit(key) + } +} diff --git a/packages/dispatcher/src/state/storage.ts b/packages/dispatcher/src/state/storage.ts deleted file mode 100644 index 8e0d7198..00000000 --- a/packages/dispatcher/src/state/storage.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { MaybeAsync } from '@mtcute/client' - -// ⚠️ Important: when modifying the below interface, also update it -// in packages/core/src/storage/storage.test-utils.ts - -/** - * Interface for FSM storage for the dispatcher. - * - * All of the officially supported storages already implement - * this interface, so you can just re-use it. - * - * Current scene is a special case of a `string` state, - * Most of the time you can just store it the same way - * as normal state, prefixing with something like `$current_state_` - * (scene name can't start with `$`). - * Alternatively, you can store them as simple strings - */ -export interface IStateStorage { - /** - * Load state from some external storage. - * Should be used either to load session content from file/network/etc - * to memory, or to open required connections to fetch session content later - * - * This method may be called multiple times and should handle that. - */ - load?(): MaybeAsync - /** - * Save state to some external storage. - * Should be used to commit pending changes in the session. - * For example, saving session content to file/network/etc, - * or committing a database transaction - */ - save?(): MaybeAsync - /** - * Cleanup storage and release all used resources. - * - * This method may be called multiple times and should handle that. - */ - destroy?(): MaybeAsync - - /** - * Retrieve state from the storage - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - */ - getState(key: string): MaybeAsync - - /** - * Save state to the storage - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - * @param state Object representing the state - * @param ttl TTL for the state, in seconds - */ - setState(key: string, state: unknown, ttl?: number): MaybeAsync - - /** - * Delete state from the storage - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - */ - deleteState(key: string): MaybeAsync - - /** - * Retrieve the current scene UID from the storage - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - */ - getCurrentScene(key: string): MaybeAsync - - /** - * Change current scene's UID from the storage - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - * @param scene New scene - * @param ttl TTL for the scene, in seconds - */ - setCurrentScene(key: string, scene: string, ttl?: number): MaybeAsync - - /** - * Delete current scene from the storage, effectively "exiting" to root. - * - * @param key Key of the state, as defined by {@link StateKeyDelegate} - */ - deleteCurrentScene(key: string): MaybeAsync - - /** - * Get information about a rate limit. - * - * It is recommended that you use sliding window or leaky bucket - * to implement rate limiting ([learn more](https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm/)), - * - * @param key Key of the rate limit - * @param limit Maximum number of requests in `window` - * @param window Window size in seconds - * @returns Tuple containing the number of remaining and - * unix time in ms when the user can try again - */ - getRateLimit(key: string, limit: number, window: number): MaybeAsync<[number, number]> - - /** - * Reset a rate limit. - * - * @param key Key of the rate limit - */ - resetRateLimit(key: string): MaybeAsync -} - -export function isCompatibleStorage(storage: unknown): storage is IStateStorage { - return ( - typeof storage === 'object' && - storage !== null && - 'getState' in storage && - 'setState' in storage && - 'deleteState' in storage && - 'getCurrentScene' in storage && - 'setCurrentScene' in storage && - 'deleteCurrentScene' in storage && - 'getRateLimit' in storage && - 'resetRateLimit' in storage - ) -} diff --git a/packages/dispatcher/src/state/update-state.ts b/packages/dispatcher/src/state/update-state.ts index 12fbc69c..8e983b6f 100644 --- a/packages/dispatcher/src/state/update-state.ts +++ b/packages/dispatcher/src/state/update-state.ts @@ -1,10 +1,10 @@ /* eslint-disable dot-notation */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MtArgumentError, MtcuteError } from '@mtcute/client' -import { sleep } from '@mtcute/client/utils.js' +import { MtArgumentError, MtcuteError } from '@mtcute/core' +import { sleep } from '@mtcute/core/utils.js' import type { Dispatcher } from '../dispatcher.js' -import { IStateStorage } from './storage.js' +import { StateService } from './service.js' /** * Error thrown by `.rateLimit()` @@ -25,21 +25,21 @@ export class UpdateState { private _key: string private _localKey!: string - private _storage: IStateStorage + private _storage: StateService private _scene: string | null private _scoped?: boolean private _cached?: State | null - private _localStorage: IStateStorage + private _localStorage: StateService private _localKeyBase: string constructor( - storage: IStateStorage, + storage: StateService, key: string, scene: string | null, scoped?: boolean, - customStorage?: IStateStorage, + customStorage?: StateService, customKey?: string, ) { this._storage = storage diff --git a/packages/dispatcher/src/wizard.ts b/packages/dispatcher/src/wizard.ts index bf7e6240..2cf864ad 100644 --- a/packages/dispatcher/src/wizard.ts +++ b/packages/dispatcher/src/wizard.ts @@ -1,4 +1,4 @@ -import { MaybeAsync } from '@mtcute/client' +import { MaybePromise } from '@mtcute/core' import { MessageContext } from './context/message.js' import { Dispatcher } from './dispatcher.js' @@ -94,7 +94,7 @@ export class WizardScene extends Dispatcher, - ) => MaybeAsync, + ) => MaybePromise, ): void { const step = this._steps++ diff --git a/packages/dispatcher/tests/dispatcher.test.ts b/packages/dispatcher/tests/dispatcher.test.ts index 269cf6b0..5299b10e 100644 --- a/packages/dispatcher/tests/dispatcher.test.ts +++ b/packages/dispatcher/tests/dispatcher.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { PeersIndex, TelegramClient } from '@mtcute/client' +import { PeersIndex, TelegramClient } from '@mtcute/core' import { Dispatcher, PropagationAction } from '../src/index.js' diff --git a/packages/dispatcher/tsconfig.json b/packages/dispatcher/tsconfig.json index 554df3b9..6a3f7a53 100644 --- a/packages/dispatcher/tsconfig.json +++ b/packages/dispatcher/tsconfig.json @@ -6,8 +6,5 @@ }, "include": [ "./src", - ], - "references": [ - { "path": "../client" } ] } diff --git a/packages/file-id/package.json b/packages/file-id/package.json index e4e96100..1d305ae0 100644 --- a/packages/file-id/package.json +++ b/packages/file-id/package.json @@ -19,6 +19,7 @@ } }, "dependencies": { - "@mtcute/core": "workspace:^" + "@mtcute/tl-runtime": "workspace:^", + "long": "5.2.3" } } diff --git a/packages/file-id/src/index.ts b/packages/file-id/src/index.ts index ce6e80e5..26c53e53 100644 --- a/packages/file-id/src/index.ts +++ b/packages/file-id/src/index.ts @@ -1,4 +1,3 @@ -export * from './convert.js' export * from './parse.js' export * from './serialize.js' export * from './serialize-unique.js' diff --git a/packages/file-id/src/parse.test.ts b/packages/file-id/src/parse.test.ts index a1ea0433..5ed848ea 100644 --- a/packages/file-id/src/parse.test.ts +++ b/packages/file-id/src/parse.test.ts @@ -1,7 +1,7 @@ +import Long from 'long' import { describe, expect, it } from 'vitest' -import { Long } from '@mtcute/core' -import { hexDecodeToBuffer } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer } from '@mtcute/tl-runtime' import { parseFileId } from './parse.js' import { tdFileId as td } from './types.js' diff --git a/packages/file-id/src/parse.ts b/packages/file-id/src/parse.ts index 8a3bd257..1099c1a4 100644 --- a/packages/file-id/src/parse.ts +++ b/packages/file-id/src/parse.ts @@ -1,4 +1,4 @@ -import { base64DecodeToBuffer, base64Encode, TlBinaryReader } from '@mtcute/core/utils.js' +import { base64DecodeToBuffer, base64Encode, TlBinaryReader } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' import { telegramRleDecode } from './utils.js' diff --git a/packages/file-id/src/serialize-unique.ts b/packages/file-id/src/serialize-unique.ts index e8c727af..9602ac1b 100644 --- a/packages/file-id/src/serialize-unique.ts +++ b/packages/file-id/src/serialize-unique.ts @@ -1,8 +1,7 @@ -import { assertNever } from '@mtcute/core' -import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/core/utils.js' +import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' -import { telegramRleEncode } from './utils.js' +import { assertNever, telegramRleEncode } from './utils.js' export type InputUniqueLocation = | Pick diff --git a/packages/file-id/src/serialize.ts b/packages/file-id/src/serialize.ts index d3343d0d..6b563748 100644 --- a/packages/file-id/src/serialize.ts +++ b/packages/file-id/src/serialize.ts @@ -1,8 +1,7 @@ -import { assertNever } from '@mtcute/core' -import { base64Encode, byteLengthUtf8, concatBuffers, TlBinaryWriter } from '@mtcute/core/utils.js' +import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' -import { telegramRleEncode } from './utils.js' +import { assertNever, telegramRleEncode } from './utils.js' const SUFFIX = new Uint8Array([td.CURRENT_VERSION, td.PERSISTENT_ID_VERSION]) @@ -104,5 +103,10 @@ export function toFileId(location: Omit): str assertNever(loc) } - return base64Encode(concatBuffers([telegramRleEncode(writer.result()), SUFFIX]), true) + const result = telegramRleEncode(writer.result()) + const withSuffix = new Uint8Array(result.length + SUFFIX.length) + withSuffix.set(result) + withSuffix.set(SUFFIX, result.length) + + return base64Encode(withSuffix, true) } diff --git a/packages/file-id/src/types.ts b/packages/file-id/src/types.ts index b4e31521..924f1e1a 100644 --- a/packages/file-id/src/types.ts +++ b/packages/file-id/src/types.ts @@ -1,4 +1,4 @@ -import { Long } from '@mtcute/core' +import Long from 'long' // eslint-disable-next-line @typescript-eslint/no-namespace export namespace tdFileId { diff --git a/packages/file-id/src/utils.test.ts b/packages/file-id/src/utils.test.ts index a7602aa7..59cbccec 100644 --- a/packages/file-id/src/utils.test.ts +++ b/packages/file-id/src/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' import { telegramRleDecode, telegramRleEncode } from './utils.js' diff --git a/packages/file-id/src/utils.ts b/packages/file-id/src/utils.ts index 1cc6def2..7661c34a 100644 --- a/packages/file-id/src/utils.ts +++ b/packages/file-id/src/utils.ts @@ -50,3 +50,7 @@ export function telegramRleDecode(buf: Uint8Array): Uint8Array { return new Uint8Array(ret) } + +export function assertNever(_: never): never { + throw new Error('unreachable') +} diff --git a/packages/html-parser/package.json b/packages/html-parser/package.json index 66e38449..7ef67867 100644 --- a/packages/html-parser/package.json +++ b/packages/html-parser/package.json @@ -23,6 +23,6 @@ "long": "5.2.3" }, "devDependencies": { - "@mtcute/client": "workspace:^" + "@mtcute/core": "workspace:^" } } diff --git a/packages/html-parser/src/html-parser.test.ts b/packages/html-parser/src/html-parser.test.ts index 6d8e82d8..d159375b 100644 --- a/packages/html-parser/src/html-parser.test.ts +++ b/packages/html-parser/src/html-parser.test.ts @@ -1,7 +1,7 @@ import Long from 'long' import { describe, expect, it } from 'vitest' -import { MessageEntity, TextWithEntities, tl } from '@mtcute/client' +import { MessageEntity, TextWithEntities, tl } from '@mtcute/core' // prettier has "html" special-cased which breaks the formatting // this is not an issue when using normally, since we properly handle newlines/spaces, diff --git a/packages/html-parser/src/index.ts b/packages/html-parser/src/index.ts index 07b86116..705dfeec 100644 --- a/packages/html-parser/src/index.ts +++ b/packages/html-parser/src/index.ts @@ -1,7 +1,7 @@ import { Parser } from 'htmlparser2' import Long from 'long' -import type { InputText, MessageEntity, TextWithEntities, tl } from '@mtcute/client' +import type { InputText, MessageEntity, TextWithEntities, tl } from '@mtcute/core' const MENTION_REGEX = /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/ diff --git a/packages/html-parser/tsconfig.json b/packages/html-parser/tsconfig.json index 554df3b9..6a5497ee 100644 --- a/packages/html-parser/tsconfig.json +++ b/packages/html-parser/tsconfig.json @@ -8,6 +8,6 @@ "./src", ], "references": [ - { "path": "../client" } + { "path": "../core" } ] } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 1eb766ce..6552d456 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -23,7 +23,7 @@ } }, "devDependencies": { - "@mtcute/client": "workspace:^", + "@mtcute/core": "workspace:^", "@mtcute/dispatcher": "workspace:^" } } diff --git a/packages/i18n/src/types.ts b/packages/i18n/src/types.ts index b2782a2d..f2f77277 100644 --- a/packages/i18n/src/types.ts +++ b/packages/i18n/src/types.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { tl } from '@mtcute/client' +import type { tl } from '@mtcute/core' type Values = T[keyof T] type SafeGet = T extends Record ? T[K] : never diff --git a/packages/i18n/tests/i18n.test.ts b/packages/i18n/tests/i18n.test.ts index 97d99b01..35d98278 100644 --- a/packages/i18n/tests/i18n.test.ts +++ b/packages/i18n/tests/i18n.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { Message, PeersIndex } from '@mtcute/client' +import { Message, PeersIndex } from '@mtcute/core' import { MessageContext } from '@mtcute/dispatcher' import { createMtcuteI18n, OtherLanguageWrap } from '../src/index.js' diff --git a/packages/markdown-parser/package.json b/packages/markdown-parser/package.json index 7978a551..07ca0578 100644 --- a/packages/markdown-parser/package.json +++ b/packages/markdown-parser/package.json @@ -22,6 +22,6 @@ "long": "5.2.3" }, "devDependencies": { - "@mtcute/client": "workspace:^" + "@mtcute/core": "workspace:^" } } diff --git a/packages/markdown-parser/src/index.ts b/packages/markdown-parser/src/index.ts index e97aa9eb..617d18d9 100644 --- a/packages/markdown-parser/src/index.ts +++ b/packages/markdown-parser/src/index.ts @@ -1,6 +1,6 @@ import Long from 'long' -import type { InputText, MessageEntity, TextWithEntities, tl } from '@mtcute/client' +import type { InputText, MessageEntity, TextWithEntities, tl } from '@mtcute/core' const MENTION_REGEX = /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/ const EMOJI_REGEX = /^tg:\/\/emoji\?id=(-?\d+)/ diff --git a/packages/markdown-parser/src/markdown-parser.test.ts b/packages/markdown-parser/src/markdown-parser.test.ts index b12f50b7..45a13c39 100644 --- a/packages/markdown-parser/src/markdown-parser.test.ts +++ b/packages/markdown-parser/src/markdown-parser.test.ts @@ -1,7 +1,7 @@ import Long from 'long' import { describe, expect, it } from 'vitest' -import { MessageEntity, TextWithEntities, tl } from '@mtcute/client' +import { MessageEntity, TextWithEntities, tl } from '@mtcute/core' // md is special cased in prettier, we don't want that here import { md as md_ } from './index.js' diff --git a/packages/markdown-parser/tsconfig.json b/packages/markdown-parser/tsconfig.json index 554df3b9..6a5497ee 100644 --- a/packages/markdown-parser/tsconfig.json +++ b/packages/markdown-parser/tsconfig.json @@ -8,6 +8,6 @@ "./src", ], "references": [ - { "path": "../client" } + { "path": "../core" } ] } diff --git a/packages/node/index.ts b/packages/node/index.ts index 6a9a18af..da2c1b3a 100644 --- a/packages/node/index.ts +++ b/packages/node/index.ts @@ -1,10 +1,10 @@ import { createRequire } from 'module' import { createInterface, Interface as RlInterface } from 'readline' -import { TelegramClient, TelegramClientOptions, User } from '@mtcute/client' +import { TelegramClient, TelegramClientOptions, User } from '@mtcute/core' import { SqliteStorage } from '@mtcute/sqlite' -export * from '@mtcute/client' +export * from '@mtcute/core' export * from '@mtcute/html-parser' export * from '@mtcute/markdown-parser' export { SqliteStorage } @@ -29,6 +29,12 @@ try { */ export class NodeTelegramClient extends TelegramClient { constructor(opts: TelegramClientOptions) { + if ('client' in opts) { + super(opts) + + return + } + super({ // eslint-disable-next-line crypto: nativeCrypto ? () => new nativeCrypto() : undefined, @@ -99,6 +105,6 @@ export class NodeTelegramClient extends TelegramClient { this.start(params) .then(then) - .catch((err) => this._emitError(err)) + .catch((err) => this.emitError(err)) } } diff --git a/packages/node/package.json b/packages/node/package.json index 92bc5d03..79e0ca9c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -24,7 +24,7 @@ } }, "dependencies": { - "@mtcute/client": "workspace:^", + "@mtcute/core": "workspace:^", "@mtcute/sqlite": "workspace:^", "@mtcute/markdown-parser": "workspace:^", "@mtcute/html-parser": "workspace:^" diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 5733f923..a67acb6f 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -8,7 +8,7 @@ "./utils.ts", ], "references": [ - { "path": "../client" }, + { "path": "../core" }, { "path": "../sqlite" }, { "path": "../dispatcher" }, { "path": "../html-parser" }, diff --git a/packages/node/typedoc.cjs b/packages/node/typedoc.cjs index 1ba7965d..cc62716c 100644 --- a/packages/node/typedoc.cjs +++ b/packages/node/typedoc.cjs @@ -2,7 +2,6 @@ module.exports = { extends: ['../../.config/typedoc/config.base.cjs'], entryPoints: ['./index.ts'], externalPattern: [ - '../client/**', '../core/**', '../html-parser/**', '../markdown-parser/**', diff --git a/packages/node/utils.ts b/packages/node/utils.ts index 791435e9..3356b98c 100644 --- a/packages/node/utils.ts +++ b/packages/node/utils.ts @@ -1 +1 @@ -export * from '@mtcute/client/utils.js' +export * from '@mtcute/core/utils.js' diff --git a/packages/sqlite/package.json b/packages/sqlite/package.json index 2317b879..41a46c37 100644 --- a/packages/sqlite/package.json +++ b/packages/sqlite/package.json @@ -22,7 +22,7 @@ "dependencies": { "@mtcute/core": "workspace:^", "@mtcute/tl-runtime": "workspace:^", - "better-sqlite3": "8.4.0" + "better-sqlite3": "9.2.2" }, "devDependencies": { "@mtcute/test": "workspace:^", diff --git a/packages/sqlite/src/driver.ts b/packages/sqlite/src/driver.ts new file mode 100644 index 00000000..fadedbbb --- /dev/null +++ b/packages/sqlite/src/driver.ts @@ -0,0 +1,177 @@ +import sqlite3, { Database, Options, Statement } from 'better-sqlite3' + +import { BaseStorageDriver, MtUnsupportedError } from '@mtcute/core' +import { beforeExit } from '@mtcute/core/utils.js' + +export interface SqliteStorageDriverOptions { + /** + * By default, WAL mode is enabled, which + * significantly improves performance. + * [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md) + * + * However, you might encounter some issues, + * and if you do, you can disable WAL by passing `true` + * + * @default false + */ + disableWal?: boolean + + /** + * Additional options to pass to `better-sqlite3` + */ + options?: Options +} + +const MIGRATIONS_TABLE_NAME = 'mtcute_migrations' +const MIGRATIONS_TABLE_SQL = ` +create table if not exists ${MIGRATIONS_TABLE_NAME} ( + repo text not null primary key, + version integer not null +); +`.trim() + +type MigrationFunction = (db: Database) => void + +export class SqliteStorageDriver extends BaseStorageDriver { + db!: Database + + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super() + } + + private _pending: [Statement, unknown[]][] = [] + private _runMany!: (stmts: [Statement, unknown[]][]) => void + private _cleanup?: () => void + + private _migrations: Map> = new Map() + private _maxVersion: Map = new Map() + + registerMigration(repo: string, version: number, migration: MigrationFunction): void { + if (this.loaded) { + throw new Error('Cannot register migrations after loading') + } + + let map = this._migrations.get(repo) + + if (!map) { + map = new Map() + this._migrations.set(repo, map) + } + + if (map.has(version)) { + throw new Error(`Migration for ${repo} version ${version} is already registered`) + } + + map.set(version, migration) + + const prevMax = this._maxVersion.get(repo) ?? 0 + + if (version > prevMax) { + this._maxVersion.set(repo, version) + } + } + + private _onLoad = new Set<(db: Database) => void>() + + onLoad(cb: (db: Database) => void): void { + if (this.loaded) { + cb(this.db) + } else { + this._onLoad.add(cb) + } + } + + _writeLater(stmt: Statement, params: unknown[]): void { + this._pending.push([stmt, params]) + } + + _initialize(): void { + const hasLegacyTables = this.db + .prepare("select name from sqlite_master where type = 'table' and name = 'kv'") + .get() + + if (hasLegacyTables) { + throw new MtUnsupportedError( + 'This database was created with an older version of mtcute, and cannot be used anymore. ' + + 'Please delete the database and try again.', + ) + } + + this.db.exec(MIGRATIONS_TABLE_SQL) + + const writeVersion = this.db.prepare( + `insert or replace into ${MIGRATIONS_TABLE_NAME} (repo, version) values (?, ?)`, + ) + const getVersion = this.db.prepare(`select version from ${MIGRATIONS_TABLE_NAME} where repo = ?`) + + const didUpgrade = new Set() + + for (const repo of this._migrations.keys()) { + const res = getVersion.get(repo) as { version: number } | undefined + + const startVersion = res?.version ?? 0 + let fromVersion = startVersion + + const migrations = this._migrations.get(repo)! + const targetVer = this._maxVersion.get(repo)! + + while (fromVersion < targetVer) { + const nextVersion = fromVersion + 1 + const migration = migrations.get(nextVersion) + + if (!migration) { + throw new Error(`No migration for ${repo} to version ${nextVersion}`) + } + + migration(this.db) + + fromVersion = nextVersion + didUpgrade.add(repo) + } + + if (fromVersion !== startVersion) { + writeVersion.run(repo, targetVer) + } + } + } + + _load(): void { + this.db = sqlite3(this.filename, { + ...this.params?.options, + verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined, + }) + + if (!this.params?.disableWal) { + this.db.pragma('journal_mode = WAL') + } + + this._runMany = this.db.transaction((stmts: [Statement, unknown[]][]) => { + stmts.forEach((stmt) => { + stmt[0].run(stmt[1]) + }) + }) + + this._initialize() + this._cleanup = beforeExit(() => { + this._save() + this._destroy() + }) + for (const cb of this._onLoad) cb(this.db) + } + + _save(): void { + if (!this._pending.length) return + + this._runMany(this._pending) + this._pending = [] + } + + _destroy(): void { + this.db.close() + this._cleanup?.() + this._cleanup = undefined + } +} diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 7518ccad..2fb42852 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -1,935 +1,24 @@ -// noinspection SqlResolve +import { IMtStorageProvider, ITelegramStorageProvider } from '@mtcute/core' -import sqlite3, { Options } from 'better-sqlite3' +import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js' +import { SqliteAuthKeysRepository } from './repository/auth-keys.js' +import { SqliteKeyValueRepository } from './repository/kv.js' +import { SqlitePeersRepository } from './repository/peers.js' +import { SqliteRefMessagesRepository } from './repository/ref-messages.js' -import { ITelegramStorage, mtp, tl, toggleChannelIdMark } from '@mtcute/core' -import { - beforeExit, - Logger, - longFromFastString, - longToFastString, - LruMap, - throttle, - ThrottledFunction, - TlBinaryReader, - TlBinaryWriter, - TlReaderMap, - TlWriterMap, -} from '@mtcute/core/utils.js' +export { SqliteStorageDriver } from './driver.js' +export type { Statement } from 'better-sqlite3' -// todo: add testMode to "self" - -function getInputPeer(row: SqliteEntity | ITelegramStorage.PeerInfo): tl.TypeInputPeer { - const id = row.id - - switch (row.type) { - case 'user': - return { - _: 'inputPeerUser', - userId: id, - accessHash: 'accessHash' in row ? row.accessHash : longFromFastString(row.hash), - } - case 'chat': - return { - _: 'inputPeerChat', - chatId: -id, - } - case 'channel': - return { - _: 'inputPeerChannel', - channelId: toggleChannelIdMark(id), - accessHash: 'accessHash' in row ? row.accessHash : longFromFastString(row.hash), - } - } - - throw new Error(`Invalid peer type: ${row.type}`) -} - -const CURRENT_VERSION = 5 - -// language=SQLite format=false -const TEMP_AUTH_TABLE = ` - create table temp_auth_keys ( - dc integer not null, - idx integer not null, - key blob not null, - expires integer not null, - primary key (dc, idx) - ); -` - -// language=SQLite format=false -const MESSAGE_REFS_TABLE = ` - create table message_refs ( - peer_id integer primary key, - chat_id integer not null, - msg_id integer not null - ); - create index idx_message_refs on message_refs (chat_id, msg_id); -` - -// language=SQLite format=false -const SCHEMA = ` - create table kv ( - key text primary key, - value text not null - ); - - create table state ( - key text primary key, - value text not null, - expires number - ); - - create table auth_keys ( - dc integer primary key, - key blob not null - ); - - ${TEMP_AUTH_TABLE} - - create table pts ( - channel_id integer primary key, - pts integer not null - ); - - create table entities ( - id integer primary key, - hash text not null, - type text not null, - username text, - phone text, - updated integer not null, - "full" blob - ); - create index idx_entities_username on entities (username); - create index idx_entities_phone on entities (phone); - - ${MESSAGE_REFS_TABLE} -` - -// language=SQLite format=false -const RESET = ` - delete from kv where key <> 'ver'; - delete from state; - delete from pts; - delete from entities; - delete from message_refs; -` -const RESET_AUTH_KEYS = ` - delete from auth_keys; - delete from temp_auth_keys; -` - -const USERNAME_TTL = 86400000 // 24 hours - -interface SqliteEntity { - id: number - hash: string - type: string - username?: string - phone?: string - updated: number - full: Uint8Array -} - -interface CacheItem { - peer: tl.TypeInputPeer - full: tl.TypeUser | tl.TypeChat | null -} - -interface FsmItem { - value: T - expires?: number -} - -interface MessageRef { - peer_id: number - chat_id: number - msg_id: number -} - -const STATEMENTS = { - getKv: 'select value from kv where key = ?', - setKv: 'insert or replace into kv (key, value) values (?, ?)', - delKv: 'delete from kv where key = ?', - - getState: 'select value, expires from state where key = ?', - setState: 'insert or replace into state (key, value, expires) values (?, ?, ?)', - delState: 'delete from state where key = ?', - - getAuth: 'select key from auth_keys where dc = ?', - getAuthTemp: 'select key from temp_auth_keys where dc = ? and idx = ? and expires > ?', - setAuth: 'insert or replace into auth_keys (dc, key) values (?, ?)', - setAuthTemp: 'insert or replace into temp_auth_keys (dc, idx, key, expires) values (?, ?, ?, ?)', - delAuth: 'delete from auth_keys where dc = ?', - delAuthTemp: 'delete from temp_auth_keys where dc = ? and idx = ?', - delAllAuthTemp: 'delete from temp_auth_keys where dc = ?', - - getPts: 'select pts from pts where channel_id = ?', - setPts: 'insert or replace into pts (channel_id, pts) values (?, ?)', - - updateUpdated: 'update entities set updated = ? where id = ?', - updateCachedEnt: 'update entities set username = ?, phone = ?, updated = ?, "full" = ? where id = ?', - upsertEnt: - 'insert or replace into entities (id, hash, type, username, phone, updated, "full") values (?, ?, ?, ?, ?, ?, ?)', - getEntById: 'select * from entities where id = ?', - getEntByPhone: 'select * from entities where phone = ? limit 1', - getEntByUser: 'select * from entities where username = ? limit 1', - - storeMessageRef: 'insert or replace into message_refs (peer_id, chat_id, msg_id) values (?, ?, ?)', - getMessageRef: 'select chat_id, msg_id from message_refs where peer_id = ?', - delMessageRefs: 'delete from message_refs where chat_id = ? and msg_id = ?', - delAllMessageRefs: 'delete from message_refs where peer_id = ?', - - delStaleState: 'delete from state where expires < ?', -} as const - -const EMPTY_BUFFER = new Uint8Array(0) - -/** - * SQLite backed storage for mtcute. - * - * Uses `better-sqlite3` library - */ -export class SqliteStorage implements ITelegramStorage /*, IStateStorage*/ { - private _db!: sqlite3.Database - private _statements!: Record - private readonly _filename: string - - private _pending: [sqlite3.Statement, unknown[]][] = [] - private _pendingUnimportant: Record = {} - - private _cache?: LruMap - private _fsmCache?: LruMap - private _rlCache?: LruMap - - private _wal?: boolean - - private _reader!: TlBinaryReader - - private _saveUnimportantLater: ThrottledFunction - - private _vacuumTimeout?: NodeJS.Timeout - private _vacuumInterval: number - private _cleanupUnregister?: () => void - - private log!: Logger - private readerMap!: TlReaderMap - private writerMap!: TlWriterMap - - /** - * @param filename Database file name, or `:memory:` for in-memory DB - * @param params - */ +export class SqliteStorage implements IMtStorageProvider, ITelegramStorageProvider { constructor( - filename = ':memory:', - params?: { - /** - * Entities cache size, in number of entities. - * - * Recently encountered entities are cached in memory, - * to avoid redundant database calls. Set to 0 to - * disable caching (not recommended) - * - * Note that by design in-memory cached is only - * used when finding peer by ID, since other - * kinds of lookups (phone, username) may get stale quickly - * - * @default `100` - */ - cacheSize?: number + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) {} - /** - * FSM states cache size, in number of keys. - * - * Recently created/fetched FSM states are cached - * in memory to avoid redundant database calls. - * If you are having problems with this (e.g. stale - * state in case of concurrent accesses), you - * can disable this by passing `0` - * - * @default `100` - */ - fsmCacheSize?: number + readonly driver = new SqliteStorageDriver(this.filename, this.params) - /** - * Rate limit states cache size, in number of keys. - * - * Recently created/used rate limits are cached - * in memory to avoid redundant database calls. - * If you are having problems with this (e.g. stale - * state in case of concurrent accesses), you - * can disable this by passing `0` - * - * @default `100` - */ - rlCacheSize?: number - - /** - * By default, WAL mode is enabled, which - * significantly improves performance. - * [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md) - * - * However, you might encounter some issues, - * and if you do, you can disable WAL by passing `true` - * - * @default false - */ - disableWal?: boolean - - /** - * Updates to already cached in-memory entities are only - * applied in DB once in a while, to avoid redundant - * DB calls. - * - * If you are having issues with this, you can set this to `0` - * - * @default `30000` (30 sec) - */ - unimportantSavesDelay?: number - - /** - * Interval in milliseconds for vacuuming the storage. - * - * When vacuuming, the storage will remove expired FSM - * states to reduce disk and memory usage. - * - * @default `300_000` (5 minutes) - */ - vacuumInterval?: number - - /** - * Whether to finalize database before exiting. - * - * @default `true` - */ - cleanup?: boolean - }, - ) { - this._filename = filename - - if (params?.cacheSize !== 0) { - this._cache = new LruMap(params?.cacheSize ?? 100) - } - - if (params?.fsmCacheSize !== 0) { - this._fsmCache = new LruMap(params?.fsmCacheSize ?? 100) - } - - if (params?.rlCacheSize !== 0) { - this._rlCache = new LruMap(params?.rlCacheSize ?? 100) - } - - this._wal = !params?.disableWal - - this._saveUnimportant = this._saveUnimportant.bind(this) - this._saveUnimportantLater = throttle(this._saveUnimportant, params?.unimportantSavesDelay ?? 30000) - - this._vacuumInterval = params?.vacuumInterval ?? 300_000 - - if (params?.cleanup !== false) { - this._cleanupUnregister = beforeExit(() => this._destroy()) - } - } - - setup(log: Logger, readerMap: TlReaderMap, writerMap: TlWriterMap): void { - this.log = log.create('sqlite') - this.readerMap = readerMap - this.writerMap = writerMap - this._reader = new TlBinaryReader(readerMap, EMPTY_BUFFER) - } - - private _readFullPeer(data: Uint8Array): tl.TypeUser | tl.TypeChat | null { - this._reader = new TlBinaryReader(this.readerMap, data) - let obj - - try { - obj = this._reader.object() - } catch (e) { - // object might be from an older tl layer, in which case - // it should be ignored (i guess?????) - obj = null - } - - return obj as tl.TypeUser | tl.TypeChat | null - } - - private _addToCache(id: number, item: CacheItem): void { - if (this._cache) { - this._cache.set(id, item) - } - } - - private _getFromKv(key: string): T | null { - const row = this._statements.getKv.get(key) as { value: string } | null - - return row ? (JSON.parse(row.value) as T) : null - } - - private _setToKv(key: string, value: unknown, now = false): void { - const query = value === null ? this._statements.delKv : this._statements.setKv - const params = value === null ? [key] : [key, JSON.stringify(value)] - - if (now) { - query.run(params) - } else { - this._pending.push([query, params]) - } - } - - private _runMany!: (stmts: [sqlite3.Statement, unknown[]][]) => void - private _updateManyPeers!: (updates: unknown[][]) => void - - private _upgradeDatabase(from: number): void { - if (from < 2 || from > CURRENT_VERSION) { - // 1 version was skipped during development - // yes i am too lazy to make auto-migrations for them - throw new Error('Unsupported session version, please migrate manually') - } - - if (from === 2) { - // PFS support added - this._db.exec(TEMP_AUTH_TABLE) - from = 3 - } - - if (from === 3) { - // media dc support added - const oldDc = this._db.prepare("select value from kv where key = 'def_dc'").get() - - if (oldDc) { - const oldDcValue = JSON.parse((oldDc as { value: string }).value) as tl.RawDcOption - this._db.prepare("update kv set value = ? where key = 'def_dc'").run([ - JSON.stringify({ - main: oldDcValue, - media: oldDcValue, - }), - ]) - } - from = 4 - } - - if (from === 4) { - // message references support added - this._db.exec(MESSAGE_REFS_TABLE) - from = 5 - } - - if (from !== CURRENT_VERSION) { - // an assertion just in case i messed up - throw new Error('Migration incomplete') - } - } - - private _initializeStatements(): void { - this._statements = {} as unknown as typeof this._statements - Object.entries(STATEMENTS).forEach(([name, sql]) => { - this._statements[name as keyof typeof this._statements] = this._db.prepare(sql) - }) - } - - private _initialize(): void { - const hasTables = this._db.prepare("select name from sqlite_master where type = 'table' and name = 'kv'").get() - - if (hasTables) { - // tables already exist, check version - const versionResult = this._db.prepare("select value from kv where key = 'ver'").get() - const version = Number((versionResult as { value: number }).value) - - this.log.debug('current db version = %d', version) - - if (version < CURRENT_VERSION) { - this._upgradeDatabase(version) - this._db.prepare("update kv set value = ? where key = 'ver'").run(CURRENT_VERSION) - } - - // prepared statements expect latest schema, so we need to upgrade first - this._initializeStatements() - } else { - // create tables - this.log.debug('creating tables, db version = %d', CURRENT_VERSION) - this._db.exec(SCHEMA) - this._initializeStatements() - this._setToKv('ver', CURRENT_VERSION, true) - } - } - - private _vacuum(): void { - this._statements.delStaleState.run(Date.now()) - // local caches aren't cleared because it would be too expensive - } - - load(): void { - this._db = sqlite3(this._filename, { - verbose: this.log.mgr.level >= 5 ? (this.log.verbose as Options['verbose']) : undefined, - }) - - this._initialize() - - // init wal if needed - if (this._wal) { - this._db.pragma('journal_mode = WAL') - } - - // helper methods - this._runMany = this._db.transaction((stmts: [sqlite3.Statement, unknown[]][]) => { - stmts.forEach((stmt) => { - stmt[0].run(stmt[1]) - }) - }) - - this._updateManyPeers = this._db.transaction((data: unknown[][]) => { - data.forEach((it: unknown) => { - this._statements.updateCachedEnt.run(it) - }) - }) - - clearInterval(this._vacuumTimeout) - this._vacuumTimeout = setInterval(this._vacuum.bind(this), this._vacuumInterval) - } - - private _saveUnimportant() { - // unimportant changes are changes about cached in memory entities, - // that don't really need to be cached right away. - // to avoid redundant DB calls, these changes are persisted - // no more than once every 30 seconds. - // - // additionally, to avoid redundant changes that - // are immediately overwritten, we use object instead - // of an array, where the key is marked peer id, - // and value is the arguments array, since - // the query is always `updateCachedEnt` - const items = Object.values(this._pendingUnimportant) - if (!items.length) return - - this._updateManyPeers(items) - this._pendingUnimportant = {} - } - - save(): void { - if (!this._pending.length) return - - this._runMany(this._pending) - this._pending = [] - - this._saveUnimportantLater() - } - - private _destroy() { - this._saveUnimportant() - this._db.close() - clearInterval(this._vacuumTimeout) - this._saveUnimportantLater.reset() - } - - destroy(): void { - this._destroy() - this._cleanupUnregister?.() - } - - reset(withAuthKeys = false): void { - this._db.exec(RESET) - if (withAuthKeys) this._db.exec(RESET_AUTH_KEYS) - - this._pending = [] - this._pendingUnimportant = {} - this._cache?.clear() - this._fsmCache?.clear() - this._rlCache?.clear() - this._saveUnimportantLater.reset() - } - - setDefaultDcs(dc: ITelegramStorage.DcOptions | null): void { - return this._setToKv('def_dc', dc, true) - } - - getDefaultDcs(): ITelegramStorage.DcOptions | null { - return this._getFromKv('def_dc') - } - - getFutureSalts(dcId: number): mtp.RawMt_future_salt[] | null { - return ( - this._getFromKv(`futureSalts:${dcId}`)?.map((it) => { - const [salt, validSince, validUntil] = it.split(',') - - return { - _: 'mt_future_salt', - validSince: Number(validSince), - validUntil: Number(validUntil), - salt: longFromFastString(salt), - } - }) ?? null - ) - } - - setFutureSalts(dcId: number, salts: mtp.RawMt_future_salt[]): void { - return this._setToKv( - `futureSalts:${dcId}`, - salts.map((salt) => `${longToFastString(salt.salt)},${salt.validSince},${salt.validUntil}`), - true, - ) - } - - getAuthKeyFor(dcId: number, tempIndex?: number): Uint8Array | null { - let row - - if (tempIndex !== undefined) { - row = this._statements.getAuthTemp.get(dcId, tempIndex, Date.now()) - } else { - row = this._statements.getAuth.get(dcId) - } - - return row ? (row as { key: Uint8Array }).key : null - } - - setAuthKeyFor(dcId: number, key: Uint8Array | null): void { - if (key !== null) { - this._statements.setAuth.run(dcId, key) - } else { - this._statements.delAuth.run(dcId) - } - } - - setTempAuthKeyFor(dcId: number, index: number, key: Uint8Array | null, expires: number): void { - if (key !== null) { - this._statements.setAuthTemp.run(dcId, index, key, expires) - } else { - this._statements.delAuthTemp.run(dcId, index) - } - } - - dropAuthKeysFor(dcId: number): void { - this._statements.delAuth.run(dcId) - this._statements.delAllAuthTemp.run(dcId) - } - - private _cachedSelf?: ITelegramStorage.SelfInfo | null - getSelf(): ITelegramStorage.SelfInfo | null { - if (this._cachedSelf !== undefined) return this._cachedSelf - - const self = this._getFromKv('self') - this._cachedSelf = self - - return self - } - - setSelf(self: ITelegramStorage.SelfInfo | null): void { - this._cachedSelf = self - - return this._setToKv('self', self, true) - } - - getUpdatesState(): [number, number, number, number] | null { - const pts = this._getFromKv('pts') - if (pts == null) return null - - return [ - pts, - this._getFromKv('qts') ?? 0, - this._getFromKv('date') ?? 0, - this._getFromKv('seq') ?? 0, - ] - } - - setUpdatesPts(val: number): void { - return this._setToKv('pts', val) - } - - setUpdatesQts(val: number): void { - return this._setToKv('qts', val) - } - - setUpdatesDate(val: number): void { - return this._setToKv('date', val) - } - - setUpdatesSeq(val: number): void { - return this._setToKv('seq', val) - } - - getChannelPts(entityId: number): number | null { - const row = this._statements.getPts.get(entityId) - - return row ? (row as { pts: number }).pts : null - } - - setManyChannelPts(values: Map): void { - for (const [cid, pts] of values) { - this._pending.push([this._statements.setPts, [cid, pts]]) - } - } - - updatePeers(peers: ITelegramStorage.PeerInfo[]): void { - peers.forEach((peer) => { - const cached = this._cache?.get(peer.id) - - if (cached && 'accessHash' in cached.peer && cached.peer.accessHash.eq(peer.accessHash)) { - // when entity is cached and hash is the same, an update query is needed, - // since some field in the full entity might have changed, or the username/phone - // - // since it is cached, we know for sure that it already exists in db, - // so we can safely use `update` instead of `insert or replace` - // - // to avoid too many DB calls, and since these updates are pretty common, - // they are grouped and applied in batches no more than once every 30sec (or user-defined). - // - // until then, they are either served from in-memory cache, - // or an older version is fetched from DB - - this._pendingUnimportant[peer.id] = [ - peer.username, - peer.phone, - Date.now(), - TlBinaryWriter.serializeObject(this.writerMap, peer.full), - peer.id, - ] - cached.full = peer.full - } else { - // entity is not cached in memory, or the access hash has changed - // we need to update it in the DB asap, and also update the in-memory cache - this._pending.push([ - this._statements.upsertEnt, - [ - peer.id, - longToFastString(peer.accessHash), - peer.type, - peer.username, - peer.phone, - Date.now(), - TlBinaryWriter.serializeObject(this.writerMap, peer.full), - ], - ]) - this._addToCache(peer.id, { - peer: getInputPeer(peer), - full: peer.full, - }) - - // we have the full peer, we no longer need the references - // we can skip this in the other branch, since in that case it would've already been deleted - if (!this._cachedSelf?.isBot) { - this._pending.push([this._statements.delAllMessageRefs, [peer.id]]) - } - } - }) - } - - private _findPeerByReference(peerId: number): tl.TypeInputPeer | null { - const row = this._statements.getMessageRef.get(peerId) as MessageRef | null - if (!row) return null - - const chat = this.getPeerById(row.chat_id, false) - if (!chat) return null - - if (peerId > 0) { - // user - return { - _: 'inputPeerUserFromMessage', - peer: chat, - userId: peerId, - msgId: row.msg_id, - } - } - - // channel - return { - _: 'inputPeerChannelFromMessage', - peer: chat, - channelId: toggleChannelIdMark(peerId), - msgId: row.msg_id, - } - } - - getPeerById(peerId: number, allowRefs = true): tl.TypeInputPeer | null { - const cached = this._cache?.get(peerId) - if (cached) return cached.peer - - const row = this._statements.getEntById.get(peerId) as SqliteEntity | null - - if (row) { - const peer = getInputPeer(row) - this._addToCache(peerId, { - peer, - full: this._readFullPeer(row.full), - }) - - return peer - } - - if (allowRefs) { - return this._findPeerByReference(peerId) - } - - return null - } - - getPeerByPhone(phone: string): tl.TypeInputPeer | null { - const row = this._statements.getEntByPhone.get(phone) as SqliteEntity | null - - if (row) { - const peer = getInputPeer(row) - this._addToCache(row.id, { - peer, - full: this._readFullPeer(row.full), - }) - - return peer - } - - return null - } - - getPeerByUsername(username: string): tl.TypeInputPeer | null { - const row = this._statements.getEntByUser.get(username.toLowerCase()) as SqliteEntity | null - if (!row || Date.now() - row.updated > USERNAME_TTL) return null - - if (row) { - const peer = getInputPeer(row) - this._addToCache(row.id, { - peer, - full: this._readFullPeer(row.full), - }) - - return peer - } - - return null - } - - getFullPeerById(id: number): tl.TypeUser | tl.TypeChat | null { - const cached = this._cache?.get(id) - if (cached) return cached.full - - const row = this._statements.getEntById.get(id) as SqliteEntity | null - - if (row) { - const full = this._readFullPeer(row.full) - this._addToCache(id, { - peer: getInputPeer(row), - full, - }) - - return full - } - - return null - } - - saveReferenceMessage(peerId: number, chatId: number, messageId: number): void { - this._pending.push([this._statements.storeMessageRef, [peerId, chatId, messageId]]) - } - - deleteReferenceMessages(chatId: number, messageIds: number[]): void { - for (const id of messageIds) { - this._pending.push([this._statements.delMessageRefs, [chatId, id]]) - } - } - - // IStateStorage implementation - - getState(key: string, parse = true): unknown { - let val: FsmItem | undefined = this._fsmCache?.get(key) - const cached = val - - if (!val) { - val = this._statements.getState.get(key) as FsmItem | undefined - - if (val && parse) { - val.value = JSON.parse(val.value as string) - } - } - - if (!val) return null - - if (val.expires && val.expires < Date.now()) { - // expired - if (cached) { - // hot path. if it's cached, then cache is definitely enabled - - this._fsmCache!.delete(key) - } - this._statements.delState.run(key) - - return null - } - - return val.value - } - - setState(key: string, state: unknown, ttl?: number, parse = true): void { - const item: FsmItem = { - value: state, - expires: ttl ? Date.now() + ttl * 1000 : undefined, - } - - this._fsmCache?.set(key, item) - this._statements.setState.run(key, parse ? JSON.stringify(item.value) : item.value, item.expires) - } - - deleteState(key: string): void { - this._fsmCache?.delete(key) - this._statements.delState.run(key) - } - - getCurrentScene(key: string): string | null { - return this.getState(`$current_scene_${key}`, false) as string | null - } - - setCurrentScene(key: string, scene: string, ttl?: number): void { - return this.setState(`$current_scene_${key}`, scene, ttl, false) - } - - deleteCurrentScene(key: string): void { - this.deleteState(`$current_scene_${key}`) - } - - getRateLimit(key: string, limit: number, window: number): [number, number] { - // leaky bucket - const now = Date.now() - - let val = this._rlCache?.get(key) as FsmItem | undefined - const cached = val - - if (!val) { - const got = this._statements.getState.get(`$rate_limit_${key}`) - - if (got) { - val = got as FsmItem - } - } - - // hot path. rate limit fsm entries always have an expiration date - - if (!val || val.expires! < now) { - // expired or does not exist - const item: FsmItem = { - expires: now + window * 1000, - value: limit, - } - - this._statements.setState.run(`$rate_limit_${key}`, item.value, item.expires) - this._rlCache?.set(key, item) - - return [item.value, item.expires!] - } - - if (val.value > 0) { - val.value -= 1 - - this._statements.setState.run(`$rate_limit_${key}`, val.value, val.expires) - - if (!cached) { - // add to cache - // if cached, cache is updated since `val === cached` - this._rlCache?.set(key, val) - } - } - - return [val.value, val.expires!] - } - - resetRateLimit(key: string): void { - this._rlCache?.delete(key) - this._statements.delState.run(`$rate_limit_${key}`) - } + readonly authKeys = new SqliteAuthKeysRepository(this.driver) + readonly kv = new SqliteKeyValueRepository(this.driver) + readonly refMessages = new SqliteRefMessagesRepository(this.driver) + readonly peers = new SqlitePeersRepository(this.driver) } diff --git a/packages/sqlite/src/repository/auth-keys.ts b/packages/sqlite/src/repository/auth-keys.ts new file mode 100644 index 00000000..affe5699 --- /dev/null +++ b/packages/sqlite/src/repository/auth-keys.ts @@ -0,0 +1,98 @@ +import { Statement } from 'better-sqlite3' + +import { IAuthKeysRepository } from '@mtcute/core' + +import { SqliteStorageDriver } from '../driver.js' + +interface AuthKeyDto { + dc: number + key: Uint8Array +} + +interface TempAuthKeyDto extends AuthKeyDto { + expires?: number + idx?: number +} + +export class SqliteAuthKeysRepository implements IAuthKeysRepository { + constructor(readonly _driver: SqliteStorageDriver) { + _driver.registerMigration('auth_keys', 1, (db) => { + db.exec(` + create table auth_keys ( + dc integer primary key, + key blob not null + ); + create table temp_auth_keys ( + dc integer not null, + idx integer not null, + key blob not null, + expires integer not null, + primary key (dc, idx) + ); + `) + }) + _driver.onLoad((db) => { + this._get = db.prepare('select key from auth_keys where dc = ?') + this._getTemp = db.prepare('select key from temp_auth_keys where dc = ? and idx = ? and expires > ?') + + this._set = db.prepare('insert or replace into auth_keys (dc, key) values (?, ?)') + this._setTemp = this._driver.db.prepare('insert or replace into temp_auth_keys (dc, idx, key, expires) values (?, ?, ?, ?)') + + this._del = db.prepare('delete from auth_keys where dc = ?') + this._delTemp = db.prepare('delete from temp_auth_keys where dc = ? and idx = ?') + this._delTempAll = db.prepare('delete from temp_auth_keys where dc = ?') + this._delAll = db.prepare('delete from auth_keys') + }) + } + + private _set!: Statement + private _del!: Statement + set(dc: number, key: Uint8Array | null): void { + if (!key) { + this._del.run(dc) + + return + } + + this._set.run(dc, key) + } + + private _get!: Statement + get(dc: number): Uint8Array | null { + const row = this._get.get(dc) + if (!row) return null + + return (row as AuthKeyDto).key + } + + private _setTemp!: Statement + private _delTemp!: Statement + setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void { + if (!key) { + this._delTemp.run(dc, idx) + + return + } + + this._setTemp.run(dc, idx, key, expires) + } + + private _getTemp!: Statement + getTemp(dc: number, idx: number, now: number): Uint8Array | null { + const row = this._getTemp.get(dc, idx, now) + if (!row) return null + + return (row as TempAuthKeyDto).key + } + + private _delTempAll!: Statement + deleteByDc(dc: number): void { + this._del.run(dc) + this._delTempAll.run(dc) + } + + private _delAll!: Statement + deleteAll(): void { + this._delAll.run() + } +} diff --git a/packages/sqlite/src/repository/kv.ts b/packages/sqlite/src/repository/kv.ts new file mode 100644 index 00000000..01cd347a --- /dev/null +++ b/packages/sqlite/src/repository/kv.ts @@ -0,0 +1,52 @@ +import { Statement } from 'better-sqlite3' + +import { IKeyValueRepository } from '@mtcute/core' + +import { SqliteStorageDriver } from '../driver.js' + +interface KeyValueDto { + key: string + value: Uint8Array +} + +export class SqliteKeyValueRepository implements IKeyValueRepository { + constructor(readonly _driver: SqliteStorageDriver) { + _driver.registerMigration('kv', 1, (db) => { + db.exec(` + create table key_value ( + key text primary key, + value blob not null + ); + `) + }) + _driver.onLoad((db) => { + this._get = db.prepare('select value from key_value where key = ?') + this._set = db.prepare('insert or replace into key_value (key, value) values (?, ?)') + this._del = db.prepare('delete from key_value where key = ?') + this._delAll = db.prepare('delete from key_value') + }) + } + + private _set!: Statement + set(key: string, value: Uint8Array): void { + this._driver._writeLater(this._set, [key, value]) + } + + private _get!: Statement + get(key: string): Uint8Array | null { + const res = this._get.get(key) + if (!res) return null + + return (res as KeyValueDto).value + } + + private _del!: Statement + delete(key: string): void { + this._del.run(key) + } + + private _delAll!: Statement + deleteAll(): void { + this._delAll.run() + } +} diff --git a/packages/sqlite/src/repository/peers.ts b/packages/sqlite/src/repository/peers.ts new file mode 100644 index 00000000..cab6e23e --- /dev/null +++ b/packages/sqlite/src/repository/peers.ts @@ -0,0 +1,100 @@ +import { Statement } from 'better-sqlite3' + +import { IPeersRepository } from '@mtcute/core' + +import { SqliteStorageDriver } from '../driver.js' + +interface PeerDto { + id: number + hash: string + usernames: string + updated: number + phone: string | null + // eslint-disable-next-line no-restricted-globals + complete: Buffer +} + +function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo { + return { + id: dto.id, + accessHash: dto.hash, + usernames: JSON.parse(dto.usernames) as string[], + updated: dto.updated, + phone: dto.phone || undefined, + complete: dto.complete, + } +} + +export class SqlitePeersRepository implements IPeersRepository { + constructor(readonly _driver: SqliteStorageDriver) { + _driver.registerMigration('peers', 1, (db) => { + db.exec(` + create table peers ( + id integer primary key, + hash text not null, + usernames json not null, + updated integer not null, + phone text, + complete blob + ); + create index idx_peers_usernames on peers (usernames); + create index idx_peers_phone on peers (phone); + `) + }) + _driver.onLoad((db) => { + this._store = db.prepare( + 'insert or replace into peers (id, hash, usernames, updated, phone, complete) values (?, ?, ?, ?, ?, ?)', + ) + + this._getById = db.prepare('select * from peers where id = ?') + this._getByUsername = db.prepare( + 'select * from peers where exists (select 1 from json_each(usernames) where value = ?)', + ) + this._getByPhone = db.prepare('select * from peers where phone = ?') + + this._delAll = db.prepare('delete from peers') + }) + } + + private _store!: Statement + store(peer: IPeersRepository.PeerInfo): void { + this._driver._writeLater(this._store, [ + peer.id, + peer.accessHash, + // add commas to make it easier to search with LIKE + JSON.stringify(peer.usernames), + peer.updated, + peer.phone, + peer.complete, + ]) + } + + private _getById!: Statement + getById(id: number): IPeersRepository.PeerInfo | null { + const row = this._getById.get(id) + if (!row) return null + + return mapPeerDto(row as PeerDto) + } + + private _getByUsername!: Statement + getByUsername(username: string): IPeersRepository.PeerInfo | null { + const row = this._getByUsername.get(username) + if (!row) return null + + return mapPeerDto(row as PeerDto) + } + + private _getByPhone!: Statement + getByPhone(phone: string): IPeersRepository.PeerInfo | null { + const row = this._getByPhone.get(phone) + if (!row) return null + + return mapPeerDto(row as PeerDto) + } + + private _delAll!: Statement + deleteAll(): void { + this._delAll.run() + } +} diff --git a/packages/sqlite/src/repository/ref-messages.ts b/packages/sqlite/src/repository/ref-messages.ts new file mode 100644 index 00000000..9c139148 --- /dev/null +++ b/packages/sqlite/src/repository/ref-messages.ts @@ -0,0 +1,68 @@ +import { Statement } from 'better-sqlite3' + +import { IReferenceMessagesRepository } from '@mtcute/core' + +import { SqliteStorageDriver } from '../driver.js' + +interface ReferenceMessageDto { + peer_id: number + chat_id: number + msg_id: number +} + +export class SqliteRefMessagesRepository implements IReferenceMessagesRepository { + constructor(readonly _driver: SqliteStorageDriver) { + _driver.registerMigration('ref_messages', 1, (db) => { + db.exec(` + create table message_refs ( + peer_id integer not null, + chat_id integer not null, + msg_id integer not null + ); + create index idx_message_refs_peer on message_refs (peer_id); + create index idx_message_refs on message_refs (chat_id, msg_id); + `) + }) + _driver.onLoad(() => { + this._store = this._driver.db.prepare('insert or replace into message_refs (peer_id, chat_id, msg_id) values (?, ?, ?)') + + this._getByPeer = this._driver.db.prepare('select chat_id, msg_id from message_refs where peer_id = ?') + + this._del = this._driver.db.prepare('delete from message_refs where chat_id = ? and msg_id = ?') + this._delByPeer = this._driver.db.prepare('delete from message_refs where peer_id = ?') + this._delAll = this._driver.db.prepare('delete from message_refs') + }) + } + + private _store!: Statement + store(peerId: number, chatId: number, msgId: number): void { + this._store.run(peerId, chatId, msgId) + } + + private _getByPeer!: Statement + getByPeer(peerId: number): [number, number] | null { + const res = this._getByPeer.get(peerId) + if (!res) return null + + const res_ = res as ReferenceMessageDto + + return [res_.chat_id, res_.msg_id] + } + + private _del!: Statement + delete(chatId: number, msgIds: number[]): void { + for (const msgId of msgIds) { + this._driver._writeLater(this._del, [chatId, msgId]) + } + } + + private _delByPeer!: Statement + deleteByPeer(peerId: number): void { + this._delByPeer.run(peerId) + } + + private _delAll!: Statement + deleteAll(): void { + this._delAll.run() + } +} diff --git a/packages/sqlite/test/sqlite.test.ts b/packages/sqlite/test/sqlite.test.ts index 6dedb356..c3709f8e 100644 --- a/packages/sqlite/test/sqlite.test.ts +++ b/packages/sqlite/test/sqlite.test.ts @@ -1,53 +1,30 @@ -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, describe } from 'vitest' -import { stubPeerUser, testStateStorage, testStorage } from '@mtcute/test' +import { LogManager } from '@mtcute/core/utils.js' +import { + testAuthKeysRepository, + testKeyValueRepository, + testPeersRepository, + testRefMessagesRepository, +} from '@mtcute/test' import { SqliteStorage } from '../src/index.js' if (import.meta.env.TEST_ENV === 'node') { describe('SqliteStorage', () => { - testStorage(new SqliteStorage(), { - // sqlite implements "unimportant" updates, which are batched once every 30sec (tested below) - skipEntityOverwrite: true, - customTests: (s) => { - describe('batching', () => { - beforeAll(() => void vi.useFakeTimers()) - afterAll(() => void vi.useRealTimers()) + const storage = new SqliteStorage(':memory:') - it('should batch entity writes', async () => { - s.updatePeers([stubPeerUser]) - s.updatePeers([{ ...stubPeerUser, username: 'test123' }]) - s.save() - - // eslint-disable-next-line - expect(Object.keys(s['_pendingUnimportant'])).toEqual([String(stubPeerUser.id)]) - // not yet updated - expect(s.getPeerByUsername(stubPeerUser.username!)).not.toBeNull() - expect(s.getPeerByUsername('test123')).toBeNull() - - await vi.advanceTimersByTimeAsync(30001) - - expect(s.getPeerByUsername(stubPeerUser.username!)).toBeNull() - expect(s.getPeerByUsername('test123')).not.toBeNull() - }) - - it('should batch update state writes', () => { - s.setUpdatesPts(123) - s.setUpdatesQts(456) - s.setUpdatesDate(789) - s.setUpdatesSeq(999) - - // not yet updated - expect(s.getUpdatesState()).toBeNull() - - s.save() - - expect(s.getUpdatesState()).toEqual([123, 456, 789, 999]) - }) - }) - }, + beforeAll(async () => { + storage.driver.setup(new LogManager()) + await storage.driver.load() }) - testStateStorage(new SqliteStorage()) + + testAuthKeysRepository(storage.authKeys) + testKeyValueRepository(storage.kv, storage.driver) + testPeersRepository(storage.peers, storage.driver) + testRefMessagesRepository(storage.refMessages, storage.driver) + + afterAll(() => storage.driver.destroy()) }) } else { describe.skip('SqliteStorage', () => {}) diff --git a/packages/test/src/client.ts b/packages/test/src/client.ts index c0c6b2da..6c2ab281 100644 --- a/packages/test/src/client.ts +++ b/packages/test/src/client.ts @@ -1,4 +1,11 @@ -import { BaseTelegramClient, BaseTelegramClientOptions, MaybeAsync, MustEqual, RpcCallOptions, tl } from '@mtcute/core' +import { + BaseTelegramClient, + BaseTelegramClientOptions, + MaybePromise, + MustEqual, + RpcCallOptions, + tl, +} from '@mtcute/core' import { StubMemoryTelegramStorage } from './storage.js' import { StubTelegramTransport } from './transport.js' @@ -24,19 +31,20 @@ export class StubTelegramClient extends BaseTelegramClient { apiHash: '', logLevel: 0, storage, + disableUpdates: true, transport: () => { const transport = new StubTelegramTransport({ onMessage: (data) => { if (!this._onRawMessage) { if (this._responders.size) { - this._emitError(new Error('Unexpected outgoing message')) + this.emitError(new Error('Unexpected outgoing message')) } return } const dcId = transport._currentDc!.id - const key = storage.getAuthKeyFor(dcId) + const key = storage.authKeys.get(dcId) if (key) { this._onRawMessage(storage.decryptOutgoingMessage(transport._crypto, data, dcId)) @@ -101,7 +109,7 @@ export class StubTelegramClient extends BaseTelegramClient { this._knownChats.set(peer.id, peer) } - await this._cachePeersFrom(peer) + await this.storage.peers.updatePeersFrom(peer) } } @@ -173,7 +181,7 @@ export class StubTelegramClient extends BaseTelegramClient { respondWith< T extends tl.RpcMethod['_'], - Fn extends(data: tl.FindByName) => MaybeAsync, + Fn extends(data: tl.FindByName) => MaybePromise, >(method: T, response: Fn): Fn { // eslint-disable-next-line this._responders.set(method, response as any) @@ -279,13 +287,8 @@ export class StubTelegramClient extends BaseTelegramClient { // helpers // - async connectAndWait() { + async with(fn: () => MaybePromise): Promise { await this.connect() - await new Promise((resolve) => this.once('usable', resolve)) - } - - async with(fn: () => MaybeAsync): Promise { - await this.connectAndWait() let error: unknown diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index 6b67ca75..e563c9ba 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -1,7 +1,7 @@ export * from './client.js' export * from './crypto.js' export * from './storage.js' -export * from './storage-test.js' +export * from './storage/index.js' export * from './stub.js' export * from './transport.js' export * from './types.js' diff --git a/packages/test/src/storage-test.ts b/packages/test/src/storage-test.ts deleted file mode 100644 index 5b3ed9b6..00000000 --- a/packages/test/src/storage-test.ts +++ /dev/null @@ -1,488 +0,0 @@ -import Long from 'long' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' - -import { ITelegramStorage, MaybeAsync } from '@mtcute/core' -import { defaultProductionDc, hexEncode, Logger, LogManager, TlReaderMap, TlWriterMap } from '@mtcute/core/utils.js' -import { mtp, tl } from '@mtcute/tl' -import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' -import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' - -import { createStub } from './stub.js' - -export const stubPeerUser: ITelegramStorage.PeerInfo = { - id: 123123, - accessHash: Long.fromBits(123, 456), - type: 'user', - username: 'some_user', - phone: '78005553535', - full: createStub('user', { id: 123123 }), -} -const peerUserInput: tl.TypeInputPeer = { - _: 'inputPeerUser', - userId: 123123, - accessHash: Long.fromBits(123, 456), -} - -const peerChannel: ITelegramStorage.PeerInfo = { - id: -1001183945448, - accessHash: Long.fromBits(666, 555), - type: 'channel', - username: 'some_channel', - full: createStub('channel', { id: 123123 }), -} - -const peerChannelInput: tl.TypeInputPeer = { - _: 'inputPeerChannel', - channelId: 1183945448, - accessHash: Long.fromBits(666, 555), -} - -function maybeHexEncode(x: Uint8Array | null): string | null { - if (x == null) return null - - return hexEncode(x) -} - -export function testStorage( - s: T, - params?: { - skipEntityOverwrite?: boolean - customTests?: (s: T) => void - }, -): void { - beforeAll(async () => { - const logger = new LogManager() - logger.level = 0 - s.setup?.(logger, __tlReaderMap, __tlWriterMap) - - await s.load?.() - }) - - afterAll(() => s.destroy?.()) - beforeEach(() => s.reset(true)) - - describe('default dc', () => { - it('should store', async () => { - await s.setDefaultDcs(defaultProductionDc) - expect(await s.getDefaultDcs()).toEqual(defaultProductionDc) - }) - - it('should remove', async () => { - await s.setDefaultDcs(null) - expect(await s.getDefaultDcs()).toBeNull() - }) - }) - - describe('auth keys', () => { - beforeAll(() => void vi.useFakeTimers()) - afterAll(() => void vi.useRealTimers()) - - const key2 = new Uint8Array(256).fill(0x42) - const key3 = new Uint8Array(256).fill(0x43) - - const key2i0 = new Uint8Array(256).fill(0x44) - const key2i1 = new Uint8Array(256).fill(0x45) - const key3i0 = new Uint8Array(256).fill(0x46) - const key3i1 = new Uint8Array(256).fill(0x47) - - it('should store perm auth key', async () => { - await s.setAuthKeyFor(2, key2) - await s.setAuthKeyFor(3, key3) - - expect(maybeHexEncode(await s.getAuthKeyFor(2))).toEqual(hexEncode(key2)) - expect(maybeHexEncode(await s.getAuthKeyFor(3))).toEqual(hexEncode(key3)) - }) - - it('should store temp auth keys', async () => { - const expire = Date.now() + 1000 - - await s.setTempAuthKeyFor(2, 0, key2i0, expire) - await s.setTempAuthKeyFor(2, 1, key2i1, expire) - await s.setTempAuthKeyFor(3, 0, key3i0, expire) - await s.setTempAuthKeyFor(3, 1, key3i1, expire) - - expect(maybeHexEncode(await s.getAuthKeyFor(2, 0))).toEqual(hexEncode(key2i0)) - expect(maybeHexEncode(await s.getAuthKeyFor(2, 1))).toEqual(hexEncode(key2i1)) - expect(maybeHexEncode(await s.getAuthKeyFor(3, 0))).toEqual(hexEncode(key3i0)) - expect(maybeHexEncode(await s.getAuthKeyFor(3, 1))).toEqual(hexEncode(key3i1)) - }) - - it('should expire temp auth keys', async () => { - const expire = Date.now() + 1000 - - await s.setTempAuthKeyFor(2, 0, key2i0, expire) - await s.setTempAuthKeyFor(2, 1, key2i1, expire) - await s.setTempAuthKeyFor(3, 0, key3i0, expire) - await s.setTempAuthKeyFor(3, 1, key3i1, expire) - - vi.advanceTimersByTime(10000) - - expect(await s.getAuthKeyFor(2, 0)).toBeNull() - expect(await s.getAuthKeyFor(2, 1)).toBeNull() - expect(await s.getAuthKeyFor(3, 0)).toBeNull() - expect(await s.getAuthKeyFor(3, 1)).toBeNull() - }) - - it('should remove auth keys', async () => { - const expire = Date.now() + 1000 - - await s.setTempAuthKeyFor(2, 0, key2i0, expire) - await s.setTempAuthKeyFor(2, 1, key2i1, expire) - await s.setAuthKeyFor(2, key2) - await s.setAuthKeyFor(3, key3) - - await s.setAuthKeyFor(2, null) - await s.setTempAuthKeyFor(2, 0, null, 0) - await s.setTempAuthKeyFor(2, 1, null, 0) - - expect(await s.getAuthKeyFor(2)).toBeNull() - expect(await s.getAuthKeyFor(2, 0)).toBeNull() - expect(await s.getAuthKeyFor(2, 1)).toBeNull() - expect(maybeHexEncode(await s.getAuthKeyFor(3))).toEqual(hexEncode(key3)) // should not be removed - }) - - it('should remove all auth keys with dropAuthKeysFor', async () => { - const expire = Date.now() + 1000 - - await s.setTempAuthKeyFor(2, 0, key2i0, expire) - await s.setTempAuthKeyFor(2, 1, key2i1, expire) - await s.setAuthKeyFor(2, key2) - await s.setAuthKeyFor(3, key3) - - await s.dropAuthKeysFor(2) - - expect(await s.getAuthKeyFor(2)).toBeNull() - expect(await s.getAuthKeyFor(2, 0)).toBeNull() - expect(await s.getAuthKeyFor(2, 1)).toBeNull() - expect(maybeHexEncode(await s.getAuthKeyFor(3))).toEqual(hexEncode(key3)) // should not be removed - }) - - it('should not reset auth keys on reset()', async () => { - await s.setAuthKeyFor(2, key2) - await s.setAuthKeyFor(3, key3) - await s.reset() - - expect(maybeHexEncode(await s.getAuthKeyFor(2))).toEqual(hexEncode(key2)) - expect(maybeHexEncode(await s.getAuthKeyFor(3))).toEqual(hexEncode(key3)) - }) - - it('should reset auth keys on reset(true)', async () => { - await s.setAuthKeyFor(2, key2) - await s.setAuthKeyFor(3, key3) - await s.reset(true) - - expect(await s.getAuthKeyFor(2)).toBeNull() - expect(await s.getAuthKeyFor(3)).toBeNull() - }) - }) - - describe('future salts', () => { - const someFutureSalt1 = Long.fromBits(123, 456) - const someFutureSalt2 = Long.fromBits(789, 101112) - const someFutureSalt3 = Long.fromBits(131415, 161718) - const someFutureSalt4 = Long.fromBits(192021, 222324) - - const salts1: mtp.RawMt_future_salt[] = [ - { _: 'mt_future_salt', validSince: 123, validUntil: 456, salt: someFutureSalt1 }, - { _: 'mt_future_salt', validSince: 789, validUntil: 101112, salt: someFutureSalt2 }, - ] - const salts2: mtp.RawMt_future_salt[] = [ - { _: 'mt_future_salt', validSince: 123, validUntil: 456, salt: someFutureSalt3 }, - { _: 'mt_future_salt', validSince: 789, validUntil: 101112, salt: someFutureSalt4 }, - ] - - it('should store and retrieve future salts', async () => { - await s.setFutureSalts(1, salts1) - await s.setFutureSalts(2, salts2) - - expect(await s.getFutureSalts(1)).toEqual(salts1) - expect(await s.getFutureSalts(2)).toEqual(salts2) - }) - }) - - describe('peers', () => { - it('should cache and return peers', async () => { - await s.updatePeers([stubPeerUser, peerChannel]) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerById(stubPeerUser.id)).toEqual(peerUserInput) - expect(await s.getPeerById(peerChannel.id)).toEqual(peerChannelInput) - }) - - it('should cache and return peers by username', async () => { - await s.updatePeers([stubPeerUser, peerChannel]) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerByUsername(stubPeerUser.username!)).toEqual(peerUserInput) - expect(await s.getPeerByUsername(peerChannel.username!)).toEqual(peerChannelInput) - }) - - it('should cache and return peers by phone', async () => { - await s.updatePeers([stubPeerUser]) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerByPhone(stubPeerUser.phone!)).toEqual(peerUserInput) - }) - - if (!params?.skipEntityOverwrite) { - it('should overwrite existing cached peers', async () => { - await s.updatePeers([stubPeerUser]) - await s.updatePeers([{ ...stubPeerUser, username: 'whatever' }]) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerById(stubPeerUser.id)).toEqual(peerUserInput) - expect(await s.getPeerByUsername(stubPeerUser.username!)).toBeNull() - expect(await s.getPeerByUsername('whatever')).toEqual(peerUserInput) - }) - } - - it('should cache full peer info', async () => { - await s.updatePeers([stubPeerUser, peerChannel]) - await s.save?.() // update-related methods are batched, so we need to save - - expect({ - ...(await s.getFullPeerById(stubPeerUser.id)), - usernames: [], - restrictionReason: [], - }).toEqual(stubPeerUser.full) - expect({ - ...(await s.getFullPeerById(peerChannel.id)), - usernames: [], - restrictionReason: [], - }).toEqual(peerChannel.full) - }) - - describe('min peers', () => { - it('should generate *FromMessage constructors from reference messages', async () => { - await s.updatePeers([peerChannel]) - await s.saveReferenceMessage(stubPeerUser.id, peerChannel.id, 456) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerById(stubPeerUser.id)).toEqual({ - _: 'inputPeerUserFromMessage', - peer: peerChannelInput, - msgId: 456, - userId: stubPeerUser.id, - }) - }) - - it('should handle cases when referenced chat is not available', async () => { - // this shouldn't really happen, but the storage should be able to handle it - await s.saveReferenceMessage(stubPeerUser.id, peerChannel.id, 456) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getPeerById(stubPeerUser.id)).toEqual(null) - }) - - it('should return full peer if it gets available', async () => { - await s.updatePeers([peerChannel]) - await s.saveReferenceMessage(stubPeerUser.id, peerChannel.id, 456) - await s.save?.() // update-related methods are batched, so we need to save - - await s.updatePeers([stubPeerUser]) - await s.save?.() - - expect(await s.getPeerById(stubPeerUser.id)).toEqual(peerUserInput) - }) - - it('should handle cases when referenced message is deleted', async () => { - await s.updatePeers([peerChannel]) - await s.saveReferenceMessage(stubPeerUser.id, peerChannel.id, 456) - await s.save?.() // update-related methods are batched, so we need to save - - await s.deleteReferenceMessages(peerChannel.id, [456]) - await s.save?.() - - expect(await s.getPeerById(stubPeerUser.id)).toEqual(null) - }) - }) - }) - - describe('current user', () => { - const self: ITelegramStorage.SelfInfo = { - userId: 123123, - isBot: false, - } - - it('should store and return self info', async () => { - await s.setSelf(self) - expect(await s.getSelf()).toEqual(self) - }) - - it('should remove self info', async () => { - await s.setSelf(self) - await s.setSelf(null) - expect(await s.getSelf()).toBeNull() - }) - }) - - describe('updates state', () => { - it('should store and return updates state', async () => { - await s.setUpdatesPts(1) - await s.setUpdatesQts(2) - await s.setUpdatesDate(3) - await s.setUpdatesSeq(4) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getUpdatesState()).toEqual([1, 2, 3, 4]) - }) - - it('should store and return channel pts', async () => { - await s.setManyChannelPts( - new Map([ - [1, 2], - [3, 4], - ]), - ) - await s.save?.() // update-related methods are batched, so we need to save - - expect(await s.getChannelPts(1)).toEqual(2) - expect(await s.getChannelPts(3)).toEqual(4) - expect(await s.getChannelPts(2)).toBeNull() - }) - - it('should be null after reset', async () => { - expect(await s.getUpdatesState()).toBeNull() - }) - }) - - params?.customTests?.(s) -} - -interface IStateStorage { - setup?(log: Logger, readerMap: TlReaderMap, writerMap: TlWriterMap): void - - load?(): MaybeAsync - - save?(): MaybeAsync - - destroy?(): MaybeAsync - - reset(): MaybeAsync - - getState(key: string): MaybeAsync - - setState(key: string, state: unknown, ttl?: number): MaybeAsync - - deleteState(key: string): MaybeAsync - - getCurrentScene(key: string): MaybeAsync - - setCurrentScene(key: string, scene: string, ttl?: number): MaybeAsync - - deleteCurrentScene(key: string): MaybeAsync - - getRateLimit(key: string, limit: number, window: number): MaybeAsync<[number, number]> - - resetRateLimit(key: string): MaybeAsync -} - -export function testStateStorage(s: IStateStorage) { - beforeAll(async () => { - const logger = new LogManager() - logger.level = 0 - s.setup?.(logger, __tlReaderMap, __tlWriterMap) - - await s.load?.() - }) - - afterAll(() => s.destroy?.()) - beforeEach(() => s.reset()) - - describe('key-value state', () => { - beforeAll(() => void vi.useFakeTimers()) - afterAll(() => void vi.useRealTimers()) - - it('should store and return state', async () => { - await s.setState('a', 'b') - await s.setState('c', 'd') - await s.setState('e', 'f') - - expect(await s.getState('a')).toEqual('b') - expect(await s.getState('c')).toEqual('d') - expect(await s.getState('e')).toEqual('f') - }) - - it('should remove state', async () => { - await s.setState('a', 'b') - await s.setState('c', 'd') - await s.setState('e', 'f') - - await s.deleteState('a') - await s.deleteState('c') - await s.deleteState('e') - - expect(await s.getState('a')).toBeNull() - expect(await s.getState('c')).toBeNull() - expect(await s.getState('e')).toBeNull() - }) - - it('should expire state', async () => { - await s.setState('a', 'b', 1) - await s.setState('c', 'd', 1) - await s.setState('e', 'f', 1) - - vi.advanceTimersByTime(10000) - - expect(await s.getState('a')).toBeNull() - expect(await s.getState('c')).toBeNull() - expect(await s.getState('e')).toBeNull() - }) - }) - - describe('scenes', () => { - it('should store and return scenes', async () => { - await s.setCurrentScene('a', 'b') - await s.setCurrentScene('c', 'd') - await s.setCurrentScene('e', 'f') - - expect(await s.getCurrentScene('a')).toEqual('b') - expect(await s.getCurrentScene('c')).toEqual('d') - expect(await s.getCurrentScene('e')).toEqual('f') - }) - - it('should remove scenes', async () => { - await s.setCurrentScene('a', 'b') - await s.setCurrentScene('c', 'd') - await s.setCurrentScene('e', 'f') - - await s.deleteCurrentScene('a') - await s.deleteCurrentScene('c') - await s.deleteCurrentScene('e') - - expect(await s.getCurrentScene('a')).toBeNull() - expect(await s.getCurrentScene('c')).toBeNull() - expect(await s.getCurrentScene('e')).toBeNull() - }) - }) - - describe('rate limit', () => { - beforeAll(() => void vi.useFakeTimers()) - afterAll(() => void vi.useRealTimers()) - - const check = () => s.getRateLimit('test', 3, 1) - - it('should implement basic rate limiting', async () => { - vi.setSystemTime(0) - - expect(await check()).toEqual([3, 1000]) - expect(await check()).toEqual([2, 1000]) - expect(await check()).toEqual([1, 1000]) - expect(await check()).toEqual([0, 1000]) - - vi.setSystemTime(1001) - - expect(await check()).toEqual([3, 2001]) - }) - - it('should allow resetting rate limit', async () => { - vi.setSystemTime(0) - - await check() - await check() - - await s.resetRateLimit('test') - expect(await check()).toEqual([3, 1000]) - }) - }) -} diff --git a/packages/test/src/storage.test.ts b/packages/test/src/storage.test.ts deleted file mode 100644 index dda9f810..00000000 --- a/packages/test/src/storage.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { BaseTelegramClient } from '@mtcute/core' - -import { StubMemoryTelegramStorage } from './storage.js' -import { createStub } from './stub.js' -import { StubTelegramTransport } from './transport.js' - -describe('storage stub', () => { - it('should correctly intercept calls', async () => { - const log: string[] = [] - - const client = new BaseTelegramClient({ - apiId: 0, - apiHash: '', - logLevel: 0, - defaultDcs: { - main: createStub('dcOption', { ipAddress: '1.2.3.4', port: 1234 }), - media: createStub('dcOption', { ipAddress: '1.2.3.4', port: 5678 }), - }, - transport: () => - new StubTelegramTransport({ - onMessage: (msg) => { - if (msg.slice(0, 8).reduce((a, b) => a + b, 0) === 0) { - // should not happen, since we're providing stub keys - log.push('unauthed_message') - } - setTimeout(() => { - client.close().catch(() => {}) - }, 10) - }, - }), - storage: new StubMemoryTelegramStorage({ - hasKeys: true, - onLoad: () => log.push('load'), - onSave: () => log.push('save'), - onDestroy: () => log.push('destroy'), - onReset: () => log.push('reset'), - }), - }) - - await client.connect() - await client.call({ _: 'help.getConfig' }).catch(() => {}) // ignore "client closed" error - - expect(log).toEqual(['load', 'save', 'destroy']) - }) -}) diff --git a/packages/test/src/storage.ts b/packages/test/src/storage.ts index b4395dcb..a776faff 100644 --- a/packages/test/src/storage.ts +++ b/packages/test/src/storage.ts @@ -1,8 +1,7 @@ -import { ITelegramStorage, MtArgumentError } from '@mtcute/core' -import { MemoryStorage } from '@mtcute/core/src/storage/memory.js' +import { MemoryStorage, MtArgumentError } from '@mtcute/core' import { createAesIgeForMessage, ICryptoProvider } from '@mtcute/core/utils.js' -export class StubMemoryTelegramStorage extends MemoryStorage implements ITelegramStorage { +export class StubMemoryTelegramStorage extends MemoryStorage { constructor( readonly params: { /** @@ -20,56 +19,40 @@ export class StubMemoryTelegramStorage extends MemoryStorage implements ITelegra * @default true */ hasTempKeys?: boolean | number[] - - onLoad?: () => void - onSave?: () => void - onDestroy?: () => void - onReset?: () => void } = { hasKeys: true, hasTempKeys: true, }, ) { super() - } - getAuthKeyFor(dcId: number, tempIndex?: number | undefined): Uint8Array | null { - if (tempIndex === undefined && this.params.hasKeys) { - if (this.params.hasKeys === true || this.params.hasKeys.includes(dcId)) { - return new Uint8Array(256) + const _origGet = this.authKeys.get + + this.authKeys.get = (dcId) => { + if (this.params.hasKeys) { + if (this.params.hasKeys === true || this.params.hasKeys.includes(dcId)) { + return new Uint8Array(256) + } } + + return _origGet.call(this.authKeys, dcId) } - if (tempIndex === undefined && this.params.hasTempKeys) { - if (this.params.hasTempKeys === true || this.params.hasTempKeys.includes(dcId)) { - return new Uint8Array(256) + const _origGetTemp = this.authKeys.getTemp + + this.authKeys.getTemp = (dcId, idx, now) => { + if (this.params.hasTempKeys) { + if (this.params.hasTempKeys === true || this.params.hasTempKeys.includes(dcId)) { + return new Uint8Array(256) + } } + + return _origGetTemp.call(this.authKeys, dcId, idx, now) } - - return super.getAuthKeyFor(dcId, tempIndex) - } - - load(): void { - this.params.onLoad?.() - super.load() - } - - save(): void { - this.params.onSave?.() - } - - destroy(): void { - this.params.onDestroy?.() - super.destroy() - } - - reset(withKeys = false): void { - this.params?.onReset?.() - super.reset(withKeys) } decryptOutgoingMessage(crypto: ICryptoProvider, data: Uint8Array, dcId: number, tempIndex?: number | undefined) { - const key = this.getAuthKeyFor(dcId, tempIndex) + const key = tempIndex ? this.authKeys.getTemp(dcId, tempIndex, Date.now()) : this.authKeys.get(dcId) if (!key) { throw new MtArgumentError(`No auth key for DC ${dcId}`) diff --git a/packages/test/src/storage/auth-keys.ts b/packages/test/src/storage/auth-keys.ts new file mode 100644 index 00000000..74c0510a --- /dev/null +++ b/packages/test/src/storage/auth-keys.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { IAuthKeysRepository } from '@mtcute/core' + +export function fakeAuthKeysRepository(): IAuthKeysRepository { + return { + get: vi.fn(), + set: vi.fn(), + getTemp: vi.fn(), + setTemp: vi.fn(), + deleteByDc: vi.fn(), + deleteAll: vi.fn(), + } +} + +function fixBuffer(buf: Uint8Array | null): Uint8Array | null { + if (!buf) return buf + + // eslint-disable-next-line no-restricted-globals + return typeof Buffer !== 'undefined' && buf instanceof Buffer ? new Uint8Array(buf) : buf +} + +export function testAuthKeysRepository(repo: IAuthKeysRepository) { + const key2 = new Uint8Array(256).fill(0x42) + const key3 = new Uint8Array(256).fill(0x43) + + const key2i0 = new Uint8Array(256).fill(0x44) + const key2i1 = new Uint8Array(256).fill(0x45) + const key3i0 = new Uint8Array(256).fill(0x46) + const key3i1 = new Uint8Array(256).fill(0x47) + + describe('auth keys', () => { + afterEach(() => repo.deleteAll()) + + it('should be empty by default', async () => { + expect(fixBuffer(await repo.get(2))).toEqual(null) + expect(fixBuffer(await repo.get(3))).toEqual(null) + }) + + it('should store and retrieve auth keys', async () => { + await repo.set(2, key2) + await repo.set(3, key3) + + expect(fixBuffer(await repo.get(2))).toEqual(key2) + expect(fixBuffer(await repo.get(3))).toEqual(key3) + }) + + it('should delete auth keys', async () => { + await repo.set(2, key2) + await repo.set(3, key3) + + await repo.set(2, null) + await repo.set(3, null) + + expect(fixBuffer(await repo.get(2))).toEqual(null) + expect(fixBuffer(await repo.get(3))).toEqual(null) + }) + + it('should store and retrieve temp auth keys', async () => { + await repo.setTemp(2, 0, key2i0, 1) + await repo.setTemp(2, 1, key2i1, 1) + await repo.setTemp(3, 0, key3i0, 1) + await repo.setTemp(3, 1, key3i1, 1) + + expect(fixBuffer(await repo.getTemp(2, 0, 0))).toEqual(key2i0) + expect(fixBuffer(await repo.getTemp(2, 1, 0))).toEqual(key2i1) + expect(fixBuffer(await repo.getTemp(3, 0, 0))).toEqual(key3i0) + expect(fixBuffer(await repo.getTemp(3, 1, 0))).toEqual(key3i1) + + expect(fixBuffer(await repo.getTemp(2, 0, 100))).toEqual(null) + expect(fixBuffer(await repo.getTemp(2, 1, 100))).toEqual(null) + expect(fixBuffer(await repo.getTemp(3, 0, 100))).toEqual(null) + expect(fixBuffer(await repo.getTemp(3, 1, 100))).toEqual(null) + }) + + it('should delete temp auth keys', async () => { + await repo.setTemp(2, 0, key2i0, 1) + await repo.setTemp(2, 1, key2i1, 1) + await repo.setTemp(3, 0, key3i0, 1) + await repo.setTemp(3, 1, key3i1, 1) + + await repo.setTemp(2, 0, null, 1) + await repo.setTemp(2, 1, null, 1) + await repo.setTemp(3, 0, null, 1) + await repo.setTemp(3, 1, null, 1) + + expect(fixBuffer(await repo.getTemp(2, 0, 0))).toEqual(null) + expect(fixBuffer(await repo.getTemp(2, 1, 0))).toEqual(null) + expect(fixBuffer(await repo.getTemp(3, 0, 0))).toEqual(null) + expect(fixBuffer(await repo.getTemp(3, 1, 0))).toEqual(null) + }) + + it('should delete all auth keys by DC', async () => { + await repo.set(2, key2) + await repo.set(3, key3) + + await repo.setTemp(2, 0, key2i0, 1) + await repo.setTemp(2, 1, key2i1, 1) + await repo.setTemp(3, 0, key3i0, 1) + await repo.setTemp(3, 1, key3i1, 1) + + await repo.deleteByDc(2) + + expect(fixBuffer(await repo.get(2))).toEqual(null) + expect(fixBuffer(await repo.get(3))).toEqual(key3) + + expect(fixBuffer(await repo.getTemp(2, 0, 0))).toEqual(null) + expect(fixBuffer(await repo.getTemp(2, 1, 0))).toEqual(null) + expect(fixBuffer(await repo.getTemp(3, 0, 0))).toEqual(key3i0) + expect(fixBuffer(await repo.getTemp(3, 1, 0))).toEqual(key3i1) + }) + }) +} diff --git a/packages/test/src/storage/index.ts b/packages/test/src/storage/index.ts new file mode 100644 index 00000000..03df2e41 --- /dev/null +++ b/packages/test/src/storage/index.ts @@ -0,0 +1,4 @@ +export * from './auth-keys.js' +export * from './key-value.js' +export * from './peers.js' +export * from './ref-messages.js' diff --git a/packages/test/src/storage/key-value.ts b/packages/test/src/storage/key-value.ts new file mode 100644 index 00000000..e2537c50 --- /dev/null +++ b/packages/test/src/storage/key-value.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { IKeyValueRepository, IStorageDriver } from '@mtcute/core' + +export function fakeKeyValueRepository(): IKeyValueRepository { + return { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + deleteAll: vi.fn(), + } +} + +function fixBuffer(buf: Uint8Array | null): Uint8Array | null { + if (!buf) return buf + + // eslint-disable-next-line no-restricted-globals + return typeof Buffer !== 'undefined' && buf instanceof Buffer ? new Uint8Array(buf) : buf +} + +export function testKeyValueRepository(repo: IKeyValueRepository, driver: IStorageDriver) { + describe('key-value', () => { + afterEach(() => repo.deleteAll()) + + it('should be empty by default', async () => { + expect(fixBuffer(await repo.get('key'))).toEqual(null) + }) + + it('should store and retrieve values', async () => { + await repo.set('key', new Uint8Array([1, 2, 3])) + await driver.save?.() + + expect(fixBuffer(await repo.get('key'))).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('should delete values', async () => { + await repo.set('key', new Uint8Array([1, 2, 3])) + await driver.save?.() + + await repo.delete('key') + await driver.save?.() + + expect(fixBuffer(await repo.get('key'))).toEqual(null) + }) + }) +} diff --git a/packages/test/src/storage/peers.ts b/packages/test/src/storage/peers.ts new file mode 100644 index 00000000..763c802b --- /dev/null +++ b/packages/test/src/storage/peers.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from 'vitest' + +import { IPeersRepository, IStorageDriver } from '@mtcute/core' +import { TlBinaryWriter } from '@mtcute/core/utils.js' +import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' + +import { createStub } from '../stub.js' + +export function fakePeersRepository(): IPeersRepository { + return { + getById: vi.fn(), + getByUsername: vi.fn(), + getByPhone: vi.fn(), + store: vi.fn(), + deleteAll: vi.fn(), + } +} + +function fixPeerInfo(peer: IPeersRepository.PeerInfo | null): IPeersRepository.PeerInfo | null { + if (!peer) return peer + + return { + ...peer, + complete: + // eslint-disable-next-line no-restricted-globals + typeof Buffer !== 'undefined' && peer.complete instanceof Buffer ? + new Uint8Array(peer.complete) : + peer.complete, + } +} + +export function testPeersRepository(repo: IPeersRepository, driver: IStorageDriver) { + const stubPeerUser: IPeersRepository.PeerInfo = { + id: 123123, + accessHash: '123|456', + usernames: ['some_user'], + phone: '78005553535', + updated: 666, + complete: TlBinaryWriter.serializeObject(__tlWriterMap, createStub('user', { id: 123123 })), + } + + const stubPeerChannel: IPeersRepository.PeerInfo = { + id: -1001183945448, + accessHash: '666|555', + usernames: ['some_channel'], + updated: 777, + complete: TlBinaryWriter.serializeObject(__tlWriterMap, createStub('channel', { id: 123123 })), + } + + describe('peers', () => { + it('should be empty by default', async () => { + expect(await repo.getById(123123)).toEqual(null) + expect(await repo.getByUsername('some_user')).toEqual(null) + expect(await repo.getByPhone('phone')).toEqual(null) + }) + + it('should store and retrieve peers', async () => { + await repo.store(stubPeerUser) + await repo.store(stubPeerChannel) + await driver.save?.() + + expect(fixPeerInfo(await repo.getById(123123))).toEqual(stubPeerUser) + expect(fixPeerInfo(await repo.getByUsername('some_user'))).toEqual(stubPeerUser) + expect(fixPeerInfo(await repo.getByPhone('78005553535'))).toEqual(stubPeerUser) + + expect(fixPeerInfo(await repo.getById(-1001183945448))).toEqual(stubPeerChannel) + expect(fixPeerInfo(await repo.getByUsername('some_channel'))).toEqual(stubPeerChannel) + }) + + it('should update peers usernames', async () => { + await repo.store(stubPeerUser) + await driver.save?.() + + const modUser = { ...stubPeerUser, usernames: ['some_user2'] } + await repo.store(modUser) + await driver.save?.() + + expect(fixPeerInfo(await repo.getById(123123))).toEqual(modUser) + expect(await repo.getByUsername('some_user')).toEqual(null) + expect(fixPeerInfo(await repo.getByUsername('some_user2'))).toEqual(modUser) + }) + }) +} diff --git a/packages/test/src/storage/ref-messages.ts b/packages/test/src/storage/ref-messages.ts new file mode 100644 index 00000000..785690f2 --- /dev/null +++ b/packages/test/src/storage/ref-messages.ts @@ -0,0 +1,76 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { IReferenceMessagesRepository, IStorageDriver } from '@mtcute/core' + +export function fakeRefMessagesRepository(): IReferenceMessagesRepository { + return { + store: vi.fn(), + getByPeer: vi.fn(), + delete: vi.fn(), + deleteByPeer: vi.fn(), + deleteAll: vi.fn(), + } +} + +export function testRefMessagesRepository(repo: IReferenceMessagesRepository, driver: IStorageDriver) { + describe('IReferenceMessagesRepository', () => { + afterEach(() => repo.deleteAll()) + + it('should be empty by default', async () => { + expect(await repo.getByPeer(1)).toEqual(null) + }) + + it('should store and retrieve reference messages', async () => { + await repo.store(1, 2, 3) + await repo.store(1, 4, 5) + await repo.store(2, 6, 7) + await driver.save?.() + + expect(await repo.getByPeer(1)).deep.oneOf([ + [2, 3], + [4, 5], + ]) + expect(await repo.getByPeer(2)).toEqual([6, 7]) + expect(await repo.getByPeer(3)).toEqual(null) + expect(await repo.getByPeer(4)).toEqual(null) + expect(await repo.getByPeer(5)).toEqual(null) + expect(await repo.getByPeer(6)).toEqual(null) + expect(await repo.getByPeer(7)).toEqual(null) + }) + + it('should delete reference messages', async () => { + await repo.store(1, 2, 3) + await repo.store(1, 4, 5) + await repo.store(2, 6, 7) + await driver.save?.() + + await repo.delete(4, [5]) + await driver.save?.() + expect(await repo.getByPeer(1)).toEqual([2, 3]) + + await repo.delete(2, [2, 3, 4]) + await driver.save?.() + expect(await repo.getByPeer(1)).toEqual(null) + }) + + it('should delete all reference messages for a peer', async () => { + await repo.store(1, 2, 3) + await repo.store(1, 4, 5) + await repo.store(1, 6, 7) + + await repo.store(2, 20, 30) + await repo.store(2, 40, 50) + await repo.store(2, 60, 70) + await driver.save?.() + + await repo.deleteByPeer(1) + await driver.save?.() + expect(await repo.getByPeer(1)).toEqual(null) + expect(await repo.getByPeer(2)).deep.oneOf([ + [20, 30], + [40, 50], + [60, 70], + ]) + }) + }) +} diff --git a/packages/test/src/transport.test.ts b/packages/test/src/transport.test.ts index 576ecb54..32f2b7b1 100644 --- a/packages/test/src/transport.test.ts +++ b/packages/test/src/transport.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { BaseTelegramClient } from '@mtcute/core' -import { MemoryStorage } from '@mtcute/core/src/storage/memory.js' +import { BaseTelegramClient, MemoryStorage } from '@mtcute/core' import { createStub } from './stub.js' import { StubTelegramTransport } from './transport.js' diff --git a/packages/test/src/utils.ts b/packages/test/src/utils.ts index 55ad51b7..63273995 100644 --- a/packages/test/src/utils.ts +++ b/packages/test/src/utils.ts @@ -1,8 +1,7 @@ -import { getBasicPeerType, markedPeerIdToBare, tl } from '@mtcute/core' +import { parseMarkedPeerId, tl } from '@mtcute/core' export function markedIdToPeer(id: number): tl.TypePeer { - const type = getBasicPeerType(id) - const bareId = markedPeerIdToBare(id) + const [type, bareId] = parseMarkedPeerId(id) switch (type) { case 'user': diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28babacf..17cb9089 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,10 +29,10 @@ importers: version: 8.5.4 '@typescript-eslint/eslint-plugin': specifier: 6.4.0 - version: 6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.0.4) + version: 6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.1.6) '@typescript-eslint/parser': specifier: 6.4.0 - version: 6.4.0(eslint@8.47.0)(typescript@5.0.4) + version: 6.4.0(eslint@8.47.0)(typescript@5.1.6) '@vitest/browser': specifier: 0.34.6 version: 0.34.6(esbuild@0.18.20)(vitest@0.34.6) @@ -98,16 +98,16 @@ importers: version: 7.5.1 ts-node: specifier: 10.9.1 - version: 10.9.1(@types/node@20.10.0)(typescript@5.0.4) + version: 10.9.1(@types/node@20.10.0)(typescript@5.1.6) tsconfig-paths: specifier: 4.2.0 version: 4.2.0 typedoc: specifier: 0.25.3 - version: 0.25.3(typescript@5.0.4) + version: 0.25.3(typescript@5.1.6) typescript: - specifier: 5.0.4 - version: 5.0.4 + specifier: 5.1.6 + version: 5.1.6 vite: specifier: 5.0.3 version: 5.0.3(@types/node@20.10.0) @@ -115,21 +115,11 @@ importers: specifier: 0.34.6 version: 0.34.6(@vitest/browser@0.34.6)(@vitest/ui@0.34.6)(playwright@1.40.1) - packages/client: + packages/core: dependencies: - '@mtcute/core': - specifier: workspace:^ - version: link:../core '@mtcute/file-id': specifier: workspace:^ version: link:../file-id - devDependencies: - '@mtcute/test': - specifier: workspace:^ - version: link:../test - - packages/core: - dependencies: '@mtcute/tl': specifier: workspace:^ version: link:../tl @@ -155,9 +145,6 @@ importers: '@types/ws': specifier: 8.5.4 version: 8.5.4 - node-forge: - specifier: 1.3.1 - version: 1.3.1 ws: specifier: 8.13.0 version: 8.13.0 @@ -195,22 +182,29 @@ importers: version: link:../test packages/dispatcher: - dependencies: - '@mtcute/client': - specifier: workspace:^ - version: link:../client - '@mtcute/test': - specifier: workspace:^ - version: link:../test - events: - specifier: 3.2.0 - version: 3.2.0 - - packages/file-id: dependencies: '@mtcute/core': specifier: workspace:^ version: link:../core + '@mtcute/sqlite': + specifier: workspace:^ + version: link:../sqlite + events: + specifier: 3.2.0 + version: 3.2.0 + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test + + packages/file-id: + dependencies: + '@mtcute/tl-runtime': + specifier: workspace:^ + version: link:../tl-runtime + long: + specifier: 5.2.3 + version: 5.2.3 packages/html-parser: dependencies: @@ -221,9 +215,9 @@ importers: specifier: 5.2.3 version: 5.2.3 devDependencies: - '@mtcute/client': + '@mtcute/core': specifier: workspace:^ - version: link:../client + version: link:../core packages/http-proxy: dependencies: @@ -233,9 +227,9 @@ importers: packages/i18n: devDependencies: - '@mtcute/client': + '@mtcute/core': specifier: workspace:^ - version: link:../client + version: link:../core '@mtcute/dispatcher': specifier: workspace:^ version: link:../dispatcher @@ -246,9 +240,9 @@ importers: specifier: 5.2.3 version: 5.2.3 devDependencies: - '@mtcute/client': + '@mtcute/core': specifier: workspace:^ - version: link:../client + version: link:../core packages/mtproxy: dependencies: @@ -258,9 +252,9 @@ importers: packages/node: dependencies: - '@mtcute/client': + '@mtcute/core': specifier: workspace:^ - version: link:../client + version: link:../core '@mtcute/html-parser': specifier: workspace:^ version: link:../html-parser @@ -289,8 +283,8 @@ importers: specifier: workspace:^ version: link:../tl-runtime better-sqlite3: - specifier: 8.4.0 - version: 8.4.0 + specifier: 9.2.2 + version: 9.2.2 devDependencies: '@mtcute/test': specifier: workspace:^ @@ -497,13 +491,13 @@ packages: '@types/node': 20.10.0 chalk: 4.1.2 cosmiconfig: 8.1.3 - cosmiconfig-typescript-loader: 4.3.0(@types/node@20.10.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4) + cosmiconfig-typescript-loader: 4.3.0(@types/node@20.10.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.1.6) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.10.0)(typescript@5.0.4) - typescript: 5.0.4 + ts-node: 10.9.1(@types/node@20.10.0)(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -1322,7 +1316,7 @@ packages: '@types/node': 20.10.0 dev: true - /@typescript-eslint/eslint-plugin@6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.0.4): + /@typescript-eslint/eslint-plugin@6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1334,10 +1328,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.0.4) + '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.1.6) '@typescript-eslint/scope-manager': 6.4.0 - '@typescript-eslint/type-utils': 6.4.0(eslint@8.47.0)(typescript@5.0.4) - '@typescript-eslint/utils': 6.4.0(eslint@8.47.0)(typescript@5.0.4) + '@typescript-eslint/type-utils': 6.4.0(eslint@8.47.0)(typescript@5.1.6) + '@typescript-eslint/utils': 6.4.0(eslint@8.47.0)(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.4.0 debug: 4.3.4 eslint: 8.47.0 @@ -1345,13 +1339,13 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.0.4) - typescript: 5.0.4 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.4.0(eslint@8.47.0)(typescript@5.0.4): + /@typescript-eslint/parser@6.4.0(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-I1Ah1irl033uxjxO9Xql7+biL3YD7w9IU8zF+xlzD/YxY6a4b7DYA08PXUUCbm2sEljwJF6ERFy2kTGAGcNilg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1363,11 +1357,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.4.0 '@typescript-eslint/types': 6.4.0 - '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.4.0 debug: 4.3.4 eslint: 8.47.0 - typescript: 5.0.4 + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true @@ -1380,7 +1374,7 @@ packages: '@typescript-eslint/visitor-keys': 6.4.0 dev: true - /@typescript-eslint/type-utils@6.4.0(eslint@8.47.0)(typescript@5.0.4): + /@typescript-eslint/type-utils@6.4.0(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1390,12 +1384,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.0.4) - '@typescript-eslint/utils': 6.4.0(eslint@8.47.0)(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.1.6) + '@typescript-eslint/utils': 6.4.0(eslint@8.47.0)(typescript@5.1.6) debug: 4.3.4 eslint: 8.47.0 - ts-api-utils: 1.0.1(typescript@5.0.4) - typescript: 5.0.4 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true @@ -1405,7 +1399,7 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.4.0(typescript@5.0.4): + /@typescript-eslint/typescript-estree@6.4.0(typescript@5.1.6): resolution: {integrity: sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1420,13 +1414,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.0.4) - typescript: 5.0.4 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@6.4.0(eslint@8.47.0)(typescript@5.0.4): + /@typescript-eslint/utils@6.4.0(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1437,7 +1431,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 6.4.0 '@typescript-eslint/types': 6.4.0 - '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.0(typescript@5.1.6) eslint: 8.47.0 semver: 7.5.4 transitivePeerDependencies: @@ -1615,11 +1609,6 @@ packages: dependencies: type-fest: 0.21.3 - /ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - dev: false - /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1653,21 +1642,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - /aproba@1.2.0: - resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} - dev: false - /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: false - /are-we-there-yet@1.1.7: - resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==} - dependencies: - delegates: 1.0.0 - readable-stream: 2.3.7 - dev: false - /are-we-there-yet@3.0.0: resolution: {integrity: sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16} @@ -1766,12 +1744,12 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - /better-sqlite3@8.4.0: - resolution: {integrity: sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==} + /better-sqlite3@9.2.2: + resolution: {integrity: sha512-qwjWB46il0lsDkeB4rSRI96HyDQr8sxeu1MkBVLMrwusq1KRu4Bpt1TMI+8zIJkDUtZ3umjAkaEjIlokZKWCQw==} requiresBuild: true dependencies: bindings: 1.5.0 - prebuild-install: 7.1.0 + prebuild-install: 7.1.1 dev: false /big-integer@1.6.51: @@ -2020,11 +1998,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - /code-point-at@1.1.0: - resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} - engines: {node: '>=0.10.0'} - dev: false - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -2106,11 +2079,7 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false - - /cosmiconfig-typescript-loader@4.3.0(@types/node@20.10.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4): + /cosmiconfig-typescript-loader@4.3.0(@types/node@20.10.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -2121,8 +2090,8 @@ packages: dependencies: '@types/node': 20.10.0 cosmiconfig: 8.1.3 - ts-node: 10.9.1(@types/node@20.10.0)(typescript@5.0.4) - typescript: 5.0.4 + ts-node: 10.9.1(@types/node@20.10.0)(typescript@5.1.6) + typescript: 5.1.6 dev: true /cosmiconfig@8.1.3: @@ -2662,7 +2631,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.0.4) + '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.1.6) debug: 3.2.7 eslint: 8.47.0 eslint-import-resolver-node: 0.3.7 @@ -2685,7 +2654,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.0.4) + '@typescript-eslint/parser': 6.4.0(eslint@8.47.0)(typescript@5.1.6) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 @@ -3040,19 +3009,6 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /gauge@2.7.4: - resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} - dependencies: - aproba: 1.2.0 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 1.0.2 - strip-ansi: 3.0.1 - wide-align: 1.1.5 - dev: false - /gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3527,13 +3483,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - /is-fullwidth-code-point@1.0.0: - resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} - engines: {node: '>=0.10.0'} - dependencies: - number-is-nan: 1.0.1 - dev: false - /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3672,10 +3621,6 @@ packages: is-docker: 2.2.1 dev: false - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false - /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4292,15 +4237,6 @@ packages: dependencies: path-key: 4.0.0 - /npmlog@4.1.2: - resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} - dependencies: - are-we-there-yet: 1.1.7 - console-control-strings: 1.1.0 - gauge: 2.7.4 - set-blocking: 2.0.0 - dev: false - /npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4317,16 +4253,6 @@ packages: boolbase: 1.0.0 dev: true - /number-is-nan@1.0.1: - resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} - engines: {node: '>=0.10.0'} - dev: false - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -4587,8 +4513,8 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /prebuild-install@7.1.0: - resolution: {integrity: sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==} + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'} hasBin: true dependencies: @@ -4599,7 +4525,6 @@ packages: mkdirp-classic: 0.5.3 napi-build-utils: 1.0.2 node-abi: 3.15.0 - npmlog: 4.1.2 pump: 3.0.0 rc: 1.2.8 simple-get: 4.0.1 @@ -4632,10 +4557,6 @@ packages: hasBin: true dev: false - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false - /promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -4710,18 +4631,6 @@ packages: type-fest: 0.6.0 dev: true - /readable-stream@2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: false - /readable-stream@3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} engines: {node: '>= 6'} @@ -4862,10 +4771,6 @@ packages: dependencies: tslib: 2.6.2 - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: false - /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5085,15 +4990,6 @@ packages: engines: {node: '>=0.6.19'} dev: true - /string-width@1.0.2: - resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} - engines: {node: '>=0.10.0'} - dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - strip-ansi: 3.0.1 - dev: false - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5135,24 +5031,11 @@ packages: es-abstract: 1.21.2 dev: true - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: false - /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - /strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} - dependencies: - ansi-regex: 2.1.1 - dev: false - /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5321,16 +5204,16 @@ packages: engines: {node: '>=8'} dev: true - /ts-api-utils@1.0.1(typescript@5.0.4): + /ts-api-utils@1.0.1(typescript@5.1.6): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.0.4 + typescript: 5.1.6 dev: true - /ts-node@10.9.1(@types/node@20.10.0)(typescript@5.0.4): + /ts-node@10.9.1(@types/node@20.10.0)(typescript@5.1.6): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -5356,7 +5239,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.0.4 + typescript: 5.1.6 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -5431,7 +5314,7 @@ packages: is-typed-array: 1.1.10 dev: true - /typedoc@0.25.3(typescript@5.0.4): + /typedoc@0.25.3(typescript@5.1.6): resolution: {integrity: sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==} engines: {node: '>= 16'} hasBin: true @@ -5442,12 +5325,12 @@ packages: marked: 4.3.0 minimatch: 9.0.3 shiki: 0.14.2 - typescript: 5.0.4 + typescript: 5.1.6 dev: true - /typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} hasBin: true dev: true diff --git a/scripts/build-package.js b/scripts/build-package.js index 9976e5cb..4122427f 100644 --- a/scripts/build-package.js +++ b/scripts/build-package.js @@ -26,6 +26,7 @@ const buildConfig = { removeReferenceComments: true, replaceSrcImports: true, esmOnlyDirectives: false, + esmImportDirectives: false, before: () => {}, final: () => {}, ...(() => { @@ -173,6 +174,7 @@ if (buildConfig.buildTs) { console.log('[i] Building typescript (CJS)...') const originalFiles = {} + // todo - get rid of these, use @esm-replace-import instead if (buildConfig.esmOnlyDirectives) { for (const f of glob.sync(path.join(packageDir, '**/*.ts'))) { const content = fs.readFileSync(f, 'utf8') @@ -182,6 +184,15 @@ if (buildConfig.buildTs) { fs.writeFileSync(f, content.replace(/@only-if-esm.*?@\/only-if-esm/gs, '')) } } + if (buildConfig.esmImportDirectives) { + for (const f of glob.sync(path.join(packageDir, '**/*.ts'))) { + const content = fs.readFileSync(f, 'utf8') + if (!content.includes('@esm-replace-import')) continue + originalFiles[f] = content + + fs.writeFileSync(f, content.replace(/@esm-replace-import.*?await import/gs, 'require')) + } + } let error = false diff --git a/tsconfig.json b/tsconfig.json index 2b6ef48e..d3481db6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,12 @@ "node", "vite/client" ], + "lib": [ + "es2020", + "dom", + "dom.iterable", + "WebWorker", + ], "resolveJsonModule": true, "isolatedModules": true, },