diff --git a/packages/core/src/highlevel/methods/auth/start.ts b/packages/core/src/highlevel/methods/auth/start.ts index 8a43c764..ac185eca 100644 --- a/packages/core/src/highlevel/methods/auth/start.ts +++ b/packages/core/src/highlevel/methods/auth/start.ts @@ -137,6 +137,7 @@ export async function start( return me } catch (e) { + console.log(e) if (tl.RpcError.is(e)) { if (e.text === 'SESSION_PASSWORD_NEEDED') has2fa = true else if (e.text !== 'AUTH_KEY_UNREGISTERED') throw e diff --git a/packages/core/src/highlevel/methods/users/resolve-peer.ts b/packages/core/src/highlevel/methods/users/resolve-peer.ts index 33a3bbf5..6d9ebc53 100644 --- a/packages/core/src/highlevel/methods/users/resolve-peer.ts +++ b/packages/core/src/highlevel/methods/users/resolve-peer.ts @@ -6,7 +6,7 @@ import { getMarkedPeerId, parseMarkedPeerId, toggleChannelIdMark } from '../../. import type { ITelegramClient } from '../../client.types.js' import { MtPeerNotFoundError } from '../../types/errors.js' import type { InputPeerLike } from '../../types/peers/index.js' -import { toInputChannel, toInputPeer, toInputUser } from '../../utils/peer-utils.js' +import { extractUsernames, toInputChannel, toInputPeer, toInputUser } from '../../utils/peer-utils.js' export function _normalizePeerId(peerId: InputPeerLike): number | string | tl.TypeInputPeer { // for convenience we also accept tl and User/Chat objects directly @@ -161,6 +161,32 @@ export async function resolvePeer( const [peerType, bareId] = parseMarkedPeerId(peerId) if (!(peerType === 'chat' || client.storage.self.getCached(true)?.isBot)) { + // we might have a min peer in cache, which we can try to resolve by its username/phone + const cached = await client.storage.peers.getCompleteById(peerId, true) + + if (cached && (cached._ === 'channel' || cached._ === 'user')) { + // do we have a username? + const [username] = extractUsernames(cached) + + if (username) { + const resolved = await resolvePeer(client, username, true) + + // username might already be taken by someone else, so we need to check it + if (getMarkedPeerId(resolved) === peerId) { + return resolved + } + } + + if (cached._ === 'user' && cached.phone) { + // try resolving by phone + const resolved = await resolvePeer(client, cached.phone, true) + + if (getMarkedPeerId(resolved) === peerId) { + return resolved + } + } + } + throw new MtPeerNotFoundError(`Peer ${peerId} is not found in local cache`) } diff --git a/packages/core/src/highlevel/storage/repository/peers.ts b/packages/core/src/highlevel/storage/repository/peers.ts index f94cad76..da0c4194 100644 --- a/packages/core/src/highlevel/storage/repository/peers.ts +++ b/packages/core/src/highlevel/storage/repository/peers.ts @@ -8,6 +8,8 @@ export namespace IPeersRepository { id: number /** Peer access hash, as a fast string representation */ accessHash: string + /** Whether the peer is a "min" peer */ + isMin: boolean /** Peer usernames, if any */ usernames: string[] /** Timestamp (in seconds) when the peer was last updated */ @@ -26,11 +28,21 @@ export namespace IPeersRepository { 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`) */ + /** + * Find a peer by their `id`. + * + * @param allowMin Whether to allow "min" peers to be returned + */ + getById: (id: number, allowMin: boolean) => MaybePromise + /** + * Find a peer by their username (where `usernames` includes `username`). + * Should never return "min" peers + */ getByUsername: (username: string) => MaybePromise - /** Find a peer by their `phone` */ + /** + * Find a peer by their `phone`. + * Should never return "min" peers + */ getByPhone: (phone: string) => MaybePromise deleteAll: () => MaybePromise diff --git a/packages/core/src/highlevel/storage/service/peers.ts b/packages/core/src/highlevel/storage/service/peers.ts index 4ede9210..3ca3b139 100644 --- a/packages/core/src/highlevel/storage/service/peers.ts +++ b/packages/core/src/highlevel/storage/service/peers.ts @@ -64,10 +64,12 @@ export class PeersService extends BaseService { async updatePeersFrom(obj: tl.TlObject | tl.TlObject[]): Promise { let count = 0 + let minCount = 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 + if ((peer as Extract).min) { + minCount += 1 + } count += 1 @@ -76,7 +78,7 @@ export class PeersService extends BaseService { if (count > 0) { await this._driver.save?.() - this._log.debug('cached %d peers', count) + this._log.debug('cached %d peers (%d min)', count, minCount) return true } @@ -99,6 +101,7 @@ export class PeersService extends BaseService { dto = { id: peer.id, accessHash: longToFastString(peer.accessHash), + isMin: peer.min! && !(peer.phone !== undefined && peer.phone.length === 0), phone: peer.phone, usernames: extractUsernames(peer), updated: Date.now(), @@ -112,6 +115,7 @@ export class PeersService extends BaseService { dto = { id: -peer.id, accessHash: '', + isMin: false, // chats can't be "min" updated: Date.now(), complete: this._serializeTl(peer), usernames: [], @@ -130,6 +134,7 @@ export class PeersService extends BaseService { dto = { id: toggleChannelIdMark(peer.id), accessHash: longToFastString(peer.accessHash), + isMin: peer._ === 'channel' ? peer.min! : false, usernames: extractUsernames(peer as tl.RawChannel), updated: Date.now(), complete: this._serializeTl(peer), @@ -193,7 +198,7 @@ export class PeersService extends BaseService { const cached = this._cache.get(id) if (cached) return cached.peer - const dto = await this._peers.getById(id) + const dto = await this._peers.getById(id, false) if (dto) { return this._returnCaching(id, dto) @@ -248,11 +253,11 @@ export class PeersService extends BaseService { return this._returnCaching(dto.id, dto) } - async getCompleteById(id: number): Promise { + async getCompleteById(id: number, allowMin = false): Promise { const cached = this._cache.get(id) if (cached) return cached.complete - const dto = await this._peers.getById(id) + const dto = await this._peers.getById(id, allowMin) if (!dto) return null const cacheItem: CacheItem = { diff --git a/packages/core/src/storage/memory/repository/peers.ts b/packages/core/src/storage/memory/repository/peers.ts index e5b5c37f..25e43d46 100644 --- a/packages/core/src/storage/memory/repository/peers.ts +++ b/packages/core/src/storage/memory/repository/peers.ts @@ -42,22 +42,31 @@ export class MemoryPeersRepository implements IPeersRepository { this.state.entities.set(peer.id, peer) } - getById(id: number): IPeersRepository.PeerInfo | null { - return this.state.entities.get(id) ?? null + getById(id: number, allowMin: boolean): IPeersRepository.PeerInfo | null { + const ent = this.state.entities.get(id) + if (!ent || (ent.isMin && !allowMin)) return null + + return ent } 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 + const ent = this.state.entities.get(id) + if (!ent || ent.isMin) return null + + return ent } getByPhone(phone: string): IPeersRepository.PeerInfo | null { const id = this.state.phoneIndex.get(phone) if (!id) return null - return this.state.entities.get(id) ?? null + const ent = this.state.entities.get(id) + if (!ent || ent.isMin) return null + + return ent } deleteAll(): void { diff --git a/packages/core/src/storage/sqlite/repository/peers.ts b/packages/core/src/storage/sqlite/repository/peers.ts index 770978e5..b9887668 100644 --- a/packages/core/src/storage/sqlite/repository/peers.ts +++ b/packages/core/src/storage/sqlite/repository/peers.ts @@ -6,6 +6,7 @@ import type { ISqliteStatement } from '../types.js' interface PeerDto { id: number hash: string + isMin: 1 | 0 usernames: string updated: number phone: string | null @@ -16,6 +17,7 @@ function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo { return { id: dto.id, accessHash: dto.hash, + isMin: dto.isMin === 1, usernames: JSON.parse(dto.usernames) as string[], updated: dto.updated, phone: dto.phone || undefined, @@ -41,18 +43,22 @@ export class SqlitePeersRepository implements IPeersRepository { create index idx_peers_phone on peers (phone); `) }) + _driver.registerMigration('peers', 2, (db) => { + db.exec('alter table peers add column isMin integer not null default false;') + }) _driver.onLoad((db) => { this._loaded = true this._store = db.prepare( - 'insert or replace into peers (id, hash, usernames, updated, phone, complete) values (?, ?, ?, ?, ?, ?)', + 'insert or replace into peers (id, hash, isMin, usernames, updated, phone, complete) values (?, ?, ?, ?, ?, ?, ?)', ) - this._getById = db.prepare('select * from peers where id = ?') + this._getById = db.prepare('select * from peers where id = ? and isMin = false') + this._getByIdAllowMin = db.prepare('select * from peers where id = ?') this._getByUsername = db.prepare( - 'select * from peers where exists (select 1 from json_each(usernames) where value = ?)', + 'select * from peers where exists (select 1 from json_each(usernames) where value = ?) and isMin = false', ) - this._getByPhone = db.prepare('select * from peers where phone = ?') + this._getByPhone = db.prepare('select * from peers where phone = ? and isMin = false') this._delAll = db.prepare('delete from peers') }) @@ -77,6 +83,7 @@ export class SqlitePeersRepository implements IPeersRepository { this._driver._writeLater(this._store, [ peer.id, peer.accessHash, + peer.isMin ? 1 : 0, JSON.stringify(peer.usernames), peer.updated, peer.phone ?? null, @@ -85,9 +92,10 @@ export class SqlitePeersRepository implements IPeersRepository { } private _getById!: ISqliteStatement - getById(id: number): IPeersRepository.PeerInfo | null { + private _getByIdAllowMin!: ISqliteStatement + getById(id: number, allowMin: boolean): IPeersRepository.PeerInfo | null { this._ensureLoaded() - const row = this._getById.get(id) + const row = (allowMin ? this._getByIdAllowMin : this._getById).get(id) if (!row) return null return mapPeerDto(row as PeerDto) diff --git a/packages/test/src/storage/peers.ts b/packages/test/src/storage/peers.ts index e93a83b5..a5ba7a07 100644 --- a/packages/test/src/storage/peers.ts +++ b/packages/test/src/storage/peers.ts @@ -32,6 +32,7 @@ export function testPeersRepository(repo: IPeersRepository, driver: IStorageDriv const stubPeerUser: IPeersRepository.PeerInfo = { id: 123123, accessHash: '123|456', + isMin: false, usernames: ['some_user'], phone: '78005553535', updated: 666, @@ -41,14 +42,17 @@ export function testPeersRepository(repo: IPeersRepository, driver: IStorageDriv const stubPeerChannel: IPeersRepository.PeerInfo = { id: -1001183945448, accessHash: '666|555', + isMin: false, usernames: ['some_channel'], updated: 777, complete: TlBinaryWriter.serializeObject(__tlWriterMap, createStub('channel', { id: 123123 })), } + const stupPeerMinUser: IPeersRepository.PeerInfo = { ...stubPeerUser, isMin: true } + describe('peers', () => { it('should be empty by default', async () => { - expect(await repo.getById(123123)).toEqual(null) + expect(await repo.getById(123123, false)).toEqual(null) expect(await repo.getByUsername('some_user')).toEqual(null) expect(await repo.getByPhone('phone')).toEqual(null) }) @@ -58,11 +62,11 @@ export function testPeersRepository(repo: IPeersRepository, driver: IStorageDriv await repo.store(stubPeerChannel) await driver.save?.() - expect(fixPeerInfo(await repo.getById(123123))).toEqual(stubPeerUser) + expect(fixPeerInfo(await repo.getById(123123, false))).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.getById(-1001183945448, false))).toEqual(stubPeerChannel) expect(fixPeerInfo(await repo.getByUsername('some_channel'))).toEqual(stubPeerChannel) }) @@ -74,9 +78,21 @@ export function testPeersRepository(repo: IPeersRepository, driver: IStorageDriv await repo.store(modUser) await driver.save?.() - expect(fixPeerInfo(await repo.getById(123123))).toEqual(modUser) + expect(fixPeerInfo(await repo.getById(123123, false))).toEqual(modUser) expect(await repo.getByUsername('some_user')).toEqual(null) expect(fixPeerInfo(await repo.getByUsername('some_user2'))).toEqual(modUser) }) + + it('should not return min peers by default', async () => { + await repo.deleteAll() + await repo.store(stupPeerMinUser) + await driver.save?.() + + expect(await repo.getById(123123, false)).toEqual(null) + expect(await repo.getByUsername('some_user')).toEqual(null) + expect(await repo.getByPhone('78005553535')).toEqual(null) + + expect(fixPeerInfo(await repo.getById(123123, true))).toEqual(stupPeerMinUser) + }) }) } diff --git a/packages/web/src/idb/repository/peers.ts b/packages/web/src/idb/repository/peers.ts index 71294744..07922247 100644 --- a/packages/web/src/idb/repository/peers.ts +++ b/packages/web/src/idb/repository/peers.ts @@ -22,10 +22,14 @@ export class IdbPeersRepository implements IPeersRepository { return this._driver.db.transaction(TABLE, mode).objectStore(TABLE) } - async getById(id: number): Promise { + async getById(id: number, allowMin: boolean): Promise { const it = await reqToPromise(this.os().get(id) as IDBRequest) - return it ?? null + if (!it) return null + // NB: older objects might not have isMin field + if (it.isMin && !allowMin) return null + + return it } async getByUsername(username: string): Promise { @@ -33,13 +37,19 @@ export class IdbPeersRepository implements IPeersRepository { this.os().index('by_username').get(username) as IDBRequest, ) - return it ?? null + // NB: older objects might not have isMin field + if (!it || it.isMin) return null + + return it } async getByPhone(phone: string): Promise { const it = await reqToPromise(this.os().index('by_phone').get(phone) as IDBRequest) - return it ?? null + // NB: older objects might not have isMin field + if (!it || it.isMin) return null + + return it } deleteAll(): Promise {