fix(core)!: improved min chats handling
breaking: - `IPeersRepository.PeerInfo` has new field `isMin` - `getById` has a new argument `allowMin` describing whether it's allowed to return peers where `.isMin == true` - `getByUsername`, `getByPhone` changed logic: they should *never* return peers where `.isMin == true`
This commit is contained in:
parent
c92292249b
commit
4952d33261
8 changed files with 116 additions and 29 deletions
|
@ -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
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void>
|
||||
/** Find a peer by their `id` */
|
||||
getById: (id: number) => MaybePromise<IPeersRepository.PeerInfo | null>
|
||||
/** 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<IPeersRepository.PeerInfo | null>
|
||||
/**
|
||||
* Find a peer by their username (where `usernames` includes `username`).
|
||||
* Should never return "min" peers
|
||||
*/
|
||||
getByUsername: (username: string) => MaybePromise<IPeersRepository.PeerInfo | null>
|
||||
/** Find a peer by their `phone` */
|
||||
/**
|
||||
* Find a peer by their `phone`.
|
||||
* Should never return "min" peers
|
||||
*/
|
||||
getByPhone: (phone: string) => MaybePromise<IPeersRepository.PeerInfo | null>
|
||||
|
||||
deleteAll: () => MaybePromise<void>
|
||||
|
|
|
@ -64,10 +64,12 @@ export class PeersService extends BaseService {
|
|||
|
||||
async updatePeersFrom(obj: tl.TlObject | tl.TlObject[]): Promise<boolean> {
|
||||
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<typeof peer, { min?: unknown }>).min) continue
|
||||
if ((peer as Extract<typeof peer, { min?: unknown }>).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<tl.TypeUser | tl.TypeChat | null> {
|
||||
async getCompleteById(id: number, allowMin = false): Promise<tl.TypeUser | tl.TypeChat | null> {
|
||||
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 = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,10 +22,14 @@ export class IdbPeersRepository implements IPeersRepository {
|
|||
return this._driver.db.transaction(TABLE, mode).objectStore(TABLE)
|
||||
}
|
||||
|
||||
async getById(id: number): Promise<IPeersRepository.PeerInfo | null> {
|
||||
async getById(id: number, allowMin: boolean): Promise<IPeersRepository.PeerInfo | null> {
|
||||
const it = await reqToPromise(this.os().get(id) as IDBRequest<IPeersRepository.PeerInfo>)
|
||||
|
||||
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<IPeersRepository.PeerInfo | null> {
|
||||
|
@ -33,13 +37,19 @@ export class IdbPeersRepository implements IPeersRepository {
|
|||
this.os().index('by_username').get(username) as IDBRequest<IPeersRepository.PeerInfo>,
|
||||
)
|
||||
|
||||
return it ?? null
|
||||
// NB: older objects might not have isMin field
|
||||
if (!it || it.isMin) return null
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
async getByPhone(phone: string): Promise<IPeersRepository.PeerInfo | null> {
|
||||
const it = await reqToPromise(this.os().index('by_phone').get(phone) as IDBRequest<IPeersRepository.PeerInfo>)
|
||||
|
||||
return it ?? null
|
||||
// NB: older objects might not have isMin field
|
||||
if (!it || it.isMin) return null
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
deleteAll(): Promise<void> {
|
||||
|
|
Loading…
Reference in a new issue