diff --git a/packages/client/src/types/media/photo.ts b/packages/client/src/types/media/photo.ts index 73494fb4..e23d2f9d 100644 --- a/packages/client/src/types/media/photo.ts +++ b/packages/client/src/types/media/photo.ts @@ -28,6 +28,8 @@ export class Photo extends FileLocation { /** Biggest available photo height */ readonly height: number + private _bestSize?: tl.RawPhotoSize | tl.RawPhotoSizeProgressive + constructor(client: TelegramClient, raw: tl.RawPhoto) { const location = { _: 'inputPhotoFileLocation', @@ -38,6 +40,8 @@ export class Photo extends FileLocation { } as tl.Mutable let size, width, height: number + let bestSize: tl.RawPhotoSize | tl.RawPhotoSizeProgressive + const progressive = raw.sizes.find( (it) => it._ === 'photoSizeProgressive' ) as tl.RawPhotoSizeProgressive | undefined @@ -46,6 +50,8 @@ export class Photo extends FileLocation { size = Math.max(...progressive.sizes) width = progressive.w height = progressive.h + + bestSize = progressive } else { let max: tl.RawPhotoSize | null = null for (const sz of raw.sizes) { @@ -59,6 +65,8 @@ export class Photo extends FileLocation { size = max.size width = max.w height = max.h + + bestSize = max } else { // does this happen at all? throw new MtCuteArgumentError('Photo does not have any sizes') @@ -66,6 +74,7 @@ export class Photo extends FileLocation { } super(client, location, size, raw.dcId) + this._bestSize = bestSize this.raw = raw this.width = width this.height = height @@ -106,6 +115,44 @@ export class Photo extends FileLocation { return this.thumbnails.find((it) => it.raw.type === type) ?? null } + private _fileId?: string + /** + * Get TDLib and Bot API compatible File ID + * representing this photo's best thumbnail. + */ + get fileId(): string { + if (!this._fileId) { + if (!this._bestSize) { + throw new MtCuteArgumentError( + 'Cannot get File ID for this photo' + ) + } + + this._fileId = this.getThumbnail(this._bestSize.type)!.fileId + } + + return this._fileId + } + + private _uniqueFileId?: string + /** + * Get TDLib and Bot API compatible Unique File ID + * representing this photo's best thumbnail. + */ + get uniqueFileId(): string { + if (!this._uniqueFileId) { + if (!this._bestSize) { + throw new MtCuteArgumentError( + 'Cannot get File ID for this photo' + ) + } + + this._uniqueFileId = this.getThumbnail(this._bestSize.type)!.uniqueFileId + } + + return this._uniqueFileId + } + /** * Input media generated from this object, * to be used in {@link InputMediaLike} and diff --git a/packages/client/src/types/media/thumbnail.ts b/packages/client/src/types/media/thumbnail.ts index b8613ce1..06068a8e 100644 --- a/packages/client/src/types/media/thumbnail.ts +++ b/packages/client/src/types/media/thumbnail.ts @@ -168,8 +168,6 @@ export class Thumbnail extends FileLocation { _: 'photo', id: this._media.id, accessHash: this._media.accessHash, - volumeId: bigInt.zero, - localId: 0, source: { _: 'thumbnail', fileType: td.FileType.Photo, @@ -202,8 +200,13 @@ export class Thumbnail extends FileLocation { ? td.FileType.Photo : td.FileType.Thumbnail, { - _: 'common', + _: 'photo', id: this._media.id, + source: { + _: 'thumbnail', + fileType: td.FileType.Photo, + thumbnailType: this.raw.type, + }, } ) } diff --git a/packages/client/src/types/peers/chat-photo.ts b/packages/client/src/types/peers/chat-photo.ts index 92eafcfd..d55c07f9 100644 --- a/packages/client/src/types/peers/chat-photo.ts +++ b/packages/client/src/types/peers/chat-photo.ts @@ -76,8 +76,6 @@ export class ChatPhotoSize extends FileLocation { _: 'photo', id: this.obj.photoId, accessHash: bigInt.zero, - volumeId: bigInt.zero, - localId: 0, source: { _: 'dialogPhoto', big: this.big, @@ -97,12 +95,15 @@ export class ChatPhotoSize extends FileLocation { */ get uniqueFileId(): string { if (!this._uniqueFileId) { - // todo: check how tdlib handles big/small photos here this._uniqueFileId = toUniqueFileId( tdFileId.FileType.ProfilePhoto, { - _: 'common', + _: 'photo', id: this.obj.photoId, + source: { + _: 'dialogPhoto', + big: this.big + } as any } ) } diff --git a/packages/file-id/src/convert.ts b/packages/file-id/src/convert.ts index b05fe1e4..85176cff 100644 --- a/packages/file-id/src/convert.ts +++ b/packages/file-id/src/convert.ts @@ -3,13 +3,14 @@ import { tdFileId, tdFileId as td } from './types' import { parseFileId } from './parse' import { getBasicPeerType, markedPeerIdToBare } from '@mtcute/core' import FileType = tdFileId.FileType +import bigInt from 'big-integer' const EMPTY_BUFFER = Buffer.alloc(0) type FileId = td.RawFullRemoteFileLocation function dialogPhotoToInputPeer( - dialog: td.RawPhotoSizeSourceDialogPhoto + dialog: td.RawPhotoSizeSourceDialogPhoto | td.RawPhotoSizeSourceDialogPhotoLegacy ): tl.TypeInputPeer { const markedPeerId = dialog.id.toJSNumber() const peerType = getBasicPeerType(markedPeerId) @@ -78,13 +79,16 @@ export function fileIdToInputFileLocation( 'Expected legacy photo to have file reference' ) + // for some reason tdlib removed this thing altogether + // https://github.com/tdlib/td/commit/4bb76a7b6f47bf9e0d0d01a72aac579ec73557ee#diff-8cc4f7c60a8261a8cf782e7fb51b95105bfb08710a1c2b63f80a48263ae0fb9bL401 + // still leaving this, but not sure if passing 0 as volume_id and local_id is correct at all lol return { _: 'inputPhotoLegacyFileLocation', fileReference: fileId.fileReference, id: loc.id, accessHash: loc.accessHash, - volumeId: loc.volumeId, - localId: loc.localId, + volumeId: bigInt.zero, + localId: 0, secret: loc.source.secret, } case 'thumbnail': @@ -119,6 +123,9 @@ export function fileIdToInputFileLocation( photoId: loc.id, } case 'stickerSetThumbnail': + // this was also removed from tdlib: + // https://github.com/tdlib/td/commit/4bb76a7b6f47bf9e0d0d01a72aac579ec73557ee#diff-8cc4f7c60a8261a8cf782e7fb51b95105bfb08710a1c2b63f80a48263ae0fb9bR432 + // also leaving this one though. return { _: 'inputStickerSetThumb', stickerset: { @@ -126,7 +133,51 @@ export function fileIdToInputFileLocation( id: loc.source.id, accessHash: loc.source.accessHash, }, - thumbVersion: 0, // todo: check how tdlib stores this + thumbVersion: 0, + } + case 'fullLegacy': + if (!fileId.fileReference) + throw new td.InvalidFileIdError( + 'Expected legacy photo to have file reference' + ) + + return { + _: 'inputPhotoLegacyFileLocation', + fileReference: fileId.fileReference, + id: loc.id, + accessHash: loc.accessHash, + volumeId: loc.source.volumeId, + localId: loc.source.localId, + secret: loc.source.secret, + } + case 'dialogPhotoLegacy': + return { + _: 'inputPeerPhotoFileLocationLegacy', + big: loc.source.big, + peer: dialogPhotoToInputPeer(loc.source), + volumeId: loc.source.volumeId, + localId: loc.source.localId, + } + case 'stickerSetThumbnailLegacy': + return { + _: 'inputStickerSetThumbLegacy', + stickerset: { + _: 'inputStickerSetID', + id: loc.source.id, + accessHash: loc.source.accessHash, + }, + volumeId: loc.source.volumeId, + localId: loc.source.localId, + } + case 'stickerSetThumbnailVersion': + return { + _: 'inputStickerSetThumb', + stickerset: { + _: 'inputStickerSetID', + id: loc.source.id, + accessHash: loc.source.accessHash, + }, + thumbVersion: loc.source.version } } diff --git a/packages/file-id/src/parse.ts b/packages/file-id/src/parse.ts index a9fce7d0..d9c0193e 100644 --- a/packages/file-id/src/parse.ts +++ b/packages/file-id/src/parse.ts @@ -58,6 +58,59 @@ function parsePhotoSizeSource(reader: BinaryReader): td.TypePhotoSizeSource { id: reader.long(), accessHash: reader.long(), } + case 5 /* FULL_LEGACY */: { + const res: td.RawPhotoSizeSourceFullLegacy = { + _: 'fullLegacy', + volumeId: reader.long(), + secret: reader.long(), + localId: reader.int32() + } + + if (res.localId < 0) { + throw new td.InvalidFileIdError('Wrong local_id (< 0)') + } + + return res + } + case 6 /* DIALOG_PHOTO_SMALL_LEGACY */: + case 7 /* DIALOG_PHOTO_BIG_LEGACY */: { + const res: td.RawPhotoSizeSourceDialogPhotoLegacy = { + _: 'dialogPhotoLegacy', + big: variant === 7, + id: reader.long(), + accessHash: reader.long(), + volumeId: reader.long(), + localId: reader.int32() + } + + if (res.localId < 0) { + throw new td.InvalidFileIdError('Wrong local_id (< 0)') + } + + return res + } + case 8 /* STICKERSET_THUMBNAIL_LEGACY */: { + const res: td.RawPhotoSizeSourceStickerSetThumbnailLegacy = { + _: 'stickerSetThumbnailLegacy', + id: reader.long(), + accessHash: reader.long(), + volumeId: reader.long(), + localId: reader.int32() + } + + if (res.localId < 0) { + throw new td.InvalidFileIdError('Wrong local_id (< 0)') + } + + return res + } + case 9 /* STICKERSET_THUMBNAIL_VERSION */: + return { + _: 'stickerSetThumbnailVersion', + id: reader.long(), + accessHash: reader.long(), + version: reader.int32() + } default: throw new td.UnsupportedError( `Unsupported photo size source ${variant} (${reader.data.toString( @@ -72,19 +125,70 @@ function parsePhotoFileLocation( version: number ): td.RawPhotoRemoteFileLocation { // todo: check how tdlib handles volume ids + + const id = reader.long() + const accessHash = reader.long() + let source: td.TypePhotoSizeSource + + if (version >= 32) { + source = parsePhotoSizeSource(reader) + } else { + const volumeId = reader.long() + let localId = 0 + + if (version >= 22) { + source = parsePhotoSizeSource(reader) + localId = reader.int32() + } else { + source = { + _: 'fullLegacy', + secret: reader.long(), + localId: reader.int32(), + volumeId + } + } + + switch (source._) { + case 'legacy': + source = { + _: 'fullLegacy', + secret: reader.long(), + localId: reader.int32(), + volumeId + } + break + case 'fullLegacy': + case 'thumbnail': + break + case 'dialogPhoto': + source = { + _: 'dialogPhotoLegacy', + id: source.id, + accessHash: source.accessHash, + big: source.big, + localId, + volumeId + } + break + case 'stickerSetThumbnail': + source = { + _: 'stickerSetThumbnailLegacy', + id: source.id, + accessHash: source.accessHash, + localId, + volumeId + } + break + default: + throw new td.InvalidFileIdError('Invalid PhotoSizeSource in legacy PhotoRemoteFileLocation') + } + } + return { _: 'photo', - id: reader.long(), - accessHash: reader.long(), - volumeId: reader.long(), - source: - version >= 22 - ? parsePhotoSizeSource(reader) - : { - _: 'legacy', - secret: reader.long(), - }, - localId: reader.int32(), + id, + accessHash, + source } } @@ -166,6 +270,7 @@ function fromPersistentIdV23( } break case 'dialogPhoto': + case 'dialogPhotoLegacy': if (fileType !== td.FileType.ProfilePhoto) { throw new td.InvalidFileIdError( 'Invalid FileType in PhotoRemoteFileLocation DialogPhoto' @@ -173,6 +278,8 @@ function fromPersistentIdV23( } break case 'stickerSetThumbnail': + case 'stickerSetThumbnailLegacy': + case 'stickerSetThumbnailVersion': if (fileType !== td.FileType.Thumbnail) { throw new td.InvalidFileIdError( 'Invalid FileType in PhotoRemoteFileLocation StickerSetThumbnail' diff --git a/packages/file-id/src/serialize-unique.ts b/packages/file-id/src/serialize-unique.ts index dfde5de2..4b316d13 100644 --- a/packages/file-id/src/serialize-unique.ts +++ b/packages/file-id/src/serialize-unique.ts @@ -5,7 +5,7 @@ import FileType = tdFileId.FileType type InputUniqueLocation = | Pick - | Pick + | Pick | Pick /** @@ -76,12 +76,69 @@ export function toUniqueFileId( let writer: BinaryWriter switch (inputLocation._) { - case 'photo': - writer = BinaryWriter.alloc(16) - writer.int32(type) - writer.long(inputLocation.volumeId) - writer.int32(inputLocation.localId) + case 'photo': { + const source = inputLocation.source + switch (source._) { + case 'legacy': { + // tdlib does not implement this + writer = BinaryWriter.alloc(16) + writer.int32(type) + writer.int32(100) + writer.long(source.secret) + break + } + case 'stickerSetThumbnail': { + // tdlib does not implement this + writer = BinaryWriter.alloc(24) + writer.int32(type) + writer.int32(150) + writer.long(source.id) + writer.long(source.accessHash) + break + } + case 'dialogPhoto': { + writer = BinaryWriter.alloc(13) + writer.int32(type) + writer.long(inputLocation.id) + writer.raw(Buffer.from([+source.big])) + // it doesn't matter to which Dialog the photo belongs + break + } + case 'thumbnail': { + writer = BinaryWriter.alloc(13) + + let thumbType = source.thumbnailType.charCodeAt(0) + if (thumbType === 97 /* 'a' */) { + thumbType = 0 + } else if (thumbType === 99 /* 'c' */) { + thumbType = 1 + } else { + thumbType += 5 + } + + writer.int32(type) + writer.long(inputLocation.id) + writer.raw(Buffer.from([thumbType])) + break + } + case 'fullLegacy': + case 'dialogPhotoLegacy': + case 'stickerSetThumbnailLegacy': + writer = BinaryWriter.alloc(16) + writer.int32(type) + writer.long(source.volumeId) + writer.int32(source.localId) + break + case 'stickerSetThumbnailVersion': + writer = BinaryWriter.alloc(17) + writer.int32(type) + writer.raw(Buffer.from([2])) + writer.long(source.id) + writer.int32(source.version) + break + } break + } case 'web': writer = BinaryWriter.alloc( Buffer.byteLength(inputLocation.url, 'utf-8') + 8 diff --git a/packages/file-id/src/serialize.ts b/packages/file-id/src/serialize.ts index 2b37acae..8f22ab08 100644 --- a/packages/file-id/src/serialize.ts +++ b/packages/file-id/src/serialize.ts @@ -46,7 +46,6 @@ export function toFileId( // todo: check how tdlib handles volume ids writer.long(loc.id) writer.long(loc.accessHash) - writer.long(loc.volumeId) switch (loc.source._) { case 'legacy': @@ -68,9 +67,34 @@ export function toFileId( writer.long(loc.source.id) writer.long(loc.source.accessHash) break + case 'fullLegacy': + writer.int32(5) + writer.long(loc.source.volumeId) + writer.long(loc.source.secret) + writer.int32(loc.source.localId) + break + case 'dialogPhotoLegacy': + writer.int32(loc.source.big ? 7 : 6) + writer.long(loc.source.id) + writer.long(loc.source.accessHash) + writer.long(loc.source.volumeId) + writer.int32(loc.source.localId) + break + case 'stickerSetThumbnailLegacy': + writer.int32(8) + writer.long(loc.source.id) + writer.long(loc.source.accessHash) + writer.long(loc.source.volumeId) + writer.int32(loc.source.localId) + break + case 'stickerSetThumbnailVersion': + writer.int32(8) + writer.long(loc.source.id) + writer.long(loc.source.accessHash) + writer.int32(loc.source.version) + break } - writer.int32(loc.localId) break case 'common': writer.long(loc.id) diff --git a/packages/file-id/src/types.ts b/packages/file-id/src/types.ts index 819d1652..e7dca4f7 100644 --- a/packages/file-id/src/types.ts +++ b/packages/file-id/src/types.ts @@ -9,7 +9,7 @@ export namespace tdFileId { export const WEB_LOCATION_FLAG = 1 << 24 export const FILE_REFERENCE_FLAG = 1 << 25 - export const CURRENT_VERSION = 31 + export const CURRENT_VERSION = 32 /** * An error occurred while parsing or serializing a File ID @@ -114,11 +114,55 @@ export namespace tdFileId { readonly accessHash: Long } + /** + * This photo is a legacy photo containing + * volume_id, local_id and secret + */ + export interface RawPhotoSizeSourceFullLegacy { + readonly _: 'fullLegacy' + readonly volumeId: Long + readonly localId: number + readonly secret: Long + } + + /** + * This photo is a legacy dialog photo + */ + export interface RawPhotoSizeSourceDialogPhotoLegacy + extends Omit { + readonly _: 'dialogPhotoLegacy' + readonly volumeId: Long + readonly localId: number + } + + /** + * This photo is a legacy sticker set thumbnail + */ + export interface RawPhotoSizeSourceStickerSetThumbnailLegacy + extends Omit { + readonly _: 'stickerSetThumbnailLegacy' + readonly volumeId: Long + readonly localId: number + } + + /** + * This photo is a legacy sticker set identified by a version + */ + export interface RawPhotoSizeSourceStickerSetThumbnailVersion + extends Omit { + readonly _: 'stickerSetThumbnailVersion' + readonly version: number + } + export type TypePhotoSizeSource = | RawPhotoSizeSourceLegacy | RawPhotoSizeSourceThumbnail | RawPhotoSizeSourceDialogPhoto | RawPhotoSizeSourceStickerSetThumbnail + | RawPhotoSizeSourceFullLegacy + | RawPhotoSizeSourceDialogPhotoLegacy + | RawPhotoSizeSourceStickerSetThumbnailLegacy + | RawPhotoSizeSourceStickerSetThumbnailVersion /** * An external web file @@ -139,9 +183,7 @@ export namespace tdFileId { readonly _: 'photo' readonly id: Long readonly accessHash: Long - readonly volumeId: Long readonly source: TypePhotoSizeSource - readonly localId: number } /**