gosha20777

.NET Core в действии: пишем бота для telegram.

April 02, 2018 | 16 Minute Read

В предыдущей статье я немного затронул .NET Core и ввел некоторые определения. Этой статьей я хочу показать как с ним работать и что из этого может получиться. Примером - будет простой бот для telegram. Помимо самого кода, в этой статье я постараюсь объяснить некоторые принципы проектирования Web приложений на C# и .NET Core 2.0, а также рассказать о некоторых особенностях telegram, и дать некоторую литературу по данному направлению. Для всех ГУРУ и тех кому не терпится посмотреть: смотрите вот этот репозиторий на github. Тут уже находится необходимый шаблон для написания бота (кстати лежит он у меня довольно давно и эта и предыдущая статьи должны были выйти раньше, но время... его как всегда не хватает). Ну хватит лирики. Погнали кодить!

Шаг 1: Теория.

Тут я расскажу о ASP.NET Core 2.0, немного затрону его отличия от обычного человеческого ASP.NET, расскажу о паттерне проектирования MVC. Знающие люди смело могут перейти к шагу 2. Вся литература все равно будет продублирована в конце. Вы ничего не пропустите.

Еще немного теории о .NET Core…

Платформа ASP.NET Core представляет технологию от компании Microsoft, предназначенную для создания различного рода веб-приложений: от небольших веб-сайтов до крупных веб-порталов и веб-сервисов.

С одной стороны, ASP.NET Core является продолжением развития платформы ASP.NET. Но с другой стороны, это не просто очередной релиз. ASP.NET Core фактически делает революцию всей платформы, ее качественное изменение. Как вы уже поняли, ASP.NET Core работает поверх кросс-платформенной среды .NET Core, которая может быть развернута на основных популярных ОС: Windows, Mac OS X, Linux.

Как следует из примера 2 прошлой статьи - теперь развертывания сайта на ASP.NET Core может производится внутри самого приложения. Для этого используется кросс-платформенный веб-сервер Kestrel, который уже строен “из коробки”, однако традиционный IIS - никто не отменял. Это позволяет осуществлять более гибкую настройку: пользуетесь Linux и хотите демонезировать приложение (обернуть в Service) - пожалуйста, обернуть сайт в docker - никаких проблем. С .NET Core все стало гораздо прозрачнее и понятней.

Благодаря модульности фреймворка все необходимые компоненты веб-приложения могут загружаться как отдельные модули через Nuget (он к стати совсем недавно стал работать намного быстрее, хотя иногда он тупит и тогда приходится идти пить чай после команды restore). В добавок, в .NET Core 2.0, в отличие от 1.0 больше не обязательно использовать библиотеку System.Web.dll.

ASP.NET Core включает в себя фреймворк MVC, который объединяет функциональность MVC, Web API и Web Pages. В предыдущих версии платформы данные технологии реализовались отдельно и поэтому содержали много дублирующей функциональности. Сейчас же они объединены в одну программную модель ASP.NET Core MVC. А Web Forms полностью ушли в прошлое.

Кроме объединения вышеупомянутых технологий в одну модель в MVC был добавлен ряд дополнительных функций.

Одной из таких функций являются тэг-хелперы (tag helper), которые позволяют более органично соединять синтаксис html с кодом С#. Да, в отличии от ASP.NET хтмл фойлы тепеть хранятся в новом формате CSHTML. Признаюсь, когда я впервые увидел tag helper-ы с их @ и { - у меня потекла кровь из глаз. почему-то сразу вспомнился qBASIC, , template-ы в крестах и GOTO (Никогда не пишите на qBASIC))). Но после пары часов я понял, что это в прицепе помогает (хотя нет, это ужасно и я стараюсь их избегать). И напоследок, для обработки запросов теперь используется новый конвейер HTTP, который основан на компонентах Katana и спецификации OWIN. А его модульность позволяет легко добавить свои собственные компоненты. Да, web-api стало писать намного удобнее.

