Telegram бот для мероприятий (Часть 1)

Bot


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


Дальнейший материал рассчитан на людей, которые представляют себе как создается простой express сервер, а также имеют базовый опыт работы с MongoDB.


Несколько лет назад, я со своей командой знакомых, столкнулся с интересным заказом. Нужно было реализовать инструмент для одной IT конференции. Этот сервис должен был уметь собирать моментальный feedback от аудитории и делиться информацией о ходе мероприятия. В результате обсуждений мы пришли к созданию Telegram бота. Это было самое простое и дешевое решение на тот момент.


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


Что должен уметь наш бот?


  • Отправлять расписание мероприятия в виде telegra.ph ссылки.
  • Шарить ссылку на сайт или чат мероприятия.
  • Уметь рассылать уведомления пользователям из админки.

Систему голосования мы реализуем в следующей части.


Что нам понадобится для реализации проекта?


  • mongo db — база данных, в которой мы будем хранить пользователей и тексты сообщений.
  • express.js — библиотека для создания сервера на node.js, который поможет реализовать администраторскую панель для рассылки сообщений и управления настройками бота.
  • node-telegram-bot-api — библиотека для работы с самим ботом из кода.
  • mongoose — orm для mongodb.

Получаем все необходимое от Telegram


BotFather


Для начала, необходимо создать бота на стороне Telegram и получить его токен.


Чтобы это сделать, находим в самом Telegram бота по имени @BotFather.


После короткого опроса, BotFather выдаст нам токен и ссылку на созданного бота.


Разворачиваем базу и коннектим с Mongoose


Mlab


Можно воспользоваться сервисом mlab.com для создания бесплатной mongo базы.
На скриншоте я подчеркнул место где располагается токен.


Обратите внимание: dbuser и dbpassword нужно заменить на указанные при создании базы данные для входа.


Для работы с базой воспользуемся библиотекой mongoose.js


В начале главного файла app.js коннектим mongoose с базой через токен, полученный от mlab.


var mongoose = require('mongoose');
var token = 'ваш токен';
mongoose.connect(token, { useNewUrlParser: true, useUnifiedTopology: true });

Bot-backend


Прежде чем мы приступим к разработке нашего проекта, давайте посмотрим как создается простой Telgram bot.


Чтобы четко разделить бота, с которым работет клиент на стороне Telegram и его логику обработки событий на стороне нашего сервера, назовем серверную часть Bot-backend. Вся суть реализации бот-бэкенда заключается в обработке ответных действий на сообщения пользователя.


Сами обработчики "handlers" бывают нескольких типов. Мы можем реагировать на какой-то конкретный текст, нажатие кнопки или отправку пользователем файлов. Посмотрим как это работает на примере.


Для начала заимпортим библиотеку:


var TelegramBot = require('node-telegram-bot-api');

После этого создадим конкретную реализацию api, отправив в конструктор полученный от телеграмма токен.


var bot = new TelegramBot(telegramToken, { polling: true });

Теперь у нас есть объект на который мы можем навешивать наши обработчики событий.


Для начала можно повесить на бота обработчик события polling_error. Это позволит нам выводить в консоль детали ошибок обращения бота к апи telegram.


bot.on("polling_error", (m) => console.log(m));

Отправка сообщения пользользователю


Для отправки сообщения нам понадобятся три вещи:


  • Id пользователя, которое мы можем вытащить из пришедшего сообщения.
  • Текст сообщения (можно использовать Markdown или Html разметку)
  • Набор опций сообщения.

bot.sendMessage(clientId, 'Привет, хабр!', messageOptions);

Что представляют из себя опции сообщения? Это некий объект, который содержит в себе настройки для отображения сообщения, а также набор кнопок. В данном случае, мы говорим что сообщение может быть распарсено как html разметка, выключаем превью ссылок, если таковые содержаться в сообщении, и передаем набор кнопок. Более подробно с кнопками мы разберемся дальше.


var messageOptions = {
    parse_mode: "HTML",
    disable_web_page_preview: false,
    reply_markup: JSON.stringify({
        inline_keyboard: [[{
            text: 'Название кнопки',
            callback_data: 'do_something'
        }]]
    })
}

Обратите внимание, callback_data имеет ограниченный размер. Более подробно об этом читайте в докумнтации Telegram к BotApi.


Обработчики событий


Все обработчики событий я рекомендую выносить в отдельный каталог handlers.


