В этой статье я хочу рассказать о том, как происходило написание Telegram-бота, который мог бы отправлять статьи с Habrahabr, сначала на Python, а потом и на Go, и что из этого вышло.


Небольшая предыстория


Началось всё летом, когда Telegram всё сильнее начал проникать в мою жизнь. После написания пары тестовых ботов на Python возникла идея написать что-нибудь и для Habrahabr. Первая задумка была простая: бот должен был просто присылать уведомления о новых статьях.


Первая версия (на Python 3.5)


После недолгого гугления обнаружилось, что у сайта есть RSS-лента. Процесс рассылки статей сразу упростился: теперь не надо было парсить весь сайт, достаточно смотреть только RSS-ленту раз в несколько минут (в моём случае – 10).


Довольно быстро была написана первая версия бота. Для взаимодействия с API Telegram использовалась библиотека pyTelegramBotAPI, для парсинга RSS-ленты – feedparser.


В процессе работы структура сильно усложнилась: теперь бот не просто присылал новые статьи, но и мог отсеивать те статьи, которые не содержали теги, на которые пользователь был подписан. Реализация была очень простой: использовалась база данных SQLite, в которой было всего 2 столбца (id и теги), благо, Python3 имел встроенную библиотеку для взаимодействия с ней. Также добавились команды для редактирования тегов: пользователь мог удалять, добавлять и копировать теги. Последнее происходило очень просто: пользователь присылал боту ссылку на профиль, после чего происходил поиск тегов с помощью (Каюсь! Не знал о том, что это плохо) регулярного выражения.


Проблемы


Конечная программа получилось довольно нестабильной: довольно часто падала (для того, чтобы это избежать пришлось обернуть часть кода в while True:). Не стоит забывать и про то регулярное выражение. В итоге, бот, хоть и криво, но работал, поэтому было решено оставить всё как есть и позволить ему жить (как оказалось, до конца февраля). Исходники можно (но нужно ли?) найти GitHub.


Вторая версия (на Go)


Довольно давно в моей голове засела идея попробовать Go, но руки никак не доходили. И вот, наконец, было найдено свободное время. После чтения документации так и хотелось что-нибудь написать, а тут как раз бот на Python упал (даже while True не помог). Понимая, что это знак свыше, я начал переписывать бота на Go.


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


Начну с библиотек. Для взаимодействия с API Telegram используется telegram-bot-api.v4, для взаимодействия с SQLite3 – go-sqlite3. RSS-лента парсится с помощью gofeed. Для копирования тегов пользователя используется аналог Beautiful Soup на Go – soup.


Теперь перейдем к коду. Существует основной цикл, который получает новые сообщения от бота. В нём происходит определение типа команд:


for update := range updateChannel {
    if update.Message == nil {
        continue
    } else if update.Message.Command() == "start" {
        startChan <- update.Message
    } else if update.Message.Command() == "help" {
        helpChan <- update.Message
    ...
    } else {
        message := tgbotapi.NewMessage(update.Message.Chat.ID, "...")
        message.ReplyToMessageID = update.Message.MessageID
        bot.send(message)
    }
}

Как можно было понять из кода, каждая команда обрабатывается в отдельной goroutine, передача сообщений осуществляется с помощью каналов:


startChan := make(chan *tgbotapi.Message, 50)
helpChan := make(chan *tgbotapi.Message, 50)
getTagsChan := make(chan *tgbotapi.Message, 50)
...

// Goroutines
go bot.start(startChan)
go bot.help(helpChan)
go bot.getTags(getTagsChan)
...

Также имеется возможность рассылать оповещения пользователям. Это происходит с помощью специальной веб-страницы.


Веб-страница


Проект решено было разбить на несколько пакетов:


  • main – происходит загрузка конфиг-файла, запуск бота и сайта
  • bot – отвечает за работу бота
  • website – отвечает за работу сайта
  • logging – логгирует ошибки

