feat(client): sending document-like media (files, audios, videos, gifs, voices)
This commit is contained in:
parent
0901f97e0d
commit
7e4142a572
8 changed files with 549 additions and 2 deletions
|
@ -27,6 +27,7 @@ import { iterHistory } from './methods/messages/iter-history'
|
|||
import { _parseEntities } from './methods/messages/parse-entities'
|
||||
import { searchGlobal } from './methods/messages/search-global'
|
||||
import { searchMessages } from './methods/messages/search-messages'
|
||||
import { sendMedia } from './methods/messages/send-media'
|
||||
import { sendPhoto } from './methods/messages/send-photo'
|
||||
import { sendText } from './methods/messages/send-text'
|
||||
import {
|
||||
|
@ -54,6 +55,7 @@ import {
|
|||
Chat,
|
||||
FileDownloadParameters,
|
||||
InputFileLike,
|
||||
InputMediaLike,
|
||||
InputPeerLike,
|
||||
MaybeDynamic,
|
||||
Message,
|
||||
|
@ -738,6 +740,60 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
): AsyncIterableIterator<Message> {
|
||||
return searchMessages.apply(this, arguments)
|
||||
}
|
||||
/**
|
||||
* Send a single media.
|
||||
*
|
||||
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
|
||||
* @param media Media contained in the message
|
||||
* @param params Additional sending parameters
|
||||
*/
|
||||
sendMedia(
|
||||
chatId: InputPeerLike,
|
||||
media: InputMediaLike,
|
||||
params?: {
|
||||
/**
|
||||
* Message to reply to. Either a message object or message ID.
|
||||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
*
|
||||
* Passing `null` will explicitly disable formatting.
|
||||
*/
|
||||
parseMode?: string | null
|
||||
|
||||
/**
|
||||
* Whether to send this message silently.
|
||||
*/
|
||||
silent?: boolean
|
||||
|
||||
/**
|
||||
* If set, the message will be scheduled to this date.
|
||||
* When passing a number, a UNIX time in ms is expected.
|
||||
*/
|
||||
schedule?: Date | number
|
||||
|
||||
/**
|
||||
* For bots: inline or reply markup or an instruction
|
||||
* to hide a reply keyboard or to force a reply.
|
||||
*/
|
||||
replyMarkup?: ReplyMarkup
|
||||
|
||||
/**
|
||||
* Function that will be called after some part has been uploaded.
|
||||
* Only used when a file that requires uploading is passed,
|
||||
* and not used when uploading a thumbnail.
|
||||
*
|
||||
* @param uploaded Number of bytes already uploaded
|
||||
* @param total Total file size
|
||||
*/
|
||||
progressCallback?: (uploaded: number, total: number) => void
|
||||
}
|
||||
): Promise<Message> {
|
||||
return sendMedia.apply(this, arguments)
|
||||
}
|
||||
/**
|
||||
* Send a single photo
|
||||
*
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
UpdateFilter,
|
||||
Message,
|
||||
ReplyMarkup,
|
||||
InputMediaLike
|
||||
} from '../types'
|
||||
|
||||
// @copy
|
||||
|
|
|
@ -22,6 +22,11 @@ try {
|
|||
|
||||
const debug = require('debug')('mtcute:upload')
|
||||
|
||||
const OVERRIDE_MIME: Record<string, string> = {
|
||||
// tg doesn't interpret `audio/opus` files as voice messages for some reason
|
||||
'audio/opus': 'audio/ogg'
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Telegram servers, without actually
|
||||
* sending a message anywhere. Useful when an `InputFile` is required.
|
||||
|
@ -247,6 +252,8 @@ export async function uploadFile(
|
|||
}
|
||||
}
|
||||
|
||||
if (fileMime! in OVERRIDE_MIME) fileMime = OVERRIDE_MIME[fileMime!]
|
||||
|
||||
return {
|
||||
inputFile,
|
||||
size: fileSize,
|
||||
|
|
201
packages/client/src/methods/messages/send-media.ts
Normal file
201
packages/client/src/methods/messages/send-media.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import { TelegramClient } from '../../client'
|
||||
import {
|
||||
BotKeyboard,
|
||||
InputMediaLike,
|
||||
InputPeerLike,
|
||||
isUploadedFile,
|
||||
Message,
|
||||
MtCuteArgumentError,
|
||||
ReplyMarkup,
|
||||
} from '../../types'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { extractFileName } from '../../utils/file-utils'
|
||||
import { normalizeToInputPeer } from '../../utils/peer-utils'
|
||||
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
|
||||
|
||||
/**
|
||||
* Send a single media.
|
||||
*
|
||||
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
|
||||
* @param media Media contained in the message
|
||||
* @param params Additional sending parameters
|
||||
* @internal
|
||||
*/
|
||||
export async function sendMedia(
|
||||
this: TelegramClient,
|
||||
chatId: InputPeerLike,
|
||||
media: InputMediaLike,
|
||||
params?: {
|
||||
/**
|
||||
* Message to reply to. Either a message object or message ID.
|
||||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
*
|
||||
* Passing `null` will explicitly disable formatting.
|
||||
*/
|
||||
parseMode?: string | null
|
||||
|
||||
/**
|
||||
* Whether to send this message silently.
|
||||
*/
|
||||
silent?: boolean
|
||||
|
||||
/**
|
||||
* If set, the message will be scheduled to this date.
|
||||
* When passing a number, a UNIX time in ms is expected.
|
||||
*/
|
||||
schedule?: Date | number
|
||||
|
||||
/**
|
||||
* For bots: inline or reply markup or an instruction
|
||||
* to hide a reply keyboard or to force a reply.
|
||||
*/
|
||||
replyMarkup?: ReplyMarkup
|
||||
|
||||
/**
|
||||
* Function that will be called after some part has been uploaded.
|
||||
* Only used when a file that requires uploading is passed,
|
||||
* and not used when uploading a thumbnail.
|
||||
*
|
||||
* @param uploaded Number of bytes already uploaded
|
||||
* @param total Total file size
|
||||
*/
|
||||
progressCallback?: (uploaded: number, total: number) => void
|
||||
}
|
||||
): Promise<Message> {
|
||||
if (!params) params = {}
|
||||
|
||||
if (media.type === 'photo') {
|
||||
return this.sendPhoto(chatId, media.file, {
|
||||
caption: media.caption,
|
||||
entities: media.entities,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
let inputMedia: tl.TypeInputMedia | null = null
|
||||
|
||||
let inputFile: tl.TypeInputFile | undefined = undefined
|
||||
let thumb: tl.TypeInputFile | undefined = undefined
|
||||
let mime = 'application/octet-stream'
|
||||
|
||||
const input = media.file
|
||||
if (typeof input === 'string' && input.match(/^https?:\/\//)) {
|
||||
inputMedia = {
|
||||
_: 'inputMediaDocumentExternal',
|
||||
url: input,
|
||||
}
|
||||
} else if (isUploadedFile(input)) {
|
||||
inputFile = input.inputFile
|
||||
mime = input.mime
|
||||
} 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
|
||||
}
|
||||
|
||||
if (!inputMedia) {
|
||||
if (!inputFile) throw new Error('should not happen')
|
||||
|
||||
if ('thumb' in media && media.thumb) {
|
||||
const t = media.thumb
|
||||
if (typeof t === 'string' && t.match(/^https?:\/\//)) {
|
||||
throw new MtCuteArgumentError("Thumbnail can't be external")
|
||||
} else if (isUploadedFile(t)) {
|
||||
thumb = t.inputFile
|
||||
} else if (typeof t === 'object' && tl.isAnyInputFile(t)) {
|
||||
thumb = t
|
||||
} else {
|
||||
const uploaded = await this.uploadFile({
|
||||
file: t,
|
||||
})
|
||||
thumb = uploaded.inputFile
|
||||
}
|
||||
}
|
||||
|
||||
const attributes: tl.TypeDocumentAttribute[] = []
|
||||
|
||||
if (media.type !== 'voice') {
|
||||
attributes.push({
|
||||
_: 'documentAttributeFilename',
|
||||
fileName:
|
||||
media.fileName ||
|
||||
(typeof media.file === 'string'
|
||||
? extractFileName(media.file)
|
||||
: 'unnamed'),
|
||||
})
|
||||
}
|
||||
|
||||
if (media.type === 'video') {
|
||||
attributes.push({
|
||||
_: 'documentAttributeVideo',
|
||||
duration: media.duration || 0,
|
||||
w: media.width || 0,
|
||||
h: media.height || 0,
|
||||
supportsStreaming: media.supportsStreaming,
|
||||
roundMessage: media.isRound
|
||||
})
|
||||
if (media.isAnimated) attributes.push({ _: 'documentAttributeAnimated' })
|
||||
}
|
||||
|
||||
if (media.type === 'audio' || media.type === 'voice') {
|
||||
attributes.push({
|
||||
_: 'documentAttributeAudio',
|
||||
voice: media.type === 'voice',
|
||||
duration: media.duration || 0,
|
||||
title: media.type === 'audio' ? media.title : undefined,
|
||||
performer: media.type === 'audio' ? media.performer : undefined,
|
||||
waveform: media.type === 'voice' ? media.waveform : undefined
|
||||
})
|
||||
}
|
||||
|
||||
inputMedia = {
|
||||
_: 'inputMediaUploadedDocument',
|
||||
nosoundVideo: media.type === 'video' && media.isAnimated,
|
||||
forceFile: media.type === 'document',
|
||||
file: inputFile,
|
||||
thumb,
|
||||
mimeType: mime,
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
const [message, entities] = await this._parseEntities(
|
||||
media.caption,
|
||||
params.parseMode,
|
||||
media.entities
|
||||
)
|
||||
|
||||
const peer = normalizeToInputPeer(await this.resolvePeer(chatId))
|
||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||
|
||||
|
||||
const res = await this.call({
|
||||
_: 'messages.sendMedia',
|
||||
peer,
|
||||
media: inputMedia,
|
||||
silent: params.silent,
|
||||
replyToMsgId: params.replyTo
|
||||
? typeof params.replyTo === 'number'
|
||||
? params.replyTo
|
||||
: params.replyTo.id
|
||||
: undefined,
|
||||
randomId: randomUlong(),
|
||||
scheduleDate: normalizeDate(params.schedule),
|
||||
replyMarkup,
|
||||
message,
|
||||
entities,
|
||||
})
|
||||
|
||||
return this._findMessageInUpdate(res)
|
||||
}
|
|
@ -5,8 +5,6 @@ import {
|
|||
BotKeyboard,
|
||||
ReplyMarkup,
|
||||
isUploadedFile,
|
||||
filters,
|
||||
Photo,
|
||||
} from '../../types'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { TelegramClient } from '../../client'
|
||||
|
|
|
@ -8,3 +8,4 @@ export * from './video'
|
|||
export * from './location'
|
||||
export * from './voice'
|
||||
export * from './sticker'
|
||||
export * from './input-media'
|
||||
|
|
275
packages/client/src/types/media/input-media.ts
Normal file
275
packages/client/src/types/media/input-media.ts
Normal file
|
@ -0,0 +1,275 @@
|
|||
import { InputFileLike } from '../files'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
interface BaseInputMedia {
|
||||
/**
|
||||
* File to be sent
|
||||
*/
|
||||
file: InputFileLike
|
||||
|
||||
/**
|
||||
* Caption of the media
|
||||
*/
|
||||
caption?: string
|
||||
|
||||
/**
|
||||
* Caption entities of the media.
|
||||
* If passed, {@link caption} is ignored
|
||||
*/
|
||||
entities?: tl.TypeMessageEntity[]
|
||||
|
||||
/**
|
||||
* Override file name for the file.
|
||||
*/
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export interface InputMediaAuto extends BaseInputMedia {
|
||||
type: 'auto'
|
||||
}
|
||||
|
||||
/**
|
||||
* An audio file or voice message to be sent
|
||||
*/
|
||||
export interface InputMediaAudio extends BaseInputMedia {
|
||||
type: 'audio'
|
||||
|
||||
/**
|
||||
* Thumbnail of the audio file album cover.
|
||||
*
|
||||
* The thumbnail should be in JPEG format and less than 200 KB in size.
|
||||
* A thumbnail's width and height should not exceed 320 pixels.
|
||||
* Thumbnails can't be reused and can be only uploaded as a new file.
|
||||
*/
|
||||
thumb?: InputFileLike
|
||||
|
||||
/**
|
||||
* Duration of the audio in seconds
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* Performer of the audio
|
||||
*/
|
||||
performer?: string
|
||||
|
||||
/**
|
||||
* Title of the audio
|
||||
*/
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Voice message to be sent
|
||||
*/
|
||||
export interface InputMediaVoice extends BaseInputMedia {
|
||||
type: 'voice'
|
||||
|
||||
/**
|
||||
* Duration of the voice message in seconds
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* Waveform of the voice message
|
||||
*/
|
||||
waveform?: Buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic file to be sent
|
||||
*/
|
||||
export interface InputMediaDocument extends BaseInputMedia {
|
||||
type: 'document'
|
||||
|
||||
/**
|
||||
* Thumbnail of the document.
|
||||
*
|
||||
* The thumbnail should be in JPEG format and less than 200 KB in size.
|
||||
* A thumbnail's width and height should not exceed 320 pixels.
|
||||
* Thumbnails can't be reused and can be only uploaded as a new file.
|
||||
*/
|
||||
thumb?: InputFileLike
|
||||
}
|
||||
|
||||
/**
|
||||
* A photo to be sent
|
||||
*/
|
||||
export interface InputMediaPhoto extends BaseInputMedia {
|
||||
type: 'photo'
|
||||
}
|
||||
|
||||
/**
|
||||
* A video to be sent
|
||||
*/
|
||||
export interface InputMediaVideo extends BaseInputMedia {
|
||||
type: 'video'
|
||||
|
||||
/**
|
||||
* Thumbnail of the video.
|
||||
*
|
||||
* The thumbnail should be in JPEG format and less than 200 KB in size.
|
||||
* A thumbnail's width and height should not exceed 320 pixels.
|
||||
* Thumbnails can't be reused and can be only uploaded as a new file.
|
||||
*/
|
||||
thumb?: InputFileLike
|
||||
|
||||
/**
|
||||
* Width of the video in pixels
|
||||
*/
|
||||
width?: number
|
||||
|
||||
/**
|
||||
* Height of the video in pixels
|
||||
*/
|
||||
height?: number
|
||||
|
||||
/**
|
||||
* Duration of the video in seconds
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* Whether the video is suitable for streaming
|
||||
*/
|
||||
supportsStreaming?: boolean
|
||||
|
||||
/**
|
||||
* Whether this video is an animated GIF
|
||||
*/
|
||||
isAnimated?: boolean
|
||||
|
||||
/**
|
||||
* Whether this video is a round message (aka video note)
|
||||
*/
|
||||
isRound?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Input media that can be sent somewhere.
|
||||
*
|
||||
* Note that meta-fields (like `duration`) are only
|
||||
* applicable if `file` is {@link UploadFileLike},
|
||||
* otherwise they are ignored.
|
||||
*
|
||||
* @see InputMedia
|
||||
*/
|
||||
export type InputMediaLike =
|
||||
| InputMediaAudio
|
||||
| InputMediaVoice
|
||||
| InputMediaDocument
|
||||
| InputMediaPhoto
|
||||
| InputMediaVideo
|
||||
| InputMediaAuto
|
||||
|
||||
export namespace InputMedia {
|
||||
type OmitTypeAndFile<T extends InputMediaLike> = Omit<T, 'type' | 'file'>
|
||||
|
||||
/**
|
||||
* Create an animation to be sent
|
||||
*/
|
||||
export function animation(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaVideo>
|
||||
): InputMediaVideo {
|
||||
return {
|
||||
type: 'video',
|
||||
file,
|
||||
isAnimated: true,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an audio to be sent
|
||||
*/
|
||||
export function audio(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaAudio>
|
||||
): InputMediaAudio {
|
||||
return {
|
||||
type: 'audio',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an document to be sent
|
||||
*/
|
||||
export function document(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaDocument>
|
||||
): InputMediaDocument {
|
||||
return {
|
||||
type: 'document',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an photo to be sent
|
||||
*/
|
||||
export function photo(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaPhoto>
|
||||
): InputMediaPhoto {
|
||||
return {
|
||||
type: 'photo',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an video to be sent
|
||||
*/
|
||||
export function video(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaVideo>
|
||||
): InputMediaVideo {
|
||||
return {
|
||||
type: 'video',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a voice message to be sent
|
||||
*/
|
||||
export function voice(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaVoice>
|
||||
): InputMediaVoice {
|
||||
return {
|
||||
type: 'voice',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document to be sent, which subtype
|
||||
* is inferred automatically by file contents.
|
||||
*
|
||||
* **Does not** infer photos, they will be sent as simple files.
|
||||
*/
|
||||
export function auto(
|
||||
file: InputFileLike,
|
||||
params?: OmitTypeAndFile<InputMediaAuto>
|
||||
): InputMediaAuto {
|
||||
return {
|
||||
type: 'auto',
|
||||
file,
|
||||
...(params || {}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,3 +98,11 @@ export function svgPathToFile(path: string): Buffer {
|
|||
'</svg>'
|
||||
)
|
||||
}
|
||||
|
||||
const FILENAME_REGEX = /^(\/?.+[/\\])*(.+\..+)$/
|
||||
|
||||
export function extractFileName(path: string): string {
|
||||
const m = path.match(FILENAME_REGEX)
|
||||
if (m) return m[2]
|
||||
return ''
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue