Как я создал Telegram-бота для хранения файлов и чуть не стал библиотекарем

от автора

Или история о том, как я превратил свой 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, "Добро пожаловать! Вот что я умею:\n\n"                                           "/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: Получение всех файлов из публичной папки.


Запуск бота

Чтобы запустить бота, выполните следующие шаги:

  1. Установите зависимости:

    pip install -r requirements.txt 
  2. Создайте файл config.py с вашим токеном:

    BOT_TOKEN = "ВАШ_ТОКЕН_ОТ_TELEGRAM" DATA_FILE = 'user_data.json' 
  3. Запустите бота:

    python bot.py 

Если всё прошло успешно, бот должен начать работать, и вы сможете воспользоваться всеми его замечательными (и не очень) функциями. Если бот вдруг перестанет отвечать, не паникуйте — скорее всего, он решил взять перерыв (или вы допустили какую-то ошибку в коде, что тоже вполне возможно).


Заключение

Если вы хотите улучшить бота, вот несколько идей:

  • Реализовать поиск по файлам и папкам.

  • Добавить поддержку стикеров и голосовых сообщений.

  • Оптимизировать хранение данных (использовать базу данных вместо JSON-файла).


Спасибо, что дочитали до конца! Надеюсь, эта статья была для вас полезной. А я пойду разбираться, почему мой бот внезапно перестал отвечать на команды.

Полный исходный код бота доступен на GitHub: GitHub — tg_file_bot_3000


ссылка на оригинал статьи https://habr.com/ru/articles/862434/