В 2020 году я создал проект Intelligence (Storm) на Python, который изменил информационную политику госинститута. Его задача — собирать новости 55 торговых представительств за рубежом, и предоставить нам цифровую инфраструктуру для их обработки, подсчёта, а также ретрансляции наиболее важных в различные каналы. Включая, конечно, Telegram.
Канал "Торгпред" собрал аудиторию в 5,5 тысяч подписчиков. Без накруток и плана, и начинался как тестовый контур для сбора новостей. Его аудитория — это российские экспортёры, федеральные и региональные органы власти, вовлечённые в работу по продвижению отечественной продукции и услуг за рубежом.
Telegram всё ещё востребованный инструмент в международной работе. Поэтому его использование, пусть и в ограниченном формате, пока желательно. У него есть полный тёзка в MAХ, но ещё проходит этап становления.
Однако любое наше действие не должно нарушать российское законодательство.
— Какие есть варианты? — спросил я себя. Виделось четыре сценария:
- Отказаться от Telegram. Разумно, но резко теряем работающий и востребованный информационный канал об экспорте.
- Использовать VPN, делать посты вручную. Опасный путь, со множеством ограничений и недостатков.
- Оставить прямую интеграцию из основного контура Intelligence. Технически удобно, но снова вопросы развёртывания VPN, а значит пограничные вопросы соблюдения законодательных ограничений, с дополнительными лишними рисками для инфраструктуры ПО, которая должна оставаться предсказуемой и изолированной.
- Вынести взаимодействие с Telegram в отдельный сервис, — приземлилась в моих мыслях идея. — Это то, что мы попробуем!
Telestorm
Как должен выглядеть этот сервис?
Первое — с доступом к серверам Telegram (мощности дата-центров расположены по всему миру).
Второе — работать быстро и безопасно, и максимально самостоятельно. Поэтому язык программирования — компилируемый Rust, а приложение обёрнуто в Docker-контейнер, разворачиваемый и поддерживающий жизнь автоматически.
В качестве облачной платформы для кода вместо GitHub я стал использовать отечественный аналог: GitVerse. В некоторых функциях он ещё сырой, но мне нравится его использовать, для моих задач подходит (поговорим об этом в следующих выпусках).
И что он делает?
Это так называемый API-сервис. То есть, сервер имеет набор статичных адресов, при обращении по которым можно воспользоваться его функциональностью.
Например, мы можем спросить его про самочувствие:
curl --location 'https://<host>.com/api/v1/health'
Пояснение: curl — мощная утилита для сетевых запросов. Ключ --location — даёт указание переходить по внутренним редиректам (например, http -> https). В конце веб-адрес, которому уходит запрос.
Если всё в порядке, сервер ответит:
{
"status": "True"
}
Чтобы сделать публикацию в Telegram, надо сделать запрос к API, передать параметры (например, идентификатор канала) и сам текст сообщения, можно со ссылкой на изображение, если оно есть.
Таким образом, Intelligence продолжает собирать и обрабатывать новости, готовить их к публикациям. Когда требуется разместить их в Telegram, он теперь использует не собственные сервисы, а делает запрос к Telestorm. Передаёт данные при этом по защищённому каналу, с цифровой подписью, чтобы минимизировать риск подмены на маршруте.
В свою очередь, Telestorm отправляет новость в канал Telegram, а затем уведомляет Intelligence о результате (получилось или нет).