Рассмотрим базовый handler, который срабатывает при первом обращении к боту, В самом начале диалога с ботом, Telegram автоматически отсылает сообщение "/start", которое мы ловим регуляркой. После этого достаем telegram id пользователя и отсылаем ему ответное соообщение.


bot.onText(new RegExp('\/start'), function (message, match) {
    // вытаскиваем id клиента из пришедшего сообщения
    var clientId = message.hasOwnProperty('chat') ? message.chat.id : message.from.id;
    // посылаем ответное сообщение
    bot.sendMessage(clientId, 'Some message', messageOptions);
});

Ниже реализован handler другого типа, он реагирует на нажатие кнопки из тех, что были посланы вместе с MessageOptions.


bot.on('callback_query', function (message) {
    var clientId = message.hasOwnProperty('chat') ? message.chat.id : message.from.id;
    // То что мы записали в callback_data у кнопок приходит в message.data
    if(message.data === 'do_something'){
        bot.sendMessage(clientId, 'Button clicked!', messageOptions);
    }
});

Для удобства можно сделать несколько методов для создания кнопок


function buildDefaultButton(text, callback_data) {
    return [{
        text: text,
        callback_data: callback_data
    }]
}

function buildUrlButton(text, url) {
    return [{
        text: text,
        url: url
    }]
}

function buildShareButton(text, shareUrl) {
    return [{
        text: text,
        url: 'https://telegram.me/share/url?url=' + shareUrl
    }]
}

function buildMessageOptions(buttons) {
    return {
        parse_mode: "HTML",
        disable_web_page_preview: false,
        reply_markup: JSON.stringify({
            inline_keyboard: buttons
        })
    }
}

После этого мы сможем добавлять кнопки к MessageOptions вот таким образом


var buttons = [
    botUtils.buildDefaultButton('Кнопка', 'button1'),
    botUtils.buildShareButton('Поделиться', 'url'),
    botUtils.buildUrlButton('Ссылка', 'link')
];

var messageOptions = botUtils.buildMessageOptions(buttons);

Главное меню


Hello


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


Регистрацию хэндлера я вынес в отдельный метод


function addStartHandler(bot, messageOptions) {
    var clientMessage = new RegExp('\/start');

    bot.onText(clientMessage, (query, _) => {
        var clientInfo = botUtils.getClientInfo(query);

        userService.saveUser(clientInfo, (saveErr, _) => {
            if (saveErr) {
                bot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                return;
            }

            messagesService.getByTitle('start', (getErr, message) => {
                if (getErr) {
                    bot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                } else {
                    bot.sendMessage(clientInfo.telegramId, message.text, messageOptions);
                }
            });
        });
    });
}

В коде хэндлера был использован метод getClientInfo, вот его код


function getClientInfo(message) {
    return {
        firstName: message.from.first_name,
        lastName: message.from.last_name,
        telegramId: message.hasOwnProperty('chat') ? message.chat.id : message.from.id
    };
}

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


Теперь давайте реализуем работу всех сервисов.


Работа с MongoDB


Collections


В самой базе нам потребуется две коллекции users и messages. Можно создать их прямо на сайте mlab.


Теперь опишем основные модели данных.


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


 var mongoose = require('mongoose');
 var Schema = mongoose.Schema;
 var UserSchema = new Schema({
     telegramId: String
 });
 var userModel = mongoose.model('user', UserSchema);

Message будет содержать название и текст сообщения. Их можно будет редактировать из админки. Изменение текста не потребует вмешательства в сам проект.


 var mongoose = require('mongoose');
 var Schema = mongoose.Schema;
 var MessageSchema = new Schema({
     title : String,
     text: String
 });
 var messageModel = mongoose.model('message', MessageSchema);

Сервисы


Для работы с пользователями, сообщениями и другими сущностями, я создаю несколько сервисов userService.js, messageService.js и.т.д
Для начала рассмотрим сохранение пользователя, о котором говорилось выше. Сделаем метод проверки на то что пользователь новый (его нет в базе).


function isNew(telegramId, callback) {
    userModel.findOne({ telegramId: telegramId }, (err, existingUser) => {
        if (err) {
            callback(err, null);
            return;
        }

        if (existingUser) {
            callback(null, false);
        } else {
            callback(null, true);
        }
    });
}

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


