
Telegram бот для мероприятий (Часть 1)
Доброго времени суток, Хабрахабр! В этой статье я хочу поделиться небольшим опытом во фрилансе по разработке чат-ботов для бизнеса. Статья будет полезна начинающим разработчикам, а также может подкинуть пару интересных идей для более опытной аудитории.
Дальнейший материал рассчитан на людей, которые представляют себе как создается простой express сервер, а также имеют базовый опыт работы с MongoDB.
Несколько лет назад, я со своей командой знакомых, столкнулся с интересным заказом. Нужно было реализовать инструмент для одной IT конференции. Этот сервис должен был уметь собирать моментальный feedback от аудитории и делиться информацией о ходе мероприятия. В результате обсуждений мы пришли к созданию Telegram бота. Это было самое простое и дешевое решение на тот момент.
Сегодня мы попробуем реализовать нечто похожее, а также разберемся с основным принципом работы чат-ботов.
Что должен уметь наш бот?
- Отправлять расписание мероприятия в виде telegra.ph ссылки.
- Шарить ссылку на сайт или чат мероприятия.
- Уметь рассылать уведомления пользователям из админки.
Систему голосования мы реализуем в следующей части.
Что нам понадобится для реализации проекта?
- mongo db — база данных, в которой мы будем хранить пользователей и тексты сообщений.
- express.js — библиотека для создания сервера на node.js, который поможет реализовать администраторскую панель для рассылки сообщений и управления настройками бота.
- node-telegram-bot-api — библиотека для работы с самим ботом из кода.
- mongoose — orm для mongodb.
Получаем все необходимое от Telegram
Для начала, необходимо создать бота на стороне Telegram и получить его токен.
Чтобы это сделать, находим в самом Telegram бота по имени @BotFather.
После короткого опроса, BotFather выдаст нам токен и ссылку на созданного бота.
Разворачиваем базу и коннектим с Mongoose
Можно воспользоваться сервисом 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);
Главное меню
После того как мы разобрались как происходит работа с ботом, давайте реализуем более сложную логику для нашей задачи.
Регистрацию хэндлера я вынес в отдельный метод
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
В самой базе нам потребуется две коллекции 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)
Делаем простой 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 бот для конференций (Продолжение)
Спасибо за внимание, хабровчане!
Comments 2
Only users with full accounts can post comments. Log in, please.