diff --git a/packages/client/src/methods/files/normalize-input-media.ts b/packages/client/src/methods/files/normalize-input-media.ts index 2a8f4b9d..94f2c9e4 100644 --- a/packages/client/src/methods/files/normalize-input-media.ts +++ b/packages/client/src/methods/files/normalize-input-media.ts @@ -132,6 +132,9 @@ export async function _normalizeInputMedia( data: JSON.stringify(media.providerData), }, startParam: media.startParam, + extendedMedia: media.extendedMedia + ? await this._normalizeInputMedia(media.extendedMedia, params) + : undefined, } } diff --git a/packages/client/src/types/media/input-media.ts b/packages/client/src/types/media/input-media.ts index 6f0fe881..6baed9e8 100644 --- a/packages/client/src/types/media/input-media.ts +++ b/packages/client/src/types/media/input-media.ts @@ -435,6 +435,11 @@ export interface InputMediaInvoice extends CaptionMixin { * Can be a URL, or a TL object with input web document */ photo?: string | tl.TypeInputWebDocument + + /** + * Extended media (i.e. media that will be available once the invoice is paid) + */ + extendedMedia?: InputMediaLike } /** diff --git a/packages/client/src/types/media/invoice.ts b/packages/client/src/types/media/invoice.ts index 3e933cda..f7b19d2b 100644 --- a/packages/client/src/types/media/invoice.ts +++ b/packages/client/src/types/media/invoice.ts @@ -3,7 +3,63 @@ import { tl } from '@mtcute/tl' import { TelegramClient } from '../../client' import { makeInspectable } from '../utils' import { WebDocument } from '../files/web-document' -import { MtArgumentError } from '../errors' +import { MtArgumentError, MtTypeAssertionError } from '../errors' +import { _messageMediaFromTl, MessageMedia } from '../messages' +import { Thumbnail } from './thumbnail' + +/** + * Information about invoice's extended media. + * - `none`: there is no extended media in this invoice + * - `preview`: there is only a preview of this invoice's media ({@link Invoice.extendedMediaPreview}) + * - `full`: there is a full version of this invoice's media available ({@link Invoice.extendedMedia}) + */ +export type InvoiceExtendedMediaState = 'none' | 'preview' | 'full' + +export class InvoiceExtendedMediaPreview { + constructor( + public readonly client: TelegramClient, + public readonly raw: tl.RawMessageExtendedMediaPreview + ) {} + + /** + * Width of the preview, in pixels (if available, else 0) + */ + get width(): number { + return this.raw.w ?? 0 + } + + /** + * Height of the preview, in pixels (if available, else 0) + */ + get height(): number { + return this.raw.h ?? 0 + } + + private _thumbnail?: Thumbnail + get thumbnail(): Thumbnail | null { + if (!this.raw.thumb) { + return null + } + + if (!this._thumbnail) { + this._thumbnail = new Thumbnail( + this.client, + this.raw, + this.raw.thumb + ) + } + + return this._thumbnail + } + + /** + * If this is a video, the duration of the video, + * in seconds (if available, else 0) + */ + get videoDuration(): number { + return this.raw.videoDuration ?? 0 + } +} /** * An invoice @@ -92,6 +148,58 @@ export class Invoice { return this.raw.startParam } + /** + * If this invoice has extended media + */ + get extendedMediaState(): InvoiceExtendedMediaState { + if (!this.raw.extendedMedia) return 'none' + if (this.raw.extendedMedia._ === 'messageExtendedMediaPreview') + return 'preview' + return 'full' + } + + private _extendedMediaPreview?: InvoiceExtendedMediaPreview + /** + * Get the invoice's extended media preview. + * Only available if {@link extendedMediaState} is `preview`. + * Otherwise, throws an error. + */ + get extendedMediaPreview(): InvoiceExtendedMediaPreview { + if (this.raw.extendedMedia?._ !== 'messageExtendedMediaPreview') + throw new MtArgumentError('No extended media preview available') + + if (!this._extendedMediaPreview) { + this._extendedMediaPreview = new InvoiceExtendedMediaPreview( + this.client, + this.raw.extendedMedia + ) + } + + return this._extendedMediaPreview + } + + private _extendedMedia?: MessageMedia + /** + * Get the invoice's extended media. + * Only available if {@link extendedMediaState} is `full`. + * Otherwise, throws an error. + */ + get extendedMedia(): MessageMedia { + if (this.raw.extendedMedia?._ !== "messageExtendedMedia") { + throw new MtArgumentError('No extended media available') + } + + if (!this._extendedMedia) { + this._extendedMedia = _messageMediaFromTl( + this.client, + null, + this.raw.extendedMedia.media + ) + } + + return this._extendedMedia + } + /** * Input media TL object generated from this object, * to be used inside {@link InputMediaLike} and @@ -108,3 +216,4 @@ export class Invoice { } makeInspectable(Invoice, undefined, ['inputMedia']) +makeInspectable(InvoiceExtendedMediaPreview) diff --git a/packages/client/src/types/media/thumbnail.ts b/packages/client/src/types/media/thumbnail.ts index 62a7a405..9166659c 100644 --- a/packages/client/src/types/media/thumbnail.ts +++ b/packages/client/src/types/media/thumbnail.ts @@ -54,11 +54,19 @@ export class Thumbnail extends FileLocation { readonly height: number private _path?: string - private _media: tl.RawPhoto | tl.RawDocument | tl.RawStickerSet + private _media: + | tl.RawPhoto + | tl.RawDocument + | tl.RawStickerSet + | tl.RawMessageExtendedMediaPreview constructor( client: TelegramClient, - media: tl.RawPhoto | tl.RawDocument | tl.RawStickerSet, + media: + | tl.RawPhoto + | tl.RawDocument + | tl.RawStickerSet + | tl.RawMessageExtendedMediaPreview, sz: tl.TypePhotoSize | tl.TypeVideoSize ) { switch (sz._) { @@ -99,6 +107,13 @@ export class Thumbnail extends FileLocation { }, thumbVersion: media.thumbVersion!, } + } else if (media._ === 'messageExtendedMediaPreview') { + // according to tdlib and tdesktop sources, sz can only be photoStrippedSize + throw new MtTypeAssertionError( + 'messageExtendedMediaPreview#thumb', + 'photoStrippedSize', + sz._ + ) } else { location = { _: @@ -124,7 +139,11 @@ export class Thumbnail extends FileLocation { client, location, size, - media._ === 'stickerSet' ? media.thumbDcId : media.dcId + media._ === 'stickerSet' + ? media.thumbDcId + : media._ === 'messageExtendedMediaPreview' + ? 0 + : media.dcId ) this.raw = sz this.width = width @@ -176,7 +195,8 @@ export class Thumbnail extends FileLocation { if ( this.raw._ !== 'photoSize' && this.raw._ !== 'photoSizeProgressive' && - this.raw._ !== 'videoSize' + this.raw._ !== 'videoSize' || + this._media._ === 'messageExtendedMediaPreview' // just for type safety ) { throw new MtArgumentError( `Cannot generate a file ID for "${this.raw.type}"` @@ -238,7 +258,8 @@ export class Thumbnail extends FileLocation { if ( this.raw._ !== 'photoSize' && this.raw._ !== 'photoSizeProgressive' && - this.raw._ !== 'videoSize' + this.raw._ !== 'videoSize' || + this._media._ === 'messageExtendedMediaPreview' // just for type safety ) { throw new MtArgumentError( `Cannot generate a unique file ID for "${this.raw.type}"` diff --git a/packages/client/src/types/messages/message-media.ts b/packages/client/src/types/messages/message-media.ts index 0a1e06b2..0ebed8ac 100644 --- a/packages/client/src/types/messages/message-media.ts +++ b/packages/client/src/types/messages/message-media.ts @@ -19,6 +19,9 @@ import { } from '../media' import { parseDocument } from '../media/document-utils' import { Message } from './message' +import { TelegramClient } from '../../client' +import { PeersIndex } from '../peers' +import { MtTypeAssertionError } from '../errors' /** A media inside of a {@link Message} */ export type MessageMedia = @@ -38,41 +41,53 @@ export type MessageMedia = | Poll | Invoice | null + // todo: successful_payment, connected_website /** @internal */ export function _messageMediaFromTl( - this: Message, + client: TelegramClient, + peers: PeersIndex | null, m: tl.TypeMessageMedia ): MessageMedia { switch (m._) { case 'messageMediaPhoto': if (!(m.photo?._ === 'photo')) return null - return new Photo(this.client, m.photo) + return new Photo(client, m.photo) case 'messageMediaDice': return new Dice(m) case 'messageMediaContact': return new Contact(m) case 'messageMediaDocument': if (!(m.document?._ === 'document')) return null - return parseDocument(this.client, m.document) as MessageMedia + return parseDocument(client, m.document) as MessageMedia case 'messageMediaGeo': if (!(m.geo._ === 'geoPoint')) return null - return new Location(this.client, m.geo) + return new Location(client, m.geo) case 'messageMediaGeoLive': if (!(m.geo._ === 'geoPoint')) return null - return new LiveLocation(this.client, m) + return new LiveLocation(client, m) case 'messageMediaGame': - return new Game(this.client, m.game) + return new Game(client, m.game) case 'messageMediaWebPage': if (!(m.webpage._ === 'webPage')) return null - return new WebPage(this.client, m.webpage) + return new WebPage(client, m.webpage) case 'messageMediaVenue': - return new Venue(this.client, m) + return new Venue(client, m) case 'messageMediaPoll': - return new Poll(this.client, m.poll, this._peers, m.results) + if (!peers) { + // should only be possible in extended media + // (and afaik polls can't be there) + throw new MtTypeAssertionError( + "can't create poll without peers index", + 'PeersIndex', + 'null' + ) + } + + return new Poll(client, m.poll, peers, m.results) case 'messageMediaInvoice': - return new Invoice(this.client, m) + return new Invoice(client, m) default: return null } diff --git a/packages/client/src/types/messages/message.ts b/packages/client/src/types/messages/message.ts index 76a26396..7db370f6 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/client/src/types/messages/message.ts @@ -431,7 +431,7 @@ export class Message { ) { this._media = null } else { - this._media = _messageMediaFromTl.call(this, this.raw.media) + this._media = _messageMediaFromTl(this.client, this._peers, this.raw.media) } }