Во время конференции 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!
Поделиться с друзьями
-->

Комментарии (21)


  1. AdaStreamer
    10.08.2016 14:26

    Извиняюсь, если пропустил, просто пробежался бегло по статье.
    Интересует вот такие вопросы:
    1. как быть с 3ds?
    2. как собирать данные, чтобы они не отображались в истории? имхо, это не секурно. видел вариант с кнопочной клавиатурой, но не знаю как это работает на практике.


    1. JustStas
      10.08.2016 14:36

      1. 3ds — АльфаБанк возвращает URL, на который надо перейти и ввести код подтверждения из СМС, это происходит вне бота.
      2. У Telegrma защищенный протокол, на сервере никакой информации не записывается, единственное — пользователь должен сам почистить на клиенте.


      1. AdaStreamer
        10.08.2016 14:59

        На чьем сервере? Если сообщения проходят через вашего бота, данные априори проходят через ваш сервер. Насколько я понимаю, вам нужна pci dss сертификация, чтобы это был законно.


        1. JustStas
          10.08.2016 15:01

          Проходят — да, но не хранятся, поэтому это немного другой уровень PCI DSS, но вы все верно говорите. Интеграция только с тестовым, так что реальные данные пока не ходят — все законно.


          1. samodum
            10.08.2016 15:24
            +1

            >не хранятся…

            Джентльменам верят на слово?


            1. JustStas
              10.08.2016 15:38

              1 — в рамках данного POC весь исходный код представлен публике
              2 — в настоящей жизни, каждый год к вам приходит компания, которая сканирует и изучает вашу инфраструктуру, базу данных, диски с целью выявления не только номеров карточек / cvv / exp date, но и PII: personally identifiable information Чтобы подготовиться, чуществует различные тулзы (в основном, дорогие, а если PCI DSS compliant, так вообще), для проверки среды (файлов, БД и прочего) на предмет таких данных. Обычно аудит также любит посканировать порты, поискать XSS уязвимости. (говорю не голословно, со всем этим сталкиваюсь по работе)


          1. shpaker
            10.08.2016 19:28

            2. А где по вашему сервера Телеграма историю хранят?


            1. holywalley
              15.08.2016 11:37
              -1

              https://tlgrm.ru/privacy#2-storing-data


              1. shpaker
                15.08.2016 20:03

                Ну да. Только как это связано с тем, что данные посланные боту идут так или иначе через ваш сервер, но по пути ещё остаются на серверах Телеграма?


      1. AdaStreamer
        10.08.2016 14:59

        А если обработка платежа будет на стороне АльфаБанка, то зачем бот вообще нужен?


        1. AdaStreamer
          10.08.2016 15:00

          Под стороной обработки понимаю именно то место, куда пользователь будет вводить данные карты.


      1. rsivakov
        10.08.2016 16:00

        а тестовых карточек чего не выдал?)


        1. JustStas
          10.08.2016 16:13

          Любые отсюда подойдут! https://names.igopaygo.com/credit-card


  1. evnik
    10.08.2016 23:00

    По-моему, Dialogs позволяет работать с более гибкими, а не с более простыми сценариями. FormFlow позволяет пользователю ходить между полями формы вперед и назад, но эта последовательность полей задается жестко. А Dialog позволяет боту реагировать на каждое «неожиданное» сообщение пользователя и обрабатывать его, делая диалог более естественным. При этом, разумеется, реализация с Dialog требует больше усилий.


    1. JustStas
      11.08.2016 11:06

      Да, Dialogs сам по себе представляет простой сценарий, с помощью которого можно добиться большей гибкости за счет бОльших усилий. FormFlow позволяет упростить рутинные задачи — такие, как онлайн запись куда-либо, заказ пиццы, оставление жалобы и т.д.


  1. dmx102
    11.08.2016 09:44

    Вы нарушаете основные требования стандарта безопасности PCI DSS! Это можно показать как для примера, но не как рабочую модель.


    1. JustStas
      11.08.2016 10:00

      Да, абсолютно согласен, что это просто пример, как можно использовать бота. Для выхода в продакшен нужно решить много вопросов )


      1. dmx102
        11.08.2016 10:01

        Для продакшена эта модель не годится в принципе. Вы же читали PCI DSS, да?)


        1. JustStas
          11.08.2016 11:07

          Да, знаю / умею / практикую )


          1. dmx102
            11.08.2016 11:24

            Тогда вы должны знать, что после совершения транзакции СТРОГО ЗАПРЕЩЕНО хранение CVC. В вашем случае чувствительные платежные данные «оседают» на серверах Telegram, история диалога сохраняется, они могут попадать в логи как на клиенте, так и на стороне серверов Telegram или вашего.
            Когда вы используете Telegram в качестве транспорта, он должен соответствовать стандартам безопасности, основанным не на кустарных алгоритмах, а только на тех, которые прописаны в NIST как безопасные. MTProto среди них нет.


            1. evnik
              11.08.2016 19:34

              Более того, при использовании Bot Framework, сообщения еще и проходят через сервера Microsoft и никто не может гарантировать, что они не оседают там.
              Также, Bot Framework позволяет подключить бота к Facebook Messenger (правда придется пройти ревью), в котором вся история переписки с ботом сохраняется в явном виде и доступна администратору привязанной страницы Facebook в любой момент.