Skip to content

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, downloadBufferor downloadToFile method on the object that represents a file, for example:

ts
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:

ts
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 API
  • Response from window.fetch or node-fetch
  • UploadedFile, see Uploading files manually
  • string with URL: https://example.com/file.jpg (only supported sometimes)
  • string with file path (only Node.js): file:path/to/file.jpg (note the file: prefix)
  • string with File ID
ts
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:

ts
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:

ts
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:

ts
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:

ts
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):

ts
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:

ts
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:

ts
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:

ts
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:

ts
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))

mtcute is not affiliated with Telegram.