Initial commit
This commit is contained in:
commit
2dfa7eb70a
|
|
@ -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
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
.env
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
downloads/*
|
||||
logs/
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -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
|
||||
|
||||
That’s all.
|
||||
|
||||
---
|
||||
|
||||
## A few guidelines
|
||||
|
||||
- Keep changes small and focused
|
||||
- Follow the existing code style where possible
|
||||
- Don’t 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 project’s 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!
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
# Telegram → S3 Uploader Bot
|
||||
|
||||

|
||||

|
||||

|
||||
-green)
|
||||

|
||||

|
||||
|
||||
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:
|
||||
|
||||
[](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.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest version of this project is supported.
|
||||
|
||||
If you’re 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**.
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,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)
|
||||
|
|
@ -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."
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
pyrofork==2.3.69
|
||||
TgCrypto-pyrofork==1.2.8
|
||||
boto3==1.42.47
|
||||
python-dotenv==1.2.1
|
||||
|
|
@ -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,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()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import os
|
||||
|
||||
def ensure_dir(path: str):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
Loading…
Reference in New Issue