Files
Working with files and media is important in almost any bot, and mtcute makes it very simple.
Downloading files
To download a file, just use downloadIterable
, downloadStream
, downloadBuffer
or downloadToFile
method on the object that represents a file, for example:
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
await tg.downloadToFile('download.jpg', msg.media)
}
})
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
await tg.downloadToFile('download.jpg', msg.media)
}
})
Or in case you don't have the object, you can download a file by its File ID:
await tg.downloadToFile('download.jpg', 'AgACAgEAAxkBA...33gAgABHwQ')
await tg.downloadToFile('download.jpg', 'AgACAgEAAxkBA...33gAgABHwQ')
TIP
downloadToFile
is only available for Node.js. Other methods are environment agnostic.
Uploading files
In Telegram, files are primarily used to back a media (a photo, a chat avatar, a document, etc.)
Client methods (like setProfilePhoto
) accept InputFileLike
, which is a type containing information on how to upload a certain file, or to re-use an existing file (see File IDs).
InputFileLike
can be one of:
Buffer
, in this case contents of the buffer will be uploaded as file- Readable stream (both.js and Web are supported)
File
from the Web APIResponse
fromwindow.fetch
ornode-fetch
UploadedFile
, see Uploading files manuallystring
with URL:https://example.com/file.jpg
(only supported sometimes)string
with file path (only Node.js):file:path/to/file.jpg
(note thefile:
prefix)string
with File ID
await tg.setProfilePhoto('photo', Buffer.from(...))
await tg.setProfilePhoto('photo', fs.createReadableStream('assets/renge.jpg'))
await tg.setProfilePhoto('photo', await fetch('https://nyanpa.su/renge.jpg'))
await tg.setProfilePhoto('photo', await tg.uploadFile(...))
await tg.setProfilePhoto('photo', 'file:assets/renge.jpg')
await tg.setProfilePhoto('photo', 'BQACAgEAAx...Z2mGB8E')
// setProfilePhoto and some other methods don't support URLs (TG limitation)
// await tg.setProfilePhoto('photo', 'https://nyanpa.su/renge.jpg')
await tg.setProfilePhoto('photo', Buffer.from(...))
await tg.setProfilePhoto('photo', fs.createReadableStream('assets/renge.jpg'))
await tg.setProfilePhoto('photo', await fetch('https://nyanpa.su/renge.jpg'))
await tg.setProfilePhoto('photo', await tg.uploadFile(...))
await tg.setProfilePhoto('photo', 'file:assets/renge.jpg')
await tg.setProfilePhoto('photo', 'BQACAgEAAx...Z2mGB8E')
// setProfilePhoto and some other methods don't support URLs (TG limitation)
// await tg.setProfilePhoto('photo', 'https://nyanpa.su/renge.jpg')
Sending media
As mentioned earlier, most of the time file is used as a media in a Message. Sending media is incredibly easy with mtcute - you simply call sendMedia
and provide InputMediaLike
.
InputMediaLike
can be constructed manually, or using one of the builder functions exported in InputMedia
namespace:
await tg.sendMedia('me', InputMedia.photo('file:assets/welcome.jpg'))
await tg.sendMedia('me', InputMedia.auto('BQACAgEAAx...Z2mGB8E', {
caption: 'Backup of the project'
}))
// when using InputMedia.auto with file IDs and not using
// additonal parameters like `caption`, you can simply pass it as a string
await tg.sendMedia('me', 'CAADAgADLgZZCKNgg2JpAg')
await tg.sendMedia('me', InputMedia.photo('file:assets/welcome.jpg'))
await tg.sendMedia('me', InputMedia.auto('BQACAgEAAx...Z2mGB8E', {
caption: 'Backup of the project'
}))
// when using InputMedia.auto with file IDs and not using
// additonal parameters like `caption`, you can simply pass it as a string
await tg.sendMedia('me', 'CAADAgADLgZZCKNgg2JpAg')
First argument is InputFileLike
, so you can use any supported type when using it.
Using InputMedia
instead of separate methods (like in Bot API) allows easily switching to sendMediaGroup
:
await tg.sendMediaGroup('me', [
InputMedia.photo('file:assets/welcome.jpg'),
InputMedia.auto('AgACAgEAAxkBA...6D2AgABHwQ'),
'AgACAgEAAxkBA...33gAgABHwQ',
])
await tg.sendMediaGroup('me', [
InputMedia.photo('file:assets/welcome.jpg'),
InputMedia.auto('AgACAgEAAxkBA...6D2AgABHwQ'),
'AgACAgEAAxkBA...33gAgABHwQ',
])
TIP
Even though you technically can pass media groups of different types, do not mix them up (you can mix photos and videos), since that would result in a server-sent error.
File IDs
TIP
File IDs are implemented in @mtcute/file-id
package, which can be easily used outside of mtcute.
If you ever worked with the Bot API, you probably already know what File ID is. If you don't, it is a unique string identifier that is used to represent files already stored on Telegram servers.
It comes in very handy when dealing with files programmatically, since storing a simple string is much easier than storing a lot of different TL objects, parsing them, normalizing, converting, etc.
mtcute File IDs are a port of TDLib's File IDs (which are also used in Bot API), which means they are 100% interoperable with TDLib and Bot API.
They do have some limitations though:
- You can't get a File ID until you upload a file and use it somewhere, e.g. as a message media (or you can use uploadMedia).
- When sending by File ID, you can't change type of the file (i.e. if this is a video, you can't send it as a document) or any meta information like duration.
- File ID cannot be used for thumbnails.
- When sending a photo by File ID of one of its sizes, all sizes will be re-used.
- File ID is unique per-user and can't be used on another account (however, some developers seem to bypass this with userbots).
- For user accounts, File ID expire (after ~24-48 hours).
- The same file may have different valid File IDs.
Why is this?
All of the above points can be explained fairly easily, but to understand them you'll need to have some understanding of how files in MTProto work.
- File ID contains document/photo ID and file reference, which are not available until that file is uploaded and used somehow, or
messages.uploadMedia
method is used. - Photos and documents are completely different types in MTProto. As for documents, when using
inputDocument
, Telegram does not allow setting new document attributes, and re-uses the attributes from the original document. They contain file type (video/audio/voice/etc), file name, duration (for video/audio), and so on. - For thumbnails, Telegram requires clients to use
InputFile
, which can only contain newly uploaded files. - When sending photos, File ID is transformed to inputPhoto, which does not contain information about sizes. When downloading, however, it is transformed to inputPhotoFileLocation, which does contain
thumbSize
parameter - File IDs contain what is known as a File reference, which is unique per-user. It seems that bots can sometimes use any file reference for files that they have recently encountered, but this is pretty unreliable and should not be used.
- File reference may expire, making the File ID unusable. Telegram claims that this does not happen to bots though (source)
- File reference or File ID format might change over time.
File ID is available in .fileId
field:
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
console.log(msg.media.fileId)
}
})
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
console.log(msg.media.fileId)
}
})
Unique File ID
This is also a concept ported from TDLib. It is similar to File ID, but built in such a way that it uniquely defines some file, i.e. the same file always has the same Unique File ID, and different files have different Unique File IDs.
It can't be used to download a file, but it is the same for different users/bots.
Unique File ID is available in .uniqueFileId
field:
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
console.log(msg.media.uniqueFileId)
}
})
tg.on('new_message', async (msg) => {
if (msg.media?.type === 'photo') {
console.log(msg.media.uniqueFileId)
}
})
Uploading files manually
TIP
This method is rarely needed outside mtcute, simply because most of the methods already handle uploading automatically, and for Raw API you can use normalizeInputFile
and normalizeInputMedia
To upload files, Client provides a simple method uploadFile
. It has a bunch of options, but the only required one is file
.
Uploading a local file
To upload a local file from Node.js, you can either provide file path, or a readable stream (note that here you don't need file:
prefix):
await tg.uploadFile({ file: 'assets/renge.jpg' })
// or
await tg.uploadFile({ file: fs.createReadStream('assets/renge.jpg') })
await tg.uploadFile({ file: 'assets/renge.jpg' })
// or
await tg.uploadFile({ file: fs.createReadStream('assets/renge.jpg') })
Uploading from Buffer
To upload a Buffer
as a file, simply pass it as file
:
const data = Buffer.from(...)
await tg.uploadFile({ file: data })
const data = Buffer.from(...)
await tg.uploadFile({ file: data })
Uploading from stream
To upload from a stream, pass it as file
, and provide file size whenever possible:
await tg.uploadFile({
file: stream,
fileSize: streamExpectedLength
})
await tg.uploadFile({
file: stream,
fileSize: streamExpectedLength
})
Uploading from the Internet
To upload a file from the Internet, you can use window.fetch
and simply pass the response object:
await tg.uploadFile({ file: await fetch('https://nyanpa.su/renge.jpg') })
await tg.uploadFile({ file: await fetch('https://nyanpa.su/renge.jpg') })
If you are using some other library for HTTP(S), it probably also supports returning streams, but you'll have to extract meta from the response object manually. Rough example for axios:
async function uploadFileAxios(tg: TelegramClient, config: AxiosRequestConfig) {
const response = await axios({ ...config, responseType: 'stream' })
return tg.uploadFile({
file: response.data,
fileSize: parseInt(response.headers['content-length'] || 0),
fileMime: response.headers['content-type'],
// fileName: ...
})
}
const file = await uploadFileAxios(tg, { url: 'https://nyanpa.su/renge.jpg' })
await tg.sendMedia('me', InputMedia.photo(file))
async function uploadFileAxios(tg: TelegramClient, config: AxiosRequestConfig) {
const response = await axios({ ...config, responseType: 'stream' })
return tg.uploadFile({
file: response.data,
fileSize: parseInt(response.headers['content-length'] || 0),
fileMime: response.headers['content-type'],
// fileName: ...
})
}
const file = await uploadFileAxios(tg, { url: 'https://nyanpa.su/renge.jpg' })
await tg.sendMedia('me', InputMedia.photo(file))