Telegram бот для периодической отправки контента файлов сообщениями

В комментариях к одной из статей про Telegram CLI товарищ под ником Aluminium задался вопросом отправки контента небольшого лог-файла в чат телеграма. Как мне показалось, использовать Telegram CLI и демона для этой задачи чересчур. Для таких целей идеально подходит бот, которого написать достаточно просто. Из этого всего получился такой простенький бот. Если вам интересно, как это всё рождалось, прошу в пост.

Вместо вступления

Как раз сегодня я решил начать подбирать себе какую-нибудь систему управления временем: таймменеджмент, если по-русски. Первое, что пришло мне в голову - заезженная Pomodoro: вы определяете время для работы, в течении которого вы исключительно работаете, и такое же время для отдыха.

Я выбрал на сегодня 5 задач и выделил на каждую из неё по часу. Одна из этих задач - написание такого бота.

Quidquid latine dictum sit, altum sonatur.*

*Все, что сказано на латыни, звучит как мудрость

Главный вопрос при начале проекта - какой язык использовать? Изучить что-то новое, или писать на старом? Я решил попробовать попробовать мейнстримовый NodeJS. Убил пол часа на попытки запустить какой-нибудь hello world на этом добре. Но не судьба - времени в обрез.

Я вспомнил, что когда-то баловался с ботами для Telegram на Python. Что ж, эксгумировал старые сорцы и освежил память. В том скрипте использовалась обертка над API Telegram ботов python-telegram-bot. На нём я и остановился.

Собрал для начала из кусков разных примеров из официальной репы некую основу и понеслась.

Чё почём

Разберёмся для начала с конфигом. Сначала он был написан в json формате, но в нём нельзя писать комментарии, поэтому отказался от него в пользу YAML.

Комменты там на английском, потому что я выпендриваюсь это универсальный язык и я стараюсь писать все доки и ридмики для своих наработок на нём, чтобы охватить бОльшую аудиторию. Но коли этот блог ведётся на Великом и Могучем, считаю своим долгом перевести.

# Здесь прописывается токен Telegram бота, который вы
# должны получить при создании бота у @BotFather'a
# Выглядеть о будет как-то так
token: 000000000:AeAaAfAeOmgAAAdfAaAAFmEexzYNMBrHUAw

# Здесь задаётся интервал отправки контента из файлов в секундах
# По умолчанию - 4 часа
interval: 14400

# Список отслеживаемых файлов.
# Содержимое этих, и только этих, файлов будет отправляться вам в чат
files:
  - .gitignore
  - config.yml

# Список имен пользователей, которые могут общаться с ботом и получать контент файлов
users:
  - telegram_user_name

Первым делом

Первым делом я написал вывод файлов по команде /cat. Бот, получив такую команду прочитает все файлы из списка и поочереди выдаст их в чат.

Тут был небольшой подводный камень - Телега не может перевозить сообщения тяжелее 4096 байт, поэтому при попытке отправить прочитать большой файл всё накрывалось медным тазом.

Странно, что python-telegram-bot не решает это из коробки, но написать своё решение не составило труда: бьём файл на куски по 4КиБ и отсылать отдельными сообщениями.

Вот кусок кода, который это делает.

with open(filename, 'rb') as file:
    data = []
    while True:
        data_chunk = file.read(4096)
        if not data_chunk:
            break
        data.append(data_chunk)

Можно, конечно, не запихивать сначала весь файл в массив, а сразу слать кусками. Но файл может быть очень большим и передача может прерваться где-нибудь на середине. Уж лучше ничего. Но всё в ваших руках - можете переписать под свои нужды :)

Ещё один момент - чтобы Telegram никак не козявил содержимое наших файлов, его нужно перед отправкой обернуть в три Markdown’овские тильды, тобиш ```контент```. Но при тестах обнаружился один косячок: у многострочных файлов Телега обрубала первую строку. Причем по какому-то непонятному признаку.

