Как превратить любой скрипт в Telegram-бота

  • Tutorial

Если вам нужен простенький Telegram bot, способный выполнять скрипты (написанные на любом языке) и отвечать текстом и картинками, то вам под кат. Там вы найдёте рассказ о бот-движке, который делает то, что вам надо.


Telegram bot


Краткий список возможностей движка:


  • Движок может обслуживать сразу несколько ботов
  • Бот выполняет скрипты (написанные на любом языке)
  • Сообщение попадает на вход скрипту в виде аргументов и переменных окружения
  • Вывод скрипта может быть текстом, форматированным текстом или изображением (распознаётся автоматически)
  • Движок гарантирует поочерёдный запуск скриптов (при написании скриптов можно не думать про локи и конкурентный доступ к ресурсам)
  • Бота можно дёргать по HTTP, чтобы отправлять сообщен не в ответ на запрос, а "асинхронно" (например по cron-у)

Движок максимально неприхотлив: ему не нужны базы данных, публичные IP-шники, SSL-сертификаты… Можно просто запустить на лаптопе, сидя за НАТом с наглухо закрытыми портами. В общем, начать экспериментировать вы можете прямо не сейчас, не отрываясь от чтения.


Сейчас я покажу, как это всё запустить и оживить.


Сборка


Вам понадобится язык Go. Чтобы его поставить, не нужны даже root-права. Но, для простоты, далее, я буду предполагать, что он у вас стоит в системе.


Скачиваем и собираем проект:


cd tmp
git clone https://github.com/michurin/cnbot.git
cd cnbot
go build ./cmd/...
./cnbot

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


Начинаем разговор


Первым делом, вам надо зарегистрировать бота и получить для него токен. Это совсем не сложно: инструкция на сайте Telegram.


Создаём минимальный кофигурационный файл (config.yaml):


bots:
  firstBot:
    token: "22222222:AAAAAAAAAAAAAA"
    script: "/usr/bin/true"

Тут должен быть правильный токен и любой исполняемый файл в качестве скрипта (рекомендую выбить что-нибудь побезопасней, чем /bin/rm). Проверяем настройки (-i):


./cnbot -i -c config.yaml

Если токен правильный, вы получите отчёт о состоянии бота.


Запускаем бота (без -i)


./cnbot -c config.yaml

Пытаемся добавить его в Telegram-клиенте. В логах бота видим ошибку


user 500050880 is not allowed

Это ваш user_id (у вас он будет другой), добавляем его в конфиг


bots:
  firstBot:
    token: "22222222:AAAAAAAAAAAAAA"
    script: "/bin/echo"
    allowed_users: [500050880]

Обратите внимание, я прописал echo в параметр script. Это быстрый (хоть и кривоватый) способ сделать echo-бота. Вы уже можете поговорить с ним. Попробуйте сказать hi, Hi!, -n hi.


Из подобного разговора сразу видно как легко получить уязвимость (-n было интерпретировано как параметр echo). Так же видно как формируются аргументы скрипта: сообщение приводится к нижнему регистру; допустимыми символами считаются буквы, цифры, минус, точка и подчёркивание; все недопустимые символы считаются разделителями.


Полное сообщение тоже доступно. Давайте заменим /bin/echo на простой скрипт и посмотрим переменные окружения:


#!/bin/sh
env

Если сказать этому боту Hello! Он покажет переменные окружения


BOT_TEXT=Hello!
BOT_FROM_FIRSTNAME=Alexey
BOT_NAME=firstBot
BOT_CHAT=500050880
BOT_FROM=500050880

Видно, что доступно оригинальное сообщение, имя и ID пользователя, который отправил сообщение, название бота (согласно конфигу; напомню, что движок может обслуживать сразу несколько ботов).


Если бот получает контакт или пересылку сообщения, то к переменным окружения добавляется информация о контакте или авторе оригинального сообщения. Это удобно, когда вы хотите добавить в white list нового пользователя. Чтобы узнать его ID — просто перешлите его контакт или любое его сообщение боту. См. пример в demo.sh.


Обратите внимание, что переменной PATH не видно. Если вам нужны экзотические пути, пропишите их явно в начале скрипта или используйте полные пути.


Скрипт можно чуть усовершенствовать


#!/bin/sh
echo '%!PRE'
env | sort

Теперь вывод будет преформатированный. Можно использовать и markdown, см. всё тот же demo.sh.


