Или история о том, как я превратил свой Telegram в файловую систему и почему мой компьютер теперь обижается на меня.
Всем привет! Сегодня я расскажу вам о том, как из обычного скучного дня вырос проект, который превратил мой Telegram в персональную файловую систему. Если вы когда-нибудь хотели почувствовать себя системным администратором в мессенджере или просто ищете способ спрятать файлы от самого себя, то эта статья для вас.
Предыстория
Всё началось с того, что мой жесткий диск стал напоминать шкаф с несвежими носками: места мало, найти ничего нельзя, и где-то там спрятан подарок от прошлого Нового года, а хранить все файлы в "избранных" это как-то фу-фу-фу. Я люблю искать нестандартные решения обычных проблем. Решение пришло само собой - перенести всё в облако! Но где найти такое место, чтобы и доступ был всегда под рукой, и чтобы никто не догадался заглянуть, и оно было БЕСПЛАТНОЕ? Конечно же, Telegram!
Идея бота
Цель была проста - создать бота, который позволит сохранять файлы, организовывать их по папкам и при необходимости делиться ими с другими пользователями. И всё это с минимальными затратами усилий (и максимальными затратами времени). Ведь кто сказал, что хранение файлов должно быть скучным?
Архитектура проекта
Проект состоит из нескольких основных модулей:
-
config.py: файл конфигурации (здесь хранится токен бота).
-
utils/: вспомогательные функции для работы с данными и навигацией.
-
handlers/: обработчики команд, сообщений и колбэков.
-
bot.py: основной файл для запуска бота.
-
requirements.txt: зависимости проекта.
Давайте подробнее рассмотрим каждый компонент.
Файл config.py
Начнём с простого. Здесь мы храним конфигурацию бота и основные настройки.
# config.py
BOT_TOKEN = "ВАШ_ТОКЕН_ОТ_TELEGRAM"
DATA_FILE = 'user_data.json'
Не забудьте заменить "ВАШ_ТОКЕН_ОТ_TELEGRAM"
на реальный токен, который можно получить у @BotFather. Файл DATA_FILE
используется для хранения данных пользователей. **Почему JSON? Потому что зачем заморачиваться с базой данных, когда можно оставить всё на честном слове и JSON-файле? Ведь кто вообще использует базы данных для Telegram-ботов в 2024 году? (Спойлер: все, кроме меня.)
Вспомогательные модули
utils/data_manager.py
Этот модуль отвечает за загрузку и сохранение данных пользователей.
# utils/data_manager.py
import json
import os
from config import DATA_FILE
def load_data():
if not os.path.exists(DATA_FILE):
return {"users": {}, "shared_folders": {}}
with open(DATA_FILE, 'r', encoding='utf-8') as file:
return json.load(file)
def save_data(data):
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
def init_user(data, user_id):
if user_id not in data["users"]:
data["users"][user_id] = {
"current_path": [],
"structure": {
"folders": {},
"files": []
},
"file_mappings": {} # Сопоставление short_id и file_id
}
Использование JSON-файла вместо базы данных — это как хранить семейные реликвии в картонной коробке. Надёжно? Возможно нет. Зато быстро и без лишних сложностей!
utils/navigation.py
Функции для навигации по файловой структуре.
# utils/navigation.py
def navigate_to_path(structure, path):
current = structure
for folder in path:
current = current["folders"][folder]
return current
Эта функция — наш проводник по виртуальному файловому лабиринту. Без неё мы бы заблудились в трёх соснах... то есть, в трёх папках.
utils/keyboards.py
Генерация клавиатур для навигации по папкам и файлам.
# utils/keyboards.py
from telebot import types
import uuid
import logging
logger = logging.getLogger(__name__)
def generate_markup(current, path, shared_key=None):
markup = types.InlineKeyboardMarkup()
# Кнопка "Вверх", если мы не в корне
if path:
callback_data = "up" if not shared_key else f"shared_up:{shared_key}"
markup.add(types.InlineKeyboardButton("⬆️ Вверх", callback_data=callback_data))
# Кнопки для папок
for folder in current["folders"]:
callback_data = f"folder:{folder}" if not shared_key else f"shared_folder:{shared_key}:{folder}"
markup.add(types.InlineKeyboardButton(f"📁 {folder}", callback_data=callback_data))
# Кнопки для файлов
for idx, file in enumerate(current["files"], start=1):
display_name = {
"text": f"📝 Текст {idx}",
"document": f"📄 Документ {idx}",
"photo": f"🖼️ Фото {idx}",
"video": f"🎬 Видео {idx}",
"audio": f"🎵 Аудио {idx}"
}.get(file["type"], f"📁 Файл {idx}")
short_id = file.get("short_id")
if not short_id:
logger.error(f"Файл без short_id: {file}")
continue
callback_data = f"file:{short_id}" if not shared_key else f"shared_file:{shared_key}:{short_id}"
markup.add(types.InlineKeyboardButton(display_name, callback_data=callback_data))
# Кнопка "Вернуть Все"
callback_data = "retrieve_all" if not shared_key else f"shared_retrieve_all:{shared_key}"
markup.add(types.InlineKeyboardButton("📤 Вернуть Все", callback_data=callback_data))
return markup
Здесь мы создаем интерактивные клавиатуры, которые позволяют пользователю легко навигировать по своей файловой системе. И, конечно же, кнопку "Вернуть Все", потому что кто не любит получить всё и сразу? Хотя, честно говоря, я не уверен, что кто-то действительно хочет заспамить свой чат сотней файлов, но зачем ограничивать возможности?
Обработчики
handlers/command_handlers.py
Здесь начинается веселье! Этот файл отвечает за обработку команд, которые пользователь отправляет боту. Например, создание папок, переход между ними и даже доступ к публичным папкам (в случае, если вы решили поделиться своими секретными рецептами борща).
Обработчики команд, таких как /start
, /mkdir
, /cd
, /up
, /getmydata
, /share
и /access
.
# handlers/command_handlers.py
from telebot import types
from telebot.types import Message
from utils.data_manager import load_data, save_data, init_user
from utils.navigation import navigate_to_path
from utils.keyboards import generate_markup
import uuid
import telebot
def register_command_handlers(bot: telebot.TeleBot):
@bot.message_handler(commands=['start'])
def handle_start(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
save_data(data)
bot.send_message(message.chat.id, "Добро пожаловать! Вот что я умею:nn"
"/mkdir <имя_папки> - Создать новую папкуn"
"/cd <имя_папки> - Перейти в папкуn"
"/up - Вернуться на уровень вышеn"
"/getmydata - Показать содержимое текущей папкиn"
"/share - Сделать текущую папку публичнойn"
"/access <ключ> - Получить доступ к публичной папке по ключу")
Команда /mkdir
Создаёт новую папку в текущей директории.
@bot.message_handler(commands=['mkdir'])
def handle_mkdir(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
try:
_, folder_name = message.text.split(maxsplit=1)
except ValueError:
bot.reply_to(message, "Укажите имя папки. Пример: /mkdir НоваяПапка")
return
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
if folder_name in current["folders"]:
bot.reply_to(message, "Папка с таким именем уже существует.")
else:
current["folders"][folder_name] = {"folders": {}, "files": []}
save_data(data)
bot.reply_to(message, f"Папка '{folder_name}' создана.")
Здесь мы используем функцию navigate_to_path
, чтобы попасть в текущую директорию пользователя, и затем добавляем новую папку в структуру. Потому что почему бы не сделать свою собственную файловую систему прямо в Telegram?
Команда /cd
Позволяет перемещаться между папками.
@bot.message_handler(commands=['cd'])
def handle_cd(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
try:
_, folder_name = message.text.split(maxsplit=1)
except ValueError:
bot.reply_to(message, "Укажите имя папки. Пример: /cd МояПапка")
return
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
if folder_name in current["folders"]:
data["users"][user_id]["current_path"].append(folder_name)
save_data(data)
bot.reply_to(message, f"Перешли в папку '{folder_name}'.")
else:
bot.reply_to(message, "Папка не найдена.")
Пользователь может перемещаться по своей файловой структуре, как в терминале, только без возможности удалить системные файлы (хотя кто знает...).
Команда /up
Возвращает на уровень выше в файловой структуре.
@bot.message_handler(commands=['up'])
def handle_up(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
if data["users"][user_id]["current_path"]:
popped = data["users"][user_id]["current_path"].pop()
save_data(data)
bot.reply_to(message, f"Вернулись из папки '{popped}'.")
else:
bot.reply_to(message, "Вы уже в корневой папке.")
Похож на команду cd ..
в терминале, только тут не нужно помнить, сколько уровней подняться.
Команда /getmydata
Показывает содержимое текущей папки.
@bot.message_handler(commands=['getmydata'])
def handle_getmydata(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
markup = generate_markup(current, data["users"][user_id]["current_path"])
try:
bot.send_message(message.chat.id, "Ваша папочная структура:", reply_markup=markup)
except telebot.apihelper.ApiTelegramException as e:
bot.send_message(message.chat.id, f"Ошибка при отправке клавиатуры: {str(e)}")
Теперь можно увидеть, что у вас внутри Telegram — папки, файлы и, конечно же, кнопки для навигации. Сделаем немного интерактивности в своём файловом хранилище.
Команды /share и /access
Позволяют делать папку публичной и получать доступ к публичным папкам по ключу.
@bot.message_handler(commands=['share'])
def handle_share(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
current_path = data["users"][user_id]["current_path"]
structure = data["users"][user_id]["structure"]
# Навигация до текущей папки
try:
current = navigate_to_path(structure, current_path)
except KeyError:
bot.reply_to(message, "Текущая папка не существует.")
return
# Генерация уникального ключа
unique_key = uuid.uuid4().hex
# Сохранение связи ключа с пользователем и путем
data["shared_folders"][unique_key] = {
"user_id": user_id,
"path": current_path.copy()
}
save_data(data)
# Отправка ключа пользователю
bot.reply_to(message, f"Папка успешно сделана публичной.nВаш ключ для доступа: `{unique_key}`nИспользуйте команду /access <ключ> чтобы получить доступ.", parse_mode="Markdown")
@bot.message_handler(commands=['access'])
def handle_access(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
try:
_, access_key = message.text.split(maxsplit=1)
except ValueError:
bot.reply_to(message, "Пожалуйста, укажите ключ доступа. Пример: /access <ключ>")
return
shared = data.get("shared_folders", {}).get(access_key)
if not shared:
bot.reply_to(message, "Неверный или несуществующий ключ доступа.")
return
owner_id = shared["user_id"]
path = shared["path"]
# Проверка, существует ли пользователь и папка
if owner_id not in data["users"]:
bot.reply_to(message, "Владелец папки не существует.")
return
owner_structure = data["users"][owner_id]["structure"]
try:
shared_folder = navigate_to_path(owner_structure, path)
except KeyError:
bot.reply_to(message, "Папка не найдена.")
return
# Генерация клавиатуры для публичной папки
markup = generate_markup(shared_folder, path, shared_key=access_key)
try:
bot.send_message(message.chat.id, "Содержимое публичной папки:", reply_markup=markup)
except telebot.apihelper.ApiTelegramException as e:
bot.send_message(message.chat.id, f"Ошибка при отправке клавиатуры: {str(e)}")
handlers/message_handlers.py
Обрабатывает все сообщения, которые не являются командами: текст, фото, документы, видео и аудио.
# handlers/message_handlers.py
from telebot.types import Message
from utils.data_manager import load_data, save_data, init_user
from utils.navigation import navigate_to_path
import telebot
import uuid # Для генерации уникальных short_id
import logging
logger = logging.getLogger(__name__)
def register_message_handlers(bot: telebot.TeleBot):
@bot.message_handler(content_types=['text', 'photo', 'document', 'video', 'audio'])
def handle_message(message: Message):
user_id = str(message.chat.id)
data = load_data()
init_user(data, user_id)
# Проверяем, что это не команда
if message.content_type == 'text' and message.text.startswith('/'):
return
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
if message.content_type == 'text':
# Сохраняем текст как файл типа 'text'
current["files"].append({"type": "text", "content": message.text})
save_data(data)
bot.reply_to(message, "Текстовое сообщение сохранено в текущей папке.")
elif message.content_type == 'document':
# Сохраняем документ
file_id = message.document.file_id
short_id = uuid.uuid4().hex[:8]
current["files"].append({"type": "document", "file_id": file_id, "file_name": message.document.file_name, "short_id": short_id})
data["users"][user_id]["file_mappings"][short_id] = file_id
save_data(data)
bot.reply_to(message, "Документ сохранён в текущей папке.")
# Аналогично для фото, видео и аудио
elif message.content_type == 'photo':
file_id = message.photo[-1].file_id
short_id = uuid.uuid4().hex[:8]
current["files"].append({"type": "photo", "file_id": file_id, "short_id": short_id})
data["users"][user_id]["file_mappings"][short_id] = file_id
save_data(data)
bot.reply_to(message, "Фото сохранено в текущей папке.")
elif message.content_type == 'video':
file_id = message.video.file_id
short_id = uuid.uuid4().hex[:8]
current["files"].append({"type": "video", "file_id": file_id, "short_id": short_id})
data["users"][user_id]["file_mappings"][short_id] = file_id
save_data(data)
bot.reply_to(message, "Видео сохранено в текущей папке.")
elif message.content_type == 'audio':
file_id = message.audio.file_id
short_id = uuid.uuid4().hex[:8]
current["files"].append({"type": "audio", "file_id": file_id, "short_id": short_id})
data["users"][user_id]["file_mappings"][short_id] = file_id
save_data(data)
bot.reply_to(message, "Аудио сохранено в текущей папке.")
Мы генерируем short_id
для каждого файла, чтобы потом можно было их легко идентифицировать и получать. Это как собственная система штрих-кодов, только без сканера и очередей в супермаркете.
handlers/callback_handlers.py
Этот файл отвечает за обработку всех нажатий на кнопки. Да-да, тех самых кнопок, которые вы видите в сообщениях от бота. Здесь мы пытаемся не сойти с ума, разбирая callback_data
и понимая, что же пользователь хотел сделать.
# handlers/callback_handlers.py
from telebot.types import CallbackQuery
from utils.data_manager import load_data, save_data, init_user
from utils.navigation import navigate_to_path
from utils.keyboards import generate_markup
import telebot
import logging
logger = logging.getLogger(__name__)
def register_callback_handlers(bot: telebot.TeleBot):
@bot.callback_query_handler(func=lambda call: True)
def handle_callback(call: CallbackQuery):
user_id = str(call.message.chat.id)
data = load_data()
init_user(data, user_id)
if call.data == "up":
# Код для перехода на уровень выше
if data["users"][user_id]["current_path"]:
popped = data["users"][user_id]["current_path"].pop()
bot.answer_callback_query(call.id, f"Вернулись из папки '{popped}'.")
else:
bot.answer_callback_query(call.id, "Вы уже в корневой папке.")
elif call.data.startswith("folder:"):
# Код для перехода в другую папку
folder_name = call.data.split(":", 1)[1]
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
if folder_name in current["folders"]:
data["users"][user_id]["current_path"].append(folder_name)
bot.answer_callback_query(call.id, f"Перешли в папку '{folder_name}'.")
else:
bot.answer_callback_query(call.id, "Папка не найдена.")
elif call.data.startswith("file:"):
# Код для получения файла по short_id
short_id = call.data.split(":", 1)[1]
file_info = None
for file in navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])["files"]:
if file.get("short_id") == short_id:
file_info = file
break
if not file_info:
bot.answer_callback_query(call.id, "Файл не найден.")
return
# Отправляем файл
try:
if file_info["type"] == "text":
bot.send_message(call.message.chat.id, file_info["content"])
elif file_info["type"] == "document":
bot.send_document(call.message.chat.id, file_info["file_id"])
elif file_info["type"] == "photo":
bot.send_photo(call.message.chat.id, file_info["file_id"])
elif file_info["type"] == "video":
bot.send_video(call.message.chat.id, file_info["file_id"])
elif file_info["type"] == "audio":
bot.send_audio(call.message.chat.id, file_info["file_id"])
bot.answer_callback_query(call.id, "Файл отправлен.")
except Exception as e:
logger.error(f"Ошибка при отправке файла: {e}")
bot.answer_callback_query(call.id, f"Ошибка при отправке файла: {str(e)}")
elif call.data == "retrieve_all":
# Код для получения всех файлов в текущей папке
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
try:
for file in current["files"]:
if file["type"] == "text":
bot.send_message(call.message.chat.id, f"Текст: {file['content']}")
elif file["type"] == "document":
bot.send_document(call.message.chat.id, file["file_id"])
elif file["type"] == "photo":
bot.send_photo(call.message.chat.id, file["file_id"])
elif file["type"] == "video":
bot.send_video(call.message.chat.id, file["file_id"])
elif file["type"] == "audio":
bot.send_audio(call.message.chat.id, file["file_id"])
else:
bot.send_message(call.message.chat.id, "Неизвестный тип файла.")
bot.answer_callback_query(call.id, "Все файлы отправлены.")
except Exception as e:
logger.error(f"Ошибка при отправке файлов: {e}")
bot.answer_callback_query(call.id, f"Ошибка при отправке файлов: {str(e)}")
else:
bot.answer_callback_query(call.id, "Неизвестная команда.")
# Обновляем папочную структуру после действия, если это необходимо
if call.data.startswith("folder:") or call.data == "up":
current = navigate_to_path(data["users"][user_id]["structure"], data["users"][user_id]["current_path"])
markup = generate_markup(current, data["users"][user_id]["current_path"])
try:
bot.edit_message_reply_markup(chat_id=call.message.chat.id,
message_id=call.message.message_id,
reply_markup=markup)
except telebot.apihelper.ApiTelegramException as e:
if "message is not modified" in str(e):
# Игнорируем ошибку, если сообщение не изменилось
pass
else:
logger.error(f"Ошибка обновления клавиатуры: {e}")
bot.send_message(call.message.chat.id, f"Ошибка обновления клавиатуры: {str(e)}")
save_data(data)
Здесь мы используем callback_data
, чтобы понять, какую кнопку нажал пользователь, и выполнить соответствующее действие. Это как выбирать приключение в книге, только с кнопками и без возможности проиграть (почти).
Основной файл bot.py
Это
# bot.py
import telebot
import config
from handlers.command_handlers import register_command_handlers
from handlers.callback_handlers import register_callback_handlers
from handlers.message_handlers import register_message_handlers
import time
import requests
import logging
# Настройка логирования
logging.basicConfig(
level=logging.DEBUG, # Для максимального количества информации в логах
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("bot.log"), # Запись логов в файл
logging.StreamHandler() # И вывод в консоль, чтобы всё сразу видеть
]
)
logger = logging.getLogger(__name__)
def start_bot():
bot = telebot.TeleBot(config.BOT_TOKEN)
# Регистрация обработчиков
register_command_handlers(bot)
register_callback_handlers(bot)
register_message_handlers(bot)
# Запуск бота с обработкой возможных исключений
while True:
try:
logger.info("Бот запущен и ожидает обновлений...")
bot.infinity_polling(timeout=60, long_polling_timeout=60)
except requests.exceptions.ReadTimeout:
logger.warning("Превышено время ожидания. Перезапуск...")
time.sleep(5)
except Exception as e:
logger.error(f"Произошла ошибка: {e}")
time.sleep(5)
if __name__ == "__main__":
start_bot()
Мы настраиваем логирование в bot.py
, чтобы потом, когда бот внезапно перестанет работать, можно было долго и мучительно искать причину. А пока что наслаждаемся бесконечным циклом while True
, который заставляет бота работать круглосуточно, как ночной сторож.
Демонстрация работы бота
Что ж, теория теорией, но давайте посмотрим, как это работает на практике. Я решил испытать бота и задокументировать этот процесс.
Создание папки и сохранение файлов
Сначала я запустил бота и ввёл команду /start
. Бот приветливо рассказал мне о своих возможностях.
Скриншот 1. Приветственное сообщение бота после команды /start
.
Далее я решил создать новую папку:
/mkdir Документы
Бот ответил, что папка успешно создана.
Скриншот 2. Создание новой папки Документы
.
Перехожу в эту папку:
/cd Документы
Скриншот 3. Переход в папку Документы
.
Теперь сохраняю в неё файл в формате txt
:
1.txt
Бот подтверждает, что файл сохранен.
Скриншот 4. Сохранение файла в текущую папку.
Просмотр содержимого папки
Чтобы увидеть, что находится в текущей папке, использую команду:
/getmydata
Бот отправляет мне клавиатуру с кнопками для навигации.
Скриншот 5. Просмотр содержимого папки Документы
с интерактивной клавиатурой.
Получение сохранённого файла
Нажимаю на кнопку с названием моего файла, и бот отправляет мне его содержимое.
Скриншот 6. Получение сохранённого файла.
Делимся папкой с другом
Решил поделиться папкой с другом. Использую команду:
/share
Бот выдаёт мне уникальный ключ доступа.
Скриншот 7. Получение ключа доступа для публичной папки.
Друг вводит команду:
/access <уникальный_ключ>
И получает доступ к моей папке.
Скриншот 8. Друг получает доступ к публичной папке и видит её содержимое.
Получение всех файлов сразу
Друг решает получить все файлы из моей папки и нажимает кнопку "📤 Вернуть Все". Бот отправляет ему все сохранённые файлы.
Скриншот 9: Получение всех файлов из публичной папки.
Запуск бота
Чтобы запустить бота, выполните следующие шаги:
-
Установите зависимости:
pip install -r requirements.txt
-
Создайте файл
config.py
с вашим токеном:BOT_TOKEN = "ВАШ_ТОКЕН_ОТ_TELEGRAM" DATA_FILE = 'user_data.json'
-
Запустите бота:
python bot.py
Если всё прошло успешно, бот должен начать работать, и вы сможете воспользоваться всеми его замечательными (и не очень) функциями. Если бот вдруг перестанет отвечать, не паникуйте — скорее всего, он решил взять перерыв (или вы допустили какую-то ошибку в коде, что тоже вполне возможно).
Заключение
Если вы хотите улучшить бота, вот несколько идей:
-
Реализовать поиск по файлам и папкам.
-
Добавить поддержку стикеров и голосовых сообщений.
-
Оптимизировать хранение данных (использовать базу данных вместо JSON-файла).
Спасибо, что дочитали до конца! Надеюсь, эта статья была для вас полезной. А я пойду разбираться, почему мой бот внезапно перестал отвечать на команды.
Полный исходный код бота доступен на GitHub: GitHub - tg_file_bot_3000
Автор: ruinik