alik0211 / mtproto-core

Telegram API JS (MTProto) client library for Node.js and browser
https://mtproto-core.js.org
GNU General Public License v3.0
630 stars 113 forks source link

How to upload file (guide) #240

Closed VityaSchel closed 2 years ago

VityaSchel commented 2 years ago

it seems alik is dead so I'll have to write the guide for him, while I'm writing it here is articles that may help: https://core.telegram.org/api/files#uploading-files https://core.telegram.org/type/bytes

  1. https://core.telegram.org/method/upload.saveFilePart
  2. https://core.telegram.org/constructor/inputFile
  3. https://core.telegram.org/type/InputFile
  4. https://core.telegram.org/constructor/inputMediaUploadedPhoto
  5. https://core.telegram.org/type/InputMedia
  6. https://core.telegram.org/method/messages.uploadMedia
VityaSchel commented 2 years ago

ok so here is the steps without actual implementation yet:

  1. generate random file_id (long)
  2. go to the loop
  3. call upload.saveFilePart with arguments: file_id, file_part = index of iteration, bytes = ~array of bytes~ of file. ~bytes array~ length must be divisable by 1024 and 524288 must be divisable by ~bytes array~ size, so you can simply go to loop where you send 512KB of file everytime, except for last part — it is allowed to not satisfy these conditions. FILE SIZE MUST BE LESS THAN 10 MB. Important notice: it is not actually array of bytes, I don't know what is it, but documentation says it's an alias of string that can include invalid utf-8 sequences.
  4. Now you can finally use messages.uploadMedia with parameters: peer: use it as always { : 'peerEmptySelf' } or something, there was a lot of examples in other issues media: { : 'inputMediaUploadedPhoto', file: { _: 'inputFile', id: theIdYouGeneratedPreviously, parts: numberOfParts, name: 'file name.png' } }

I don't know if it works, but it should be working with @mtproto-core, I will write implementation and post it here. It should also work with big files (> 10 mb) the same way

VityaSchel commented 2 years ago

So this is my test implementation but it doesn't work :(

// 'image' is path to file
// global.mtprotoapi is instance of MTProto 
async function sendMedia(image) {
  const fileID = Math.ceil(Math.random() * 0xffffff) + Math.ceil(Math.random() * 0xffffff)
  const imageBuffer = await fs.readFile(image)
  const imageSize = Buffer.byteLength(imageBuffer)
  const chunks = Math.ceil(imageSize / 1024)
  for(let i = 0; i < chunks; i++) {
    const partSize = i === chunks-1 ? imageSize % 1024 : 1024
    const part = Buffer.alloc(partSize)
    imageBuffer.copy(part, 0, i*1024, i*1024 + partSize)
    console.log(i, await global.mtprotoapi.call('upload.saveFilePart', {
      file_id: fileID,
      file_part: i,
      bytes: part.toString()
    }))
  }

  global.mtprotoapi.call('messages.uploadMedia', {
    peer: botFatherPeer,
    media: {
      _: 'inputMediaUploadedPhoto',
      file: {
        _: 'inputFile',
        id: fileID,
        parts: chunks,
        name: 'testfile.png'
      }
    }
  }).then(console.log).catch(console.error)
}

sendMediaToBotFather()

messages.uploadMedia results in

{
  _: 'mt_rpc_error',
  error_code: 400,
  error_message: 'IMAGE_PROCESS_FAILED'
}

I tried to set md5_checksum field but it adds another error and it seems to be optional

{
  _: 'mt_rpc_error',
  error_code: 400,
  error_message: 'MD5_CHECKSUM_INVALID'
}

Can anyone give me a hint why image processing is failing?

VityaSchel commented 2 years ago

So far I found that you have to start from 0 chunk index and there are no issues with reading my file and slicing it to buffers. Perhaps, I should cast buffer to string other way than by simply calling .toString() on each?

VityaSchel commented 2 years ago

I also tried binary method but it doesn't work neither

import _ from 'lodash'

async function sendMediaToBotFather(image) {
  await getAPI()

  const fileID = Math.ceil(Math.random() * 0xffffff) + Math.ceil(Math.random() * 0xffffff)
  const imageBuffer = await fs.readFile(image, 'binary')
  const chunks = _.chunk(imageBuffer.split(''), 1024)
  for(let i = 0; i < chunks.length; i++) {
    const chunk = new Array(1024).fill().map((_, i) => chunks[i] ?? '') // optional
    console.log(i, await global.mtprotoapi.call('upload.saveFilePart', {
      file_id: fileID,
      file_part: i,
      bytes: chunk.join('') // can be replaced with chunks[i].join('')
    }))
  }

  global.mtprotoapi.call('messages.uploadMedia', {
    peer: botFatherPeer,
    media: {
      _: 'inputMediaUploadedPhoto',
      file: {
        _: 'inputFile',
        id: fileID,
        parts: chunks.length,
        name: 'testfile.png'
      }
    }
  }).then(console.log).catch(console.error)
}