Чтобы ответить картинкой, её достаточно просто вывалить на stdout:


#!/bin/sh
curl -qfs https://golang.org/lib/godoc/images/footer-gopher.jpg

Если скрипт не выдаст ничего, то бот отправит сообщение "empty", чтобы бот действительно ничего не ответил, скрипт должен ответить одним единственным символом "точка".


Все примеры можно найти в demo-скрипте, а я бы чуть подробнее остановился бы на асинхронной нотификации.


Бот говорит сам


В боте можно включить HTTP сервер добавлением одной строки bind_address в конфиг:


bots:
  firstBot:
    token: "22222222:AAAAAAAAAAAAAA"
    script: "/bin/echo"
    allowed_users: [500050880]
    bind_address: ":9091"

Теперь вы можете отправить асинхронное сообщение:


echo "ok" | curl -qfsX POST --data-binary @- "http://:9091/500050880"

То есть, пользователь получит его не в ответ на своё сообщение, а просто как нотификацию. Тело сообщения обрабатывается по тем же правилам, что и output скрипта. То есть, вы можете отправлять форматированный текст и картинки.


Можно использовать и multipart/form-data:


curl -qfsX POST -F to=500050880 -F msg=ok "http://:9091"

Думаю, нет смысла обсасывать каждую деталь работы бота. У вас уже есть достаточно информации, чтобы понять, нужно оно вам или нет. Полный спектр возможностей можно посмотреть, поговорив с demo-сриптом. В конфиге можно указать таймауты для http-клиента и выполнения скрипта. Все опции есть в readme проекта, хотя, думаю, большинству будет комфортно жить и с дефолтами.


Любые вопросы, пожелания, предложения — приветствуются.


Приятного ботостроительства!


Если вы всё ещё читаете...


..., то могу рассказать, как я дошел до жизни такой.


Мы как-то делали очень глубокий редевелопмент системы. Фактически, мы запускали абсолютно новый проект сразу под огромной нагрузкой. Конечно, где могли, мы подстелили соломки. Но у нас не было ресурсов, что бы поддерживать на лету сразу два проекта такого масштаба. То есть мы не имели возможности быстрого переключения на старую версию.


В день старта я уходил домой очень поздно, но мне хотелось держать руку на пульсе постоянно. Мне не подходил сценарий: найти местечко в метро, достать ноут, поднять VPN, посмотреть логи… Хотелось иметь возможность глянуть основные вещи с телефона и, возможно, что-то подтюнить, сбегая по эскалатору.


Я гуглонул, что на это тему знает Интернет, и оказалось, что Telegram предоставляет бесплатное и великолепное API для ботов. Я написал бота-уродца в несколько строчек на bash+curl+jq, который умел выполнять буквально три команды, и поехал домой.


Бот оправдал себя полностью и идея мне очень понравилась. Только реализация полностью на шеле была уж очень кривая и неуклюжая. На досуге, я переписывал эту штуку несколько раз на ноде и питоне, но всё это были какие-то сырые поделки.


Тем временем, я начал использовать таких ботов во многих бытовых делах. У меня есть бот для учёта тренировок и отслеживания прогресса (графики), есть бот для управления домашним микротиком… В конце концов, у меня накопился очень чёткий список фичей, которыми должен обладать бот. А так же, список тупиковых идей, которые выглядят хорошо, но на деле мало полезны.


В 2018 я начал учить Go и, просто для эксперимента, заимплементил движок ботов на Go. Это был мой первый код на новом языке и он был ужасен :-) Но, глобально, Go оказался очень удобной штукой для подобных вещей. И вот, поднабравшись опыта в Go, я решал вернуться к этому проекту и переписать его на Go, но уже "правильно" (ну или близко к тому).


Так и появилось это поделье. Почему репа называется cnbot, я так и не смог вспомнить по прошествии лет.


Куда я планирую всё это развивать?


Я подумываю о расширении функциональности, но очень осторожно. Очень не хотелось бы переусложнять. Если вам нужно какое-то специфичное решение, — просто напишите своего бота. А этот движок я хотел бы оставить максимально простым.


Но я бы хотел развивать движок в сторону встраиваемости: выделить из него какую-то простую часть, которую можно было бы подключить библиотекой к любой Go-программе. Условно, если у вас уже есть микросервис для… для чего годно… хоть для рендеринга 3D-сцен, — вы можете в одну-две строчки встроить в него чат бота для оперативной диагностики/мониторинга/управления… Вот это, мне кажется, было бы полезно. На самом деле, вы уже сейчас можете так сделать. Просто это не очень удобно.


