From 143f19b1b78ca0b67f307d9efb6bd386bed1ae10 Mon Sep 17 00:00:00 2001 From: Kaki Filem Team Date: Sat, 31 Jan 2026 20:35:11 +0800 Subject: [PATCH] Initial commit --- .dockerignore | 17 ++ .env.example | 146 ++++++++++++ .gitignore | 19 ++ CONTRIBUTING.md | 45 ++++ Dockerfile | 13 ++ LICENSE | 85 +++++++ NOTICE | 7 + README.md | 298 ++++++++++++++++++++++++ admin/auth.py | 21 ++ admin/bootstrap.py | 43 ++++ admin/routes.py | 198 ++++++++++++++++ admin/settings_store.py | 33 +++ admin/templates/dashboard.html | 123 ++++++++++ admin/templates/error.html | 38 ++++ admin/templates/login.html | 37 +++ admin/templates/settings.html | 54 +++++ api/__init__.py | 1 + api/routes.py | 119 ++++++++++ app/main.py | 128 +++++++++++ app/state.py | 83 +++++++ bot/__init__.py | 0 bot/bot.py | 23 ++ bot/handlers/__init__.py | 2 + bot/handlers/start.py | 29 +++ bot/handlers/upload.py | 178 +++++++++++++++ bot/utils/__init__.py | 0 bot/utils/access.py | 18 ++ bot/utils/mode.py | 137 +++++++++++ cache/__init__.py | 0 cache/redis.py | 36 +++ config.py | 71 ++++++ db/__init__.py | 0 db/database.py | 84 +++++++ docs/screenshots/admin-dashboard.jpg | Bin 0 -> 29680 bytes docs/screenshots/telegram-upload.jpg | Bin 0 -> 113613 bytes requirements.txt | 13 ++ static/css/admin.css | 328 +++++++++++++++++++++++++++ static/js/admin.js | 38 ++++ 38 files changed, 2465 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 admin/auth.py create mode 100644 admin/bootstrap.py create mode 100644 admin/routes.py create mode 100644 admin/settings_store.py create mode 100644 admin/templates/dashboard.html create mode 100644 admin/templates/error.html create mode 100644 admin/templates/login.html create mode 100644 admin/templates/settings.html create mode 100644 api/__init__.py create mode 100644 api/routes.py create mode 100644 app/main.py create mode 100644 app/state.py create mode 100644 bot/__init__.py create mode 100644 bot/bot.py create mode 100644 bot/handlers/__init__.py create mode 100644 bot/handlers/start.py create mode 100644 bot/handlers/upload.py create mode 100644 bot/utils/__init__.py create mode 100644 bot/utils/access.py create mode 100644 bot/utils/mode.py create mode 100644 cache/__init__.py create mode 100644 cache/redis.py create mode 100644 config.py create mode 100644 db/__init__.py create mode 100644 db/database.py create mode 100644 docs/screenshots/admin-dashboard.jpg create mode 100644 docs/screenshots/telegram-upload.jpg create mode 100644 requirements.txt create mode 100644 static/css/admin.css create mode 100644 static/js/admin.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..23abc7f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.env +.env.* + +__pycache__/ +*.pyc + +uploads/ + +.venv/ +venv/ + +.git +.gitignore +.DS_Store +Thumbs.db + +docs/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c38c7ed --- /dev/null +++ b/.env.example @@ -0,0 +1,146 @@ +# ========================= +# Telegram Bot +# ========================= + +# Get API_ID and API_HASH from: +# https://my.telegram.org -> Development Tools +API_ID=123456 +API_HASH=your_api_hash + +# Create a bot and get BOT_TOKEN from: +# https://t.me/BotFather +BOT_TOKEN=your_bot_token + + +# ========================= +# Database & Cache +# ========================= + +# PostgreSQL connection string +# Railway: Add a PostgreSQL plugin and copy DATABASE_URL +# Local: postgresql://user:password@localhost:5432/filelink +DATABASE_URL=postgresql://user:password@localhost:5432/filelink + +# Redis connection string +# Railway: Add a Redis plugin and copy REDIS_URL +# Local: redis://localhost:6379 +REDIS_URL=redis://localhost:6379 + + +# ========================= +# Public URL +# ========================= + +# Publicly accessible base URL +# +# Local development: +# BASE_URL=http://localhost:8000 +# +# Railway: +# BASE_URL=${RAILWAY_PUBLIC_DOMAIN} +# +# Custom domain: +# BASE_URL=https://files.example.com +# +# Note: +# - If no scheme is provided, https:// is assumed. +# - Railway public domains are always served over HTTPS. +# +BASE_URL=http://localhost:8000 + + +# ========================= +# Limits +# ========================= + +# Maximum file size accepted by the bot (in MB) +# Telegram limits still apply: +# - Free account: up to 2 GB +# - Telegram Premium: up to 4 GB +MAX_FILE_MB=4096 + +# Maximum number of uploads processed at the same time +# Prevents server overload and Telegram flood limits +# +# Recommended values: +# 1 = Local testing / debugging +# 2–3 = Railway or small VPS (recommended) +# 4+ = Large VPS (advanced users only) +# +# Does NOT limit downloads +MAX_CONCURRENT_TRANSFERS=3 + + +# ========================= +# Rate Limiting +# ========================= + +# Maximum number of requests per IP +GLOBAL_RATE_LIMIT_REQUESTS=60 + +# Time window in seconds +GLOBAL_RATE_LIMIT_WINDOW=10 + + +# ========================= +# Access Control (Optional) +# ========================= + +# Comma-separated Telegram user IDs allowed to use the bot +# Example: 123456789,987654321 +# Leave empty to allow everyone +ALLOWED_USER_IDS=123456789 + + +# ========================= +# Admin Dashboard (Optional) +# ========================= + +# Enable or disable admin dashboard entirely +ADMIN_ENABLED=true + +# Admin login credentials +# Used to access: /admin +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=change-me-now + +# Secret key used to sign admin session cookies +# Generate with: +# python -c "import secrets; print(secrets.token_urlsafe(32))" +SESSION_SECRET=change-this-to-a-long-random-string + +# ========================= +# Storage Backend (Optional) +# ========================= + +# Storage backend to use for uploaded files +# +# Available options: +# - local (default): store files in ./uploads +# - s3: store files in an S3-compatible bucket +# +# To enable S3-compatible storage: +STORAGE_BACKEND=local + +# ------------------------- +# S3-Compatible Storage +# ------------------------- +# +# This project supports S3-compatible object storage such as: +# - Railway Storage Buckets (recommended on Railway) +# - AWS S3 +# - Cloudflare R2 +# +# IMPORTANT: +# - On Railway, these variables are injected automatically +# when you connect a Storage Bucket to the service. +# - You do NOT need to set them manually on Railway. +# +# Uncomment and configure ONLY if you are using S3 outside Railway. +# +# AWS_ENDPOINT_URL=https://storage.example.com +# AWS_S3_BUCKET_NAME=your-bucket-name +# AWS_DEFAULT_REGION=auto +# AWS_ACCESS_KEY_ID=your-access-key +# AWS_SECRET_ACCESS_KEY=your-secret-key + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13f2d6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.env +.env.* +!.env.example + +__pycache__/ +*.pyc +*.pyo +*.pyd + +.venv/ +venv/ + +uploads/ + +.git/ +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0ade59e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing Guide + +Thanks for your interest in contributing! + +## How to Contribute + +You can help by: +- Reporting bugs +- Suggesting new features +- Improving documentation +- Submitting pull requests + +## Before Submitting a PR + +- Open an issue first for large changes +- Keep changes focused and minimal +- Follow the existing code style +- Test your changes locally + +## Feature Requests + +This project intentionally stays **simple and self-hosted**. + +Good feature ideas: +- Performance improvements +- UX improvements +- Security hardening +- Better error handling +- Documentation improvements + +Avoid: +- Torrent / mirror functionality +- Download-count based restrictions +- Anything violating platform ToS + +## Legal Notice + +You are responsible for ensuring your contribution does not introduce +illegal behavior or violate third-party terms of service. + +## License of Contributions + +By submitting a contribution, you agree that your contribution +will be licensed under the same license as the project +(Apache License 2.0). \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..724069e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} --log-level warning"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a9b600 --- /dev/null +++ b/LICENSE @@ -0,0 +1,85 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work. + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + Derivative Works of, publicly display, publicly perform, sublicense, + and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this section) patent + license to make, have made, use, offer to sell, sell, import, and + otherwise transfer the Work. + +4. Redistribution. + You may reproduce and distribute copies of the Work or Derivative Works + thereof in any medium, with or without modifications, and in Source or + Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that + You distribute, all copyright, patent, trademark, and attribution + notices from the Source form of the Work; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file. + +5. Submission of Contributions. +6. Trademarks. +7. Disclaimer of Warranty. +8. Limitation of Liability. +9. Accepting Warranty or Additional Liability. + +END OF TERMS AND CONDITIONS diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..cf9e90f --- /dev/null +++ b/NOTICE @@ -0,0 +1,7 @@ +This project was created by Aman (2025). + +You may use, modify, and distribute this software +under the Apache License, Version 2.0. + +Any redistribution must retain this NOTICE file +and all copyright notices in source files. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfa8b64 --- /dev/null +++ b/README.md @@ -0,0 +1,298 @@ +![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg) +![Platform](https://img.shields.io/badge/platform-Telegram-blue) +![Python](https://img.shields.io/badge/python-3.13-blue) +![Framework](https://img.shields.io/badge/FastAPI-async-green) +![Deploy](https://img.shields.io/badge/deploy-Railway-purple) +![Docker](https://img.shields.io/badge/docker-supported-blue) + +# πŸ“Ž Telegram File Link Bot + +A self-hosted **Telegram bot** that generates **secure, rate-limited download links** for uploaded files, with **time-based expiration (TTL)**, an **optional admin dashboard**, and **automatic cleanup**. + +The project can run **locally**, on a **VPS**, or on any cloud platform. +For the easiest setup, **Railway is recommended**. + +--- + +## πŸš€ Deployment (Railway – Recommended) + +[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/telegram-file-to-link-bot?referralCode=nIQTyp&utm_medium=integration&utm_source=template&utm_campaign=generic) + +This repository is designed to be deployed **directly as a Railway template**. + +### Why Railway? +- **Free public domain** (`*.railway.app`) included +- **Automatic HTTPS** (no SSL setup needed) +- **One-click PostgreSQL & Redis** +- **Easy environment variable management** +- **No server maintenance** or manual provisioning + +### Steps +1. Click **Deploy on Railway** +2. Railway will create a new project from this template +3. Add PostgreSQL and Redis plugins +4. Set the required environment variables +5. Optional: Enable Persistent Storage (Railway Buckets) +6. Deploy + +Your bot and download API will be live within minutes. + +> You can still deploy this project locally or on any VPS. + +--- + +## ✨ Features + +### πŸ€– Telegram Bot +- Upload files via Telegram and receive a public download link +- Supports documents, videos, audio, photos, animations, voice, and video notes +- Preserves original file quality +- Optional private bot mode (allowed user IDs) +- Built with Pyrogram / Pyrofork + +--- + +### πŸ”— File Links +- Unique file IDs +- Direct downloads via FastAPI +- Correct filenames and headers +- Supports HTTP range requests (206 Partial Content) + +--- + +### ⏳ Expiration (TTL Only) +- Optional expiration per file +- Time-based expiration only (no download limits) +- Unlimited downloads are always allowed +- Files expire automatically when TTL is reached + +--- + +### πŸ”„ Upload Concurrency Control +- Limits how many file uploads are processed at the same time +- Prevents server overload and Telegram flood limits +- Extra uploads are automatically queued +- Fully configurable via environment variables + +--- + +### βš™οΈ Mode System (TTL Control) + +TTL is controlled **per user** via Telegram commands. + +Examples: +- `/mode ttl 30` β†’ 30 minutes +- `/mode ttl 2h` β†’ 2 hours +- `/mode ttl 1d` β†’ 1 day +- `/mode ttl 0` or `/mode reset` β†’ Never expire + +TTL is stored internally in **seconds**. + +--- + +### πŸ“Š Admin Dashboard (Optional) + +Enabled only if `ADMIN_ENABLED=true`. + +Features: +- Secure session-based login +- View total files, downloads, and active files +- Search files by name +- Disable files (expire immediately) +- Delete files +- View top downloads, recent uploads, and expiring files +- Light / Dark mode toggle + +When enabled, the admin dashboard is available at: + +``` +https://your-domain.com/admin +``` + +> If `ADMIN_ENABLED=false`, the admin dashboard routes are not registered and the bot still works normally. + +--- + +## πŸ“Έ Screenshots + +### Admin Dashboard +![Admin Dashboard](docs/screenshots/admin-dashboard.jpg) + +### Telegram Upload +![Telegram Upload](docs/screenshots/telegram-upload.jpg) + +--- + +### 🧹 Automatic Cleanup +- Background task removes expired files +- Cleans database records and Redis cache +- Deletes expired objects from S3-compatible storage automatically +- Safe against partial failures + +--- + +### 🚦 Rate Limiting +- Global per-IP rate limiting +- Redis-backed +- Proper `Retry-After` headers + +--- + +## πŸ—„οΈ Storage Backends + +This project supports **two storage backends**, selectable via environment variables. + +### Local Filesystem (default) +- Files are stored in the `uploads/` directory +- Suitable for local development and small deployments +- Files are removed automatically when TTL expires + +### S3-Compatible Object Storage (Recommended for Production) +- Files are stored in an S3-compatible bucket +- Supports **Railway Storage Buckets**, AWS S3, Cloudflare R2, and similar services +- Files are served via **presigned URLs** +- No service egress for downloads +- Files persist across deployments +- Automatic cleanup when TTL expires + +On Railway, S3 credentials are injected automatically when you connect a Storage Bucket. + +Enable S3 storage by setting: +``` +STORAGE_BACKEND=s3 +``` + +--- + +### S3-Compatible Storage (Advanced / Non-Railway) + +If you are using S3-compatible storage **outside of Railway** +(e.g. AWS S3, Cloudflare R2, MinIO, Backblaze B2), +you must provide the following environment variables: + +```env +AWS_ENDPOINT_URL=https://storage.example.com +AWS_S3_BUCKET_NAME=your-bucket-name +AWS_DEFAULT_REGION=auto +AWS_ACCESS_KEY_ID=your-access-key +AWS_SECRET_ACCESS_KEY=your-secret-key +``` + +--- + +## 🧱 Tech Stack +- Python **3.11+** (tested on 3.13) +- FastAPI +- Pyrogram / Pyrofork +- PostgreSQL (asyncpg) +- Redis +- Jinja2 +- Vanilla HTML / CSS / JS +- S3-compatible object storage + +--- + +## ⚠️ Database Schema Management + +This project automatically creates and maintains its database schema at startup. + +Migrations are intentionally omitted to keep the template simple and easy to deploy. +For larger or multi-tenant deployments, adding a migration system is recommended. + +--- + +## βš™οΈ Environment Variables + +Create a `.env` file in the project root. +> Tip: Rename `.env.example` to `.env` and fill in your values. + +### Telegram Bot +``` +API_ID=your_api_id +API_HASH=your_api_hash +BOT_TOKEN=your_bot_token +``` + +### Upload Concurrency +``` +MAX_CONCURRENT_TRANSFERS=3 +``` + +### Database & Cache +``` +DATABASE_URL=postgresql://user:password@localhost:5432/filelink +REDIS_URL=redis://localhost:6379 +``` + +### Storage Backend +Choose where uploaded files are stored. + +Local filesystem (default): +``` +STORAGE_BACKEND=local +``` + +S3-compatible storage: +``` +STORAGE_BACKEND=s3 +``` +> If not set, the bot defaults to local filesystem storage. + +### Public URL +Local development: +``` +BASE_URL=http://localhost:8000 +``` + +Railway: +``` +BASE_URL=${RAILWAY_PUBLIC_DOMAIN} +``` + +Custom domain: +``` +BASE_URL=https://files.example.com +``` + +### Rate Limiting +``` +GLOBAL_RATE_LIMIT_REQUESTS=60 +GLOBAL_RATE_LIMIT_WINDOW=10 +``` + +### Access Control (Optional) +``` +ALLOWED_USER_IDS=123456789 +``` + +### Admin Dashboard (Optional) +``` +ADMIN_ENABLED=true +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=change-me-now +SESSION_SECRET=change-me +``` + +--- + +## ▢️ Running Locally + +``` +pip install -r requirements.txt +uvicorn app.main:app --reload --log-level warning +``` + +--- + +## 🐳 Running with Docker + +``` +docker build -t telegram-file-link-bot . +docker run -d --env-file .env -p 8000:8000 telegram-file-link-bot +``` + +--- + +## πŸ“œ License + +Apache License 2.0 diff --git a/admin/auth.py b/admin/auth.py new file mode 100644 index 0000000..a8c9652 --- /dev/null +++ b/admin/auth.py @@ -0,0 +1,21 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from fastapi import Request +from fastapi.responses import RedirectResponse + +def admin_required(request: Request): + if not request.session.get("admin_id"): + return RedirectResponse( + url="/admin/login", + status_code=303, + ) diff --git a/admin/bootstrap.py b/admin/bootstrap.py new file mode 100644 index 0000000..76d55ca --- /dev/null +++ b/admin/bootstrap.py @@ -0,0 +1,43 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +from passlib.hash import argon2 +from db.database import Database + +async def bootstrap_admin(): + if os.getenv("ADMIN_ENABLED", "false").lower() != "true": + return + + email = os.getenv("ADMIN_EMAIL") + password = os.getenv("ADMIN_PASSWORD") + + if not email or not password: + return + + count = await Database.pool.fetchval( + "SELECT COUNT(*) FROM admins" + ) + + if count > 0: + return + + await Database.pool.execute( + """ + INSERT INTO admins (email, password_hash) + VALUES ($1, $2) + """, + email, + argon2.hash(password), + ) + + print("βœ… Admin bootstrapped (Argon2)") diff --git a/admin/routes.py b/admin/routes.py new file mode 100644 index 0000000..ef57b42 --- /dev/null +++ b/admin/routes.py @@ -0,0 +1,198 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +from fastapi import APIRouter, Request, Depends, Form +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from passlib.hash import argon2 + +from db.database import Database +from admin.settings_store import get_setting +from admin.auth import admin_required + +router = APIRouter(prefix="/admin", tags=["admin"]) +templates = Jinja2Templates(directory="admin/templates") + +@router.get("") +async def admin_root(): + return RedirectResponse("/admin/", status_code=302) + +@router.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + return templates.TemplateResponse("login.html", {"request": request}) + +@router.post("/login") +async def login( + request: Request, + email: str = Form(...), + password: str = Form(...), +): + admin = await Database.pool.fetchrow( + "SELECT id, password_hash FROM admins WHERE email=$1", + email, + ) + + if not admin or not argon2.verify(password, admin["password_hash"]): + return templates.TemplateResponse( + "login.html", + {"request": request, "error": "Invalid credentials"}, + status_code=401, + ) + + request.session["admin_id"] = admin["id"] + return RedirectResponse("/admin/", status_code=303) + +@router.get("/settings", response_class=HTMLResponse) +async def settings_page( + request: Request, + auth=Depends(admin_required), +): + if isinstance(auth, RedirectResponse): + return auth + + stats = await Database.pool.fetchrow(""" + SELECT + COALESCE(SUM(file_size), 0) AS used_bytes, + COUNT(*) AS total_files, + COALESCE(MAX(file_size), 0) AS largest_file + FROM files + """) + + cleanup = await get_setting("cleanup_enabled", "true") + + return templates.TemplateResponse( + "settings.html", + { + "request": request, + "used_bytes": stats["used_bytes"], + "total_files": stats["total_files"], + "largest_file": stats["largest_file"], + "cleanup_enabled": cleanup == "true", + }, + ) + +@router.post("/settings/save") +async def save_settings( + request: Request, + auth=Depends(admin_required), +): + if isinstance(auth, RedirectResponse): + return auth + + return RedirectResponse("/admin/settings", status_code=303) + +@router.post("/logout") +async def logout(request: Request): + request.session.clear() + return RedirectResponse("/admin/login", status_code=303) + +@router.get("/", response_class=HTMLResponse) +async def dashboard( + request: Request, + q: str | None = None, + auth=Depends(admin_required), +): + if isinstance(auth, RedirectResponse): + return auth + + stats = await Database.pool.fetchrow(""" + SELECT + COUNT(*) AS total_files, + COALESCE(SUM(downloads), 0) AS total_downloads, + COUNT(*) FILTER ( + WHERE expires_at IS NULL OR expires_at > NOW() + ) AS active_files + FROM files + """) + + files = await Database.pool.fetch(""" + SELECT file_id, name, downloads, expires_at + FROM files + WHERE name ILIKE $1 + ORDER BY downloads DESC + LIMIT 50 + """, f"%{q or ''}%") + + top_files = await Database.pool.fetch(""" + SELECT name, downloads + FROM files + ORDER BY downloads DESC + LIMIT 5 + """) + + recent_files = await Database.pool.fetch(""" + SELECT name, created_at + FROM files + ORDER BY created_at DESC + LIMIT 5 + """) + + expiring_files = await Database.pool.fetch(""" + SELECT name, expires_at + FROM files + WHERE expires_at IS NOT NULL + AND expires_at > NOW() + ORDER BY expires_at ASC + LIMIT 5 + """) + + return templates.TemplateResponse( + "dashboard.html", + { + "request": request, + "stats": stats, + "files": files, + "top_files": top_files, + "recent_files": recent_files, + "expiring_files": expiring_files, + "query": q or "", + }, + ) + +@router.post("/file/{file_id}/delete") +async def delete_file(file_id: str, auth=Depends(admin_required)): + if isinstance(auth, RedirectResponse): + return auth + + row = await Database.pool.fetchrow( + "SELECT path FROM files WHERE file_id=$1", + file_id, + ) + + if row: + try: + os.remove(row["path"]) + except Exception: + pass + + await Database.pool.execute( + "DELETE FROM files WHERE file_id=$1", + file_id, + ) + + return RedirectResponse("/admin/", status_code=303) + + +@router.post("/file/{file_id}/disable") +async def disable_file(file_id: str, auth=Depends(admin_required)): + if isinstance(auth, RedirectResponse): + return auth + + await Database.pool.execute( + "UPDATE files SET expires_at = NOW() WHERE file_id=$1", + file_id, + ) + + return RedirectResponse("/admin/", status_code=303) + + diff --git a/admin/settings_store.py b/admin/settings_store.py new file mode 100644 index 0000000..6597dcb --- /dev/null +++ b/admin/settings_store.py @@ -0,0 +1,33 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from db.database import Database + +async def get_setting(key: str, default: str): + row = await Database.pool.fetchrow( + "SELECT value FROM settings WHERE key=$1", + key, + ) + return row["value"] if row else default + + +async def set_setting(key: str, value: str): + await Database.pool.execute( + """ + INSERT INTO settings (key, value) + VALUES ($1, $2) + ON CONFLICT (key) + DO UPDATE SET value = EXCLUDED.value + """, + key, + value, + ) diff --git a/admin/templates/dashboard.html b/admin/templates/dashboard.html new file mode 100644 index 0000000..54e8360 --- /dev/null +++ b/admin/templates/dashboard.html @@ -0,0 +1,123 @@ + + + + + + Admin Dashboard + + + + + + +
+