Такие языки как Rust, обычно, требуют больше кода, по сравнению со многими высокоуровневыми. Например, Telestorm сейчас — это около 1700 строчек, а такая же функциональность на Python заняла бы в 2 раза меньше, как минимум, но в награду значительно более высокая производительность и безопасность.
В сетевых же сервисах, как в бане, по сути все равны. Не важно с помощью чего написан бэкенд, потому что протоколы используются единые. Соответственно, клиенты могут быть и на Go, Python, Ruby, C и так далее. В примере выше запрос о здоровье вообще сделан через утилиту в терминале.
Так что под капотом?
Здесь больше технических деталей для тех, кому интересно (и здесь редкий читатель закрыл страницу браузера).
Представим, что запрос к API мы делаем из приложения на Python. Это простая структура сообщения, с иллюстрацией.
@dataclass(slots=True)
class News:
id_news: int
text: str
image_url: str | None
Поле id_news получает уникальный идентификатор новости, text — её текст (если нужно, с оформлением в поддерживаемой Telegram разметке HTML или Markdown), image_url — веб-ссылка на иллюстрацию (в коде виден None, а значит поле не обязательное).
Всё это погружается в структуру Body, которая содержит также токен доступа к боту в Telegram (от его имени делаются публикации) и адрес канала размещения новости.
Слишком техническая деталь (можно пропустить): данные тела преобразуются в массив байтов формата MessagePack. Плюс, в отличии, например, от популярного JSON, в более компактном размере, а значит данные передаются быстрее и меньше риски неудачных трансферов.
packed = msgspec.msgpack.encode(body)
Всё почти готово к отправке к Telestorm. Однако приняты будут для публикации только те данные, которые подписаны специальным криптографически механизмом. Именно подписаны, а не зашифрованы. Шифрование обеспечивает стандартный транспорт TSL (мы пользуемся им каждый день, когда заходим на сайты, которые начинаются на HTTPS).
Подпись включает ряд данных, которые будут в распоряжении у сервиса Telestorm, проверяющего подпись. Например: секретный ключ (токен, пароль), временная метка отправки (её передают в заголовке запроса), тело запроса, ссылка на ресурс API и т.д.
Примерно так будет выглядеть цифровая подпись для нашей новости:
793dc79911d1f452914c0d387663d2542629d61c7f186f9baff7d816bc3adb0c
Подделать такую подпись, скажем так, весьма непросто, а если она не совпадёт с той, которую сформирует сервер, то Telestorm ответит ошибкой: "авторизация не пройдена". Это означает, что посторонний не смог воспользоваться нашим сервисом.
Итак, данные готовы к отправке, подпись сформирована. Делаем запрос:
req = httpx.post(url=path, headers=headers, content=body, timeout=60)
Да, одной строкой мы можем обратиться к любому сетевому ресурсу по всему миру. Веб-ссылка содержится в переменной path, которая передаёт это значение методу post через атрибут url.
По Интернету полетели байты с нашей информацией. Через шлюзы, распределительные центры к дата-центру, где на серверах выделено немного места для Telestorm. Наше приложение на Rust не дремлет. Даже без квалифицированных клиентов его постоянно бомбардируют различные сканеры и боты. Небольшой клочок log-файла:
INFO 172.18.0.1 "GET /prod-wp-config.php HTTP/1.1" 404 0 "-" "-" 0.000124
INFO 172.18.0.1 "GET /phprelease.php HTTP/1.1" 404 0 "-" "-" 0.000123
INFO 172.18.0.1 "GET /php-7.php HTTP/1.1" 404 0 "-" "-" 0.000149
INFO 172.18.0.1 "GET /php-opcode.php HTTP/1.1" 404 0 "-" "-" 0.000132
INFO 172.18.0.1 "GET /k90.php HTTP/1.1" 404 0 "-" "-" 0.000165
INFO 172.18.0.1 "GET /uwu2.php HTTP/1.1" 404 0 "-" "-" 0.000121
INFO 172.18.0.1 "GET /ahax.php HTTP/1.1" 404 0 "-" "-" 0.000166
INFO 172.18.0.1 "GET /akcc.php HTTP/1.1" 404 0 "-" "-" 0.000164
INFO 172.18.0.1 "GET /zews.php HTTP/1.1" 404 0 "-" "-" 0.000156
INFO 172.18.0.1 "GET /124.php HTTP/1.1" 404 0 "-" "-" 0.000117
INFO 172.18.0.1 "GET /shout.php HTTP/1.1" 404 0 "-" "-" 0.000148
INFO 172.18.0.1 "GET /wp-gr.php HTTP/1.1" 404 0 "-" "-" 0.000099
INFO 172.18.0.1 "GET /wp-mn.php HTTP/1.1" 404 0 "-" "-" 0.000362
INFO 172.18.0.1 "GET /wp-mt.php HTTP/1.1" 404 0 "-" "-" 0.000295
INFO 172.18.0.1 "GET /ova.php HTTP/1.1" 404 0 "-" "-" 0.000154
INFO 172.18.0.1 "GET /abcd.php HTTP/1.1" 404 0 "-" "-" 0.000142
INFO 172.18.0.1 "GET /we.php HTTP/1.1" 404 0 "-" "-" 0.000124
INFO 172.18.0.1 "GET /ioxi-o.php HTTP/1.1" 404 0 "-" "-" 0.000117
INFO 172.18.0.1 "GET /ar.php HTTP/1.1" 404 0 "-" "-" 0.000117
INFO 172.18.0.1 "GET /qing.php HTTP/1.1" 404 0 "-" "-" 0.000124
INFO 172.18.0.1 "GET /mms.php HTTP/1.1" 404 0 "-" "-" 0.000149
Круто, да? Это делаются постоянные запросы к серверу по разным сгенерированным адресам. Мы видим 404 — стандартная HTTP ошибка Not Found — то есть о такой странице ресурсу неизвестно. Но ботам всё равно, они выполняют свою программу. Какие-то делают это из благих побуждений (например, поисковые сервисы или сервисы выявляющие ошибки безопасности для предупреждения), какие-то в поисках уязвимости. Например, тихим образом стащить базу данных.
Наш запрос ушёл на правильный адрес, поэтому Telestorm взял его в работу. Сначала он проверит подпись и если всё в порядке пропустит дальше. Вот фрагмент функции-хендлера на Rust, которая встречает на входе.
#[post("/sendNews")]
async fn send_news(body: Bytes, tg_service: Data<TelegramService>)
Затем мы распаковали (если получилось, конечно) данные из MessagePack:
let send_news_dto = rmp_serde::from_slice::<SendNewsDto>(&body)?;
Переменная send_news_dto теперь содержит точную копию тела запроса, которую мы делали выше в Python. Данные декодированы и первично проверены.
Наконец, отдали внутреннему сервису для публикации в Telegram.
let result = tg_service.send_messages(&send_news_dto).await?;
Спешим в канал "Торгпред" и там нас ждёт результат работы двух сервисов.

Мы искренне рады за экономические успехи в ЮАР, и восторгаемся маленьким достижениям в работе двух ИТ-сервисов.
— Работает! — шепчем, смотря в монитор.
Завершается обмен между сервисами тем, что Telestorm тоже собирает небольшое ответное сообщение и передаёт его клиенту. Во-первых, сообщая стандартным HTTP-кодом 200 (ОК), что запрос обработан успешно, а внутри тела сообщения техническая информация о результатах. Ответ уже не подписывается криптографически, потому что чувствительной информации в нём не содержится, а при первом запросе сервер и клиент подтвердили свои полномочия.
Intelligence получает информацию от Telestorm. Раскрывает её и видя, что результат успешный ставит галочку: "Опубликовано". Эта задача для него выполнена и он переходит к следующим.