Вот как выглядит процесс запуска программы:


  1. Открываются файлы для логгирования
  2. Происходит чтение конфиг-файла
  3. Происходит инициализация бота и открытие базы данных
  4. Запускается бот в отдельной goroutine (при запуске бота идёт запуск goroutine с функциями, которые обрабатывают команды)
  5. Запускается сайт в отдельной goroutine

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


Проблемы


К сожалению, проблем не удалось избежать и в этот раз. Но они связаны не с работой бота, а с разработкой программы и собиранием исполнительного файла под Linux (разработка происходит на Windows 10, сервер работает на Ubuntu 16.04): для работы библиотеки go-sqlite3 нужен CGO, а при выполнение команды go build с флагом CGO_ENABLED=1 появляются ошибки. Из-за этого приходится компилировать проект на виртуальной машине с Ubuntu.


Зачем?


Вы можете спросить: "Ну и зачем использовать этого бота, если есть сайт?". Постараюсь ответить. Но перед этим хочу сказать, что бот ни в коем случае не является заменой сайта, лишь дополнением.


  • Возможность собрать "всё в одну кучу". Лично для меня Telegram – это источник всех новостей и интересных материалов, поэтому мне хотелось иметь возможность получать статьи с Хабра и там
  • Возможность просматривать статьи с помощью Instant View. Для этого был написан свой шаблон. Благодаря этому, можно читать почти все статьи в удобном формате прямо с телефона (некоторые статьи содержат теги, которые IV не может правильно интерпретировать, поэтому часть статей недоступна для просмотра)
    Пример сообщения от бота
  • Также была реализована возможность отправить боту ссылку на статью с Habrahabr, после чего он вернёт её в таком же формате, в каком отправляет сам (с Instant View, ссылкой на статью и на комментарии)
    Пример (с клиента для Windows)

Заключение


Подводя итог, хочется сказать, что опыт написания бота на Go был довольно интересным и полезным (надеюсь). Если вы заинтересовались ботом, то его самого можно найти здесь, а его исходники – опять же на GitHub.

