gram-js / gramjs

NodeJS/Browser MTProto API Telegram client library,
MIT License
1.31k stars 180 forks source link

Here is my code to convert bot file id to media object to download big files using GramJs. You can integrate it. #554

Open soberhacker opened 1 year ago

soberhacker commented 1 year ago
import bigInt from "big-integer";
import { Api } from "telegram";
import { Buffer } from "buffer";

const WEB_LOCATION_FLAG = 1 << 24; // substitute with actual value
const FILE_REFERENCE_FLAG = 1 << 25; // substitute with actual value

enum FileType {
    THUMBNAIL = 0,
    CHAT_PHOTO = 1, // ProfilePhoto
    PHOTO = 2,
    VOICE = 3, // VoiceNote
    VIDEO = 4,
    DOCUMENT = 5,
    ENCRYPTED = 6,
    TEMP = 7,
    STICKER = 8,
    AUDIO = 9,
    ANIMATION = 10,
    ENCRYPTED_THUMBNAIL = 11,
    WALLPAPER = 12,
    VIDEO_NOTE = 13,
    SECURE_RAW = 14,
    SECURE = 15,
    BACKGROUND = 16,
    DOCUMENT_AS_FILE = 17,
}

// Photo-like file ids are longer and contain extra info, the rest are all documents
const PHOTO_TYPES = new Set([
    FileType.THUMBNAIL,
    FileType.CHAT_PHOTO,
    FileType.PHOTO,
    FileType.WALLPAPER,
    FileType.ENCRYPTED_THUMBNAIL,
]);

const DOCUMENT_TYPES = new Set(Object.values(FileType));

for (const item of PHOTO_TYPES) {
    DOCUMENT_TYPES.delete(item);
}

// converting Telegram Bot Api file_id to Telegram Client Api media object
export function convertBotFileToMessageMedia(fileId: string, fileSize: number): Api.TypeMessageMedia {
    const decoded = rle_decode(b64_decode(fileId));
    const major = decoded[decoded.length - 1];
    const buffer = major < 4 ? decoded.slice(0, -1) : decoded.slice(0, -2);

    let bufferPosition = 0;
    let fileType = buffer.readInt32LE(bufferPosition);
    bufferPosition += 4;
    const dcId = buffer.readInt32LE(bufferPosition);
    bufferPosition += 4;

    const hasWebLocation = Boolean(fileType & WEB_LOCATION_FLAG);
    const hasFileReference = Boolean(fileType & FILE_REFERENCE_FLAG);

    fileType &= ~WEB_LOCATION_FLAG;
    fileType &= ~FILE_REFERENCE_FLAG;

    if (!(fileType in FileType)) {
        throw new Error(`Unknown file_type ${fileType} of file_id ${fileId}`);
    }

    if (hasWebLocation) {
        const { result: url, newPosition } = readString(buffer, bufferPosition);
        bufferPosition = newPosition;

        const accessHash = buffer.readBigInt64LE(bufferPosition);
        bufferPosition += 8;

        // Fake type to return url
        const webpage = new Api.WebPage({
            id: bigInt(accessHash),
            hash: Number(accessHash.toString),
            url: url,
            displayUrl: url,
            attributes: [],
        });

        return new Api.MessageMediaWebPage({
            webpage: webpage,
        });
    }

    let fileReference: Buffer;
    if (hasFileReference) {
        const { result, newPosition } = readBytes(buffer, bufferPosition);
        fileReference = result;
        bufferPosition = newPosition;
    } else {
        fileReference = Buffer.alloc(0);
    }

    const media_id = BigInt(buffer.readBigInt64LE(bufferPosition).toString());
    bufferPosition += 8;
    const access_hash = BigInt(buffer.readBigInt64LE(bufferPosition).toString());
    bufferPosition += 8;

    if (PHOTO_TYPES.has(fileType)) {
        const photo = new Api.Photo({
            id: bigInt(media_id),
            accessHash: bigInt(access_hash),
            fileReference: fileReference,
            dcId: dcId,
            date: 0,
            sizes: [],
        });

        return new Api.MessageMediaPhoto({
            photo: photo,
        });
    }

    const document = new Api.Document({
        id: bigInt(media_id),
        accessHash: bigInt(access_hash),
        mimeType: "",
        date: 0,
        size: bigInt(fileSize),
        dcId: dcId,
        fileReference: fileReference,
        attributes: [],
    });

    return new Api.MessageMediaDocument({
        document: document,
    });
}

// converting Telegram Bot Api file_id to Telegram Client Api media object
export function extractMediaId(fileId: string): number {
    const decoded = rle_decode(b64_decode(fileId));
    const major = decoded[decoded.length - 1];
    const buffer = major < 4 ? decoded.slice(0, -1) : decoded.slice(0, -2);

    let bufferPosition = 0;
    let fileType = buffer.readInt32LE(bufferPosition);
    bufferPosition += 4;
    buffer.readInt32LE(bufferPosition);
    bufferPosition += 4;

    const hasFileReference = Boolean(fileType & FILE_REFERENCE_FLAG);

    fileType &= ~WEB_LOCATION_FLAG;
    fileType &= ~FILE_REFERENCE_FLAG;

    if (!(fileType in FileType)) {
        throw new Error(`Unknown file_type ${fileType} of file_id ${fileId}`);
    }

    if (hasFileReference) {
        const { newPosition } = readBytes(buffer, bufferPosition);
        bufferPosition = newPosition;
    }

    const mediaId = Number(buffer.readBigInt64LE(bufferPosition).toString());
    return mediaId;
}

function b64_decode(s: string): Buffer {
    const base64Padded = s + "=".repeat(mod(-s.length, 4));
    return Buffer.from(base64Padded, "base64");
}

function rle_decode(s: Buffer): Buffer {
    const r: number[] = [];
    let z = false;

    for (let i = 0; i < s.length; i++) {
        const b = s[i];

        if (!b) {
            z = true;
            continue;
        }

        if (z) {
            r.push(...Array(b).fill(0));
            z = false;
        } else {
            r.push(b);
        }
    }

    return Buffer.from(r);
}

function readBytes(buffer: Buffer, position: number): { result: Buffer; newPosition: number } {
    let length = buffer.readUInt8(position);
    position += 1;
    let padding = 0;

    if (length > 253) {
        length = buffer.readUIntLE(position, 3);
        position += 3;
        padding = mod(-length, 4);
    } else {
        padding = mod(-(length + 1), 4);
    }

    const result = buffer.slice(position, position + length);
    position += length + padding;
    return { result, newPosition: position };
}

function readString(buffer: Buffer, position: number): { result: string; newPosition: number } {
    const { result, newPosition } = readBytes(buffer, position);
    return { result: result.toString("utf8"), newPosition };
}

function mod(n: number, m: number): number {
    return ((n % m) + m) % m;
}
PurpleTape commented 4 months ago

Hello, @soberhacker! Can you please share where you got the file type enumeration FileType from? I want to get to know the documentation in more detail, but I couldn't find information about it anywhere on my own. Thanks!