Compare commits

..

10 Commits

Author SHA1 Message Date
61b5205503 大幅增加超时时间, 增加重命名机制 2025-12-27 10:45:21 +08:00
f0d1343955 更新 gitignore 2025-12-27 09:52:49 +08:00
nickdu088
07c64209c4 Add screenshot to README_cn.md 2025-11-03 10:49:28 +11:00
nickdu088
208ce13444 Revise example folder structure in README
Updated the example folder structure section to reflect server organization and added a screenshot.
2025-11-03 10:48:44 +11:00
nickdu088
40505239f2 Add files via upload 2025-11-03 10:45:28 +11:00
nickdu088
a937053d71 Add Chinese README link and update formatting 2025-11-03 10:44:59 +11:00
nickdu088
355f492c7e Update project title in README_cn.md 2025-11-03 09:48:27 +11:00
nickdu088
8305ae4d52 Update README_cn.md for bot file name changes
Updated README_cn.md to reflect changes in file names and descriptions.
2025-11-03 09:47:59 +11:00
nickdu088
8fa4c1315d Update README for bot filename and description 2025-11-03 09:46:25 +11:00
nickdu088
271b4432ae Add Chinese README for Secure File Bot
添加中文 README 文件,详细介绍了私有 Telegram 文件存储机器人的功能、安装步骤和指令说明。
2025-11-03 08:25:31 +11:00
9 changed files with 422 additions and 42 deletions

9
.env Normal file
View File

