diff --git a/packages/client/src/methods/chats/set-chat-photo.ts b/packages/client/src/methods/chats/set-chat-photo.ts index 290448bd..d20f87be 100644 --- a/packages/client/src/methods/chats/set-chat-photo.ts +++ b/packages/client/src/methods/chats/set-chat-photo.ts @@ -34,35 +34,41 @@ export async function setChatPhoto( if (!(chat._ === 'inputPeerChat' || chat._ === 'inputPeerChannel')) throw new MtCuteInvalidPeerTypeError(chatId, 'chat or channel') - let photo: tl.TypeInputChatPhoto + let photo: tl.TypeInputChatPhoto | undefined = undefined + let inputFile: tl.TypeInputFile if (tdFileId.isFileIdLike(media)) { if (typeof media === 'string' && media.match(/^https?:\/\//)) throw new MtCuteArgumentError("Chat photo can't be external") - - const input = fileIdToInputPhoto(media) - photo = { - _: 'inputChatPhoto', - id: input - } - } else { - let inputFile: tl.TypeInputFile - if (typeof media === 'object' && tl.isAnyInputMedia(media)) { - throw new MtCuteArgumentError("Chat photo can't be InputMedia") - } else if (isUploadedFile(media)) { - inputFile = media.inputFile - } else if (typeof media === 'object' && tl.isAnyInputFile(media)) { - inputFile = media - } else { + if (typeof media === 'string' && media.match(/^file:/)) { const uploaded = await this.uploadFile({ - file: media, + file: media.substr(5), }) inputFile = uploaded.inputFile + } else { + const input = fileIdToInputPhoto(media) + photo = { + _: 'inputChatPhoto', + id: input + } } + } else if (typeof media === 'object' && tl.isAnyInputMedia(media)) { + throw new MtCuteArgumentError("Chat photo can't be InputMedia") + } else if (isUploadedFile(media)) { + inputFile = media.inputFile + } else if (typeof media === 'object' && tl.isAnyInputFile(media)) { + inputFile = media + } else { + const uploaded = await this.uploadFile({ + file: media, + }) + inputFile = uploaded.inputFile + } + if (!photo) { photo = { _: 'inputChatUploadedPhoto', - [type === 'photo' ? 'file' : 'video']: inputFile, + [type === 'photo' ? 'file' : 'video']: inputFile!, videoStartTs: previewSec } } diff --git a/packages/client/src/methods/messages/send-media.ts b/packages/client/src/methods/messages/send-media.ts index 2c2ddaf7..e139adfa 100644 --- a/packages/client/src/methods/messages/send-media.ts +++ b/packages/client/src/methods/messages/send-media.ts @@ -6,7 +6,7 @@ import { isUploadedFile, Message, MtCuteArgumentError, - ReplyMarkup, + ReplyMarkup, UploadFileLike, } from '../../types' import { tl } from '@mtcute/tl' import { extractFileName } from '../../utils/file-utils' @@ -88,7 +88,7 @@ export async function sendMedia( if (typeof media === 'string') { media = { type: 'auto', - file: media + file: media, } } @@ -102,10 +102,29 @@ export async function sendMedia( let inputMedia: tl.TypeInputMedia | null = null + // my condolences to those poor souls who are going to maintain this (myself included) + let inputFile: tl.TypeInputFile | undefined = undefined let thumb: tl.TypeInputFile | undefined = undefined let mime = 'application/octet-stream' + const upload = async (media: InputMediaLike, file: UploadFileLike): Promise => { + const uploaded = await this.uploadFile({ + file, + fileName: media.fileName, + progressCallback: params!.progressCallback, + fileMime: + media.type === 'sticker' + ? media.isAnimated + ? 'application/x-tgsticker' + : 'image/webp' + : media.mime, + fileSize: media.fileSize + }) + inputFile = uploaded.inputFile + mime = uploaded.mime + } + const input = media.file if (tdFileId.isFileIdLike(input)) { if (typeof input === 'string' && input.match(/^https?:\/\//)) { @@ -113,6 +132,8 @@ export async function sendMedia( _: 'inputMediaDocumentExternal', url: input, } + } else if (typeof input === 'string' && input.match(/^file:/)) { + await upload(media, input.substr(5)) } else { const parsed = typeof input === 'string' ? parseFileId(input) : input @@ -145,13 +166,7 @@ export async function sendMedia( } else if (typeof input === 'object' && tl.isAnyInputFile(input)) { inputFile = input } else { - const uploaded = await this.uploadFile({ - file: input, - fileName: media.fileName, - progressCallback: params.progressCallback, - }) - inputFile = uploaded.inputFile - mime = uploaded.mime + await upload(media, input) } if (!inputMedia) { @@ -162,7 +177,16 @@ export async function sendMedia( if (typeof t === 'object' && tl.isAnyInputMedia(t)) { throw new MtCuteArgumentError("Thumbnail can't be InputMedia") } else if (tdFileId.isFileIdLike(t)) { - throw new MtCuteArgumentError("Thumbnail can't be a URL or a File ID") + if (typeof t === 'string' && t.match(/^file:/)) { + const uploaded = await this.uploadFile({ + file: t.substr(5), + }) + thumb = uploaded.inputFile + } else { + throw new MtCuteArgumentError( + "Thumbnail can't be a URL or a File ID" + ) + } } else if (isUploadedFile(t)) { thumb = t.inputFile } else if (typeof t === 'object' && tl.isAnyInputFile(t)) { @@ -212,6 +236,16 @@ export async function sendMedia( }) } + if (media.type === 'sticker') { + attributes.push({ + _: 'documentAttributeSticker', + stickerset: { + _: 'inputStickerSetEmpty', + }, + alt: media.alt ?? '', + }) + } + inputMedia = { _: 'inputMediaUploadedDocument', nosoundVideo: media.type === 'video' && media.isAnimated, diff --git a/packages/client/src/methods/messages/send-photo.ts b/packages/client/src/methods/messages/send-photo.ts index 585a80cd..51948a12 100644 --- a/packages/client/src/methods/messages/send-photo.ts +++ b/packages/client/src/methods/messages/send-photo.ts @@ -5,6 +5,7 @@ import { BotKeyboard, ReplyMarkup, isUploadedFile, + UploadFileLike, } from '../../types' import { tl } from '@mtcute/tl' import { TelegramClient } from '../../client' @@ -90,12 +91,31 @@ export async function sendPhoto( * Defaults to `false` */ clearDraft?: boolean + + /** + * File size. Only used when uploading from streams without + * known length. + */ + fileSize?: number } ): Promise { if (!params) params = {} let media: tl.TypeInputMedia + const upload = async (photo: UploadFileLike) => { + const uploaded = await this.uploadFile({ + file: photo, + progressCallback: params!.progressCallback, + fileSize: params!.fileSize, + }) + media = { + _: 'inputMediaUploadedPhoto', + file: uploaded.inputFile, + ttlSeconds: params!.ttlSeconds, + } + } + if (tdFileId.isFileIdLike(photo)) { if (typeof photo === 'string' && photo.match(/^https?:\/\//)) { media = { @@ -103,11 +123,13 @@ export async function sendPhoto( url: photo, ttlSeconds: params.ttlSeconds, } + } else if (typeof photo === 'string' && photo.match(/^file:/)) { + await upload(photo.substr(5)) } else { const input = fileIdToInputPhoto(photo) media = { _: 'inputMediaPhoto', - id: input + id: input, } } } else if (typeof photo === 'object' && tl.isAnyInputMedia(photo)) { @@ -125,15 +147,7 @@ export async function sendPhoto( ttlSeconds: params.ttlSeconds, } } else { - const uploaded = await this.uploadFile({ - file: photo, - progressCallback: params.progressCallback, - }) - media = { - _: 'inputMediaUploadedPhoto', - file: uploaded.inputFile, - ttlSeconds: params.ttlSeconds, - } + await upload(photo) } const [message, entities] = await this._parseEntities( @@ -147,7 +161,7 @@ export async function sendPhoto( const res = await this.call({ _: 'messages.sendMedia', - media, + media: media!, peer, silent: params.silent, replyToMsgId: params.replyTo diff --git a/packages/client/src/types/files/utils.ts b/packages/client/src/types/files/utils.ts index 734cb26c..dd4bca38 100644 --- a/packages/client/src/types/files/utils.ts +++ b/packages/client/src/types/files/utils.ts @@ -35,12 +35,10 @@ export type UploadFileLike = * - `Readable` (for NodeJS, base readable stream) * - {@link UploadedFile} returned from {@link TelegramClient.uploadFile} * - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects + * - `string` with a path to a local file prepended with `file:` (NodeJS only) (e.g. `file:image.jpg`) * - `string` with a URL to remote files (e.g. `https://example.com/image.jpg`) * - `string` with TDLib and Bot API compatible File ID. * - `td.RawFullRemoteFileLocation` (parsed File ID) - * - * > **Note**: Unlike {@link UploadFileLike}, you can't pass - * > a file path directly. Use `fs.createReadStream('/path/to/file.png')` */ export type InputFileLike = | UploadFileLike diff --git a/packages/client/src/types/media/input-media.ts b/packages/client/src/types/media/input-media.ts index fcd2dd28..da919f5a 100644 --- a/packages/client/src/types/media/input-media.ts +++ b/packages/client/src/types/media/input-media.ts @@ -22,13 +22,24 @@ interface BaseInputMedia { * Override file name for the file. */ fileName?: string + + /** + * Override MIME type for the file + */ + mime?: string + + /** + * Override file size for the file + */ + fileSize?: number } /** * Automatically detect media type based on file contents. * * Only works for files that are internally documents, i.e. - * *does not* infer photos, so use {@link InputMediaPhoto} instead. + * *does not* infer photos, so use {@link InputMediaPhoto} instead + * (except for File IDs, from which photos *are* inferred) */ export interface InputMediaAuto extends BaseInputMedia { type: 'auto' @@ -105,6 +116,31 @@ export interface InputMediaPhoto extends BaseInputMedia { type: 'photo' } +/** + * A sticker to be sent + */ +export interface InputMediaSticker extends BaseInputMedia { + type: 'sticker' + + caption?: never + entities?: never + + /** + * Whether this sticker is animated? + * + * Note that animated stickers must be in TGS + * format, which is Lottie JSON compressed using GZip + * + * Defaults to `false` + */ + isAnimated?: boolean + + /** + * An emoji representing this sticker + */ + alt?: string +} + /** * A video to be sent */ @@ -167,6 +203,7 @@ export type InputMediaLike = | InputMediaPhoto | InputMediaVideo | InputMediaAuto + | InputMediaSticker export namespace InputMedia { type OmitTypeAndFile = Omit @@ -256,6 +293,20 @@ export namespace InputMedia { } } + /** + * Create a sticker to be sent + */ + export function sticker( + file: InputFileLike, + params?: OmitTypeAndFile + ): InputMediaSticker { + return { + type: 'sticker', + file, + ...(params || {}), + } + } + /** * Create a document to be sent, which subtype * is inferred automatically by file contents.