Привет, Хабр! Эта статья является продолжением первой статьи.
Telegram как NAS/FTP .
Речь идет об одном и том же боте — TeleFS, он приобрел важную составляющую — публичность.
Точнее, пользователи ботов теперь могут делиться своими файлами и папками с любыми другими пользователями Telegram. И на этот раз мы поговорим о том, как и с помощью чего создавался бот.
Куча
Язык разработки — Java 8. Движок представляет собой веб-сервис на основе Play! Фреймворк версии 2.7. Хранилище — Postgres v10 Mapper — MyBatis (плагин mybatis-guice) Фронт-сервер — nginx (его задача ограничивается предоставлением tls) В общем смысле этот стек совершенно не критичен.Можно использовать любой язык и технологию; есть только два строгих ограничения:
- движок должен уметь использовать http, так как Telegram Bot API является http-endpoints и получать обновления все равно приятнее без длинного опроса
- база данных должна быть реляционной, поскольку бот управляет иерархией, основанной на первичных ключах с подзапросами.
Архитектура хранилища
Задача бота — хранить структуры «файловой системы» (ФС) для каждого пользователя.Идея бота изначально рассматривалась как некое «общее» хранилище для всех пользователей, но после нескольких интенсивных экспериментов была отвергнута как несостоятельная.
Взамен каждый пользователь получал собственную файловую систему, которая хранится в отдельной плоской таблице со своей иерархией.
Каждый файл, каталог и заметка представляют собой одну физическую запись в таблице владельцев, а все представления записи (дочерний узел в ФС, открытый доступ для других пользователей, результат поиска) являются представлениями в базе данных.
Давайте посмотрим на особенности.
Вот таблица одного пользователя (с id 990823086):
Именно так хранятся физические записи всех узлов в его файловой системе.
Вверху этой таблицы находится представление для отображения иерархии:
Мы получили минимально достаточную структуру хранения для функциональности в режиме «каждый сам за себя».create view fs_paths_990823086(id, parent_id, owner, path) as WITH RECURSIVE tree AS ( SELECT fs_user_990823086.id, fs_user_990823086.name, fs_user_990823086.parent_id, fs_user_990823086.owner, ARRAY [fs_user_990823086.name] AS fpath FROM fs_user_990823086 WHERE fs_user_990823086.parent_id IS NULL UNION ALL SELECT si.id, si.name, si.parent_id, si.owner, sp.fpath || si.name AS fpath FROM fs_user_990823086 si JOIN tree sp ON si.parent_id = sp.id ) SELECT tree.id, tree.parent_id, tree.owner, array_to_string(tree.fpath, '/'::text) AS path FROM tree;
Но наша первоначальная идея заключалась именно в «разделении» ресурсов.
Эта проблема решается за счет дополнительной физической таблицы с декларацией разделения доступа и некоторым количеством дополнительных представлений.
Во-первых, давайте договоримся, что мы вообще не будем использовать вышеупомянутую таблицу в примерах поиска контента.
Давайте абстрагируемся от него на другой слой, который тоже будет представлением, но агрегирующим: create view fs_user_990823086(id, parent_id, name, type, ref_id, options, owner, rw) as
SELECT fs_data_990823086.id,
fs_data_990823086.parent_id,
fs_data_990823086.name,
fs_data_990823086.type,
fs_data_990823086.ref_id,
fs_data_990823086.options,
990823086::bigint AS owner,
true AS rw
FROM fs_data_990823086;
В этом слое мы добавим то, что необходимо для организации разделения доступа: понятие «владелец» и примитивные «права доступа» в виде rw (чтение/запись).
И теперь мы получим все элементы ФС из этого слоя.
Следующий шаг предполагает, что кто-то, кто хочет поделиться доступом к своему каталогу, должен каким-то образом выделить часть иерархии своей файловой системы; кусок, в котором этот каталог будет корневым.
Делается это очень просто — через следующее представление: create view fs_share_990823086_990823086_5786da(id, name, type, parent_id, ref_id, options, owner, rw) as
WITH RECURSIVE tree AS (
SELECT fs_data_990823086.id,
fs_data_990823086.name,
fs_data_990823086.type,
'8b990db3-e41a-4130-990d-b3e41a71305a'::uuid AS parent_id,
fs_data_990823086.options,
fs_data_990823086.ref_id
FROM fs_data_990823086
WHERE fs_data_990823086.id = 'c07b37a1-4abf-4a0c-bb37-a14abf6a0c0b'::uuid
UNION ALL
SELECT si.id,
si.name,
si.type,
si.parent_id,
si.options,
si.ref_id
FROM fs_data_990823086 si
JOIN tree sp ON si.parent_id = sp.id
)
SELECT tree.id,
tree.name,
tree.type,
tree.parent_id,
tree.ref_id,
tree.options,
share.owner,
share.rw
FROM tree
LEFT JOIN shares share ON share.id = '5786da'::text;
Тогда нам нужно где-то регулировать доступ к этой штуке; для этого мы используем отдельную таблицуshares, в которой хранятся именно следующие настройки:
Далее, как вы, наверное, уже догадались, получателю доступа достаточно включить это представление в свой уровень представления, чтобы в его ФС встроился чужой «кусочек»: create or replace view fs_user_990823086(id, parent_id, name, type, ref_id, options, owner, rw) as
SELECT fs_data_990823086.id,
fs_data_990823086.parent_id,
fs_data_990823086.name,
fs_data_990823086.type,
fs_data_990823086.ref_id,
fs_data_990823086.options,
990823086::bigint AS owner,
true AS rw
FROM fs_data_990823086
UNION ALL
SELECT fs_share_990823086_990823086_5786da.id,
fs_share_990823086_990823086_5786da.parent_id,
fs_share_990823086_990823086_5786da.name,
fs_share_990823086_990823086_5786da.type,
fs_share_990823086_990823086_5786da.ref_id,
fs_share_990823086_990823086_5786da.options,
fs_share_990823086_990823086_5786da.owner,
fs_share_990823086_990823086_5786da.rw
FROM fs_share_990823086_990823086_5786da;
Конечно, это итоговое представление необходимо перестраивать каждый раз, когда пользователь добавляет/удаляет чужие ресурсы, но это очень небольшая цена за динамическую систему хранения без копирования и проверки целостности.
Сервисный движок
Сервис реализуется конечной машиной.В любой момент времени каждый пользователь условно находится в одном из 5 основных состояний: «просмотрщик», «редактор», «охранник», «распространитель» и «создатель».
Каждый запрос пользователя — это либо «новый файл», либо «ввод текста», либо «нажатие кнопки».
Модель пользователя содержит набор неизменяемых данных (идентификатор пользователя Telegram и идентификатор корневой папки) и аморфное представление атрибутов текущего состояния (роли) в виде json. Каждый запрос извлекает пользовательские данные из базы данных, интерпретирует их в определенное состояние согласно маркеру роли, производит необходимые действия с записями ФС, записывает новое состояние и сохраняет его обратно в базу данных.
Никаких сессий, никаких авторизаций/токенов, ничего подобного.
Как видите, в самом двигателе нет ничего «умного», он простой.
Несколько слов о Telegram Bot Api
Хотелось бы остановиться на некоторых неочевидных аспектах Bot API. Несмотря на то, что эти моменты так или иначе описаны в документация , это было упущено во время разработки.
- Каждый входящий запрос из Telegram к боту — это уведомление.
В документации сам документ называется Update, но его почему-то так не восприняли.
Уведомление, не требующее никакого структурированного ответа, только положительный статус о его уведомлении, принятии, т.е.
пустой ОК 200. Нет необходимости обрабатывать запрос и давать на него ответ. Вам нужно как можно быстрее дать ему пустой ответ об успешном приеме и дальше заниматься своими делами, в том числе обрабатывать это самое уведомление.
- Каждый тип входящего запроса ответколлбэккуери на самом деле требуется асинхронный «ответ» — отдельный вызов Bot API, независимый от всех остальных действий, чтобы пользователь не смотрел на индикатор загрузки.
- Рекомендуемый лимит исходящих запросов от бота к API — 1 сообщение в секунду за один чат .
Возможно, наука когда-нибудь объяснит избирательность восприятия, но слова об одном чате были полностью забыты при разработке, остался только лимит «1 сообщение в секунду».
Впоследствии, в ходе разбора полетов, это было исправлено, но это было потом, не сразу.
- Любое сообщение в личных диалогах пользователя/бота можно редактировать в любое время.
Хотя бы через год. Но удалить его можно только в течение 2 дней с момента отправки.
Здесь сделаем небольшое отступление, стоит объяснить, почему этот момент важен: Бот следует принципу «единого окна».
То есть, по возможности, бот не отправляет пользователю каждый раз новое сообщение, а пытается отредактировать содержимое того, что было отправлено ранее.
Потому что, как показала практика полевых испытаний, пользователи путаются в сообщениях, нажимают не ту кнопку, попадают не туда и т. д. Такой подход всем хорош, но иногда бот отправляет т.н.
«разговорные» сообщения, которые не предполагают взаимодействия и должны быть удалены через некоторое время.
Но если пользователь остановился на полпути: например, захотел создать новую папку, получил приглашение ввести имя и не пошел дальше, то через 2 дня такое сообщение уже не будет удалено.
Решение подобных ситуаций пока в планах.
- Всего у бота есть два типа сообщений: текстовые и медиа.
Текстовые — это те, которые могут содержать текст и кнопки.
Медиа — это те, которые могут содержать один тип медиаконтента, текст (в 4 раза меньше, чем текстовых сообщений) и кнопки.
Типы несовместимы друг с другом.
Это означает, что ранее отправленное мультимедийное сообщение не может быть преобразовано в текстовое сообщение.
Как понятно из предыдущего пункта, если пользователь только что просмотрел файл, а затем перешел в режим просмотра каталогов - т.е.
было медиа-сообщение, но оно должно стать текстовым сообщением - в этом случае бот должен удалить предыдущее.
и отправь новый
что расточительно.
Конфиденциальность/Безопасность
Этот абзац написан потому, что самый распространенный вопрос о боте в различных вариациях сводится к средней форме: «Где мои файлы и кто может к ним получить доступ».Для начала немного общей теории о Telegram и файлах в нем.
Когда вы отправляете файл в Telegram (неважно кому), если это не «секретный чат», то файл физически загружается в облако Telegram, где он шифруется.
Ваш собеседник (например, бот) получает не сам файл, а его уникальный идентификатор и определенные атрибуты контента (имя, размер и т. д. в зависимости от типа файла).
Далее, если этот идентификатор передать в Telegram, файл станет доступен для скачивания тому, кто его запросил.
Но для того, чтобы иметь возможность скачать файл, этот идентификатор должен быть из вашего диалога.
То есть вы не можете взять какой-либо идентификатор из любого диалога и скачать файл.
Проще говоря, чтобы кто-то мог загрузить ваш файл из облака Telegram, он должен получить от вас его идентификатор через Telegram. Лично или через группу/канал и никак иначе.
Теперь к практике, т. е.
к боту TeleFS. Тот факт, что вы отправляете файлы боту, означает, что бот может создать действительную ссылку для скачивания вашего файла.
И он способен переслать текущий идентификатор любому из своих собеседников.
Бот этого не делает, но умеет. Важно то, что он может сделать это только для своих собеседников.
Если кто-то неожиданно получит данные из базы бота, то используйте эти идентификаторы вне диалога с этим конкретным ботом он не сможет. То есть, если вы подняли свою копию бота и ваша база данных слилась злоумышленникам, они ничего не смогут получить, даже если поднимут копию того же TFS-бота.
Далее следует: не храните ничего личного, секретного и/или очень важного в публичной структуре TFS. Поднимите своего личного TFS-бота и храните там все.
Это все, о чем мне хотелось бы поговорить на этот раз.
Спасибо всем за внимание.
ссылки
Бот в Telegram Бот на GitHub Инструкция по сборке и установке на ваш сервер Группа обсуждения/предложений Первая статья о боте Теги: #программирование #postgresql #мессенджеры #java #Telegram #nas #play framework-
Простое И Надежное Облачное Erp-Решение Sage
19 Oct, 24 -
Вержбицка, Анна
19 Oct, 24 -
Делиться - Это...
19 Oct, 24 -
Вопросы И Ответы О Частице Хиггса
19 Oct, 24