Резюмируя, можно выделить следующие ключевые отличия ASP.NET Core от предыдущих версий ASP.NET:

  • Новый легковесный и модульный конвейер HTTP-запросов
  • Возможность развертывать приложение как на IIS, так и в рамках своего собственного процесса
  • Использование платформы .NET Core и ее функциональности
  • Распространение пакетов платформы через NuGet
  • Интегрированная поддержка для создания и использования пакетов NuGet
  • Единый стек веб-разработки, сочетающий Web UI и Web API
  • Конфигурация для упрощенного использования в облаке
  • Встроенная поддержка для внедрения зависимостей
  • Расширяемость
  • Кроссплатформенность: возможность разработки и развертывания приложений ASP.NET на Windows, Mac и Linux
  • Развитие как open source, открытость к изменениям

Паттерн MVC и с чем его едят.

По словам Википедии, паттерн (англ. design pattern) — повторимая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. Например способ проектирования "сверху вниз" можно отнести к одним из первых паттернов проектирования.

Model-View-Controller. MVC — это фундаментальный паттерн, который нашел применение во многих технологиях, дал развитие новым технологиям и каждый день облегчает жизнь разработчикам.

Впервые паттерн MVC появился в языке SmallTalk. Разработчики должны были придумать архитектурное решение, которое позволяло бы отделить графический интерфейс от бизнес логики, а бизнес логику от данных. Таким образом, в классическом варианте, MVC состоит из трех частей, которые и дали ему название.

Model.

Под Моделью, обычно понимается часть содержащая в себе функциональную бизнес-логику приложения. Модель должна быть полностью независима от остальных частей продукта. Модельный слой ничего не должен знать об элементах дизайна, и каким образом он будет отображаться. Достигается результат, позволяющий менять представление данных, то как они отображаются, не трогая саму Модель.

Модель обладает следующими признаками:

  • Модель — это бизнес-логика приложения;
  • Модель обладает знаниями о себе самой и не знает о контроллерах и представлениях;
  • Для некоторых проектов модель — это просто слой данных (DAO, база данных, XML-файл);
  • Для других проектов модель — это менеджер базы данных, набор объектов или просто логика приложения;

View.

В обязанности Представления входит отображение данных полученных от Модели. Однако, представление не может напрямую влиять на модель. Можно говорить, что представление обладает доступом «только на чтение» к данным.

Представление обладает следующими признаками:

  • В представлении реализуется отображение данных, которые получаются от модели любым способом;
  • В некоторых случаях, представление может иметь код, который реализует некоторую бизнес-логику. Но только в некоторых случаях(!) и вообще я этого не говорил:-)

Примеры представления: HTML-страница, WPF форма, Windows Form.

Controller.

Контроллер обеспечивает «связи» между пользователем и системой. Контролирует и направляет данные от пользователя к системе и наоборот. Использует модель и представление для реализации необходимого действия.

  • Контроллер определяет, какие представление должно быть отображено в данный момент;
  • События представления могут повлиять только на контроллер.контроллер может повлиять на модель и определить другое представление.
  • Возможно несколько представлений только для одного контроллера;

MVC

Основная идея этого паттерна в том, что и контроллер и представление зависят от модели, но модель никак не зависит от этих двух компонент.

Итак, контроллер перехватывает событие извне и в соответствии с заложенной в него логикой, реагирует на это событие изменяя Mодель, посредством вызова соответствующего метода. После изменения Модель использует событие о том что она изменилась, и все подписанные на это события Представления, получив его, обращаются к Модели за обновленными данными, после чего их и отображают.

