Доброго времени суток, Хабрахабр!


Vote


Сегодня мы разберемся как расширить функционал нашего бота. Перейдем сразу к сути...


Чему мы научим бота в этот раз?


  • Получать больше информации по пользователям
  • Делать глобальные опросы и хранить их результаты

В прошлой части мы научили бота:


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

Первая часть статьи тут!


Едим слона по частям


Давайте добавим рассылку массовых сообщений с возможностью получить feedback от пользователя. В данном примере мы сделаем реализацию двух кнопок (Да/Нет).


Задача будет состоять из нескольких частей:


  • Описать модель данных результатов для базы
  • Реализовать сервис для их сохранения и получения
  • Подготовить сообщение с двумя кнопками для голосования
  • Отловить нажатие на кнопку пользователем и сохранить его голос в базу
  • Добавить форму для создания опросов в админке
  • Отобразить результаты голосования в админке

Модель данных


Нам потребуется новая модель данных для результатов голосования


Что мы собираемся хранить:


  • Ид пользователя
  • Текст самого вопроса (Вы можете реализовать сохранение id вопроса)
  • Ответ на вопрос (текст кнопки)
  • Время

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var VoteSchema = new Schema({
    telegramId: String,
    question: String,
    answer: String,
    time: String
});

var Vote = mongoose.model('vote', VoteSchema);

VoteService


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


isNew: function (telegramId, question, callback) {
    // В качестве примера, сопоставим ид пользователя и вопрос голосования
    // Для реального проекта я рекомендую отдельно сделать id для конкретного голосования
    VoteModel.findOne({telegramId: telegramId, question: question}, function (err, existingVote) {
        if (err) {
            callback(err, null);
            return;
        }
        if (existingVote) {
            callback(null, false);
        } else {
            callback(null, true);
        }
    });
}

Сохранение результатов голосования будет выглядеть примерно так:


saveVote: function (voteInfo, callback) {
    this.isNew(voteInfo.telegramId, voteInfo.question, function (err, result) {
        if (err) {
            callback(err, null);
            return;
        }
        if (result) {
            var newVoteDto = new VoteModel({
                telegramId: voteInfo.telegramId,
                question: voteInfo.question,
                answer: voteInfo.answer,
                time: voteInfo.time
            });
            newVoteDto.save(function (err) {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, true);
                }
            });
        }else{
            callback(null, false);
        }
    })
}

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


Используем VoteService в новом Handlerе для кнопок голосования.
(Более подробно о том как устроены Handlers смотрите в предыдущей части)


Реализуем вспомогательный метод getLastMessageText в BotUtils.
В нем мы достаем текст сообщения, на которое ответил пользователь.


getLastMessageText: function (message) {
    return message.message.text;
}

Перейдем к VoteHandler:


var VoteHandler = {
    register: function (telegramBot, messageOptions) {
        telegramBot.on('callback_query', function (message) {
            // Достаем информацию о клиенте из сообщения
            var clientInfo = BotUtils.getClientInfo(message);
            // Достаем текст опроса
            var lastMessageText = BotUtils.getLastMessageText(message);
            // Здесь мы обрататываем реакцию на конкретный текст кнопок
            // Для более сложных опросов, нужно вынести сборку кнопок в отдельный конструктор
            // В текущем примере мы ограничимся простым опросом вида "да или нет"
            if(message.data === 'yes' || message.data === 'no'){
                // Соберем все данные о голосовании для сохранения
                var voteInfo = {
                    telegramId: clientInfo.telegramId,
                    question: lastMessageText,
                    answer: message.data,
                    time: Date.now().toString()
                };
                // Вызовем сохранение результатов.
                // Можно также описать вариант диалога, при котором мы сообщаем пользователю
                // что он уже голосовал (возьмите на заметку)
                VoteService.saveVote(voteInfo, function (saveErr, result) {
                    if (saveErr) {
                        telegramBot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                        return;
                    }
                    MessagesService.getByTitle('thanks', function (err, message) {
                        if(err){
                            telegramBot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                        }else{
                            telegramBot.sendMessage(clientInfo.telegramId, message.text, messageOptions);
                        }
                    });
                });
            }
        });
    }
};

Сообщение и кнопки


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


Начнем с формирования MessageOptions, которые будут содержать две кнопки.
Для этого добавим такой метод в BotUtils:
(В прошлой части можно найти краткий рассказ о том что такое MessageOptions и callback_data)


buildMessageOptionsForVoting: function () {
        return {
            parse_mode: "HTML",
            disable_web_page_preview: false,
            reply_markup : JSON.stringify({
                inline_keyboard: [
                    [{ text: 'Да', callback_data: 'yes' }, { text: 'Нет', callback_data: 'no'}]
                ]
            })
        };
    }

Новый контроллер


Добавим форму на страницу админки:


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

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


votingController: function (request, response) {
    // Получаем текст опроса с формы
    var message = request.body.message;
    var telegramBot = this.telegramBot;
    // Итерируемся по всем пользователям
    UserService.getAll(function (err, users) {
        if (err) {
            Logger.notify('Some error!' + err.message);
            return;
        }
        // Формируем настройки для сообщения, в которых описаны наши кнопки.
        var messageOptionsForOptions = BotUtils.buildMessageOptionsForVoting();
        users.forEach(function (user) {
            telegramBot.sendMessage(user.telegramId, message, messageOptionsForOptions);
        });
    });
    response.redirect('/');
}

Результаты голосования


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


В HomeControllerе достанем все результаты из базы:


VoteService.getAll(function (getVotesErr, votes) {
    if (getVotesErr) {
        Logger.notify('Some error!' + getVotesErr.message);
    }
    response.render('main', {users: users, votes: votes});
});

Добавим список результатов на вьюшку:


<h2>Результаты голосования:</h2>
<ul>
    <% for(var i=0; i<votes.length; i++) {%>
    <li class="list-group-item list-group-item-info">
        <%= votes[i].telegramId %>
        <%= votes[i].question %>
        <%= votes[i].answer %>
    </li>
    <% } %>
</ul>

Больше данных о пользователе


Чтобы добавить к своей рассылке обращение по имени, можно расширить получаемую информацию о пользователе. (При первом обращении пользователя к боту)


Добавим такой метод в BotUtils. Не забудьте добавить новые поля firstName и lastName к модели данных UserModel.


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

Исходники


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


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


Продолжение


Если появятся интересные предложения, я попробую реализовать что-то новое у нашего Telegram бота.
Спасибо за внимание, хабровчане!

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