sendMediaToBotFather()
{
  _: 'mt_rpc_error',
  error_code: 400,
  error_message: 'IMAGE_PROCESS_FAILED'
}
VityaSchel commented 2 years ago

I guess I'll have to make file upload through bot api because it's much easier, and then forward image to my telegram account which uses mtproto to get access_hash. If @alik0211 or other contributor decides to write a guide, please reopen this issue or leave a link here :)

VityaSchel commented 2 years ago

I found implementation in javascript from another library (tg client): https://github.com/gram-js/gramjs/blob/9e048dcdbff44c8c851a17a2269e7da144917529/gramjs/client/uploads.ts#L100

The only difference is that this code uses Buffer.slice instead of allocating new buffer and copying to it. I decided not to use slice because it's deprecated and is not recommended to use by NodeJS docs, but I'll give it a try anyway and also test my code with other images today.

VityaSchel commented 2 years ago

I just found that when you stringify buffer it becomes larger, so I tried sending parts without toString() part and it worked!!!!!

Feel free to copy the following script and use it, or you can modify it based on existing Telegram client:

import fs from 'fs/promises'

global.mtprotoapi = new MTProto()

const partsSizes = [1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288]
async function sendMedia(imageBuffer) {
  let fileID = ''
  for(let i = 0; i < 19; ++i) fileID += Math.floor(Math.random() * 10)

  const imageSize = Buffer.byteLength(imageBuffer)
  const partMaxSize = imageSize >= _.last(partsSizes) ? _.last(partsSizes) : partsSizes.find(size => imageSize <= size)
  const chunks = Math.ceil(imageSize / partMaxSize)
  for(let i = 0; i < chunks; i++) {
    const partSize = i === chunks-1 ? imageSize % partMaxSize : partMaxSize
    const part = imageBuffer.slice(i*partMaxSize, i*partMaxSize + partSize)
    await global.mtprotoapi.call('upload.saveFilePart', {
      file_id: fileID,
      file_part: i,
      bytes: part
    })
  }

  await global.mtprotoapi.call('messages.sendMedia', {
    media: {
      _: 'inputMediaUploadedPhoto',
      file: {
        _: 'inputFile',
        id: fileID,
        parts: chunks,
        name: fileID,
        md5Checksum: ''
      }
    },
    peer: {
      _: 'inputPeerUser',
      user_id: 123,
      access_hash: '1231236671278312'
    },
    message: '',
    random_id: Math.ceil(Math.random() * 0xffffff) + Math.ceil(Math.random() * 0xffffff),
  })
}

sendMedia(Buffer.from(await fs.readFile('/Users/You/Desktop/testfile.png')))
Upload and assign to chat without sending ```javascript import fs from 'fs/promises' global.mtprotoapi = new MTProto() async function uploadImage(image) { let fileID = '' for(let i = 0; i < 19; ++i) fileID += Math.floor(Math.random() * 10) const imageBuffer = Buffer.from(await fs.readFile(image)) const imageSize = Buffer.byteLength(imageBuffer) const chunks = Math.ceil(imageSize / 1024) for(let i = 0; i < chunks; i++) { const partSize = i === chunks-1 ? imageSize % 1024 : 1024 const part = imageBuffer.slice(i*1024, i*1024 + partSize) await global.mtprotoapi.call('upload.saveFilePart', { file_id: fileID, file_part: i, bytes: part }) } global.mtprotoapi.call('messages.uploadMedia', { peer: { _: 'peerEmptySelf' }, // change to other peer media: { _: 'inputMediaUploadedPhoto', file: { _: 'inputFile', id: fileID, parts: chunks, name: 'testfile.png', // change to actual file name, extension doesn't matter md5Checksum: '' } } }).then(console.log).catch(console.error) } uploadImage('/Users/You/Desktop/testfile.png') // or modify script to pass buffer instance and use it without reading ```

⚠️ One last thing to mention (read, it's important!): before uploading you should decide which part size you will be using. Telegram currently allows the following parts sizes:

1024
2048
4096
8192
16384
32768
65536
131072
262144
524288

You should pick as big part as possible up until 524288 bytes because this will greatly decrease uploading time. Compare actual file size to these numbers and pick closest (to higher side). This is the results of changing 1024 part size to 65536 part size while uploading picture that has filesize of 33757 bytes:

1024 part size
Uploading time total: 4.639s

65536 part size
Uploading time total: 373.383ms

I already refactored code above so you can continue to use it.