Чтобы этого избежать, до и после контента нужно поставить символ новой строки. И у нас получается ```\nконтент\n```. И, да, нужно ещё прописать, что сообщение уходит в формате Markdown.

А вот строчка кода, которая отправляет мессагу таким образом:

bot.sendMessage(chat_id, parse_mode="Markdown", text="```\n%s\n```" % data_chunk)

Вторым делом

Далее я реализовал отправку содержимого файла с заданным интервалом.

Библиотека-обертка работает асинхронно, поэтому ни о каких while True с счетчиками и речи быть не может. (Такое вообще нельзя писать никогда).

К счастью, умный люди уже за нас подумали и написать очередь задач JobQueue. И он нам идеально подходит.

Создаём объект Job с нужными параметрами и запихиваем его в очередь:

notification_job = Job(callback_cat, config['interval'], context=chat_id)
job_queue.put(notification_job, next_t=0.0)

2 важных момента:

  1. context=chat_id - вызов функции callback_cat, как понятно из названия, будет производиться асинхронно, поэтому я нужно сохранить информацию о чате, в котрый нужно отправить сообщение.
  2. next_t=0.0 - указывает, что первый раз notification_job нужно выполнить сразу.

Чтобы это добро начало нам слаться каждые interval секунд, нужно написать в чат команду /start.

Кстати, в доке секунды прописывались типом float, но int тоже сканал.

Напоследок

Допилил ещё парсинг аргументов к команде /cat, так что ей можно передавать названия файлов через пробел и она выведет только их. Допустим запрос только файлов, указанных в конфиге(да, я параноик).

Чтобы остановить отправку файлов достаточно написать в чат /stop. Но даже после этого можно вручную запрашивать содержимое файлов с помощью /cat

Ещё момент: в терминал при запуске ничего выводиться не будет, все логи будут писаться в файл bot.log, который при каждом запуске будет перетираться. Так что не откладывайте напильник далеко - он вам пригодится :)

TODOшечка

Обернуть это дело в демона и прописать его в автозапуск я оставлю вам на домашнее задание. Я пока для себя не придумал способов применения этого бота, поэтому писать демона просто не вижу необходимым. А как это сделать под Fedora мы уже разбирались здесь.

Кстати, о помидорах: в конце первого часа мне казалось, что вот-вот ещё пять минут и тут всё будет готово. Короче, потратил я на это всё, вместе с написанием этой статьи, 4 часа. Систему нужно дорабатывать…

А в процессе написания приходили на ум всякие идеи, которые вы, мои дорогие читатели, при желании можете с легкостью дописать. А я просто оставлю эти идеи здесь, пусть они ждут своих героев :)

Добавить возможность редактирования конфига на лету с помощью чата. Я сразу этого не хотел делать, потому что ещё тогда ещё не придумал, как буду ограничивать доступ к доту. Хотя метод с именем пользователя выглядит достаточно надежно, но какой-нибудь злой дядька, получивший доступ к компу с открытым Telegram’ом может натворить дел, так что нужно секурить каким-нибудь паролем.

  • /unlock_config <password> - включение возможности редактирования конфига из чата. Пароль, естественно, хранится в конфиге в хэшированном виде, а после ввода пароля сообщение нужно удалять :)
  • /set_interval <interval> - изменение интервала отправки сообщений
  • /add_file <path> и /remove_file <path> - добавление\удаление файлов в\из списка допустимых.
  • /head и /tail - аналоги соответствующих комманд в *nix подобных системах - выводить кусок с начала или с конца файла соответсвтенно.
  • /watch <path> - наблюдать за файлом и отсылать изменения в реальном времени. Соответственно, нужен /unwatch <path>
  • /lock_config - после всех махинаций нужно залочить конфиг обратно.

Много чего ещё можно придумать - всё в ваших руках. Пользуйтесь на здоровье и обязательно пишите комментарии - меня они очень вдохновляют :)

До скорых встреч!