πŸ“Š Admin Dashboard

+ + +
+ +
+ +
+

Total Files

{{ stats.total_files }}

+

Total Downloads

{{ stats.total_downloads }}

+

Active Files

{{ stats.active_files }}

+
+ + + +
+
All Files
+ + + + + + + + + {% for f in files %} + + + + + + + {% endfor %} +
NameDownloadsStatusActions
{{ f.name }}{{ f.downloads }} + {% if f.expires_at %} + Expiring + {% else %} + Active + {% endif %} + +
+ +
+
+ +
+
+
+ +
+ +
+

πŸ”₯ Top Downloads

+
    + {% for f in top_files %} +
  • {{ f.name }} β€” {{ f.downloads }}
  • + {% endfor %} +
+
+ +
+

πŸ•’ Recent Uploads

+
    + {% for f in recent_files %} +
  • {{ f.name }}
  • + {% endfor %} +
+
+ +
+

⏳ Expiring Soon

+
    + {% for f in expiring_files %} +
  • {{ f.name }}
  • + {% endfor %} +
+
+ +
+ +
+ + + diff --git a/admin/templates/error.html b/admin/templates/error.html new file mode 100644 index 0000000..85c9fa0 --- /dev/null +++ b/admin/templates/error.html @@ -0,0 +1,38 @@ + + + + + + {{ title }} + + + + +
+

{{ icon }} {{ title }}

+

{{ message }}

+ + {% if hint %} +

{{ hint }}

+ {% endif %} + + {% if back_url %} + + ← Go back + + {% endif %} +
+ + diff --git a/admin/templates/login.html b/admin/templates/login.html new file mode 100644 index 0000000..c74be2a --- /dev/null +++ b/admin/templates/login.html @@ -0,0 +1,37 @@ + + + + + + Admin Login + + + + +
+

πŸ” Admin Login

+ + {% if error %} +
{{ error }}
+ {% endif %} + +
+ + + +
+
+ + diff --git a/admin/templates/settings.html b/admin/templates/settings.html new file mode 100644 index 0000000..aad338f --- /dev/null +++ b/admin/templates/settings.html @@ -0,0 +1,54 @@ + + + + + + Admin Settings + + + + + + +
+

πŸ“Š System Info

+ + +
+ +
+ + + ← Back to Dashboard + + +
+

πŸ“¦ Storage Usage

+

Used: {{ (used_bytes / (1024**2)) | round(2) }} MB

+

Total files: {{ total_files }}

+

Largest file: {{ (largest_file / (1024**2)) | round(2) }} MB

+
+ +
+ + + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..3a27ef1 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1 @@ +from .routes import router diff --git a/api/routes.py b/api/routes.py new file mode 100644 index 0000000..6a5cfb6 --- /dev/null +++ b/api/routes.py @@ -0,0 +1,119 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import boto3 +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse +from cache.redis import redis_client +from db.database import Database +from config import ( + GLOBAL_RATE_LIMIT_REQUESTS, + GLOBAL_RATE_LIMIT_WINDOW, + STORAGE_BACKEND, + AWS_ENDPOINT_URL, + AWS_S3_BUCKET_NAME, + AWS_DEFAULT_REGION, +) + +router = APIRouter() + +s3 = None +if STORAGE_BACKEND == "s3": + s3 = boto3.client( + "s3", + endpoint_url=AWS_ENDPOINT_URL, + region_name=AWS_DEFAULT_REGION, + ) + +def get_real_ip(request: Request) -> str: + return ( + request.headers.get("cf-connecting-ip") + or request.headers.get("x-forwarded-for", "").split(",")[0] + or request.client.host + ) + + +def rate_limit_response(window: int): + return JSONResponse( + status_code=429, + content={"error": "rate_limited", "retry_after": window}, + headers={"Retry-After": str(window)}, + ) + + +def check_rate_limit(key: str, limit: int, window: int): + count = redis_client.incr(key) + if count == 1: + redis_client.expire(key, window) + if count > limit: + return rate_limit_response(window) + + +@router.get("/file/{file_id}") +async def get_file(file_id: str, request: Request): + + ip = get_real_ip(request) + + if GLOBAL_RATE_LIMIT_REQUESTS > 0: + if resp := check_rate_limit( + f"rate:global:{ip}", + GLOBAL_RATE_LIMIT_REQUESTS, + GLOBAL_RATE_LIMIT_WINDOW, + ): + return resp + + key = f"file:{file_id}" + meta = redis_client.hgetall(key) + + if not meta: + row = await Database.pool.fetchrow( + "SELECT path, name, downloads FROM files WHERE file_id=$1", + file_id, + ) + if not row: + raise HTTPException(404, "File not found") + + meta = dict(row) + redis_client.hset(key, mapping=meta) + + download_key = f"downloaded:{ip}:{file_id}" + if not redis_client.exists(download_key): + redis_client.setex(download_key, 3600, 1) + + await Database.pool.execute( + "UPDATE files SET downloads = downloads + 1 WHERE file_id=$1", + file_id, + ) + + redis_client.hincrby(key, "downloads", 1) + + if STORAGE_BACKEND == "local": + if not os.path.exists(meta["path"]): + raise HTTPException(404, "File missing") + + return FileResponse( + path=meta["path"], + filename=meta["name"], + media_type="application/octet-stream", + ) + + url = s3.generate_presigned_url( + "get_object", + Params={ + "Bucket": AWS_S3_BUCKET_NAME, + "Key": meta["path"], + }, + ExpiresIn=3600, + ) + + return RedirectResponse(url, status_code=302) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0a8b3c9 --- /dev/null +++ b/app/main.py @@ -0,0 +1,128 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import asyncio +from fastapi import FastAPI, Request +from fastapi.exceptions import HTTPException +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import Response + +from config import ADMIN_ENABLED +from admin.bootstrap import bootstrap_admin +from admin.routes import router as admin_router + +from app.state import cleanup_expired_files +from api.routes import router as file_router +from bot.bot import tg_client +from db.database import Database +import bot.handlers + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +templates = Jinja2Templates( + directory=os.path.join(BASE_DIR, "admin", "templates") +) + +class CachedStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): + response: Response = await super().get_response(path, scope) + if response.status_code == 200: + response.headers["Cache-Control"] = ( + "public, max-age=31536000, immutable" + ) + return response + +app = FastAPI(title="Telegram File Link Bot") + +app.add_middleware( + SessionMiddleware, + secret_key=os.getenv("SESSION_SECRET", "dev-secret"), + same_site="lax", + https_only=True, +) + +app.include_router(file_router) + +if ADMIN_ENABLED: + app.include_router(admin_router) + +app.mount( + "/static", + CachedStaticFiles(directory="static"), + name="static", +) + +@app.exception_handler(HTTPException) +async def custom_http_exception_handler( + request: Request, + exc: HTTPException, +): + context = { + "request": request, + "title": "Error", + "icon": "⚠️", + "message": exc.detail, + "hint": None, + } + + if exc.status_code == 404: + context.update( + title="File Not Found", + icon="πŸ”", + message="This download link is invalid or no longer available.", + hint="The file may have expired or been deleted by the owner.", + ) + + elif exc.status_code == 403: + context.update( + title="Access Denied", + icon="β›”", + message="You are not allowed to access this file.", + ) + + return templates.TemplateResponse( + "error.html", + context, + status_code=exc.status_code, + ) + +async def start_bot(): + try: + await tg_client.start() + print("πŸ€– Pyrogram bot started") + except Exception as e: + print("⚠️ Bot start skipped:", e) + + +async def stop_bot(): + await tg_client.stop() + print("πŸ›‘ Pyrogram bot stopped") + +@app.on_event("startup") +async def startup(): + await Database.connect() + + if ADMIN_ENABLED: + await bootstrap_admin() + + asyncio.create_task(start_bot()) + asyncio.create_task(cleanup_expired_files()) + + +@app.on_event("shutdown") +async def shutdown(): + await Database.close() + asyncio.create_task(stop_bot()) diff --git a/app/state.py b/app/state.py new file mode 100644 index 0000000..95e6a2d --- /dev/null +++ b/app/state.py @@ -0,0 +1,83 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import asyncio +import boto3 +from db.database import Database +from cache.redis import redis_client +from config import ( + STORAGE_BACKEND, + AWS_ENDPOINT_URL, + AWS_S3_BUCKET_NAME, + AWS_DEFAULT_REGION, +) + +CLEANUP_INTERVAL = 30 + +s3 = None +if STORAGE_BACKEND == "s3": + s3 = boto3.client( + "s3", + endpoint_url=AWS_ENDPOINT_URL, + region_name=AWS_DEFAULT_REGION, + ) + + +async def cleanup_expired_files(): + while True: + try: + async with Database.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT file_id, path + FROM files + WHERE expires_at IS NOT NULL + AND expires_at < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') + """ + ) + + for row in rows: + file_id = row["file_id"] + path = row["path"] + + try: + if STORAGE_BACKEND == "local": + # Local filesystem cleanup + if path and os.path.exists(path): + os.remove(path) + else: + # S3 bucket cleanup + s3.delete_object( + Bucket=AWS_S3_BUCKET_NAME, + Key=path, + ) + except Exception as e: + # Never crash cleanup loop + print(f"⚠️ Failed to delete file {file_id}: {e}") + + # Clear Redis cache + redis_client.delete(f"file:{file_id}") + + if rows: + await conn.execute( + """ + DELETE FROM files + WHERE expires_at IS NOT NULL + AND expires_at < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') + """ + ) + + except Exception as e: + print("❌ Cleanup error:", e) + + await asyncio.sleep(CLEANUP_INTERVAL) diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/bot.py b/bot/bot.py new file mode 100644 index 0000000..afdb62a --- /dev/null +++ b/bot/bot.py @@ -0,0 +1,23 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from pyrogram import Client +from config import API_ID, API_HASH, BOT_TOKEN + +tg_client = Client( + "filelink_bot", + api_id=API_ID, + api_hash=API_HASH, + bot_token=BOT_TOKEN, + in_memory=True +) + diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py new file mode 100644 index 0000000..f5d6ea3 --- /dev/null +++ b/bot/handlers/__init__.py @@ -0,0 +1,2 @@ +from .start import * +from .upload import * \ No newline at end of file diff --git a/bot/handlers/start.py b/bot/handlers/start.py new file mode 100644 index 0000000..d4fefe4 --- /dev/null +++ b/bot/handlers/start.py @@ -0,0 +1,29 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from pyrogram import filters +from bot.bot import tg_client +from bot.utils.access import is_allowed + +@tg_client.on_message(filters.private & filters.command("start")) +async def start_handler(_, message): + if not message.from_user: + return + + if not is_allowed(message.from_user.id): + await message.reply("🚫 This bot is private.") + return + + await message.reply( + "πŸ‘‹ Send me a file and I’ll generate a download link.\n" + "πŸ“Ž Send images as *File* to keep original quality." + ) diff --git a/bot/handlers/upload.py b/bot/handlers/upload.py new file mode 100644 index 0000000..d9a4193 --- /dev/null +++ b/bot/handlers/upload.py @@ -0,0 +1,178 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import asyncio +import uuid +import os +import re +from datetime import datetime, timedelta, timezone + +import boto3 +from pyrogram import filters +from bot.bot import tg_client +from cache.redis import redis_client +from bot.utils.access import is_allowed +from bot.utils.mode import get_mode, format_ttl +from config import ( + BASE_URL, + MAX_FILE_MB, + MAX_CONCURRENT_TRANSFERS, + STORAGE_BACKEND, + AWS_ENDPOINT_URL, + AWS_S3_BUCKET_NAME, + AWS_DEFAULT_REGION, +) +from db.database import Database + +UPLOAD_DIR = os.path.abspath("uploads") +os.makedirs(UPLOAD_DIR, exist_ok=True) + +upload_semaphore = asyncio.Semaphore(MAX_CONCURRENT_TRANSFERS) + +s3 = None +if STORAGE_BACKEND == "s3": + s3 = boto3.client( + "s3", + endpoint_url=AWS_ENDPOINT_URL, + region_name=AWS_DEFAULT_REGION, + ) + +def safe_filename(name: str) -> str: + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + +@tg_client.on_message( + filters.private & ( + filters.document + | filters.video + | filters.audio + | filters.photo + | filters.animation + | filters.voice + | filters.video_note + ) +) +async def upload_handler(_, message): + if not message.from_user: + return + + if not is_allowed(message.from_user.id): + await message.reply("🚫 Unauthorized") + return + + status = await message.reply("πŸ“₯ Queued for processing…") + + async with upload_semaphore: + await process_upload(message, status) + +async def process_upload(message, status): + media = ( + message.document + or message.video + or message.audio + or message.photo + or message.animation + or message.voice + or message.video_note + ) + + file_size = getattr(media, "file_size", None) + + if MAX_FILE_MB is not None and file_size: + max_bytes = MAX_FILE_MB * 1024 * 1024 + if file_size > max_bytes: + size_mb = file_size / (1024 * 1024) + await status.edit( + "❌ **File too large**\n\n" + f"Your file: **{size_mb:.2f} MB**\n" + f"Max allowed: **{MAX_FILE_MB} MB**" + ) + return + + await status.edit("⬇️ Downloading…") + temp_path = await message.download() + + if not temp_path: + await status.edit("❌ Download failed") + return + + if message.photo: + original_name = f"{uuid.uuid4().hex}.jpg" + elif hasattr(media, "file_name") and media.file_name: + original_name = safe_filename(media.file_name) + else: + original_name = f"{uuid.uuid4().hex}.bin" + + file_size = file_size or os.path.getsize(temp_path) + + file_id = uuid.uuid4().hex[:12] + ext = os.path.splitext(original_name)[1] + + if STORAGE_BACKEND == "local": + internal_path = os.path.join(UPLOAD_DIR, f"{file_id}{ext}") + os.replace(temp_path, internal_path) + stored_path = internal_path + else: + key = f"{file_id}{ext}" + s3.upload_file(temp_path, AWS_S3_BUCKET_NAME, key) + os.remove(temp_path) + stored_path = key + + user_mode = get_mode(message.from_user.id) + + if user_mode["ttl"] > 0: + ttl = user_mode["ttl"] + ttl_source = "πŸ‘€ Using your TTL" + else: + ttl = 0 + ttl_source = "β™Ύ No expiration" + + expires_at = ( + datetime.now(timezone.utc) + timedelta(seconds=ttl) + if ttl > 0 else None + ) + + await Database.pool.execute( + """ + INSERT INTO files ( + file_id, path, name, downloads, file_size, expires_at + ) + VALUES ($1, $2, $3, 0, $4, $5) + """, + file_id, + stored_path, + original_name, + file_size, + expires_at, + ) + + redis_client.delete(f"file:{file_id}") + redis_client.hset( + f"file:{file_id}", + mapping={ + "path": stored_path, + "name": original_name, + "downloads": 0, + "file_size": file_size, + "expires_at": int(expires_at.timestamp()) if expires_at else 0, + } + ) + + size_mb = file_size / (1024 * 1024) + + await status.edit( + "βœ… **File uploaded**\n\n" + f"{ttl_source}\n" + f"πŸ“„ **Name:** `{original_name}`\n" + f"πŸ“¦ **Size:** `{size_mb:.2f} MB`\n" + f"⏳ **Expires:** {format_ttl(ttl)}\n\n" + f"πŸ”— `{BASE_URL}/file/{file_id}`" + ) diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/utils/access.py b/bot/utils/access.py new file mode 100644 index 0000000..d5010a1 --- /dev/null +++ b/bot/utils/access.py @@ -0,0 +1,18 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from config import ALLOWED_USER_IDS + +def is_allowed(user_id: int) -> bool: + if ALLOWED_USER_IDS is None: + return True + return user_id in ALLOWED_USER_IDS diff --git a/bot/utils/mode.py b/bot/utils/mode.py new file mode 100644 index 0000000..00e146a --- /dev/null +++ b/bot/utils/mode.py @@ -0,0 +1,137 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import re +from pyrogram import filters +from bot.bot import tg_client +from cache.redis import redis_client +from bot.utils.access import is_allowed + +def get_mode(user_id: int) -> dict: + data = redis_client.hgetall(f"mode:{user_id}") + return { + "ttl": int(data.get("ttl", 0)), + } + + +def parse_ttl(value: str) -> int: + """ + Parse TTL string to seconds. + Supported: + 30 -> 30 minutes + 2h -> 2 hours + 1d -> 1 day + 0 -> never expire + """ + m = re.match(r"^(\d+)([mhd]?)$", value.lower()) + if not m: + return -1 + + amount, unit = m.groups() + amount = int(amount) + + if amount == 0: + return 0 + if unit == "h": + return amount * 3600 + if unit == "d": + return amount * 86400 + + return amount * 60 + + +def format_ttl(seconds: int) -> str: + if seconds == 0: + return "Never" + if seconds < 60: + return f"{seconds} seconds" + if seconds < 3600: + return f"{seconds // 60} minutes" + if seconds < 86400: + return f"{seconds // 3600} hours" + return f"{seconds // 86400} days" + + +@tg_client.on_message(filters.private & filters.command("mode")) +async def mode_handler(_, message): + if not message.from_user: + return + + user_id = message.from_user.id + if not is_allowed(user_id): + await message.reply("🚫 Admin only") + return + + args = message.text.split() + key = f"mode:{user_id}" + if len(args) == 1: + user = get_mode(user_id) + + if user["ttl"] > 0: + current = user["ttl"] + source = "πŸ‘€ Your TTL" + else: + current = 0 + source = "β™Ύ No expiration" + + await message.reply( + "πŸ“Œ **Mode (TTL)**\n\n" + f"Effective TTL: **{format_ttl(current)}**\n" + f"{source}\n\n" + "Set expiration for your uploads:\n" + "`/mode ttl 30` β†’ 30 minutes\n" + "`/mode ttl 2h` β†’ 2 hours\n" + "`/mode ttl 1d` β†’ 1 day\n" + "`/mode ttl 0` β†’ Never expire\n\n" + "`/mode reset`" + ) + return + + cmd = args[1].lower() + + if cmd == "ttl": + if len(args) != 3: + await message.reply("❌ Usage: `/mode ttl `") + return + + ttl_seconds = parse_ttl(args[2]) + + if ttl_seconds < 0: + await message.reply( + "❌ Invalid TTL format\n\n" + "Examples:\n" + "`/mode ttl 30`\n" + "`/mode ttl 2h`\n" + "`/mode ttl 1d`\n" + "`/mode ttl 0`" + ) + return + + if ttl_seconds > 30 * 86400: + await message.reply("❌ Max TTL is 30 days") + return + + redis_client.hset(key, mapping={"ttl": ttl_seconds}) + + await message.reply( + "⏳ TTL disabled" + if ttl_seconds == 0 + else f"⏳ TTL set to **{format_ttl(ttl_seconds)}**" + ) + return + + if cmd == "reset": + redis_client.delete(key) + await message.reply("♻️ Mode reset (Never expire)") + return + + await message.reply("❌ Unknown command") diff --git a/cache/__init__.py b/cache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cache/redis.py b/cache/redis.py new file mode 100644 index 0000000..cf7be41 --- /dev/null +++ b/cache/redis.py @@ -0,0 +1,36 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import redis +from config import REDIS_URL + +if not REDIS_URL: + raise RuntimeError("❌ REDIS_URL is required but not set") + +try: + redis_client = redis.from_url( + REDIS_URL, + decode_responses=True, + socket_connect_timeout=5, + socket_timeout=5, + ) + redis_client.ping() + print("βœ… Redis connected (required)") +except Exception as e: + raise RuntimeError(f"❌ Redis connection failed: {e}") + +def delete_pattern(pattern: str): + """ + Safely delete keys by pattern without blocking Redis. + """ + for key in redis_client.scan_iter(match=pattern): + redis_client.delete(key) diff --git a/config.py b/config.py new file mode 100644 index 0000000..cd23894 --- /dev/null +++ b/config.py @@ -0,0 +1,71 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +from dotenv import load_dotenv + +load_dotenv() + + +def str2bool(v): + return str(v).lower() in ("1", "true", "yes", "on") + + +def env_int(name: str, default=None): + try: + value = os.getenv(name) + return int(value) if value is not None else default + except ValueError: + return default + + +def require(name: str) -> str: + value = os.getenv(name) + if not value: + raise RuntimeError(f"{name} is required") + return value + +def normalize_base_url(value: str | None) -> str: + if not value: + return "http://localhost:8000" + if value.startswith("http://") or value.startswith("https://"): + return value + return f"https://{value}" + +API_ID = int(require("API_ID")) +API_HASH = require("API_HASH") +BOT_TOKEN = require("BOT_TOKEN") + +BASE_URL = normalize_base_url(os.getenv("BASE_URL")) +DATABASE_URL = require("DATABASE_URL") +REDIS_URL = require("REDIS_URL") + +GLOBAL_RATE_LIMIT_REQUESTS = env_int("GLOBAL_RATE_LIMIT_REQUESTS", 60) +GLOBAL_RATE_LIMIT_WINDOW = env_int("GLOBAL_RATE_LIMIT_WINDOW", 10) + +ALLOWED_USER_IDS = ( + list(map(int, os.getenv("ALLOWED_USER_IDS").split(","))) + if os.getenv("ALLOWED_USER_IDS") + else None +) + +ADMIN_ENABLED = str2bool(os.getenv("ADMIN_ENABLED", "false")) + +MAX_FILE_MB = env_int("MAX_FILE_MB", None) + +MAX_CONCURRENT_TRANSFERS = env_int("MAX_CONCURRENT_TRANSFERS", 3) + +STORAGE_BACKEND = os.getenv("STORAGE_BACKEND", "local") + +AWS_ENDPOINT_URL = os.getenv("AWS_ENDPOINT_URL") +AWS_S3_BUCKET_NAME = os.getenv("AWS_S3_BUCKET_NAME") +AWS_DEFAULT_REGION = os.getenv("AWS_DEFAULT_REGION", "auto") diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/database.py b/db/database.py new file mode 100644 index 0000000..b1c3402 --- /dev/null +++ b/db/database.py @@ -0,0 +1,84 @@ +# Copyright 2025 Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import asyncpg +from config import DATABASE_URL + +if not DATABASE_URL: + raise RuntimeError("❌ DATABASE_URL is required but not set") + + +CREATE_FILES_TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS files ( + file_id TEXT PRIMARY KEY, + path TEXT NOT NULL, + name TEXT NOT NULL, + downloads INTEGER NOT NULL DEFAULT 0, + file_size BIGINT, + expires_at TIMESTAMPTZ NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_files_expires +ON files (expires_at); +""" + +CREATE_ADMINS_TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS admins ( + id SERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); +""" +CREATE_SETTINGS_TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); +""" + +INSERT_DEFAULT_SETTINGS_SQL = """ +INSERT INTO settings (key, value) VALUES + ('default_max_downloads', '0'), + ('cleanup_enabled', 'true') +ON CONFLICT (key) DO NOTHING; +""" + +class Database: + pool: asyncpg.Pool | None = None + + @classmethod + async def connect(cls): + cls.pool = await asyncpg.create_pool( + DATABASE_URL, + min_size=1, + max_size=10, + command_timeout=60, + server_settings={"timezone": "UTC"}, + ) + await cls._init_schema() + print("βœ… PostgreSQL connected & schema ensured") + + @classmethod + async def _init_schema(cls): + async with cls.pool.acquire() as conn: + await conn.execute(CREATE_FILES_TABLE_SQL) + await conn.execute(CREATE_ADMINS_TABLE_SQL) + await conn.execute(CREATE_SETTINGS_TABLE_SQL) + await conn.execute(INSERT_DEFAULT_SETTINGS_SQL) + + @classmethod + async def close(cls): + if cls.pool: + await cls.pool.close() + print("πŸ›‘ PostgreSQL connection closed") diff --git a/docs/screenshots/admin-dashboard.jpg b/docs/screenshots/admin-dashboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d46747d0ce6152eccc0ac253c8e4756ed8c0a2c6 GIT binary patch literal 29680 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 J%I5!n69Axxew+XR literal 0 HcmV?d00001 diff --git a/docs/screenshots/telegram-upload.jpg b/docs/screenshots/telegram-upload.jpg new file mode 100644 index 0000000000000000000000000000000000000000..41c94784d04862df5326ac6c644dcde4df450dd1 GIT binary patch literal 113613 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..41f933a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +pyrofork==2.3.69 +fastapi==0.128.0 +uvicorn==0.38.0 +asyncpg==0.31.0 +python-dotenv==1.2.1 +redis==7.1.0 +TgCrypto-pyrofork==1.2.8 +passlib[argon2] +argon2-cffi==25.1.0 +python-multipart==0.0.20 +itsdangerous==2.2.0 +Jinja2==3.1.6 +boto3==1.42.25 \ No newline at end of file diff --git a/static/css/admin.css b/static/css/admin.css new file mode 100644 index 0000000..b7f5b93 --- /dev/null +++ b/static/css/admin.css @@ -0,0 +1,328 @@ +/* +Copyright 2025 Aman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*/ + +* { + box-sizing: border-box; +} + +html, body { + height: 100%; +} + +body { + margin: 0; + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + background: #f4f6fb; + color: #111827; +} + +main { + padding: 24px; + max-width: 1200px; + margin: 0 auto; +} + +.centered { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +body.centered { + margin: 0; + width: 100vw; + height: 100vh; +} + +.admin-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 28px; + background: #ffffff; + border-bottom: 1px solid #e5e7eb; +} + +.admin-header h1 { + font-size: 20px; + font-weight: 600; +} + +.header-actions { + display: flex; + gap: 10px; +} + +.nav-link { + text-decoration: none; + padding: 8px 12px; + border-radius: 6px; + font-size: 14px; + color: #111827; + background: #e5e7eb; +} + +.nav-link:hover { + background: #d1d5db; +} + +button { + border: none; + cursor: pointer; + font-size: 14px; +} + +.logout { + background: #ef4444; + color: white; + padding: 8px 14px; + border-radius: 6px; +} + +.logout:hover { + background: #dc2626; +} + +#theme-toggle { + background: #111827; + color: #f9fafb; + padding: 8px 12px; + border-radius: 6px; +} + +.card { + background: #ffffff; + padding: 18px 20px; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0,0,0,0.06); +} + +.card h3 { + font-size: 13px; + color: #6b7280; + margin-bottom: 6px; + font-weight: 500; +} + +.card p { + font-size: 28px; + font-weight: 700; +} + +.dashboard { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.search-bar { + margin-bottom: 16px; +} + +.search-bar input { + width: 100%; + padding: 10px 14px; + border-radius: 8px; + border: 1px solid #e5e7eb; + font-size: 14px; +} + +.table-card { + background: #ffffff; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0,0,0,0.06); + overflow: hidden; +} + +.table-header { + padding: 14px 18px; + border-bottom: 1px solid #e5e7eb; + font-weight: 600; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #6b7280; + padding: 12px 18px; + border-bottom: 1px solid #e5e7eb; +} + +td { + padding: 12px 18px; + border-bottom: 1px solid #f1f5f9; + font-size: 14px; +} + +td.right { + text-align: right; + font-weight: 600; +} + +tr:last-child td { + border-bottom: none; +} + +.badge { + padding: 4px 8px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.badge.green { + background: #dcfce7; + color: #166534; +} + +.badge.orange { + background: #ffedd5; + color: #9a3412; +} + +.action-btn { + padding: 6px 10px; + font-size: 12px; + border-radius: 6px; + margin-left: 4px; +} + +.action-btn:hover { + opacity: 0.9; +} + +.action-delete { + background: #ef4444; + color: white; +} + +.action-delete:hover { + background: #dc2626; +} + +.action-disable { + background: #f59e0b; + color: white; +} + +.action-disable:hover { + background: #d97706; +} + +.action-enable { + background: #22c55e; + color: white; +} + +.auth-page { + background: #f4f6fb; +} + +.auth-card { + max-width: 380px; + width: 100%; + padding: 28px; + text-align: center; +} + +.auth-card h2 { + margin-bottom: 16px; +} + +.auth-card input { + width: 100%; + margin-top: 12px; +} + +.auth-card button { + width: 100%; + margin-top: 16px; +} + +.alert { + padding: 10px 12px; + border-radius: 6px; + margin-bottom: 12px; + font-size: 14px; +} + +.alert-error { + background: #fee2e2; + color: #991b1b; +} + +body.dark { + background: #020617; + color: #e5e7eb; +} + +body.dark .admin-header, +body.dark .card, +body.dark .table-card { + background: #020617; + border: 1px solid #1e293b; + box-shadow: none; +} + +body.dark .table-header { + border-color: #1e293b; +} + +body.dark th { + color: #94a3b8; +} + +body.dark td { + border-color: #1e293b; +} + +body.dark .nav-link { + background: #1e293b; + color: #e5e7eb; +} + +body.dark #theme-toggle { + background: #e5e7eb; + color: #020617; +} + +body.dark input { + background: #020617; + color: #e5e7eb; + border: 1px solid #1e293b; +} + +body.dark input::placeholder { + color: #94a3b8; +} + +@media (max-width: 768px) { + .dashboard { + grid-template-columns: 1fr; + } + + th:nth-child(2), + td:nth-child(2) { + display: none; + } +} diff --git a/static/js/admin.js b/static/js/admin.js new file mode 100644 index 0000000..6636b6f --- /dev/null +++ b/static/js/admin.js @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Aman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + */ + +(function () { + const key = "admin-theme"; + const btn = document.getElementById("theme-toggle"); + + if (!btn) return; + + const saved = localStorage.getItem(key); + + if (saved === "dark") { + document.body.classList.add("dark"); + } else if (!saved) { + if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + document.body.classList.add("dark"); + } + } + + btn.onclick = () => { + document.body.classList.toggle("dark"); + localStorage.setItem( + key, + document.body.classList.contains("dark") ? "dark" : "light" + ); + }; +})();