Telegram bots can do more than reply to text messages. They can receive photos, PDF files, spreadsheets, ZIP archives, and other documents sent by users.
In this tutorial, we will build a Python bot that:
Detects photos and documents
Reads their metadata
Downloads them to a local folder
Creates safe, unique filenames
Rejects unsupported or oversized uploads
Explains the difference between file_id and file_unique_id
The example uses the asynchronous python-telegram-bot library.
Prerequisites
You need:
Python 3.10 or newer
A Telegram bot created through BotFather
Your bot token
The python-telegram-bot package
If Telegram is not yet installed on your test device, use this Telegram download guide to choose the appropriate mobile or desktop version.
Install the Python package:
pip install python-telegram-bot
Keep your bot token in an environment variable instead of placing it directly inside the source code.
Linux or macOS:
export TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
Windows PowerShell:
$env:TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
How Telegram Represents Uploaded Files
When a user sends media, Telegram does not immediately place the actual file contents inside the update.
Instead, the update contains information such as:
file_id: Used by your bot to retrieve or resend the file
file_unique_id: A stable identifier for recognizing the same file
file_size: Approximate file size in bytes
file_name: Original name of a document, when available
mime_type: Reported media type
Photo dimensions and available sizes
Your bot passes the file_id to Telegram’s getFile method. The returned File object can then be downloaded.
A photo message normally contains several PhotoSize objects. Telegram generates these versions at different resolutions. The last item is generally the largest available version:
photo = update.message.photo[-1]
Documents are available through:
document = update.message.document
Receiving and Downloading a Photo
Create a file named bot.py and start with the following handler:
from pathlib import Path
from uuid import uuid4
from telegram import Update
from telegram.ext import ContextTypes
DOWNLOAD_DIR = Path("downloads").resolve()
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
async def receive_photo(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
message = update.effective_message
if not message or not message.photo:
return
photo = message.photo[-1]
telegram_file = await context.bot.get_file(photo.file_id)
filename = f"{uuid4().hex}.jpg"
destination = DOWNLOAD_DIR / filename
await telegram_file.download_to_drive(
custom_path=destination
)
await message.reply_text(
f"Photo received successfully.\nSaved as: {filename}"
)
The handler performs four operations:
Selects the largest photo version
Retrieves its File object
Generates a unique local filename
Downloads the photo into the downloads directory
The download_to_drive() documentation describes the available download behavior in python-telegram-bot.
Receiving Documents Safely
Documents require more validation because users can upload many different file formats.
For this example, we will allow:
PDF
TXT
CSV
JSON
ZIP
We will also apply an application-level limit of 10 MB.
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_EXTENSIONS = {
".pdf",
".txt",
".csv",
".json",
".zip",
}
Now create the document handler:
async def receive_document(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
message = update.effective_message
if not message or not message.document:
return
document = message.document
if document.file_size and document.file_size > MAX_FILE_SIZE:
await message.reply_text(
"This file is too large. The current limit is 10 MB."
)
return
original_name = Path(
document.file_name or "upload.bin"
).name
extension = Path(original_name).suffix.lower()
if extension not in ALLOWED_EXTENSIONS:
await message.reply_text(
"Unsupported file type. Please send a PDF, TXT, CSV, JSON, or ZIP file."
)
return
safe_filename = f"{uuid4().hex}{extension}"
destination = DOWNLOAD_DIR / safe_filename
telegram_file = await context.bot.get_file(
document.file_id
)
await telegram_file.download_to_drive(
custom_path=destination
)
await message.reply_text(
"Document received successfully.\n"
f"Original name: {original_name}\n"
f"Stored as: {safe_filename}"
)
Why Generate a New Filename?
Saving an uploaded document using its original name can create several problems.
Two users might upload files with the same name:
report.pdf
A malicious filename might also contain path components intended to escape the download directory.
Using this line removes path information:
original_name = Path(document.file_name).name
Generating a UUID-based filename also prevents accidental overwrites:
safe_filename = f"{uuid4().hex}{extension}"
Do not assume that a filename extension or MIME type proves that a file is safe. Both values can be misleading. A production application should inspect file contents and scan untrusted uploads before further processing.
Complete Working Bot
Here is the complete example:
import logging
import os
from pathlib import Path
from uuid import uuid4
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(name)
DOWNLOAD_DIR = Path("downloads").resolve()
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_EXTENSIONS = {
".pdf",
".txt",
".csv",
".json",
".zip",
}
async def start(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
await update.effective_message.reply_text(
"Send me a photo or an allowed document.\n"
"Supported documents: PDF, TXT, CSV, JSON, and ZIP."
)
async def receive_photo(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
message = update.effective_message
if not message or not message.photo:
return
photo = message.photo[-1]
if photo.file_size and photo.file_size > MAX_FILE_SIZE:
await message.reply_text(
"This photo is too large. The current limit is 10 MB."
)
return
telegram_file = await context.bot.get_file(
photo.file_id
)
filename = f"{uuid4().hex}.jpg"
destination = DOWNLOAD_DIR / filename
await telegram_file.download_to_drive(
custom_path=destination
)
logger.info(
"Saved photo %s as %s",
photo.file_unique_id,
filename,
)
await message.reply_text(
f"Photo downloaded successfully.\nFile ID: {filename}"
)
async def receive_document(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
message = update.effective_message
if not message or not message.document:
return
document = message.document
if document.file_size and document.file_size > MAX_FILE_SIZE:
await message.reply_text(
"This document is too large. The current limit is 10 MB."
)
return
original_name = Path(
document.file_name or "upload.bin"
).name
extension = Path(original_name).suffix.lower()
if extension not in ALLOWED_EXTENSIONS:
await message.reply_text(
"Unsupported file type.\n"
"Allowed formats: PDF, TXT, CSV, JSON, and ZIP."
)
return
safe_filename = f"{uuid4().hex}{extension}"
destination = DOWNLOAD_DIR / safe_filename
telegram_file = await context.bot.get_file(
document.file_id
)
await telegram_file.download_to_drive(
custom_path=destination
)
logger.info(
"Saved document %s as %s",
document.file_unique_id,
safe_filename,
)
await message.reply_text(
"Document downloaded successfully.\n"
f"Original name: {original_name}\n"
f"File ID: {safe_filename}"
)
async def unsupported_file(
update: Update,
context: ContextTypes.DEFAULT_TYPE
) -> None:
await update.effective_message.reply_text(
"I cannot process this message type yet. "
"Please send a photo or a supported document."
)
async def error_handler(
update: object,
context: ContextTypes.DEFAULT_TYPE
) -> None:
logger.exception(
"An error occurred while processing an update",
exc_info=context.error,
)
def main() -> None:
token = os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
raise RuntimeError(
"The TELEGRAM_BOT_TOKEN environment variable is missing."
)
application = Application.builder().token(token).build()
application.add_handler(
CommandHandler("start", start)
)
application.add_handler(
MessageHandler(filters.PHOTO, receive_photo)
)
application.add_handler(
MessageHandler(
filters.Document.ALL,
receive_document,
)
)
application.add_handler(
MessageHandler(
~filters.COMMAND,
unsupported_file,
)
)
application.add_error_handler(error_handler)
application.run_polling()
if name == "main":
main()
Run the bot:
python bot.py
Open your bot in Telegram, send /start, and then upload a photo or supported document.
The downloaded files will appear in:
downloads/
file_id vs. file_unique_id
These two values serve different purposes.
file_id
A file_id can be passed to Telegram API methods. Your bot can use it to retrieve or resend a previously uploaded file without uploading that file again.
However, a file_id is associated with the bot that received it. It should not be treated as a universal public identifier.
file_unique_id
A file_unique_id helps identify the same file over time and across bots.
It is useful for:
Detecting duplicate uploads
Creating database references
Comparing previously received media
It cannot be used directly to download or resend a file.
A practical database record could contain:
file_id
file_unique_id
original_name
stored_name
file_size
uploaded_by
uploaded_at
Important Security Rules
File-upload bots should be treated like public upload forms. Never trust uploaded content automatically.
Apply these precautions:
Set your own size limit. Reject files before downloading whenever file_size is available.
Generate server-side filenames. Do not use raw user filenames as storage paths.
Use an extension allowlist. Reject formats your application does not need.
Inspect actual contents. File extensions and MIME types can be forged.
Store uploads outside the public web root.
Never execute uploaded files.
Scan untrusted files before opening or processing them.
Restrict access by user ID if the bot is intended for a private team.
Avoid exposing local server paths in bot replies.
Review Telegram’s current file limits before designing large-file workflows.
The official MessageFilter documentation lists filters for photos, documents, MIME types, extensions, videos, audio, voice messages, and other Telegram content.
Common Problems
The Photo Handler Does Not Run
Confirm that the photo handler is registered before a broad fallback handler:
application.add_handler(
MessageHandler(filters.PHOTO, receive_photo)
)
Handler order matters because an earlier matching handler in the same group may process the update first.
The Download Directory Is Missing
Create it before starting the bot:
DOWNLOAD_DIR.mkdir(
parents=True,
exist_ok=True
)
The Bot Rejects a Valid File
Print or log the received filename and MIME type:
logger.info(
"Received %s with MIME type %s",
document.file_name,
document.mime_type,
)
Then check whether its extension is included in ALLOWED_EXTENSIONS.
Downloading Large Files Fails
Telegram’s Bot API, the selected library, your network, and your hosting environment may impose different constraints. Check the current Bot API documentation and configure an application-level limit suitable for your server.
Where to Go Next
After receiving files successfully, you can extend the bot to:
Upload files to cloud object storage
Save metadata in PostgreSQL or SQLite
Generate image thumbnails
Extract text from PDF documents
Scan uploads for malware
Detect duplicate files with file_unique_id
Restrict uploads to approved Telegram users
Send processing results back to the user
The core workflow remains the same: validate the incoming message, obtain its file_id, request the File object, and download it to a controlled destination.
繁體中文摘要
這篇教學示範如何使用 Python 與 python-telegram-bot 接收 Telegram 使用者傳送的照片及文件。程式會透過 file_id 取得檔案、限制檔案大小、檢查副檔名,並以 UUID 產生不重複的安全檔名。
正式部署時,不應直接信任使用者提供的檔名、MIME 類型或副檔名。建議將上傳內容存放在網站公開目錄以外,加入檔案內容檢查、惡意程式掃描及使用者權限限制,避免上傳功能成為安全漏洞。

Top comments (0)