Начинающие программисты очень часто трактуют архитектурную модель MVC как пассивную модель MVC: модель выступает исключительно совокупностью функций для доступа к данным, а контроллер содержит бизнес-логику. В результате — код моделей по факту является средством получения данных из СУБД, а контроллер — типичным модулем, наполненным бизнес-логикой. В результате такого понимания — MVC-разработчики стали писать код, который Pádraic Brady (известный в кругах сообщества «Zend Framework» (Толстые, тупые, уродливые контроллеры или ТТУК).

Наиболее наглядно эта проблема описана статье The M in MVC: Why Models are Misunderstood and Unappreciated Pádraic Brady. Вот перевод этой статьи.

Среднестатистический ТТУК получал данные из БД (используя уровень абстракции базы данных, делая вид, что это модель) или манипулировал, проверял, записывал, а также передавал данные в Представлении. Такой подход стал очень популярен потому, что использование таких контроллеров похоже на классическую практику использования отдельного php-файла для каждой страницы приложения.

Но в объектно-ориентированном программировании используется активная модель MVC, где модельэто не только совокупность кода доступа к данным и СУБД, но и вся бизнес-логика; также, модели могут инкапсулировать в себе другие модели. Контроллеры же, — как элементы информационной системы, — ответственны лишь за:

  • приём запроса от пользователя;
  • анализ запроса;
  • выбор следующего действия системы, соответственно результатам анализа (например, передача запроса другим элементам системы).

Только в этом случае контроллер становится «тонким» и выполняет исключительно функцию связующего звена (glue layer) между отдельными компонентами информационной системы.

Ох уж эти паттерны… Я надеюсь что мои формулировки не взорвали моит немногочисленным читателям мозг. Если вы не поняли то, что я писал — не переживайте. Паттерны вообще не самая простая тема, а среди всех MV- паттернов — MVC, на мой взгляд, вообще является одним из самых сложных для понимания. Так или иначе я постарался доступно объяснить его суть. Это одна из тех вещей, которая коде выглядит не так страшно и более понятно. По этому погнали!

Шаг 2: Практика.

И так запускаем нашу любимую Visual Studio 2017 - создаем новый проект с названием TelegrammAspMvcDotNetCoreBot: .NET Core -> Веб приложение .NET Core. Тип приложения: WebAPI, Версия платформы: .NET Core 2.0.

Web API в ASP.NET Core

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

Web API в ASP.NET Core

Первым делом переименуем ValuesController.cs в MessageController, и выпилим оттуда все. Оставим пустой класс с обработкой GET-запроса:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Telegram.Bot.Types;
using TelegrammAspMvcDotNetCoreBot.Models;

namespace TelegrammAspMvcDotNetCoreBot.Controllers
{
    [Route("api/message/update")]
    public class MessageController : Controller
    {
        // GET api/values
        [HttpGet]
        public string Get()
        {
            return "Method GET unuvalable";
        }
    }
}

Запустим нашу прилку и перейдем по ссылке localhost:XXXX/api/message/update. Получили результат Method GET unuvalable (ура мы крутые прогеры!).

Теперь подкл

Model first.

Для написания проекта будем использовать подход “model first”. Он заключается в том, что сначала необходимо разработать модель приложения и написать логику, затем нарисовать Представление, а уже потом склеить это дело контроллером.

Итак, нам нужен сам бот для телеги. Давайте опишем его модель. Пошли кодить? Неет. Сначала обдумаем а как эту модель реализовать.

У бота есть несколько параметров конфигурации: токен, имя, и url сайта, где он лежит. Значит это настройки бота. и их можно описать в отдельном классе. Создадим папку Models а в ней класс AppSettings.cs:

namespace TelegrammAspMvcDotNetCoreBot.Models
{
    public static class AppSettings
    {
        public static string Url { get; set; } = "https://URL:443/{0}";
        public static string Name { get; set; } = "<BOT_NAME>";
        public static string Key { get; set; } = "<BOT_KEY>";
    }
}

Отлично! Теперь опишем бота? Подождите. Бот такая сущность которая должна содержать команды и выполнять их. А значит нам нудны еще эти самые команды. Как должна выглядеть команда? Каждая команда как-то называется, значит содержит свойство Name. Команда должна определять вызвали ее или нет: содержать булеву функцию Contains(...). И уметь выполнять себя - Execute(...). И последнее: команд может быть много, значит нужен какой то абстрактный класс.

Теперь создадим папку Commands внутри папки Models и запихнем туда класс Command.cs:

using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Types;

namespace TelegrammAspMvcDotNetCoreBot.Models.Commands
{
    public abstract class Command
    {
        public abstract string Name { get; }

        public abstract Task Execute(Message message, TelegramBotClient client);

        public abstract bool Contains(Message message);
    }
}

Здесь Execute возвращает не void, а Task, так как команда может выполняться и асинхронно.

Ну что же, пришла пора писать бота. Любой телеграм бот должен содержать клиента TelegramBotClient, а также наш бот должен содержать команды. Их будет много, а значит нам нужна их коллекция. К томуже бот должен уметь возвращать TelegramBotClient-а для вызова его из команд.

Создаем класс Bot.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Telegram.Bot;
using TelegrammAspMvcDotNetCoreBot.Models.Commands;

namespace TelegrammAspMvcDotNetCoreBot.Models
{
    public class Bot
    {
        private static TelegramBotClient botClient;
        private static List<Command> commandsList;

        public static IReadOnlyList<Command> Commands => commandsList.AsReadOnly();

        public static async Task<TelegramBotClient> GetBotClientAsync()
        {
            if (botClient != null)
            {
                return botClient;
            }

            commandsList = new List<Command>();
            commandsList.Add(new StartCommand());
            //TODO: Add more commands

            botClient = new TelegramBotClient(AppSettings.Key);
            string hook = string.Format(AppSettings.Url, "api/message/update");
            await botClient.SetWebhookAsync(hook);
            return botClient;
        }
    }
}

Я думаю, комментарии излишни.

Теперь научим его приветствовать нас. Добавим класс StartCommand.cs:

using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Types;

namespace TelegrammAspMvcDotNetCoreBot.Models.Commands
{
    public class StartCommand : Command
    {
        public override string Name => @"/start";

        public override bool Contains(Message message)
        {
            if (message.Type != Telegram.Bot.Types.Enums.MessageType.TextMessage)
                return false;

            return message.Text.Contains(this.Name);
        }

        public override async Task Execute(Message message, TelegramBotClient botClient)
        {
            var chatId = message.Chat.Id;
            await botClient.SendTextMessageAsync(chatId, "Hallo I'm ASP.NET Core Bot", parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown);
        }
    }
}

Осталось сконфигурировать бота, и сказать нашему приложению, что бот у нас есть. Идем в класс Startup и в методе Configure приписываем в конец:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	...
    //Bot Configurations
    Bot.GetBotClientAsync().Wait();
}

