Или история о том, как я превратил свой 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: Получение всех файлов из публичной папки.
Запуск бота
Чтобы запустить бота, выполните следующие шаги:
-
Установите зависимости:
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
ссылка на оригинал статьи https://habr.com/ru/articles/862434/
Добавить комментарий