Комментарии (21)


  1. Desprit
    11.03.2018 01:13

    Чтобы бот не падал, нужно было вместо long polling сделать его на веб хуках.


    1. Kirtis Автор
      11.03.2018 11:45

      Версия на сервере работала как-раз на WebHook'ах. Просто репозиторий не обновлял. Там проблема была в коде и логике работы программы, Long Polling не виноват.


  1. TyVik
    11.03.2018 10:04
    +1

    Не понимаю как мессенджер может служить аггрегатором статей. Ладно бы записей каких с новостных каналов или bash.org.ru — там они мелькают и сразу же уходят в прошлое, теряя актуальность, и за ними не надо следить. Другое дело Хабр или Медиум — тут интересных статей за день может быть несколько, но уже завтра они потеряются в истории. У меня в feedly есть статьи 20-дневной давности, и я к ним обязательно вернусь как будет время и возможность. В Telegram же я не понимаю как за этим можно следить.


    1. Kirtis Автор
      11.03.2018 11:41
      -1

      Если есть какая-то интересная статья, то, например, я сохраняю её в Saved Messages. А потом, когда есть время, возвращаюсь к ней. Да и как я писал:

      бот ни в коем случае не является заменой сайта, лишь дополнением
      Фактически, бот – это просто улучшенный вариант RSS-ленты.


      1. WGH
        11.03.2018 15:22
        -1

        Скорее, ухудшенный. Нет ни mark unread (чтобы вернуться потом), ни произвольной древовидной группировки подписок, плюс vendor lock-in на Telegram.


  1. WebProd
    11.03.2018 12:02

    Зачем для каждого вида сообщений отдельная горутина?


    1. Amega
      11.03.2018 15:15

      Мб этот подход и не очень плох, но я бы не разбирал сообщения в основном цикле, а передал эту работу следующей абстракции, будь то "контроллер" или менеджер команд. И вообще, лучше как мне кажется через webhook работать.


      1. WebProd
        11.03.2018 15:19

        Через вебхук однозначно лучше. Но мне кажется, что все горутины должны обрабатывать все типы сообщений.


        for update := range updateChannel {
        ...
        }

        во всех горутинах и все


        1. Kirtis Автор
          11.03.2018 16:06

          Можно поподробнее, что значит:


          все горутины должны обрабатывать все типы сообщений


          1. WebProd
            11.03.2018 16:18

            я же показал, все слушают updateChannel и потом уже вызывают функцию обрабатывающую конкретное сообщение


  1. Amega
    11.03.2018 15:17

    Много ли пользователей у бота? Не сталкиваетесь ли с лимитом сообщений при рассылке уведомлений? Сам на днях написал первого бота, хочу тоже рассылать уведомления, но прочитал, что есть лимиты.


    1. Kirtis Автор
      11.03.2018 16:03

      Сейчас 152 пользователя. Если честно, ожидал, что после статьи бот упадёт от нагрузки, но, как оказалось, он не так популярен. С лимитом сообщения сталкивался, но в другом своём проекте – бот для тренировки ударения к ЕГЭ. По самой его сути пользователь должен часто отвечать, поэтому иногда возникают ошибки, хотя мне кажется, что причина всё же в чём-то другом, так как на оф. сайте написано, что:


      The API will not allow more than ~30 messages to different users per second

      А таких нагрузок просто не может быть.


      1. Kirtis Автор
        11.03.2018 16:13

        Не заметил, что «users» во множественном числе. Тогда да, вполне возможно наткнуться на такую проблему. Но sleep на 1 секунду должен помочь избежать её.


      1. mihmig
        11.03.2018 22:57

        Пользуясь случаем спрошу:
        У ботов есть 2 ограничения на «интенсивность»:
        1. Не более 30 сообщений/сек в разные чаты — здесь всё понятно
        2. Не более 20 сообщений/мин в одну группу. — вот здесь хотелось бы уточнить:
        бот не должен посылать более 20 сообщений в группу за одну календарную минуту (например с 2018-03-11 22:54:00 GMT по 2018-03-11 22:54:99 включительно)
        или не более 20 сообщений за последние 60 секунд?

        Есть бот, у которого пиковая нагрузка может выходить за эти пределы.
        Подскажите, как официально обратиться в техподдержку телеграма?


        1. Kirtis Автор
          12.03.2018 00:10

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


  1. xakepmega
    11.03.2018 16:33
    +2

    Хоспаде, зачем плодить статьи о телеграм ботах? у телеграма довольно внятное апи, которое не нуждается в разъяснениях


  1. SokoloffA
    11.03.2018 17:03

    для работы библиотеки go-sqlite3 нужен CGO, а при выполнение команды go build с флагом CGO_ENABLED=1 появляются ошибки


    Может Вам заменить SQLite на чисто гошное, например на boltDB.


    1. Kirtis Автор
      11.03.2018 17:07

      Спасибо за совет. Я, на самом деле, задумывался об этом. Думаю почитать поподробнее про BoltDB. Если понравится, то перееду на неё.


  1. voidMan
    11.03.2018 18:47

    Автор, а можно ли geektimes туда подтянуть опционально? Я бы с удовольствием в одном месте это читал в телеграме.


    1. Kirtis Автор
      11.03.2018 18:55

      Вообще, достаточно поменять только 1 строчку. Вопрос лишь в том, стоит ли делать отдельного бота или лучше добавить такой функционал в этого. Я бы отдал предпочтение второму варианту, но тогда придётся чуть допилить интерфейс взаимодействия, чтобы можно было отдельно управлять подписками на Geektimes и на Habrahabr.


      1. alexgreat7
        12.03.2018 05:39

        Тоже хотел попросить добавить geektimes. И лучше отдельным ботом, да.