Доброго времени суток, Хабрахабр!
Сегодня мы разберемся как расширить функционал нашего бота. Перейдем сразу к сути...
Чему мы научим бота в этот раз?
В прошлой части мы научили бота:
- Отправлять расписание мероприятия в виде 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 бота.
Спасибо за внимание, хабровчане!