Финальный аккорд. Добавим клея!

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Telegram.Bot.Types;
using TelegrammAspMvcDotNetCoreBot.Models;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace TelegrammAspMvcDotNetCoreBot.Controllers
{
    [Route("api/message/update")]
    public class MessageController : Controller
    {
        // GET api/values
        ...

        // POST api/values
        [HttpPost]
        public async Task<OkResult> Post([FromBody]Update update)
        {
            if (update == null) return Ok();

            var commands = Bot.Commands;
            var message = update.Message;
            var botClient = await Bot.GetBotClientAsync();

            foreach (var command in commands)
            {
                if (command.Contains(message))
                {
                    await command.Execute(message, botClient);
                    break;
                }
            }
            return Ok();
        }
    }
}

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

P.s.

В нашем боте мы используем WebHook и для него необходимы HTTPS соединение + Домен. Если вам лень замораживаться, то вы всегда можете задиплоить его на Azure. Не забудьте задать ему URL перед заливкой.

Литература

.Net Core 2.0 вышла совсем недавно, а по этому по ней нет русскоязычной литературы. Честно обошел весь дом книги на Невском, и библиоглобус в Москве. В первом случае вообще нет какой-то внятной литературы (удивление), удалось найти только по .Net Core 1.0, да и то сомнительного качества (я надеюсь Петербуржцы не будут кидать в меня камнями). В Москве выбор чуть больше, но вся русскоязычная литература опять же по версии 1.0. Возможно, я не там искал, тогда буду рад какой-либо информации.

Вот скромный список того, что удалось найти:

Как видите список даже англоязычной литературы ограничивается тремя книгами. А официальных книг от MS нет даже по первой версии. Но метод научного тыка никто не отменял!