Доброго времени суток, Хабрахабр! В этой статье я хочу поделиться небольшим опытом во фрилансе по разработке чат-ботов для бизнеса. Статья будет полезна начинающим разработчикам, а также может подкинуть пару интересных идей для более опытной аудитории.
Дальнейший материал рассчитан на людей, которые представляют себе как создается простой express сервер, а также имеют базовый опыт работы с MongoDB.
Несколько лет назад, я со своей командой знакомых, столкнулся с интересным заказом. Нужно было реализовать инструмент для одной IT конференции. Этот сервис должен был уметь собирать моментальный feedback от аудитории и делиться информацией о ходе мероприятия. В результате обсуждений мы пришли к созданию Telegram бота. Это было самое простое и дешевое решение на тот момент.
Сегодня мы попробуем реализовать нечто похожее, а также разберемся с основным принципом работы чат-ботов.
Что должен уметь наш бот?
- Отправлять расписание мероприятия в виде telegra.ph ссылки.
- Шарить ссылку на сайт или чат мероприятия.
- Уметь рассылать уведомления пользователям из админки.
Систему голосования мы реализуем в следующей части.
Что нам понадобится для реализации проекта?
- mongo db — база данных, в которой мы будем хранить пользователей и тексты сообщений.
- express.js — библиотека для создания сервера на node.js, который поможет реализовать администраторскую панель для рассылки сообщений и управления настройками бота.
- node-telegram-bot-api — библиотека для работы с самим ботом из кода.
- mongoose — orm для mongo db.
Получаем все необходимое от Telegram
Для начала, необходимо создать бота на стороне Telegram и получить его токен.
Чтобы это сделать, находим в самом Telegram бота по имени @BotFather.
После короткого опроса, BotFather выдаст нам токен и ссылку на созданного бота.
Разворачиваем базу и коннектим Mongoose
Можно воспользоваться сервисом mlab.com для создания бесплатной mongo базы.
На скриншоте я подчеркнул место где располагается токен.
Обратите внимание: dbuser и dbpassword нужно заменить на указанные при создании базы данные для входа.
Для работы с базой воспользуемся библиотекой mongoose.js
В начале главного файла app.js коннектим mongoose с базой через токен, полученный от mlab.
var mongoose = require('mongoose');
mongoose.connect('Сюда передаем наш токен', { useMongoClient: true });
Bot-backend
Прежде чем мы приступим к разработке нашего проекта, давайте посмотрим как создается простой Telgram bot.
Чтобы четко разделить бота, с которым работет клиент на стороне Telegram и его логику обработки событий на стороне нашего сервера, назовем серверную часть Bot-backend. Вся суть реализации бот-бэкенда заключается в обработке ответных действий на сообщения пользователя.
Сами обработчики "handlers" бывают нескольких типов. Мы можем реагировать на какой-то конкретный текст, нажатие кнопки или отправку пользователем файлов. Посмотрим как это работает на примере.
Для начала заимпортим библиотеку:
var TelegramBot = require('node-telegram-bot-api');
После этого создадим конкретную реализацию api, отправив в конструктор полученный от телеграмма токен.
var bot = new TelegramBot(telegramToken, { polling: true });
Теперь у нас есть объект на который мы можем навешивать наши обработчики событий.
Отправка сообщения пользользователю
Для отправки сообщения нам понадобятся три вещи:
- 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);
}
});
Для удобства можно сделать несколько методов для создания кнопок
var BotUtils = {
// Получение id клиента из сообщения
getClientIdFromMessage: function (message) {
return message.hasOwnProperty('chat') ? message.chat.id : message.from.id;
},
// Создание обычной кнопки с callback_data
buildDefaultButton : function (text, callback_data) {
return [{
text: text,
callback_data: callback_data
}]
},
// Создание кнопки-ссылки на внешний ресурс
buildUrlButton : function (text, url) {
return [{
text: text,
url: url
}]
},
// Заготовка для кнопки "поделиться"
buildShareButton: function (text, shareUrl) {
return [{
text: text,
url: 'https://telegram.me/share/url?url=' + shareUrl
}]
},
// Сборка настроек для сообщения
buildMessageOptions: function (buttons) {
return {
parse_mode: "HTML",
disable_web_page_preview: false,
reply_markup : JSON.stringify({
inline_keyboard: buttons
})
}
}
};
Главное меню
После того как мы разобрались как происходит работа с ботом, давайте реализуем более сложную логику для нашей задачи.
Регистрацию handlerа я вынес в отдельный класс, который примимает в метод register самого бота и базовый набор опций.
var StartHandler = {
register: function (bot, messageOptions) {
var clientMessage = new RegExp('\/start');
bot.onText(clientMessage, function (message, match) {
//Получение Id пользователя я вынес в отдельный метод getClientIdFromMessage в классе BotUtils.
var clientId = BotUtils.getClientIdFromMessage(message);
// В этом месте мы вызываем UserService с помощью которого сохраняем пользователя в базу при первом обращении к боту.
bot.saveUser(clientId, function (saveErr, result) {
if (saveErr) {
bot.sendMessage(clientId, 'Some error! Sorry', messageOptions);
return;
}
// Теперь текст сообщения мы получаем из базы. Дальше мы сможем редактировать их из админки, не правя код самого проекта.
MessagesService.getByTitle('start', function (getErr, message) {
if (getErr) {
bot.sendMessage(clientId, 'Some error! Sorry', messageOptions);
} else {
bot.sendMessage(clientId, message.text, messageOptions);
}
});
});
});
}
};
Пользователь будет сохранен при первом обращении к боту, это потребуется нам для автоматической рассылки приватных и глобальных сообщений через админку.
Теперь давайте реализуем работу всех сервисов.
Работа с MongoDB
В самой базе нам потребуется две коллекции users и messages. Можно создать их прямо на сайте mlab.
Теперь опишем основные модели данных.
UserModel будет содержать в себе строковое поле telegramId. В дальнейшем можно добавить имя, номер телефона, ссылку на аватарку и другие данные, которые понадобятся для ваших задач.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
telegramId: String
});
var User = mongoose.model('user', UserSchema);
MessageModel будет содержать название и текст сообщения. Их можно будет редактировать из админки. Изменение текста не потребует вмешательства в сам проект.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var MessageSchema = new Schema({
title : String,
text: String
});
var Message = mongoose.model('message', MessageSchema);
Сервисы
Рассмотрим сохранение пользователя, о котором говорилось выше. Сделам метод проверки на то что пользователь новый (его нет в базе).
isNew: function (telegramId, callback) {
// Делаем поиск по ид, проверяем что такой пользователь есть, если нет то возвращаем true
UserModel.findOne({telegramId: telegramId}, function (err, existingUser) {
if (err) {
callback(err, null);
return;
}
if (existingUser) {
callback(null, false);
} else {
callback(null, true);
}
});
}
Во время сохранения пользователя используем метод, реализованный выше. Если пользователь новый, создаем новый объект и сохраняем его в базу.
saveUser: function (telegramId, callback) {
// Здесь делаем проверку на то что пользователя нет в базе
// this вызывается потому что оба метода я разместил в одном объекте UserService
this.isNew(telegramId, function (err, result) {
if (err) {
callback(err, null);
return;
}
if (result) {
// Создаем новый дата объект на основе модели вызываем метод save
var newUserDto = new UserModel({
telegramId: telegramId
});
newUserDto.save(function (err) {
if (err) {
callback(err, null);
} else {
callback(null, true);
}
});
}else{
callback(null, false);
}
});
}
Дальше реализуем сохранение и получение сообщений в MessageService.
Здесь нет ничего сложного. Пока нам потребуется только метод для получания сообщения по его наименованию. Реактированием сообщений из админки мы займемся в следующей части, пока заполним их ручками прямо в базе.
var MessagesService = {
getByTitle: function (title, callback) {
MessageModel.findOne({title: title}, function (err, message) {
if (err) {
callback(err, null);
}
else {
callback(null, message);
}
});
}
};
Express сервер и администраторская панель
HomeController
HomeController рендерит нам страницу c полем ввода для отправки глобального сообщения. На вьюшку отправляем массив пользователей, чтобы вывести список. Позже он нам пригодится для отправки личных сообщений.
homeController: function (request, response) {
UserService.getAll(function (err, users) {
if (err) {
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>
GlobalMessageController
Он обрабатывает Post запрос после нажатия кнопки "Отправить", вызывая рассылку сообщений по всем пользователям из базы, которых мы сохраняем при первом обращении к боту.
globalMessageController: function(request, response) {
var message = request.body.message;
var bot = this.bot;
UserService.getAll(function (err, users) {
if (err) {
return;
}
users.forEach(function (user) {
bot.sendMessage(user.telegramId, message, {});
});
});
response.redirect('/');
}
Конфигурация и отладка
Для запуска проекта необходимо иметь Telegram token и connection string от базы данных.
Для разработки и продакшн версии создадим две отдельные конфигурации и поместим их в config.json.
Более правильным решением было бы вынести их в переменные окружения сервера.
getConfig: function () {
var configJson = JSON.parse(fs.readFileSync('./src/config.json', 'utf8'));
if(process.env.NODE_ENV === 'production'){
return configJson.production;
}else{
return configJson.development;
}
}
Для отладки можно использовать библиотеку simple-node-logger, которая будет писать все действия с Logger.notify() в logfile.log, который можно будет просмотреть через ssh или ftp на сервере.
var nodeLogger = require('simple-node-logger');
var Logger = {
logger: nodeLogger.createSimpleLogger('logfile.log'),
notify: function (data) {
this.logger.info(data, new Date().toJSON());
}
};
Исходники
Полный код всего проекта лежит тут: GitHub
Можете свободно использовать его как основу для своих проектов. Настоятельно рекомендую провести масштабный рефакториг, а также переписать на ES6 или TypeScript.
Свой telegram token и mongo connection string необходимо прописать в файле /src/config.json
В качестве хостинга, я рекомендую использовать Heroku. Единственным неудобством будет засыпание сервера через 30 минут. Для этого существует древний лайфхак с пингованием (можете погуглить специальные сервисы для этого).
Продолжение
В следующих частях я расскажу как прикрутить систему голосования и возможность оставлять заявки. А дальше мы попробуем реализовать небольшого Scrum бота для корпоративного мессенджера Slack.
Спасибо за внимание, хабровчане!
Комментарии (2)
alphaZ
17.11.2017 14:14Heroku плохо с pooling'ом дружит. Лучше, уж если хостить на Heroku использовать WebHook.
Пример бота с WebHook.
TotalPipets
Спасибо за статью!
Как раз начальство спрашивало про подобную возможность для наших клиентов и тут ваша статья прямо в помощь. Буду ждать продолжения.