feat(client): support file:* for simpler file uploads by path

holy shit code for handling file is getting more and more ridiculous. i wonder if i could refactor it somehow...
This commit is contained in:
teidesu 2021-04-30 22:44:17 +03:00
parent f6d229f250
commit 0eb0ac91eb
5 changed files with 146 additions and 43 deletions

View file

@ -34,20 +34,25 @@ export async function setChatPhoto(
if (!(chat._ === 'inputPeerChat' || chat._ === 'inputPeerChannel')) if (!(chat._ === 'inputPeerChat' || chat._ === 'inputPeerChannel'))
throw new MtCuteInvalidPeerTypeError(chatId, 'chat or channel') 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 (tdFileId.isFileIdLike(media)) {
if (typeof media === 'string' && media.match(/^https?:\/\//)) if (typeof media === 'string' && media.match(/^https?:\/\//))
throw new MtCuteArgumentError("Chat photo can't be external") throw new MtCuteArgumentError("Chat photo can't be external")
if (typeof media === 'string' && media.match(/^file:/)) {
const uploaded = await this.uploadFile({
file: media.substr(5),
})
inputFile = uploaded.inputFile
} else {
const input = fileIdToInputPhoto(media) const input = fileIdToInputPhoto(media)
photo = { photo = {
_: 'inputChatPhoto', _: 'inputChatPhoto',
id: input id: input
} }
} else { }
let inputFile: tl.TypeInputFile } else if (typeof media === 'object' && tl.isAnyInputMedia(media)) {
if (typeof media === 'object' && tl.isAnyInputMedia(media)) {
throw new MtCuteArgumentError("Chat photo can't be InputMedia") throw new MtCuteArgumentError("Chat photo can't be InputMedia")
} else if (isUploadedFile(media)) { } else if (isUploadedFile(media)) {
inputFile = media.inputFile inputFile = media.inputFile
@ -60,9 +65,10 @@ export async function setChatPhoto(
inputFile = uploaded.inputFile inputFile = uploaded.inputFile
} }
if (!photo) {
photo = { photo = {
_: 'inputChatUploadedPhoto', _: 'inputChatUploadedPhoto',
[type === 'photo' ? 'file' : 'video']: inputFile, [type === 'photo' ? 'file' : 'video']: inputFile!,
videoStartTs: previewSec videoStartTs: previewSec
} }
} }

View file

@ -6,7 +6,7 @@ import {
isUploadedFile, isUploadedFile,
Message, Message,
MtCuteArgumentError, MtCuteArgumentError,
ReplyMarkup, ReplyMarkup, UploadFileLike,
} from '../../types' } from '../../types'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { extractFileName } from '../../utils/file-utils' import { extractFileName } from '../../utils/file-utils'
@ -88,7 +88,7 @@ export async function sendMedia(
if (typeof media === 'string') { if (typeof media === 'string') {
media = { media = {
type: 'auto', type: 'auto',
file: media file: media,
} }
} }
@ -102,10 +102,29 @@ export async function sendMedia(
let inputMedia: tl.TypeInputMedia | null = null 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 inputFile: tl.TypeInputFile | undefined = undefined
let thumb: tl.TypeInputFile | undefined = undefined let thumb: tl.TypeInputFile | undefined = undefined
let mime = 'application/octet-stream' let mime = 'application/octet-stream'
const upload = async (media: InputMediaLike, file: UploadFileLike): Promise<void> => {
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 const input = media.file
if (tdFileId.isFileIdLike(input)) { if (tdFileId.isFileIdLike(input)) {
if (typeof input === 'string' && input.match(/^https?:\/\//)) { if (typeof input === 'string' && input.match(/^https?:\/\//)) {
@ -113,6 +132,8 @@ export async function sendMedia(
_: 'inputMediaDocumentExternal', _: 'inputMediaDocumentExternal',
url: input, url: input,
} }
} else if (typeof input === 'string' && input.match(/^file:/)) {
await upload(media, input.substr(5))
} else { } else {
const parsed = const parsed =
typeof input === 'string' ? parseFileId(input) : input typeof input === 'string' ? parseFileId(input) : input
@ -145,13 +166,7 @@ export async function sendMedia(
} else if (typeof input === 'object' && tl.isAnyInputFile(input)) { } else if (typeof input === 'object' && tl.isAnyInputFile(input)) {
inputFile = input inputFile = input
} else { } else {
const uploaded = await this.uploadFile({ await upload(media, input)
file: input,
fileName: media.fileName,
progressCallback: params.progressCallback,
})
inputFile = uploaded.inputFile
mime = uploaded.mime
} }
if (!inputMedia) { if (!inputMedia) {
@ -162,7 +177,16 @@ export async function sendMedia(
if (typeof t === 'object' && tl.isAnyInputMedia(t)) { if (typeof t === 'object' && tl.isAnyInputMedia(t)) {
throw new MtCuteArgumentError("Thumbnail can't be InputMedia") throw new MtCuteArgumentError("Thumbnail can't be InputMedia")
} else if (tdFileId.isFileIdLike(t)) { } 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)) { } else if (isUploadedFile(t)) {
thumb = t.inputFile thumb = t.inputFile
} else if (typeof t === 'object' && tl.isAnyInputFile(t)) { } 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 = { inputMedia = {
_: 'inputMediaUploadedDocument', _: 'inputMediaUploadedDocument',
nosoundVideo: media.type === 'video' && media.isAnimated, nosoundVideo: media.type === 'video' && media.isAnimated,

View file

@ -5,6 +5,7 @@ import {
BotKeyboard, BotKeyboard,
ReplyMarkup, ReplyMarkup,
isUploadedFile, isUploadedFile,
UploadFileLike,
} from '../../types' } from '../../types'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -90,12 +91,31 @@ export async function sendPhoto(
* Defaults to `false` * Defaults to `false`
*/ */
clearDraft?: boolean clearDraft?: boolean
/**
* File size. Only used when uploading from streams without
* known length.
*/
fileSize?: number
} }
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
let media: tl.TypeInputMedia 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 (tdFileId.isFileIdLike(photo)) {
if (typeof photo === 'string' && photo.match(/^https?:\/\//)) { if (typeof photo === 'string' && photo.match(/^https?:\/\//)) {
media = { media = {
@ -103,11 +123,13 @@ export async function sendPhoto(
url: photo, url: photo,
ttlSeconds: params.ttlSeconds, ttlSeconds: params.ttlSeconds,
} }
} else if (typeof photo === 'string' && photo.match(/^file:/)) {
await upload(photo.substr(5))
} else { } else {
const input = fileIdToInputPhoto(photo) const input = fileIdToInputPhoto(photo)
media = { media = {
_: 'inputMediaPhoto', _: 'inputMediaPhoto',
id: input id: input,
} }
} }
} else if (typeof photo === 'object' && tl.isAnyInputMedia(photo)) { } else if (typeof photo === 'object' && tl.isAnyInputMedia(photo)) {
@ -125,15 +147,7 @@ export async function sendPhoto(
ttlSeconds: params.ttlSeconds, ttlSeconds: params.ttlSeconds,
} }
} else { } else {
const uploaded = await this.uploadFile({ await upload(photo)
file: photo,
progressCallback: params.progressCallback,
})
media = {
_: 'inputMediaUploadedPhoto',
file: uploaded.inputFile,
ttlSeconds: params.ttlSeconds,
}
} }
const [message, entities] = await this._parseEntities( const [message, entities] = await this._parseEntities(
@ -147,7 +161,7 @@ export async function sendPhoto(
const res = await this.call({ const res = await this.call({
_: 'messages.sendMedia', _: 'messages.sendMedia',
media, media: media!,
peer, peer,
silent: params.silent, silent: params.silent,
replyToMsgId: params.replyTo replyToMsgId: params.replyTo

View file

@ -35,12 +35,10 @@ export type UploadFileLike =
* - `Readable` (for NodeJS, base readable stream) * - `Readable` (for NodeJS, base readable stream)
* - {@link UploadedFile} returned from {@link TelegramClient.uploadFile} * - {@link UploadedFile} returned from {@link TelegramClient.uploadFile}
* - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects * - `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 a URL to remote files (e.g. `https://example.com/image.jpg`)
* - `string` with TDLib and Bot API compatible File ID. * - `string` with TDLib and Bot API compatible File ID.
* - `td.RawFullRemoteFileLocation` (parsed 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 = export type InputFileLike =
| UploadFileLike | UploadFileLike

View file

@ -22,13 +22,24 @@ interface BaseInputMedia {
* Override file name for the file. * Override file name for the file.
*/ */
fileName?: string 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. * Automatically detect media type based on file contents.
* *
* Only works for files that are internally documents, i.e. * 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 { export interface InputMediaAuto extends BaseInputMedia {
type: 'auto' type: 'auto'
@ -105,6 +116,31 @@ export interface InputMediaPhoto extends BaseInputMedia {
type: 'photo' 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 * A video to be sent
*/ */
@ -167,6 +203,7 @@ export type InputMediaLike =
| InputMediaPhoto | InputMediaPhoto
| InputMediaVideo | InputMediaVideo
| InputMediaAuto | InputMediaAuto
| InputMediaSticker
export namespace InputMedia { export namespace InputMedia {
type OmitTypeAndFile<T extends InputMediaLike> = Omit<T, 'type' | 'file'> type OmitTypeAndFile<T extends InputMediaLike> = Omit<T, 'type' | 'file'>
@ -256,6 +293,20 @@ export namespace InputMedia {
} }
} }
/**
* Create a sticker to be sent
*/
export function sticker(
file: InputFileLike,
params?: OmitTypeAndFile<InputMediaSticker>
): InputMediaSticker {
return {
type: 'sticker',
file,
...(params || {}),
}
}
/** /**
* Create a document to be sent, which subtype * Create a document to be sent, which subtype
* is inferred automatically by file contents. * is inferred automatically by file contents.