Во время конференции Microsoft Build 2016 был анонсирован Microsoft Bot Framework (сессия с Build 2016: видео). С его помощью можно создать бота (на C# или Node.js), которого потом можно подключить к различным каналам / приложениям: СМС, Skype, Telegram, Slack и т.д. Мы пишем бота, используя Bot Builder SDK от Microsoft, а все проблемы взаимодействия с третьесторонними API берет на себя Bot Connector (см. изображение). Звучит красиво, попробуем создать простого бота, который мог бы переводить деньги с карты на карту (логику перевода возьмем у Альфа Банка — тестовый стенд, описание API: Альфа Банк), испытав все прелести продукта, находящегося в альфа-версии.
Disclaimer: во время написания статьи Microsoft выпустил новую версию фреймворка, так что ждите вторую серию: мигрируем бота с v1 на V3.

Для успешной разработки бота нам будут нужны:
Теперь скачаем шаблон проекта для Bot Framework: aka.ms/bf-bc-vstemplate. Чтобы новый тип проекта был доступен в Visual Studio 2015, скопируем скаченный архив в папку “%USERPROFILE%\Documents\Visual Studio 2015\Templates\ProjectTemplates\Visual C#". Теперь мы готовы создать самого простого эхо-бота.
Откроем Visual Studio 2015, у нас появился новый тип проекта:

Созданный проект представляет собой Web API проект с одним контроллером — MessagesController, у которого в свою очередь всего один доступный метод Post:
Этот метод принимает единственный параметр типа Message, представляющий собой не только сообщение, отправленному нашему боту, но и событие, например, добавление нового пользователя в чат или завершение разговора. Чтобы узнать, чем именно является объект message надо проверить его свойство Type, что и делается в контроллере. Если это обычное сообщение от пользователя (message.Type == «Message»), мы можем прочитать само сообщение, обработать его и ответить — с помощью метода CreateReplyMessage. Простой бот готов, теперь попробуем его запустить и проверить работоспособность. Microsoft предоставляет удобную утилиту Bot Framework Emulator (скачать для v1), которая позволяет удобно запускать и отлаживать ботов на локальной машине. Запустим наш проект EchoBot, в браузере покажется такая страница по адресу localhost:3978/

Запустим теперь установленный Bot Framework Emulator, который знает, что нашего запущенного бота стоит искать именно на порту 3978:

Отправим сообщение боту, нам придет ответ. Как вы видим, все работает. Теперь рассмотрим создание бота, который бы на основе введенных пользователем данных мог бы перевести деньги с карты на карту.
Для того чтобы перевести деньги с карты на карту, нам нужна информация об этих картах и сумма перевода. Для облегчения задачи написания стандартных сценариев с помощью Bot Framework Microsoft была создана поддержка двух наиболее распространенных вариантов взаимодействия с ботом: Dialogs и FormFlow. В нашем случае подходит FormFlow, потому что всю работу бота можно представить как заполнение некой формы данными, а затем ее обработку. Dialogs же позволяет работать с более простыми сценариями, например, сценарий оповещения при наступлении заданного события (может пригодиться для мониторинга серверов). Начнем создание бота с добавления класса, который и будет представлять собой форму, которую пользователю необходимо заполнить. Этот класс должен быть помечен как [Serializable], для аннотации свойств используются атрибуты из пространства имен Microsoft.Bot.Builder.FormFlow:
Для того, чтобы Bot Framework мог использовать класс в FormFlow, все открытые поля или свойства должны принадлежать одному из следующих типов:
Атрибут Prompt отвечает за то, какой текст будет показан в качестве подсказки к заполнению поля, Describe — как поле будет называться для пользователя. Теперь с помощью класса FormBuilder нам нужно сказать Bot Framework, что мы хотим использовать именно класс CardToCardTransfer в качестве формы для диалога. Создадим новый класс CardToCardFormBuilder:
Мы создаем экземпляр класса FormBuilder<CardToCardTransfer>, указывая, что используем CardToCardTransfer в качестве формы. Теперь с помощью цепочки вызова методов, мы делаем следующее
Все достаточно просто, но теперь мы хотим добавить простую валидацию введенных значений и расчет комиссии. Для расчета комиссии воспользуемся тем, что у нас есть класс AlfabankService, реализующий все взаимодействие с банковским API. Для валидации номера карты создадим класс CardValidator, чтобы указать делегат, использующийся для валидации поля, методу Field надо его передать третьим параметром. Расчет комиссии также приходится делать в методе валидации, потому что в версии 1 Bot Framework не предоставлял для этого иных механизмов.
Остался последний шаг — интегрировать CardToCardFormBuilder в контроллер. Для этого нам нужен метод, возвращающий IDialog<CardToCardTransfer>, чтобы его в свою очередь передать вторым параметром в метод Conversation.SendAsync.
Собственно связывание происходит в коде Chain.From(() => FormDialog.FromForm(CardToCardFormBuilder.MakeForm)), а затем в метод Do мы передаем метод, который ожидает завершение формирования запроса и его обрабатывает, попутно отвечая за обработку ошибок. Теперь мы можем запустить бота и протестировать его работу в эмуляторе:

Можно убедиться, что бот работает так, как ожидалось, теперь нам подружить его с Bot Connector.
Для начала нам нужно загрузить нашего бота на какой-то общедоступный URL, например, в Azure (бесплатная подписка подойдет): https://alfacard2cardbot.azurewebsites.net. Теперь заходим dev.botframework.com с помощью учетной записи Microsoft. В верхнем меню выбираем «Register a Bot», вводим все обязательные поля: имя, описание, Messaging endpoint — тот самый общедоступный URL и т.д.

Не забудем обновить наш web.config, добавив туда AppId и AppSecret, сгенерированные нам на этом шаге. Задеплоим эти изменения. Теперь наш бот появился в меню «My Bots», можно убедиться, что Bot Connector правильно взаимодействует с ботом при помощи окна «Test connection to your bot» внизу слева. Теперь осталось добавить взаимодействие с Telegram, для этого в правом столбце выберем «Add another channel» — «Telegram» — «Add», откроется вот такое окно, в котором по шагам расписано, как нам добавить Telegram бота:

Telegram боту можно написать @AlfaCard2CardBot , деньги не переведутся, среда тестовая. Код можно найти в GitHub: https://github.com/StanislavUshakov/AlfaCardToCardBot.
В следующей серии будем мигрировать бота на версию 3!
Disclaimer: во время написания статьи Microsoft выпустил новую версию фреймворка, так что ждите вторую серию: мигрируем бота с v1 на V3.

Готовим среду разработки
Для успешной разработки бота нам будут нужны:
- Visual Studio 2015
- Microsoft Account, чтобы залогиниться в dev.botframework.com
- URL с задеплоенным кодом нашего бота. Этот URL должен быть доступен публично.
- Аккаунты разработчиков Telegram / Skype / etc, чтобы иметь возможность добавить каналы коммуникации (для каждого приложения свои хитрости и настройки).
Теперь скачаем шаблон проекта для Bot Framework: aka.ms/bf-bc-vstemplate. Чтобы новый тип проекта был доступен в Visual Studio 2015, скопируем скаченный архив в папку “%USERPROFILE%\Documents\Visual Studio 2015\Templates\ProjectTemplates\Visual C#". Теперь мы готовы создать самого простого эхо-бота.
Первый бот
Откроем Visual Studio 2015, у нас появился новый тип проекта:

Созданный проект представляет собой Web API проект с одним контроллером — MessagesController, у которого в свою очередь всего один доступный метод Post:
MessagesController
[BotAuthentication] public class MessagesController : ApiController { /// <summary> /// POST: api/Messages /// Receive a message from a user and reply to it /// </summary> public async Task<Message> Post([FromBody]Message message) { if (message.Type == "Message") { // calculate something for us to return int length = (message.Text ?? string.Empty).Length; // return our reply to the user return message.CreateReplyMessage($"You sent {length} characters"); } else { return HandleSystemMessage(message); } } private Message HandleSystemMessage(Message message) { if (message.Type == "Ping") { Message reply = message.CreateReplyMessage(); reply.Type = "Ping"; return reply; } else if (message.Type == "DeleteUserData") { // Implement user deletion here // If we handle user deletion, return a real message } else if (message.Type == "BotAddedToConversation") { } else if (message.Type == "BotRemovedFromConversation") { } else if (message.Type == "UserAddedToConversation") { } else if (message.Type == "UserRemovedFromConversation") { } else if (message.Type == "EndOfConversation") { } return null; } }
Этот метод принимает единственный параметр типа Message, представляющий собой не только сообщение, отправленному нашему боту, но и событие, например, добавление нового пользователя в чат или завершение разговора. Чтобы узнать, чем именно является объект message надо проверить его свойство Type, что и делается в контроллере. Если это обычное сообщение от пользователя (message.Type == «Message»), мы можем прочитать само сообщение, обработать его и ответить — с помощью метода CreateReplyMessage. Простой бот готов, теперь попробуем его запустить и проверить работоспособность. Microsoft предоставляет удобную утилиту Bot Framework Emulator (скачать для v1), которая позволяет удобно запускать и отлаживать ботов на локальной машине. Запустим наш проект EchoBot, в браузере покажется такая страница по адресу localhost:3978/

Запустим теперь установленный Bot Framework Emulator, который знает, что нашего запущенного бота стоит искать именно на порту 3978:

Отправим сообщение боту, нам придет ответ. Как вы видим, все работает. Теперь рассмотрим создание бота, который бы на основе введенных пользователем данных мог бы перевести деньги с карты на карту.
Бот для перевода денег с карты на карту
Для того чтобы перевести деньги с карты на карту, нам нужна информация об этих картах и сумма перевода. Для облегчения задачи написания стандартных сценариев с помощью Bot Framework Microsoft была создана поддержка двух наиболее распространенных вариантов взаимодействия с ботом: Dialogs и FormFlow. В нашем случае подходит FormFlow, потому что всю работу бота можно представить как заполнение некой формы данными, а затем ее обработку. Dialogs же позволяет работать с более простыми сценариями, например, сценарий оповещения при наступлении заданного события (может пригодиться для мониторинга серверов). Начнем создание бота с добавления класса, который и будет представлять собой форму, которую пользователю необходимо заполнить. Этот класс должен быть помечен как [Serializable], для аннотации свойств используются атрибуты из пространства имен Microsoft.Bot.Builder.FormFlow:
CardToCardTransfer
[Serializable] public class CardToCardTransfer { [Prompt("Номер карты отправителя:")] [Describe("Номер карты, с которой Вы хотите перевести деньги")] public string SourceCardNumber; [Prompt("Номер карты получателя:")] [Describe("Номер карты, на которую Вы хотите перевести деньги")] public string DestinationCardNumber; [Prompt("VALID THRU (месяц):")] [Describe("VALID THRU (месяц)")] public Month ValidThruMonth; [Prompt("VALID THRU (год):")] [Describe("VALID THRU (год)")] [Numeric(2016, 2050)] public int ValidThruYear; [Prompt("CVV:")] [Describe("CVV (три цифры на обороте карточки)")] public string CVV; [Prompt("Сумма перевода (руб):")] [Describe("Сумма перевода (руб)")] public int Amount; [Prompt("Комиссия (руб):")] [Describe("Комиссия (руб)")] public double Fee; }
Для того, чтобы Bot Framework мог использовать класс в FormFlow, все открытые поля или свойства должны принадлежать одному из следующих типов:
- Интегральные типы: sbyte, byte, short, ushort, int, uint, long, ulong
- Числовые типы с плавающей точкой: float, double
- Строки
- DateTime
- Перечисления
- Список из перечислений
Атрибут Prompt отвечает за то, какой текст будет показан в качестве подсказки к заполнению поля, Describe — как поле будет называться для пользователя. Теперь с помощью класса FormBuilder нам нужно сказать Bot Framework, что мы хотим использовать именно класс CardToCardTransfer в качестве формы для диалога. Создадим новый класс CardToCardFormBuilder:
CardToCardFormBuilder
public static class CardToCardFormBuilder { public static IForm<CardToCardTransfer> MakeForm() { FormBuilder<CardToCardTransfer> _order = new FormBuilder<CardToCardTransfer>(); return _order .Message("Добро пожаловать в сервис перевода денег с карты на карту!") .Field(nameof(CardToCardTransfer.SourceCardNumber)) .Field(nameof(CardToCardTransfer.ValidThruMonth)) .Field(nameof(CardToCardTransfer.ValidThruYear)) .Field(nameof(CardToCardTransfer.DestinationCardNumber), null, validateCard) .Field(nameof(CardToCardTransfer.CVV)) .Field(nameof(CardToCardTransfer.Amount)) .OnCompletionAsync(async (context, cardTocardTransfer) => { Debug.WriteLine("{0}", cardTocardTransfer); }) .Build(); } }
Мы создаем экземпляр класса FormBuilder<CardToCardTransfer>, указывая, что используем CardToCardTransfer в качестве формы. Теперь с помощью цепочки вызова методов, мы делаем следующее
- Метод Message задает приветственное сообщение.
- Метод Field задает поля, значение которых должен будет ввести пользователь, порядок важен.
- Метод OnCompletionAsync позволяет задать делегат, который будет вызван, когда пользователь заполнит все поля.
- Метод Build делает основную работу — возвращает объект, реализующий IForm<CardToCardTransfer>.
Все достаточно просто, но теперь мы хотим добавить простую валидацию введенных значений и расчет комиссии. Для расчета комиссии воспользуемся тем, что у нас есть класс AlfabankService, реализующий все взаимодействие с банковским API. Для валидации номера карты создадим класс CardValidator, чтобы указать делегат, использующийся для валидации поля, методу Field надо его передать третьим параметром. Расчет комиссии также приходится делать в методе валидации, потому что в версии 1 Bot Framework не предоставлял для этого иных механизмов.
CardToCardFormBuilder с валидацией и расчетом комиссии
public static class CardToCardFormBuilder { public static IForm<CardToCardTransfer> MakeForm() { FormBuilder<CardToCardTransfer> _order = new FormBuilder<CardToCardTransfer>(); ValidateAsyncDelegate<CardToCardTransfer> validateCard = async (state, value) => { var cardNumber = value as string; string errorMessage; ValidateResult result = new ValidateResult(); result.IsValid = CardValidator.IsCardValid(cardNumber, out errorMessage); result.Feedback = errorMessage; return result; }; return _order .Message("Добро пожаловать в сервис перевода денег с карты на карту!") .Field(nameof(CardToCardTransfer.SourceCardNumber), null, validateCard) .Field(nameof(CardToCardTransfer.Fee), state => false) .Field(nameof(CardToCardTransfer.ValidThruMonth)) .Field(nameof(CardToCardTransfer.ValidThruYear)) .Field(nameof(CardToCardTransfer.DestinationCardNumber), null, validateCard) .Field(nameof(CardToCardTransfer.CVV)) .Field(nameof(CardToCardTransfer.Amount), null, async (state, value) => { int amount = int.Parse(value.ToString()); var alfabankService = new AlfabankService(); string auth = await alfabankService.AuthorizePartner(); state.Fee = (double) await alfabankService.GetCommission(auth, state.SourceCardNumber, state.DestinationCardNumber, amount); ValidateResult result = new ValidateResult(); result.IsValid = true; return result; }) .Confirm("Вы хотите перевести {Amount} рублей с карты {SourceCardNumber} на карту {DestinationCardNumber}? Комиссия составит {Fee} рублей. (y/n)") .OnCompletionAsync(async (context, cardTocardTransfer) => { Debug.WriteLine("{0}", cardTocardTransfer); }) .Build(); } }
Остался последний шаг — интегрировать CardToCardFormBuilder в контроллер. Для этого нам нужен метод, возвращающий IDialog<CardToCardTransfer>, чтобы его в свою очередь передать вторым параметром в метод Conversation.SendAsync.
MessagesController
[BotAuthentication] public class MessagesController : ApiController { internal static IDialog<CardToCardTransfer> MakeRoot() { return Chain.From(() => FormDialog.FromForm(CardToCardFormBuilder.MakeForm)) .Do(async (context, order) => { try { var completed = await order; var alfaService = new AlfabankService(); string expDate = completed.ValidThruYear.ToString() + ((int)completed.ValidThruMonth).ToString("D2"); string confirmationUrl = await alfaService.TransferMoney(completed.SourceCardNumber, expDate, completed.CVV, completed.DestinationCardNumber, completed.Amount); await context.PostAsync($"Осталось только подтвердить платеж. Перейдите по адресу {confirmationUrl}"); } catch (FormCanceledException<CardToCardTransfer> e) { string reply; if (e.InnerException == null) { reply = $"Вы прервали операцию, попробуем позже!"; } else { reply = "Извините, произошла ошибка. Попробуйте позже."; } await context.PostAsync(reply); } }); } /// <summary> /// POST: api/Messages /// Receive a message from a user and reply to it /// </summary> public async Task<Message> Post([FromBody]Message message) { if (message.Type == "Message") { return await Conversation.SendAsync(message, MakeRoot); } else { return HandleSystemMessage(message); } }
Собственно связывание происходит в коде Chain.From(() => FormDialog.FromForm(CardToCardFormBuilder.MakeForm)), а затем в метод Do мы передаем метод, который ожидает завершение формирования запроса и его обрабатывает, попутно отвечая за обработку ошибок. Теперь мы можем запустить бота и протестировать его работу в эмуляторе:

Можно убедиться, что бот работает так, как ожидалось, теперь нам подружить его с Bot Connector.
Регистрируем бота в Bot Connector
Для начала нам нужно загрузить нашего бота на какой-то общедоступный URL, например, в Azure (бесплатная подписка подойдет): https://alfacard2cardbot.azurewebsites.net. Теперь заходим dev.botframework.com с помощью учетной записи Microsoft. В верхнем меню выбираем «Register a Bot», вводим все обязательные поля: имя, описание, Messaging endpoint — тот самый общедоступный URL и т.д.

Не забудем обновить наш web.config, добавив туда AppId и AppSecret, сгенерированные нам на этом шаге. Задеплоим эти изменения. Теперь наш бот появился в меню «My Bots», можно убедиться, что Bot Connector правильно взаимодействует с ботом при помощи окна «Test connection to your bot» внизу слева. Теперь осталось добавить взаимодействие с Telegram, для этого в правом столбце выберем «Add another channel» — «Telegram» — «Add», откроется вот такое окно, в котором по шагам расписано, как нам добавить Telegram бота:

Исходный код, тестирование, заключение
Telegram боту можно написать @AlfaCard2CardBot , деньги не переведутся, среда тестовая. Код можно найти в GitHub: https://github.com/StanislavUshakov/AlfaCardToCardBot.
В следующей серии будем мигрировать бота на версию 3!
