Подписаться на блог

WebPurple Telegram Bot

В последние пару недель я пытаюсь активно саморазвиваться, изучать что-то новое и делать чисто из интереса, даже диплом писать начал.
В первую очередь мой интерес направлен на NodeJS и всё вот это серверное, поэтому дальше BackEnd’а дипломного проекта я не дошёл, но разговор не об этом.

В Рязани наиболее активным и растущим сообществом разработчиков является WebPurple, к которому я себя смело могу отнести, ибо я являюсь активным участником (ну, я так считаю).

Для обсуждения всякого разного был создан чатик в Telegram, ну и чтобы привнести интерактивности, было принято решение сделать какого-нибудь бота, чтоб прямо круто всё было.

Изначально бот предполагался в виде типичного бота из IRC-каналов, но в итоге обсуждения он перешёл в разряд «самостоятельных» ботов. Разработкой решил заниматься я, однако в плане различных особенностей на уровне разработки, языка (NodeJS) и его использования мне помогали коллеги, чему я очень рад и благодаря чему я опять же узнал много нового.

Бот не должен был быть сложным и нагруженным, его задачей было просто джелиться информацией, составленный список функций выглядел как-то так:

  • По запросу отправлять последние новости;
  • По запросу отправлять информацию о планируемых мероприятиях;
  • По запросу отправлять ссылочки на сообщества, репозитории и всякое такое;
  • Дать юзеру возможность подписаться на рассылку новостей и как только, так сразу слать ему посты.

Вроде бы всё, изначально информацию планировалось тащить из API нашего сайта, но потом всё-таки передумали, и теперь тащим всё прямиком из группы ВКонтакте, ибо она в большей части кейсов является как бы первоисточником всего и вся.

Дабы упростить процесс разработки я, недолго думая, ввёл следующую команду в терминал и был рад.

npm install --save node-telegram-bot-api

Первые сложности возникли, когда я пытался с Long-Pulling соединения всё на WebHook’и переделать, но это я мануалы плохо читал.
По итогу первая версия, которая просто отдавала «статику» завелась за пару часов. А всё просто потому, что надо учитывать всякие слэши и вот это всё, ну и то, что у меня не чисто бот, а Express, «внутри» которого живёт бот.

const TelegramBot = require('node-telegram-bot-api');
const botUrl = `/bot${botToken}`;
const bot = new TelegramBot(botToken, {
  onlyFirstMatch: true,
});

app.post(botUrl, (req, res) => {
  bot.processUpdate(req.body);
  res.sendStatus(200);
});

app.listen(port, () => {
  console.log(`Express server is listening on ${port}`);
  bot.setWebHook(`https://example.com${botUrl}`);
});

Стоит учесть, что мне было лень разбираться с нормальным выстраиванием процессов разработки бота, поэтому всё отлаживал я прямо на Heroku, деплоя туда новую версию, т. к. всё было быстро, неудобств я не испытывал. Но для вероятных контрибьюторов я упростил задачу и сделал Long-Pooling в development-окружении, а WebHook’и только в production:

const TelegramBot = require('node-telegram-bot-api');
const botUrl = `/bot${botToken}`;
const bot = new TelegramBot(botToken, {
  onlyFirstMatch: true,
  polling: !isProduction,
});

app.listen(port, () => {
  console.log(`Express server is listening on ${port}`);
  if (isProduction) {
    bot.setWebHook(`${hookUrl}${botUrl}`);
  }
});

Следющая сложность — многоязычность. Довольно много времени было потрачено на поддержку нескольких языков, чтобы бот, в зависимости от языка, слал то или иное сообщение. Не понятно, зачем оно нужно было, но раз есть, значит есть. Поэтому тексты всех сообщений и регулярных выражений получаются из функции, которая принимает на вход яхык.
Ну, и пару раз я думал, что всё сломал, а это просто Telegram кэширует (или что-то ещё с ними делает) настройки языка.

Затем пришла пора работать с API ВКонтакте, обрабатывать хуки от Callback API и вот это всё. Документацию я опять же читал не совсем внимательно, но когда в ответ на запрос нужно отправлять помимо статуса 200 ещё и текст `ok` именно маленькими буквами было для меня чуть-чуть неожиданно.
А перед этим на этот же запрос нужно отправлять ответ в виде «сколько-то_там_значного» кода. Чтобы не производить деплой пару раз подряд я вынес эту переменную в ENV.

Callback API — лучшая часть VK API, потому что... использовать его ну очень просто. Получить текст записи, только что опубликованной в группе — элементарно! Однако, я хотел дать пользователям возможность получать контент по некоторому набору категорий, которые задавались в постах хэш-тэгами. Несколько реализаций «парсера» были испробованы, в итоге остановился на варианте ниже, т. к. проще, удобнее, теги можно обработать перед отправкой:

const formattedText = text.replace(
  regExp,
  (_, type) => {
    types.push(type);
    return `#${type}`;
  }
);

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

Таким образом получение планируемых эвентов выглядит как-то так:

fetch(/* Получаем 5 групп по определённому названию, с типом "event", старт которых только будет в будущем */)
  .then(r => r.json())
  .then((r) => {
    // Для вязкости добавим проверок
    if (r.response && r.response.items && r.response.items.length) {
      const groups = r.response.items.map(g => g.id);
      fetch(/* О каждой группе получим информацию */)
        .then(res => res.json())
        .then((res) => {
          if (res.response && res.response.length) {
            const processEvent = vkEventProcess(uid, bot);
            res.response
              .sort((a, b) => a.start_date - b.start_date)
              .reduce(
                // Тут "чейним" отправку сообщений, чтобы был точный порядок
                (promise, event) => promise.then(
                  () => processEvent(event),
                  err => console.log(err),
                ),
                Promise.resolve(),
              );
          }
        });
    } else {
      bot.sendMessage(uid, getNoEventsMessage(lang));
    }
  });

Получение ключа пользователя для сервера — отдельная повесть, в неё я углубляться не буду, просто скажу, что раньше было лучше... и проще с этим всем. Да и с документацией API дела лучше обстояли, во всяком случае не было подобного:

Описание `member_status`

Потом были этапы отладки, поиска багов и вот этого всего «предрелизного». Затем проведены интеграции, написаны тесты, небольшой README, добавлена возможность нормальной локальной отладки (был бы localetunnel, была б сказка, но там всё от параметров сети сильно зависит), CI, простой деплой на собственный инстанс Heroku и... «релиз».


После релиза я обнаружил, что событие «Репост» у ВКонтакте работает не совсем так, как я ожадал, поэтому пришлось быстренько фиксить, но все подписчики бота тогда уже получили невалидное сообщение.

Пост изначально планировался каким-то более содержательным и интересным, но из-за своей лени я остановился просто на перечислении сложностей, которые возникли в процессе разработки. Это всё-таки просто мой дневник в стиле «а что я вообще тогда делал».

Посмотреть на бота можно здесь: Telegram Bot.
Поучаствовать в разработке, предложить функционал и сообщить о багах можно здесь: GitHub.