diff --git a/packages/core/src/highlevel/methods/files/normalize-input-media.ts b/packages/core/src/highlevel/methods/files/normalize-input-media.ts index 14ae3b3f..3a14433d 100644 --- a/packages/core/src/highlevel/methods/files/normalize-input-media.ts +++ b/packages/core/src/highlevel/methods/files/normalize-input-media.ts @@ -221,6 +221,22 @@ export async function _normalizeInputMedia( } } + if (media.type === 'paid') { + let medias: tl.TypeInputMedia[] + + if (Array.isArray(media.media)) { + medias = await Promise.all(media.media.map((m) => _normalizeInputMedia(client, m, params))) + } else { + medias = [await _normalizeInputMedia(client, media.media, params)] + } + + return { + _: 'inputMediaPaidMedia', + starsAmount: Long.isLong(media.starsAmount) ? media.starsAmount : Long.fromNumber(media.starsAmount), + extendedMedia: medias, + } + } + let inputFile: tl.TypeInputFile | undefined = undefined let thumb: tl.TypeInputFile | undefined = undefined let mime = 'application/octet-stream' diff --git a/packages/core/src/highlevel/methods/files/upload-media.ts b/packages/core/src/highlevel/methods/files/upload-media.ts index f72c2bdd..f8485640 100644 --- a/packages/core/src/highlevel/methods/files/upload-media.ts +++ b/packages/core/src/highlevel/methods/files/upload-media.ts @@ -50,6 +50,7 @@ export async function uploadMedia( case 'inputMediaInvoice': case 'inputMediaPoll': case 'inputMediaDice': + case 'inputMediaPaidMedia': throw new MtArgumentError("This media can't be uploaded") } diff --git a/packages/core/src/highlevel/methods/messages/send-media.ts b/packages/core/src/highlevel/methods/messages/send-media.ts index e1e71bec..da7254c3 100644 --- a/packages/core/src/highlevel/methods/messages/send-media.ts +++ b/packages/core/src/highlevel/methods/messages/send-media.ts @@ -70,8 +70,16 @@ export async function sendMedia( file: media, } } + const { peer, replyTo, scheduleDate, chainId, quickReplyShortcut } = await _processCommonSendParameters( + client, + chatId, + params, + ) - const inputMedia = await _normalizeInputMedia(client, media, params) + const inputMedia = await _normalizeInputMedia(client, media, { + progressCallback: params.progressCallback, + uploadPeer: peer, + }) const [message, entities] = await _normalizeInputText( client, @@ -81,11 +89,6 @@ export async function sendMedia( ) const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup) - const { peer, replyTo, scheduleDate, chainId, quickReplyShortcut } = await _processCommonSendParameters( - client, - chatId, - params, - ) const randomId = randomLong() const res = await _maybeInvokeWithBusinessConnection( diff --git a/packages/core/src/highlevel/types/media/extended-media.ts b/packages/core/src/highlevel/types/media/extended-media.ts new file mode 100644 index 00000000..b918a320 --- /dev/null +++ b/packages/core/src/highlevel/types/media/extended-media.ts @@ -0,0 +1,42 @@ +import { tl } from '@mtcute/tl' + +import { makeInspectable } from '../../utils/inspectable.js' +import { memoizeGetters } from '../../utils/memoize.js' +import { Thumbnail } from './thumbnail.js' + +export class ExtendedMediaPreview { + constructor(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 + } + + get thumbnail(): Thumbnail | null { + if (!this.raw.thumb) { + return null + } + + return new Thumbnail(this.raw, this.raw.thumb) + } + + /** + * If this is a video, the duration of the video, + * in seconds (if available, else 0) + */ + get videoDuration(): number { + return this.raw.videoDuration ?? 0 + } +} + +memoizeGetters(ExtendedMediaPreview, ['thumbnail']) +makeInspectable(ExtendedMediaPreview) diff --git a/packages/core/src/highlevel/types/media/index.ts b/packages/core/src/highlevel/types/media/index.ts index 649d40ec..87e993d2 100644 --- a/packages/core/src/highlevel/types/media/index.ts +++ b/packages/core/src/highlevel/types/media/index.ts @@ -2,10 +2,12 @@ export * from './audio.js' export * from './contact.js' export * from './dice.js' export * from './document.js' +export * from './extended-media.js' export * from './game.js' export * from './input-media/index.js' export * from './invoice.js' export * from './location.js' +export * from './paid-media.js' export * from './photo.js' export * from './poll.js' export * from './sticker.js' diff --git a/packages/core/src/highlevel/types/media/input-media/factories.ts b/packages/core/src/highlevel/types/media/input-media/factories.ts index 53355acd..8129b220 100644 --- a/packages/core/src/highlevel/types/media/input-media/factories.ts +++ b/packages/core/src/highlevel/types/media/input-media/factories.ts @@ -13,6 +13,7 @@ import { InputMediaGeoLive, InputMediaInvoice, InputMediaLike, + InputMediaPaidMedia, InputMediaPhoto, InputMediaPoll, InputMediaQuiz, @@ -280,6 +281,13 @@ export function webpage(url: string, params: OmitTypeAndFile): InputMediaPaidMedia { + const ret = params as tl.Mutable + ret.type = 'paid' + + return ret +} + /** * Create a document to be sent, which subtype * is inferred automatically by file contents. diff --git a/packages/core/src/highlevel/types/media/input-media/types.ts b/packages/core/src/highlevel/types/media/input-media/types.ts index ebb474c5..b5e22c9b 100644 --- a/packages/core/src/highlevel/types/media/input-media/types.ts +++ b/packages/core/src/highlevel/types/media/input-media/types.ts @@ -578,6 +578,20 @@ export interface InputMediaWebpage extends CaptionMixin { url: string } +export interface InputMediaPaidMedia extends CaptionMixin { + type: 'paid' + + /** + * Media to be sent + */ + media: MaybeArray + + /** + * Amount of stars that should be paid for the media + */ + starsAmount: number | tl.Long +} + /** * Input media that can be sent somewhere. * @@ -606,4 +620,5 @@ export type InputMediaLike = | InputMediaQuiz | InputMediaStory | InputMediaWebpage + | InputMediaPaidMedia | tl.TypeInputMedia diff --git a/packages/core/src/highlevel/types/media/invoice.ts b/packages/core/src/highlevel/types/media/invoice.ts index 92e3623d..398dca90 100644 --- a/packages/core/src/highlevel/types/media/invoice.ts +++ b/packages/core/src/highlevel/types/media/invoice.ts @@ -5,7 +5,7 @@ import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { WebDocument } from '../files/web-document.js' import type { MessageMedia } from '../messages/message-media.js' -import { Thumbnail } from './thumbnail.js' +import { ExtendedMediaPreview } from './extended-media.js' /** * Information about invoice's extended media. @@ -15,43 +15,6 @@ import { Thumbnail } from './thumbnail.js' */ export type InvoiceExtendedMediaState = 'none' | 'preview' | 'full' -export class InvoiceExtendedMediaPreview { - constructor(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 - } - - get thumbnail(): Thumbnail | null { - if (!this.raw.thumb) { - return null - } - - return new Thumbnail(this.raw, this.raw.thumb) - } - - /** - * If this is a video, the duration of the video, - * in seconds (if available, else 0) - */ - get videoDuration(): number { - return this.raw.videoDuration ?? 0 - } -} - -memoizeGetters(InvoiceExtendedMediaPreview, ['thumbnail']) -makeInspectable(InvoiceExtendedMediaPreview) - /** * An invoice */ @@ -152,12 +115,12 @@ export class Invoice { * Only available if {@link extendedMediaState} is `preview`. * Otherwise, throws an error. */ - get extendedMediaPreview(): InvoiceExtendedMediaPreview { + get extendedMediaPreview(): ExtendedMediaPreview { if (this.raw.extendedMedia?._ !== 'messageExtendedMediaPreview') { throw new MtArgumentError('No extended media preview available') } - return new InvoiceExtendedMediaPreview(this.raw.extendedMedia) + return new ExtendedMediaPreview(this.raw.extendedMedia) } /** diff --git a/packages/core/src/highlevel/types/media/paid-media.ts b/packages/core/src/highlevel/types/media/paid-media.ts new file mode 100644 index 00000000..3b82752b --- /dev/null +++ b/packages/core/src/highlevel/types/media/paid-media.ts @@ -0,0 +1,73 @@ +import { tl } from '@mtcute/tl' + +import { makeInspectable } from '../../utils/inspectable.js' +import { memoizeGetters } from '../../utils/memoize.js' +import { MessageMedia } from '../messages/message-media.js' +import { ExtendedMediaPreview } from './extended-media.js' + +export class PaidMedia { + readonly type = 'paid' as const + + constructor( + readonly raw: tl.RawMessageMediaPaidMedia, + private readonly _extendedMedia: MessageMedia[], + ) {} + + /** Whether this media was paid for */ + get isPaid(): boolean { + return this._extendedMedia !== undefined + } + + /** Price of the media (in Telegram Stars) */ + get price(): tl.Long { + return this.raw.starsAmount + } + + /** + * Get the available media previews. + * + * If the media is already paid for, this will return an empty array. + */ + get previews(): ExtendedMediaPreview[] { + const res: ExtendedMediaPreview[] = [] + + this.raw.extendedMedia.forEach((m) => { + if (m._ !== 'messageExtendedMediaPreview') return + + res.push(new ExtendedMediaPreview(m)) + }) + + return res + } + + /** + * Get the available full media. + * + * If the media is not paid for, this will return an empty array. + */ + get medias(): MessageMedia[] { + return this._extendedMedia + } + + /** + * Input media TL object generated from this object, + * to be used inside {@link InputMediaLike} and + * {@link TelegramClient.sendMedia}. + * + * Will throw if the media is not paid for. + */ + get inputMedia(): tl.TypeInputMedia { + if (!this.isPaid) { + throw new Error('Cannot get input media for non-paid media') + } + + return { + _: 'inputMediaPaidMedia', + starsAmount: this.raw.starsAmount, + extendedMedia: this._extendedMedia.map((m) => m!.inputMedia), + } + } +} + +makeInspectable(PaidMedia, undefined, ['inputMedia']) +memoizeGetters(PaidMedia, ['previews', 'inputMedia']) diff --git a/packages/core/src/highlevel/types/messages/message-media.ts b/packages/core/src/highlevel/types/messages/message-media.ts index 17ee96a2..046a3939 100644 --- a/packages/core/src/highlevel/types/messages/message-media.ts +++ b/packages/core/src/highlevel/types/messages/message-media.ts @@ -9,6 +9,7 @@ import { parseDocument } from '../media/document-utils.js' import { Game } from '../media/game.js' import { Invoice } from '../media/invoice.js' import { LiveLocation, Location } from '../media/location.js' +import { PaidMedia } from '../media/paid-media.js' import { Photo } from '../media/photo.js' import { Poll } from '../media/poll.js' import { Sticker } from '../media/sticker.js' @@ -37,6 +38,7 @@ export type MessageMedia = | Poll | Invoice | MediaStory + | PaidMedia | null export type MessageMediaType = Exclude['type'] @@ -96,6 +98,17 @@ export function _messageMediaFromTl(peers: PeersIndex | null, m: tl.TypeMessageM return new MediaStory(m, peers) } + case 'messageMediaPaidMedia': { + const extended: MessageMedia[] = [] + + m.extendedMedia.forEach((e) => { + if (e._ !== 'messageExtendedMedia') return + + extended.push(_messageMediaFromTl(peers, e.media)) + }) + + return new PaidMedia(m, extended) + } default: return null }