LE Blog

Инженер от души

07.09.2016 firtree_right Телеграм-бот для Яндекс.ПДД

Введение

Увлечение чат-ботами докатилась и до меня. Как это может случиться наилучшим образом, — по необходимости. А необходимость возникла в совместном использовании Яндекс почты для домена. Оказалось, что веб-интерфейс для этого совершенно не приспособлен, но есть API. Но писать целый сайт для этого кажется накладным, а чат-бот — в самый раз. И вообще, мне кажется, это один из самых продуктивных способов использования технологии: интерфейс к API.

Ссылку на полный текст бота я приложу в конце. Сам бот не содержит в себе полного функционала всего API, а имеет лишь необходимую на данный момент часть. Сейчас хотел бы поделиться парой находок, которые пригодятся всем, кто захочет писать чат-ботов для Телеграм с помощью node.js.

Chat bot

Хуки в продакшне

У ботов Телеграм есть два способа работы: когда бот сам обращается за обновлениями по определённому адресу (polling) и с помощью веб-хуков, когда сервера сами дёргают заданный хук для передачи данных боту. В продакшне, конечно, удобнее работать с хуками, а при разработке — нет, поскольку сервер запускается на локальной машине. Кроме этого я рекомендую завести другого бота для разработки, чтобы те, кто пользуются вашим ботом в продакшне, не замечали, как вы разрабатываете. Возможность сделать это я нашёл пока только в одной библиотеке: node-telegram-bot-api с помощью недокументированной функции processUpdate. Делается это довольно просто. При инициализации бота в файле lib/bot.js:

if (process.env.NODE_ENV === 'production') {
    bot = new TelegramBot(config.botToken, {polling: false});
    bot.setWebHook(config.host + config.url);
} else {
    bot = new TelegramBot(config.devBotToken, {polling: true});
}

А затем уже в серверной части, которая, хоть и запускается всегда, имеет значение только для продакшна, в файле lib/web.js:

app.post(config.url, function (req, res) {
    options.bot.processUpdate(req.body);
    res.status(200).send({}).end();
});

Весь остальной код для бота работает в обоих случаях одинаково и в изменениях не нуждается, что совершенно прекрасно!

Оповещение об остановке

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

Если вы запускаете приложения с помощью pm2, то этот менеджер использует для остановки процесса тот же сигнал SIGINT, что мы используем, когда останавливаем сервер в разработке с помощью Ctrl-C. Очень удобно! В файле index.js

process.on('SIGINT', function () {
    Promise.all(config.permitUsers.map(function (userId) {
        return bot.sendMessage(userId, 'Бот временно выключается. Только спокойствие!', {
            reply_markup: {
                hide_keyboard: true
            }
        });
    })).then(gracefulClose).catch(function (err) {
        console.log(err);
        gracefulClose();
    });
});

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

Материалы для самостоятельного изучения

  1. Полный на текущий момент код Телеграм-бота для Яндекс.ПДД;
  2. API Яндекс.ПДД;
  3. Как установить приложение node.js на ubuntu 16.04.

23.06.2016 firtree_right Тестирование сервера Node.js

Вводная

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

Для тестирования кода на ноде я использую жасмин. Для запуска задач — галп.

Сегодня хочу рассказать о двух простых приёмах для тестирования кода сервера, которые позволяют выделять и тестировать только то, что нужно.

Staged reality

Кто запускает сервер

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

if (require.main === module) {
    app.use('/', require(path.join(__dirname, 'routes', 'main'))());
    var server = app.listen(5000, function () {
        ...
    });
} else {
    exports = module.exports = function (options) {
        app.use('/', require(path.join(__dirname, 'routes', 'main'))(options));
        return app;
    };
}

Таким образом, уже в тестовом фреймворке мы будем запускать:

app = require('index.js');
server = app(...).listen(6000, function (err) {...});

перед каждым тестом, а после каждого теста:

server.close(function (err) {...});

Очень удобно!

Имитация библиотек

Второе, что бы хотелось сделать при тестировании сервера, — имитировать используемые им библиотеки, чтобы тестировать только код сервера, а не библиотеки. Связки тоже важны, но только тогда, когда мы этого сами хотим. И тут появляется интересный момент. Если мы хотим имитировать библиотеки при тестировании, нам нужно использовать определённый паттерн при программировании. Паттерн фабрик. То есть мы не просто тестируем свой код, но устраиваем архитектуру и стиль так, чтобы его удобнее было тестировать.

В случае с паттерном фабрики мы возвращаем в качестве модуля не объект, а функцию, которая создаёт объект в соответствии с нашими параметрами:

exports = module.exports = function (options) {
    options = options || {};
    var customLib = options.customLib || require(...);

    /* GET main page. */
    router.get('/', function (req, res, next) {
        res.end('Real server text. CustomLib: ' + customLib.name);
    });

    return router;
};

Как видно в вызовах выше, мы передаём сквозным образом наши опции при тестировании, а при нормальном запуске, не передаём ничего.

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

Для самостоятельного изучения

Полный код примера в сборе