@@ -0,0 +1,9 @@
# 你的机器人 Token (使用你新生成的!)
BOT_TOKEN=8449769801:AAGdXIfkGfVIHg72FPXkBvS_7p5Lii8VEF8
# 你的用户 ID
AUTHORIZED_USER_ID=5701893747
# 你从 my.telegram.org 获取的凭据
TELEGRAM_API_ID=17920091
TELEGRAM_API_HASH= fcd526da4fa4f802b1a1b61c6370f8df

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
data/*
local_storage/*

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim-bookworm
# 设置环境变量,确保 Python 输出为 UTF-8
ENV PYTHONIOENCODING=utf-8
ENV LANG C.UTF-8
# 设置工作目录
WORKDIR /app
# 复制依赖文件并安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制所有的应用程序代码到工作目录
COPY bot.py .
# 声明存储目录为一个卷
# 这表明 /app/local_storage 路径下的数据应该被持久化
# 注意:实际的路径将由 bot.py 中的 STORAGE_DIR 环境变量决定
# 我们在这里声明默认值
VOLUME /app/local_storage
CMD ["python", "bot.py"]

View File

@@ -1,4 +1,5 @@
# 🛡️ Secure File Bot (aiogram) # 🛡️ Personal Secure File Bot (aiogram)
[中文说明](README_cn.md)
A private **Telegram file storage bot** built with **[aiogram 3.x](https://docs.aiogram.dev/)**. A private **Telegram file storage bot** built with **[aiogram 3.x](https://docs.aiogram.dev/)**.
It securely accepts messages, pictures, videos, and documents **only from a specific Telegram user**, It securely accepts messages, pictures, videos, and documents **only from a specific Telegram user**,
@@ -6,6 +7,8 @@ then saves them to a local storage folder.
You can search, retrieve, delete, and summarize your stored files — all via Telegram commands. You can search, retrieve, delete, and summarize your stored files — all via Telegram commands.
[How to create a telegram bot](https://core.telegram.org/bots/tutorial)
--- ---
## ✨ Features ## ✨ Features
@@ -43,7 +46,7 @@ STORAGE_DIR = "local_storage" # Folder where files are saved
## 🚀 Run the Bot ## 🚀 Run the Bot
``` ```
python secure_file_bot_aiogram.py python bot.py
``` ```
Once running, open your bot in Telegram and type: Once running, open your bot in Telegram and type:
``` ```
@@ -69,10 +72,10 @@ Command Description
* When you send a document → saved using its original name and MIME extension * When you send a document → saved using its original name and MIME extension
* Everything is organized in your local_storage/ folder * Everything is organized in your local_storage/ folder
## 🧩 Example Folder Structure ## 🧩 Example Folder Structure in Server
``` ```
secure_file_bot_aiogram/ secure_file_bot_aiogram/
├── secure_file_bot_aiogram.py ├── bot.py
├── requirements.txt ├── requirements.txt
├── README.md ├── README.md
└── local_storage/ └── local_storage/
@@ -81,4 +84,4 @@ secure_file_bot_aiogram/
├── 20251103_141223.mp4 ├── 20251103_141223.mp4
└── ... └── ...
``` ```
![screen shot](screenshot.jpeg)

87
README_cn.md Normal file
View File

@@ -0,0 +1,87 @@
# 🛡️ 个人专属TG机器人 (aiogram)
一个基于 **[aiogram 3.x](https://docs.aiogram.dev/)** 构建的 **私有 Telegram 文件存储机器人**
它只允许指定的 Telegram 用户发送消息、图片、视频和文件,并自动保存到本地文件夹中。
你可以通过 Telegram 命令来 **搜索、获取、删除、概览** 文件,未来还可集成 OpenAI 或第三方 API。
[如何申请Telegram机器人](https://core.telegram.org/bots/tutorial)
---
## ✨ 功能特点
**访问控制** — 只有配置的 `user_id` 才能使用机器人。
**自动存储** — 自动保存文本、图片、视频和文件到本地。
**支持所有格式** — JPG、PNG、WEBP、MP4、AVI、MOV 等都能识别。
**搜索与获取** — 支持关键字搜索与快速取回文件。
**删除功能** — 可通过命令删除指定文件。
**概览功能** — 查看所有文件、大小、保存时间。
**可扩展性强** — 方便未来对接 OpenAI 或其他 API。
---
## 🧰 安装步骤
### 1. 克隆项目
```bash
git clone https://github.com/yourusername/secure-file-bot.git
cd secure-file-bot
```
### 2. 安装依赖
```
pip install aiogram==3.13
```
### 3. 配置参数
打开 Python 脚本 secure_file_bot_aiogram.py修改以下变量
```
BOT_TOKEN = "YOUR_BOT_TOKEN_HERE" # 从 @BotFather 获取的机器人 Token
AUTHORIZED_USER_ID = 123456789 # 你的 Telegram 用户 ID
STORAGE_DIR = "local_storage" # 本地文件存储目录
```
## 🚀 启动机器人
```
python bot.py
```
启动后,在 Telegram 中向机器人发送:
```
/start
```
## 💬 指令说明
| 命令 | 功能 |
| ----------------- | ------------------ |
| `/start` | 初始化机器人并创建存储目录 |
| `/overview` | 查看所有已保存文件的列表、大小与时间 |
| `/search <关键词>` | 按名称搜索文件 |
| `/get <编号>` | 从搜索结果中取回文件 |
| `/delete <文件名>` | 删除匹配的文件 |
| *(发送文本、图片、视频或文档)* | 自动保存到本地 |
## 🧠 工作原理
* 发送文本 → 自动保存为 .txt
* 发送图片 → 自动识别并保存为正确格式JPG、PNG、WEBP...
* 发送视频 → 自动识别并保存为正确格式MP4、AVI、MOV...
* 发送文件 → 使用原文件名与 MIME 类型保存
* 所有内容都会存放在 local_storage/ 文件夹中
## 🧩 项目结构示例
```
secure_file_bot_aiogram/
├── bot.py
├── requirements.txt
├── README.md
└── local_storage/
├── 20251103_140501.txt
├── 20251103_141012.jpg
├── 20251103_141223.mp4
└── ...
```
![screen shot](screenshot.jpeg)

287
bot.py
View File

@@ -1,32 +1,57 @@
# -*- coding: utf-8 -*-
import os import os
import logging import logging
import mimetypes import mimetypes
import re
import asyncio
from datetime import datetime from datetime import datetime
from aiogram import Bot, Dispatcher, types, F from aiogram import Bot, Dispatcher, types, F
from aiogram.filters import Command from aiogram.filters import Command, StateFilter # <--- [修改1] 引入 StateFilter
from aiogram.types import FSInputFile from aiogram.types import FSInputFile, ForceReply
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
# === CONFIGURATION === # === CONFIGURATION ===
BOT_TOKEN = "YOUR_BOT_TOKEN_HERE" # 从环境变量中安全地读取配置
AUTHORIZED_USER_ID = 123456789 # Replace with your Telegram user_id BOT_TOKEN = os.environ.get("BOT_TOKEN")
STORAGE_DIR = "local_storage" # 将 USER_ID 转换为整数,如果未设置则默认为 0
AUTHORIZED_USER_ID = int(os.environ.get("AUTHORIZED_USER_ID", 0))
# 允许从环境变量覆盖存储目录
STORAGE_DIR = os.environ.get("STORAGE_DIR", "local_storage")
# === 启动时检查 ===
if not BOT_TOKEN:
raise ValueError("错误:未设置 BOT_TOKEN 环境变量。")
if AUTHORIZED_USER_ID == 0:
raise ValueError("错误:未设置 AUTHORIZED_USER_ID 环境变量。")
# === LOGGING === # === LOGGING ===
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# === INITIAL SETUP === # === INITIAL SETUP ===
bot = Bot(token=BOT_TOKEN) # 增加 timeout 设置,防止 session 层面的过早断开
local_api_server = TelegramAPIServer.from_base("http://bot-api:8081")
session = AiohttpSession(api=local_api_server, timeout=7200)
bot = Bot(token=BOT_TOKEN, session=session)
dp = Dispatcher() dp = Dispatcher()
# === STATE DEFINITION ===
class RenameState(StatesGroup):
waiting_for_new_name = State()
# === HELPER FUNCTIONS ===
def ensure_storage(): def ensure_storage():
"""Ensure the local storage directory exists.""" """Ensure the local storage directory exists."""
if not os.path.exists(STORAGE_DIR): if not os.path.exists(STORAGE_DIR):
os.makedirs(STORAGE_DIR) os.makedirs(STORAGE_DIR)
logger.info("Storage folder created.") logger.info(f"Storage folder created at: {STORAGE_DIR}")
else: else:
logger.info("Storage folder exists.") pass
def is_authorized(user_id: int) -> bool: def is_authorized(user_id: int) -> bool:
"""Check if the user is authorized.""" """Check if the user is authorized."""
@@ -48,10 +73,13 @@ def get_storage_summary():
for root, dirs, files in os.walk(STORAGE_DIR): for root, dirs, files in os.walk(STORAGE_DIR):
for f in files: for f in files:
path = os.path.join(root, f) path = os.path.join(root, f)
size = os.path.getsize(path) try:
mtime = datetime.fromtimestamp(os.path.getmtime(path)) size = os.path.getsize(path)
total_size += size mtime = datetime.fromtimestamp(os.path.getmtime(path))
files_info.append((f, size, mtime)) total_size += size
files_info.append((f, size, mtime))
except FileNotFoundError:
logger.warning(f"File not found during summary: {path}")
return total_size, files_info return total_size, files_info
def get_extension_from_mime(mime_type: str) -> str: def get_extension_from_mime(mime_type: str) -> str:
@@ -79,9 +107,14 @@ async def cmd_overview(message: types.Message):
await message.answer("📁 Storage is empty.") await message.answer("📁 Storage is empty.")
return return
files_info.sort(key=lambda x: x[2], reverse=True)
summary = "\n".join( summary = "\n".join(
[f"{f}{s/1024:.1f} KB — {d.strftime('%Y-%m-%d %H:%M:%S')}" for f, s, d in files_info] [f"{f}{s/1024:.1f} KB — {d.strftime('%Y-%m-%d %H:%M')}" for f, s, d in files_info]
) )
if len(summary) > 3500:
summary = summary[:3500] + "\n... (list truncated)"
await message.answer(f"📦 Files:\n{summary}\n\nTotal: {total_size/1024:.1f} KB") await message.answer(f"📦 Files:\n{summary}\n\nTotal: {total_size/1024:.1f} KB")
@dp.message(Command("search")) @dp.message(Command("search"))
@@ -102,10 +135,11 @@ async def cmd_search(message: types.Message):
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i, f in enumerate(results): for i, f in enumerate(results[:50]):
builder.button(text=f"{i+1}. {os.path.basename(f)}", callback_data=f"get|{f}") builder.button(text=f"{i+1}. {os.path.basename(f)}", callback_data=f"get|{f}")
builder.adjust(1)
await message.answer("🔍 Found files:", reply_markup=builder.as_markup()) await message.answer(f"🔍 Found {len(results)} file(s):", reply_markup=builder.as_markup())
@dp.callback_query(F.data.startswith("get|")) @dp.callback_query(F.data.startswith("get|"))
async def cb_get_file(callback: types.CallbackQuery): async def cb_get_file(callback: types.CallbackQuery):
@@ -115,10 +149,16 @@ async def cb_get_file(callback: types.CallbackQuery):
path = callback.data.split("|", 1)[1] path = callback.data.split("|", 1)[1]
if not os.path.exists(path): if not os.path.exists(path):
await callback.message.answer("File not found.") await callback.message.answer("File not found (it may have been deleted).")
await callback.answer()
return return
await callback.message.answer_document(FSInputFile(path)) await callback.answer(f"Sending {os.path.basename(path)}...")
try:
await callback.message.answer_document(FSInputFile(path))
except Exception as e:
logger.error(f"Failed to send file {path}: {e}")
await callback.message.answer(f"Error sending file: {e}")
@dp.message(Command("delete")) @dp.message(Command("delete"))
async def cmd_delete(message: types.Message): async def cmd_delete(message: types.Message):
@@ -128,7 +168,7 @@ async def cmd_delete(message: types.Message):
args = message.text.split(maxsplit=1) args = message.text.split(maxsplit=1)
if len(args) < 2: if len(args) < 2:
await message.answer("Usage: /delete <filename>") await message.answer("Usage: /delete <filename_keyword>")
return return
keyword = args[1] keyword = args[1]
@@ -137,27 +177,44 @@ async def cmd_delete(message: types.Message):
await message.answer("File not found.") await message.answer("File not found.")
return return
deleted_files = []
for f in matches: for f in matches:
os.remove(f) try:
os.remove(f)
deleted_files.append(os.path.basename(f))
except OSError as e:
logger.error(f"Failed to delete {f}: {e}")
if deleted_files:
await message.answer(f"🗑️ Deleted {len(deleted_files)} file(s):\n" + "\n".join(deleted_files))
else:
await message.answer("Could not delete matching files.")
await message.answer(f"🗑️ Deleted {len(matches)} file(s).")
# === MESSAGE HANDLER FOR FILES/TEXT === # === MESSAGE HANDLER FOR FILES/TEXT ===
@dp.message(F.photo | F.video | F.document | F.text) # [修改2] 增加 StateFilter(None),只有在当前没有处于任何 FSM 状态时,才执行这个保存逻辑
# 这样当我们在 waiting_for_new_name 状态时,就不会误触发这里
@dp.message(F.photo | F.video | F.document | F.text, StateFilter(None))
async def handle_incoming(message: types.Message): async def handle_incoming(message: types.Message):
if not is_authorized(message.from_user.id): if not is_authorized(message.from_user.id):
await message.answer("🚫 Access denied.") await message.answer("🚫 Access denied.")
return return
ensure_storage() ensure_storage()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# --- Handle text --- # --- Handle text ---
if message.text and not (message.photo or message.video or message.document): if message.text and not (message.photo or message.video or message.document):
filename = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" filename = f"{timestamp}.txt"
path = os.path.join(STORAGE_DIR, filename) subdir = "documents"
target_dir = os.path.join(STORAGE_DIR, subdir)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
path = os.path.join(target_dir, filename)
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
f.write(message.text) f.write(message.text)
await message.answer(f"💾 Text saved as `{filename}`", parse_mode="Markdown") await message.answer(f"💾 Text saved as `{filename}` (in /{subdir})", parse_mode="Markdown")
return return
# --- Handle media/documents --- # --- Handle media/documents ---
@@ -167,36 +224,192 @@ async def handle_incoming(message: types.Message):
if message.document: if message.document:
file_obj = message.document file_obj = message.document
file_name = message.document.file_name or f"{datetime.now().strftime('%Y%m%d_%H%M%S')}" file_name = message.document.file_name
mime_type = message.document.mime_type mime_type = message.document.mime_type
elif message.photo: elif message.photo:
file_obj = message.photo[-1] # Highest resolution file_obj = message.photo[-1] # Highest resolution
file_name = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg" file_name = f"{timestamp}.jpg"
mime_type = "image/jpeg" mime_type = "image/jpeg"
elif message.video: elif message.video:
file_obj = message.video file_obj = message.video
file_name = message.video.file_name or f"{datetime.now().strftime('%Y%m%d_%H%M%S')}" file_name = message.video.file_name
mime_type = message.video.mime_type mime_type = message.video.mime_type
if not file_name: # Fallback name
file_name = timestamp
if file_obj: if file_obj:
ext = os.path.splitext(file_name)[1] logger.info(f"Received file. Original file_name variable: '{file_name}' (MIME: {mime_type})")
base_name, ext = os.path.splitext(file_name)
if not ext: if not ext:
ext = get_extension_from_mime(mime_type) ext = get_extension_from_mime(mime_type)
if not ext: if not ext:
ext = ".bin" # Fallback ext = ".bin"
safe_base_name = "".join(c for c in base_name if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()
final_name = f"{timestamp}_{safe_base_name}{ext}"
# --- 子目录分类逻辑 ---
subdir = "others"
if mime_type:
if mime_type.startswith("image/"):
subdir = "pictures"
elif mime_type.startswith("video/"):
subdir = "video"
elif mime_type.startswith("audio/"):
subdir = "audio"
elif mime_type in ("application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/rtf", "application/epub+zip", "text/plain", "text/csv", "text/markdown", "text/html"):
subdir = "documents"
elif mime_type in ("application/zip", "application/vnd.rar", "application/x-zip-compressed", "application/x-rar-compressed", "application/gzip", "application/x-7z-compressed", "application/x-tar", "application/x-bzip2"):
subdir = "archives"
elif mime_type in ("application/vnd.android.package-archive", "application/octet-stream", "application/x-msdownload", "application/x-apple-diskimage", "application/x-debian-package", "application/x-rpm", "application/x-sh", "application/x-csh", "application/bat"):
subdir = "executables"
elif mime_type in ("application/x-iso9660-image",):
subdir = "disk_images"
target_dir = os.path.join(STORAGE_DIR, subdir)
if not os.path.exists(target_dir):
try:
os.makedirs(target_dir)
except OSError as e:
logger.error(f"Failed to create subdirectory {target_dir}: {e}")
target_dir = STORAGE_DIR
destination_path = os.path.join(target_dir, final_name)
logger.info(f"Target file: '{final_name}' in '{subdir}'")
# === 重试循环 ===
max_retries = 3
for attempt in range(max_retries):
try:
logger.info(f"Requesting file info for file_id: {file_obj.file_id} (Attempt {attempt+1}/{max_retries})")
# 7200秒超时
file_info = await bot.get_file(file_obj.file_id, request_timeout=7200)
source_path = file_info.file_path
logger.info(f"File info received. Path on disk: {source_path}")
final_name = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}{ext}" if not os.path.abspath(source_path).startswith(os.path.abspath(STORAGE_DIR)):
path = os.path.join(STORAGE_DIR, final_name) logger.error(f"FATAL: File path {source_path} is outside STORAGE_DIR. Fallback download.")
file_info = await bot.get_file(file_obj.file_id) await bot.download_file(file_info.file_path, destination_path)
await bot.download_file(file_info.file_path, path) else:
await message.answer(f"💾 Saved `{final_name}` ({mime_type or 'unknown'})", parse_mode="Markdown") if os.path.exists(source_path):
os.rename(source_path, destination_path)
logger.info(f"Moved file to {destination_path}")
else:
raise FileNotFoundError(f"API says file is at {source_path} but it is missing.")
break
except Exception as e:
logger.warning(f"Attempt {attempt+1} failed: {e}")
if attempt == max_retries - 1:
logger.error("All retry attempts failed.")
await message.answer(f"❌ Failed after {max_retries} attempts. Server Error: {e}")
return
await asyncio.sleep(5)
# === 文件保存成功后 ===
try:
builder = InlineKeyboardBuilder()
builder.button(text="✏️ 重命名", callback_data="rename_file")
await message.answer(
f"💾 Saved `{final_name}` (in /{subdir})",
parse_mode="Markdown",
reply_markup=builder.as_markup()
)
except Exception as e:
logger.error(f"Failed to send success message: {e}")
# === RENAME FEATURE HANDLERS ===
@dp.callback_query(F.data == "rename_file")
async def click_rename(callback: types.CallbackQuery, state: FSMContext):
if not is_authorized(callback.from_user.id):
return
message_text = callback.message.text
match = re.search(r"Saved (.+) \(in /(.+)\)", message_text)
if not match:
await callback.answer("❌ 无法解析文件路径,可能消息格式已变。", show_alert=True)
return
old_filename = match.group(1).strip()
subdir = match.group(2).strip()
full_path = os.path.join(STORAGE_DIR, subdir, old_filename)
if not os.path.exists(full_path):
await callback.answer("❌ 文件已不存在(可能已被删除或移动)。", show_alert=True)
return
await state.update_data(
file_path=full_path,
file_dir=os.path.dirname(full_path),
old_ext=os.path.splitext(old_filename)[1]
)
await state.set_state(RenameState.waiting_for_new_name)
await callback.message.answer(
f"当前文件名: `{old_filename}`\n请直接回复**新的文件名** (无需后缀):",
parse_mode="Markdown",
reply_markup=ForceReply(selective=True)
)
await callback.answer()
@dp.message(RenameState.waiting_for_new_name)
async def process_rename(message: types.Message, state: FSMContext):
if not is_authorized(message.from_user.id):
return
new_name_input = message.text.strip()
safe_new_name = "".join(c for c in new_name_input if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()
if not safe_new_name:
await message.answer("❌ 文件名无效,请重新输入。")
return
data = await state.get_data()
old_path = data.get('file_path')
file_dir = data.get('file_dir')
old_ext = data.get('old_ext')
if not old_path or not os.path.exists(old_path):
await message.answer("❌ 原文件已丢失,操作取消。")
await state.clear()
return
if not os.path.splitext(safe_new_name)[1]:
safe_new_name += old_ext
new_path = os.path.join(file_dir, safe_new_name)
if os.path.exists(new_path):
await message.answer("❌ 目标文件名已存在,请换一个名字。")
return
try:
os.rename(old_path, new_path)
await message.answer(f"✅ 重命名成功!\n`{safe_new_name}`", parse_mode="Markdown")
except OSError as e:
logger.error(f"Rename failed: {e}")
await message.answer(f"❌ 重命名失败: {e}")
await state.clear()
# === MAIN ENTRY === # === MAIN ENTRY ===
async def main(): async def main():
ensure_storage() ensure_storage()
logger.info("🚀 Bot is starting...") logger.info(f"🚀 Bot is starting... Storage: {STORAGE_DIR}")
await dp.start_polling(bot) await dp.start_polling(bot)
if __name__ == "__main__": if __name__ == "__main__":
import asyncio
asyncio.run(main()) asyncio.run(main())

41
docker-compose.yml Normal file
View File

@@ -0,0 +1,41 @@
version: '3.8'
services:
bot:
build:
context: .
container_name: secure-file-bot
restart: unless-stopped
depends_on:
- bot-api
volumes:
# 1. 路径已更改,以匹配 bot-api 的默认值
- ./data:/var/lib/telegram-bot-api
environment:
# 2. 告诉 bot.py 脚本新的存储目录
- STORAGE_DIR=/var/lib/telegram-bot-api
env_file:
- ./.env
bot-api:
image: aiogram/telegram-bot-api:latest # 使用你指定的镜像
container_name: local-bot-api
restart: unless-stopped
# 3. 删除了 'command:' 行。
# TELEGRAM_LOCAL=1 环境变量会自动处理 --local 标志。
ports:
- "8081:8081"
volumes:
# 4. 两个服务现在共享完全相同的路径
- ./data:/var/lib/telegram-bot-api
environment:
# 这些将从 .env 文件中读取
- TELEGRAM_API_ID=${TELEGRAM_API_ID}
- TELEGRAM_API_HASH=${TELEGRAM_API_HASH}
# 这一行是 aiogram 镜像所需要的,它会自动启用 --local
- TELEGRAM_LOCAL=1

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
aiogram==3.13

BIN
screenshot.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB