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