Пишем мультиплатформенного бота для перевода денег с карты на карту с помощью Microsoft Bot Framework V1 +17


Во время конференции 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.



Готовим среду разработки


Для успешной разработки бота нам будут нужны:

  1. Visual Studio 2015
  2. Microsoft Account, чтобы залогиниться в dev.botframework.com
  3. URL с задеплоенным кодом нашего бота. Этот URL должен быть доступен публично.
  4. Аккаунты разработчиков 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 в качестве формы. Теперь с помощью цепочки вызова методов, мы делаем следующее

  1. Метод Message задает приветственное сообщение.
  2. Метод Field задает поля, значение которых должен будет ввести пользователь, порядок важен.
  3. Метод OnCompletionAsync позволяет задать делегат, который будет вызван, когда пользователь заполнит все поля.
  4. Метод 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!
-->


К сожалению, не доступен сервер mySQL