function saveUser(userInfo, callback) {
    isNew(userInfo.telegramId, (err, result) => {
        if (err) {
            callback(err, null);
            return;
        }

        if (result) {
            var newUserDto = new userModel({
                telegramId: userInfo.telegramId,
                fistName: userInfo.firstName,
                lastName: userInfo.lastName
            });

            newUserDto.save((err) => {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, true);
                }
            });
        } else {
            callback(null, false);
        }
    })
}

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


function getByTitle(title, callback) {
    messageModel.findOne({ title: title }, (err, message) => {
        if (err) {
            callback(err, null);
        }
        else {
            callback(null, message);
        }
    });
}

Админка (Express)


Admin


Делаем простой Express сервер и регистрируем контроллеры. У вас получится что-то вроде.


function startServer(bot) {
    bot = bot;

    var app = express();
    app.use(express.static(path.join(__dirname, 'public')));
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));
    app.set('views', path.join(__dirname, './views'));
    app.set('view engine', 'ejs');

    app.get('/', homeController);
    app.post('/globalmessage', globalMessageController);

    var port = process.env.PORT || 5000;
    app.listen(port, function () {
        console.log(`Server started at ${port}`);
    });
}

Home


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


function homeController(request, response) {
    userService.getAll((getUsersErr, users) => {
        if (getUsersErr) {
            console.log(getUsersErr.message);
            return;
        }
        response.render('main', { users: users });
    });
}

Сама вьюшка (main.ejs) может выглядеть примерно так:


<h2>Массовая рассылка</h2>
<form method="POST" action="/globalmessage">
    <h3>Сообщение:</h3>
    <textarea class="form-control" rows="3" type="text" name="message">"Напишите что-нибудь..."</textarea>
    <br/><br/>
    <input align="right" class="btn btn-success" type="submit" value="Отправить">
</form>

<h2>Все кто взаимодейсвтовал с ботом:</h2>
<ul>
    <% for(var i=0; i<users.length; i++) {%>
    <li class="list-group-item list-group-item-info">
        <%= users[i].telegramId %>
    </li>
    <% } %>
</ul>
<br/>
<a href="/" class="btn btn-success">Обновить</a>

GlobalMessage


Он обрабатывает Post запрос после нажатия кнопки "Отправить", вызывая рассылку сообщений по всем пользователям из базы, которых мы сохраняем при первом обращении к боту.


function globalMessageController(request, response) {
    var message = request.body.message;

    userService.getAll((err, users) => {
        if (err) {
            console.log(err.message);
            return;
        }

        users.forEach((user) => {
            bot.sendMessage(user.telegramId, message, {});
        })
    });

    response.redirect('/');
}

Конфигурация и отладка


Для запуска проекта необходимо иметь telegramToken и connectionString от монги.
Для разработки и продакшн версии создадим две отдельные конфигурации и поместим их в config.json.


Более правильным решением было бы вынести их в переменные окружения сервера.


config.json


{
    "production": {
        "telegramToken": "",
        "connectionString": "",
    },
    "development": {
        "telegramToken": "",
        "connectionString": "",
    }
}

var configs = JSON.parse(fs.readFileSync('./src/config.json', 'utf8'));
var config = process.env.NODE_ENV === 'production'
    ? configs.production
    : configs.development;

Для отладки можно использовать библиотеку simple-node-logger, которая будет писать все действия с logger.notify() в logfile.log, который можно будет просмотреть через ssh или ftp на сервере.


Исходники


Полный код всего проекта лежит тут: GitHub


Можете свободно использовать его как основу для своих проектов. Настоятельно рекомендую провести масштабный рефакториг, а также переписать на ES6 или TypeScript.


Свой telegram token и mongo connection string необходимо прописать в файле /src/config.json


В качестве хостинга, я рекомендую использовать Heroku. Единственным неудобством будет засыпание сервера через 30 минут. Для этого существует древний лайфхак с пингованием (можете погуглить специальные сервисы для этого).


Продолжение


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


Следующую часть можно найти тут: Telegram бот для конференций (Продолжение)


Спасибо за внимание, хабровчане!

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 2

    0
    Спасибо за статью!
    Как раз начальство спрашивало про подобную возможность для наших клиентов и тут ваша статья прямо в помощь. Буду ждать продолжения.
      0
      Heroku плохо с pooling'ом дружит. Лучше, уж если хостить на Heroku использовать WebHook.
      Пример бота с WebHook.

      Only users with full accounts can post comments. Log in, please.