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:
[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:
[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:
 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 не предоставлял для этого иных механизмов.
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.
[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)

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

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

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

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

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

JustStas
11.08.2016 11:07Да, знаю / умею / практикую )

dmx102
11.08.2016 11:24Тогда вы должны знать, что после совершения транзакции СТРОГО ЗАПРЕЩЕНО хранение CVC. В вашем случае чувствительные платежные данные «оседают» на серверах Telegram, история диалога сохраняется, они могут попадать в логи как на клиенте, так и на стороне серверов Telegram или вашего.
Когда вы используете Telegram в качестве транспорта, он должен соответствовать стандартам безопасности, основанным не на кустарных алгоритмах, а только на тех, которые прописаны в NIST как безопасные. MTProto среди них нет.
evnik
11.08.2016 19:34Более того, при использовании Bot Framework, сообщения еще и проходят через сервера Microsoft и никто не может гарантировать, что они не оседают там.
Также, Bot Framework позволяет подключить бота к Facebook Messenger (правда придется пройти ревью), в котором вся история переписки с ботом сохраняется в явном виде и доступна администратору привязанной страницы Facebook в любой момент.
          
 
AdaStreamer
Извиняюсь, если пропустил, просто пробежался бегло по статье.
Интересует вот такие вопросы:
1. как быть с 3ds?
2. как собирать данные, чтобы они не отображались в истории? имхо, это не секурно. видел вариант с кнопочной клавиатурой, но не знаю как это работает на практике.
JustStas
1. 3ds — АльфаБанк возвращает URL, на который надо перейти и ввести код подтверждения из СМС, это происходит вне бота.
2. У Telegrma защищенный протокол, на сервере никакой информации не записывается, единственное — пользователь должен сам почистить на клиенте.
AdaStreamer
На чьем сервере? Если сообщения проходят через вашего бота, данные априори проходят через ваш сервер. Насколько я понимаю, вам нужна pci dss сертификация, чтобы это был законно.
JustStas
Проходят — да, но не хранятся, поэтому это немного другой уровень PCI DSS, но вы все верно говорите. Интеграция только с тестовым, так что реальные данные пока не ходят — все законно.
samodum
>не хранятся…
Джентльменам верят на слово?
JustStas
1 — в рамках данного POC весь исходный код представлен публике
2 — в настоящей жизни, каждый год к вам приходит компания, которая сканирует и изучает вашу инфраструктуру, базу данных, диски с целью выявления не только номеров карточек / cvv / exp date, но и PII: personally identifiable information Чтобы подготовиться, чуществует различные тулзы (в основном, дорогие, а если PCI DSS compliant, так вообще), для проверки среды (файлов, БД и прочего) на предмет таких данных. Обычно аудит также любит посканировать порты, поискать XSS уязвимости. (говорю не голословно, со всем этим сталкиваюсь по работе)
shpaker
2. А где по вашему сервера Телеграма историю хранят?
holywalley
https://tlgrm.ru/privacy#2-storing-data
shpaker
Ну да. Только как это связано с тем, что данные посланные боту идут так или иначе через ваш сервер, но по пути ещё остаются на серверах Телеграма?
AdaStreamer
А если обработка платежа будет на стороне АльфаБанка, то зачем бот вообще нужен?
AdaStreamer
Под стороной обработки понимаю именно то место, куда пользователь будет вводить данные карты.
rsivakov
а тестовых карточек чего не выдал?)
JustStas
Любые отсюда подойдут! https://names.igopaygo.com/credit-card