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

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


    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 voteModel = mongoose.model('vote', VoteSchema);

    Vote


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


    function isNew(telegramId, question, callback) {
        voteModel.findOne({ telegramId: telegramId, question: question }, (err, existingVote) => {
            if (err) {
                callback(err, null);
                return;
            }
            if (existingVote) {
                callback(null, false);
            } else {
                callback(null, true);
            }
        });
    }

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


    function saveVote(voteInfo, callback) {
        isNew(voteInfo.telegramId, voteInfo.question, (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((err) => {
                    if (err) {
                        callback(err, null);
                    } else {
                        callback(null, true);
                    }
                });
    
            } else {
                callback(null, false);
            }
        })
    }

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


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


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


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

    Перейдем к обработчику:


    function addVoteHandler(bot, messageOptions) {
        bot.on('callback_query', (query) => {
            var clientInfo = botUtils.getClientInfo(query);
            var lastMessageText = botUtils.getLastMessageText(query);
    
            if (query.data === 'yes' || query.data === 'no') {
                var voteInfo = {
                    telegramId: clientInfo.telegramId,
                    question: lastMessageText,
                    answer: query.data,
                    time: Date.now().toString()
                };
    
                voteService.saveVote(voteInfo, (saveErr, _) => {
                    if (saveErr) {
                        bot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                        return;
                    }
                    messagesService.getByTitle('thanks', (err, message) => {
                        if (err) {
                            bot.sendMessage(clientInfo.telegramId, 'Some error! Sorry', messageOptions);
                        } else {
                            bot.sendMessage(clientInfo.telegramId, message.text, messageOptions);
                        }
                    });
                });
            }
        });
    }

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


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


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


    function buildMessageOptionsForVoting() {
        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>

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


    function votingController(request, response) {
        var message = request.body.message;
    
        userService.getAll((err, users) => {
            if (err) {
                console.log(err.message);
                return;
            }
    
            var messageOptionsForOptions = botUtils.buildMessageOptionsForVoting();
    
            users.forEach((user) => {
                bot.sendMessage(user.telegramId, message, messageOptionsForOptions);
            });
        });
    
        response.redirect('/');
    }

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


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


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


    function homeController(_, response) {
        userService.getAll((getUsersErr, users) => {
            if (getUsersErr) {
                console.log(getUsersErr.message);
                return;
            }
    
            voteService.getAll((getVotesErr, votes) => {
                if (getVotesErr) {
                    console.log(getVotesErr.message);
                    return;
                }
    
                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>

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


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


    function getClientInfo(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 бота.
    Спасибо за внимание, хабровчане!

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

    More

    Comments 0

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