upd через месяц: заметка не потеряла актуальность, но произошла куча доработок. Основное:


  • добавлены inline-клавиатуры
  • добавлена возможность менять сообщение с клавиатурой, чтобы делать меню как в @BotFather
  • добавлены алерты: модальные и простые вверху окна
  • добавлена обработка редактирования сообщений пользователем
  • добавлена обработка гео-позиций
  • выкинута наркоманская магия с точкой, вместо неё сделана обычная управляющая строчка %!SILENT
  • возможность добавлять подписи к картинкам
  • добавлено много новых переменных окружения
  • запустить демо-скрипт локально стало очень просто: не надо ничего конфигурировать/редактировать
  • я запустил демо-бота @cnbot_demobot. Можно познакомиться с основными возможностями, ничего не устанавливая.

Комментарии 10

    +1
    А почему не понравился nodejs?
      +1
      Nodejs — тоже не плохо, но Go понравился больше :-)

      — Главный «объективный» довод: код на Go показался мне на много более читабельным.
      — Главный субъективный довод: я захостил всё это не VDS-ке за $10 в год. Там не очень много места. Гошный бинарь можно собрать где угодно, а туда просто положить. И всё — никакие зависимости больше не нужны.

      Но нода тоже очень и очень не плоха! Если интересует, вот вариант примерно того же самого, но на ноде github.com/michurin/instant-bot Развивать его я пока не планирую, но любой желающий может форкнуть это добро и делать с ним что угодно.
        +1
        На самом деле тоже начал писать своего бота, но пока мой выбор остановился вообще на telegraf.js.org фреймворке для этого.
          0

          Я смотрел в сторону фреймоворков и что-то не стал с ними связываться по нескольким причинам


          • Последний раз я смотрел где-то в 2017 году, и тогда всё, что я видел, показалось мне каким-то не зрелым (возможно, сейчас это уже не так)
          • В данной случае, я использую, наверно процентов 3-5 от всех возможностей API. Тащить для этого какой-то фреймворк?.. см. следующий пункт
          • Так как реально я использую только часть функциональности только двух методов (getUpdate и sendMessage (ну ладно, и чуть-чуть sendPhoto :-))), заимплементить самому API — тривиальная задача.

          Так и получилось, что я не использую фреймворки. Как видите, это связано со спецификой задачи. В других условиях фреймворки были бы вполне уместны.


          Хотя… одна из ценностей Go: не прятать вещи за слишком развесистыми абстракциями (not hiding things behind too much abstraction). API телеграма на столько простое, что я не очень понимаю, надо ли вообще строить для него какой-то фреймворк. Но тут уж как кто любит :-)

            0
            Спасибо большое! Информация к размышлению, буду думать.
      0
      отслеживания прогресса (графики)

      Можете показать примеры?
        0

        Спрашивали — отвечаем https://yadi.sk/i/G0pO9FhoK4zO1g Но там нормальному человеку ничего не понять. Это вес за 1000 дней с поездки на самокате за 1000 дней. Да, спором я занимаюсь не очень регулярно )

          0
          Спасибо
        +1
        Список тупиковых идей, которые выглядят хорошо, но на деле мало полезны.


        Вот конкретно это было бы полезным и интересным.
          0

          Тут можно посмотреть прошлые поделья на JS и Python. Основные проблемы: (1) сложный контракт, (2) преусложнения типа возможностей пробросить или зафорсить переменные окружения. Так же кучка технических проблем. Главные: (1) нет гарантий, что одновременно бежит только один скрипт, это создаёт и риск гонок и риск ДДОСа и (2) текущая версия запускает процесс в группе и по таймауту убивает не только его, но и всех его потомков, старые версии могли оставлять грязь, что тоже приводило к возможностям ДДОСа.


          Про переменные окружения могу сказать отдельно. Сейчас скрипту передаются только те переменные, которые выставил сам движок. Кажется, что это слишком жёстко. Но на самом деле, это позволяет избежать кучи плохоотлавливаемых проблем, связанных с тем что в "тесте" и в "проде" разные переменные окружения. Нужен тебе определённый LANG или PATH или LD_PRELOAD — напиши явно.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое