Initial commit

This commit is contained in:
kakifilem 2026-02-13 00:46:00 +08:00
commit 2dfa7eb70a
17 changed files with 571 additions and 0 deletions

20
.env.example Normal file
View File

@ -0,0 +1,20 @@
# Telegram
API_ID=123456
API_HASH=your_api_hash_here
BOT_TOKEN=your_bot_token_here
# Allowed Telegram user IDs (comma-separated)
ALLOWED_USERS=123456789
# S3 / S3-Compatible
S3_ENDPOINT=
S3_REGION=us-east-1
S3_ACCESS_KEY=your_access_key
S3_SECRET_KEY=your_secret_key
S3_BUCKET=your_bucket_name
# Return presigned download link after upload
ENABLE_PRESIGNED_URL=true
# Presigned URL expiration (seconds)
PRESIGNED_EXPIRE_SECONDS=3600

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
.env
__pycache__/
*.pyc
*.pyo
*.pyd
.venv/
venv/
downloads/*
logs/
.DS_Store
Thumbs.db

53
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,53 @@
# Contributing
Contributions are welcome 🙂
If you have an idea, improvement, or bug fix, feel free to jump in.
---
## How to contribute
1. Fork the repository
2. Create a branch for your change
3. Make your changes
4. Open a pull request
Thats all.
---
## A few guidelines
- Keep changes small and focused
- Follow the existing code style where possible
- Dont commit secrets or credentials
- Test your changes before opening a PR
- Keep features within the scope of **private file upload and backup**
---
## Project scope
This project is meant for:
- Personal backups
- Private file storage
- Archiving Telegram content you own or manage
PRs that go beyond this projects scope may be declined to keep things simple and focused.
---
## Ideas to work on
These are possible future improvements, not a fixed roadmap:
- Channel backup support
- Streaming uploads (no local disk)
- File deduplication
- Simple admin commands
- Better logging or error handling
---
Thanks for taking the time to contribute!

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM python:3.13-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "bot.py"]

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Aman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

185
README.md Normal file
View File

@ -0,0 +1,185 @@
# Telegram → S3 Uploader Bot
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Platform](https://img.shields.io/badge/platform-Telegram-blue)
![Python](https://img.shields.io/badge/python-3.13%2B-blue)
![Framework](https://img.shields.io/badge/framework-Pyrofork%20(async)-green)
![Deploy](https://img.shields.io/badge/deploy-Railway-purple)
![Docker](https://img.shields.io/badge/docker-supported-blue)
A simple, private Telegram bot that uploads files to **any S3-compatible storage**
(AWS S3, Cloudflare R2, MinIO, etc.) and returns a **temporary presigned download link**.
This project is designed to be:
- Simple
- Safe by default
- Easy to deploy
- Easy to extend
---
## ✨ Features
- Upload files from Telegram to S3-compatible storage
- Supports documents, videos, audio, and photos
- Private bucket (no public access required)
- Temporary **presigned download links**
- User allowlist (private bot)
- Works on local machine, VPS, Docker, and Railway
- Clean, minimal codebase
---
## 📦 Supported Storage
This bot works with any S3-compatible provider, including:
- AWS S3
- Cloudflare R2
- MinIO (local NAS)
- Wasabi
- DigitalOcean Spaces
- Backblaze B2 (S3 API)
---
## 🔐 Security Model
- Files are uploaded to a **private bucket**
- Access is granted via **temporary presigned URLs**
- Links expire automatically
- Storage credentials are never exposed to users
- Bot usage can be restricted with an allowlist
- Local files are stored temporarily and cleaned up automatically
This bot is intended for **private file upload and backup use cases**.
---
## 🚀 Deployment
### ▶ Deploy on Railway
You can deploy this bot directly to Railway:
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/telegram-s3-uploader?referralCode=nIQTyp&utm_medium=integration&utm_source=template&utm_campaign=generic)
After deployment, set the required environment variables in the Railway dashboard.
> When deployed on Railway, this template attaches a Railway Bucket by default.
> You can remove it and configure any other S3-compatible storage via environment variables.
---
### ▶ Run locally
> Python 3.13 is supported. Python 3.11+ is recommended for widest compatibility.
#### 1. Clone the repository
```bash
git clone https://github.com/BigDaddyAman/telegram-s3-uploader
cd telegram-s3-uploader
```
#### 2. Create virtual environment
```bash
python3.13 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
#### 3. Install dependencies
```bash
pip install --upgrade pip
pip install -r requirements.txt
```
#### 4. Configure environment
Copy .env.example to .env and fill in your values:
```bash
cp .env.example .env
```
#### 5. Run the bot
```bash
python bot.py
```
---
### ▶ Run with Docker (build locally)
```bash
docker build -t telegram-s3-uploader .
docker run --env-file .env telegram-s3-uploader
```
---
## ⚙️ Environment Variables
| Variable | Description |
|----------|-------------|
| API_ID | Telegram API ID |
| API_HASH | Telegram API hash |
| BOT_TOKEN | Telegram bot token |
| ALLOWED_USERS | Allowed Telegram user IDs (comma-separated) |
| S3_ENDPOINT | Custom S3 endpoint (leave empty for AWS) |
| S3_REGION | S3 region (required for AWS) |
| S3_ACCESS_KEY | S3 access key |
| S3_SECRET_KEY | S3 secret key |
| S3_BUCKET | Bucket name |
| PRESIGNED_EXPIRE_SECONDS | Presigned link expiration time (seconds) |
| ENABLE_PRESIGNED_URL | Enable or disable presigned download links (`true` / `false`) |
---
## 📁 File Storage Layout
Uploaded files are stored using a simple structure:
> Files are downloaded to a temporary local directory and removed automatically after upload.
```
users/
└── <telegram_user_id>/
└── YYYYMMDD_HHMMSS_filename.ext
```
This makes the storage easy to browse and suitable for backups.
---
## 🧭 Project Scope
This project is intentionally kept small and focused.
It is meant for:
- Personal backups
- Private file storage
- Archiving Telegram content you own or manage
Features related to public distribution, streaming, or content sharing are out of scope.
---
## 🤝 Contributing
Contributions are welcome!
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and project scope.
---
## 🔐 Security
If you discover a security issue, please follow the instructions in [SECURITY.md](SECURITY.md).
---
## 📄 License
This project is licensed under the MIT License.
See [LICENSE](LICENSE) for details.

41
SECURITY.md Normal file
View File

@ -0,0 +1,41 @@
# Security Policy
## Supported versions
Only the latest version of this project is supported.
If youre running an older version, please update before reporting issues.
---
## Reporting a security issue
If you discover a security issue, please **do not open a public issue**.
Instead:
- Open a GitHub Security Advisory
or
- Contact the maintainer privately (if contact info is available)
This helps prevent accidental exposure while the issue is being fixed.
---
## Security notes
- This bot uploads files to **private S3-compatible storage**
- Files are accessed using **temporary presigned URLs**
- Storage credentials are never shared with Telegram users
- Bot access can be restricted using an allowlist
- No public access is enabled by default
---
## Responsibility
You are responsible for:
- Protecting your credentials
- Controlling who can use your bot
- Running the bot in an environment you trust
This project is provided as-is and is intended for **private file upload and backup use cases**.

18
bot.py Normal file
View File

@ -0,0 +1,18 @@
from pyrogram import Client
from config import API_ID, API_HASH, BOT_TOKEN
from handlers.private_upload import handle_private_upload
from handlers.start import handle_start
app = Client(
"telegram-s3-uploader",
api_id=API_ID,
api_hash=API_HASH,
bot_token=BOT_TOKEN,
)
handle_start(app)
handle_private_upload(app)
handle_private_upload(app)
print("🚀 Bot started")
app.run()

43
config.py Normal file
View File

@ -0,0 +1,43 @@
import os
from dotenv import load_dotenv
load_dotenv()
API_ID = int(os.getenv("API_ID"))
API_HASH = os.getenv("API_HASH")
BOT_TOKEN = os.getenv("BOT_TOKEN")
ALLOWED_USERS = os.getenv("ALLOWED_USERS", "")
if ALLOWED_USERS:
ALLOWED_USERS = {int(x.strip()) for x in ALLOWED_USERS.split(",")}
else:
ALLOWED_USERS = set()
S3_ENDPOINT = os.getenv("S3_ENDPOINT")
S3_REGION = os.getenv("S3_REGION")
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY")
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY")
S3_BUCKET = os.getenv("S3_BUCKET")
DOWNLOAD_DIR = "downloads"
ENABLE_PRESIGNED_URL = os.getenv(
"ENABLE_PRESIGNED_URL", "true"
).lower() == "true"
PRESIGNED_EXPIRE_SECONDS = int(
os.getenv("PRESIGNED_EXPIRE_SECONDS", "3600")
)
required = [
"API_ID",
"API_HASH",
"BOT_TOKEN",
"S3_ACCESS_KEY",
"S3_SECRET_KEY",
"S3_BUCKET",
]
for var in required:
if not globals().get(var):
raise RuntimeError(f"Missing env var: {var}")

0
handlers/__init__.py Normal file
View File

View File

@ -0,0 +1,89 @@
import os
import time
import shutil
from pyrogram import filters
from s3.client import get_s3_client, generate_presigned_url
from utils.filenames import sanitize_filename
from utils.helpers import ensure_dir
from config import (
DOWNLOAD_DIR,
S3_BUCKET,
ALLOWED_USERS,
ENABLE_PRESIGNED_URL,
PRESIGNED_EXPIRE_SECONDS,
)
s3 = get_s3_client()
def handle_private_upload(app):
@app.on_message(
filters.private
& (filters.document | filters.video | filters.audio | filters.photo)
)
async def upload_handler(client, message):
user_id = message.from_user.id
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
await message.reply("⛔ You are not allowed to use this bot.")
return
status = await message.reply("📥 Downloading...")
ensure_dir(DOWNLOAD_DIR)
temp_dir = os.path.join(DOWNLOAD_DIR, f"{user_id}_{message.id}")
ensure_dir(temp_dir)
try:
file = (
message.document
or message.video
or message.audio
or message.photo
)
if file and getattr(file, "file_name", None):
ts = time.strftime("%Y%m%d_%H%M%S")
filename = f"{ts}_{sanitize_filename(file.file_name)}"
else:
if message.photo:
filename = sanitize_filename(None, fallback_ext=".jpg")
else:
filename = sanitize_filename(None)
local_path = os.path.join(temp_dir, filename)
file_path = await message.download(file_name=local_path)
s3_key = f"users/{user_id}/{filename}"
await status.edit("☁️ Uploading to storage...")
s3.upload_file(file_path, S3_BUCKET, s3_key)
if ENABLE_PRESIGNED_URL:
presigned_url = generate_presigned_url(
s3=s3,
bucket=S3_BUCKET,
key=s3_key,
expires=PRESIGNED_EXPIRE_SECONDS,
)
minutes = PRESIGNED_EXPIRE_SECONDS // 60
await status.edit(
f"✅ Uploaded!\n\n"
f"🔗 Download link (expires in {minutes} min):\n"
f"{presigned_url}"
)
else:
await status.edit(
f"✅ Uploaded!\n\n"
f"🗂 Stored at:\n"
f"`{s3_key}`"
)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)

17
handlers/start.py Normal file
View File

@ -0,0 +1,17 @@
from pyrogram import filters
from config import ALLOWED_USERS
def handle_start(app):
@app.on_message(filters.command("start") & filters.private)
async def start_handler(client, message):
user_id = message.from_user.id
if user_id not in ALLOWED_USERS:
await message.reply("⛔ You are not allowed to use this bot.")
return
await message.reply(
"✅ Bot is alive!\n\n"
"📤 Send me any file and I will upload it to storage."
)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
pyrofork==2.3.69
TgCrypto-pyrofork==1.2.8
boto3==1.42.47
python-dotenv==1.2.1

37
s3/client.py Normal file
View File

@ -0,0 +1,37 @@
import boto3
from botocore.client import Config
from config import (
S3_ENDPOINT,
S3_REGION,
S3_ACCESS_KEY,
S3_SECRET_KEY,
)
def get_s3_client():
kwargs = {
"aws_access_key_id": S3_ACCESS_KEY,
"aws_secret_access_key": S3_SECRET_KEY,
"config": Config(signature_version="s3v4"),
}
if S3_ENDPOINT:
kwargs["endpoint_url"] = S3_ENDPOINT
kwargs["region_name"] = S3_REGION or "us-east-1"
else:
if not S3_REGION:
raise RuntimeError("AWS S3 requires S3_REGION")
kwargs["region_name"] = S3_REGION
return boto3.client("s3", **kwargs)
def generate_presigned_url(s3, bucket: str, key: str, expires: int):
return s3.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": bucket,
"Key": key,
},
ExpiresIn=expires,
)

0
utils/__init__.py Normal file
View File

12
utils/filenames.py Normal file
View File

@ -0,0 +1,12 @@
import os
import re
import time
def sanitize_filename(name: str | None, fallback_ext: str = "") -> str:
if not name:
ts = time.strftime("%Y%m%d_%H%M%S")
return f"file_{ts}{fallback_ext}"
name = os.path.basename(name)
name = re.sub(r"[^\w\-. ]", "_", name)
return name.strip()

4
utils/helpers.py Normal file
View File

@ -0,0 +1,4 @@
import os
def ensure_dir(path: str):
os.makedirs(path, exist_ok=True)