Не так давно опубликовал статью об экспресс-создании бота для Telegram на фреймворке SKitLs.Bots.Telegram. С тех пор внутренний состав фреймворка солидно изменился, вместе с тем были выпущены предварительные версии *.BotProcesses и *.DataBases и вторая версия *.Core. В этой статье я бы хотел дать более детальный взгляд на возможности фреймворка, а также осветить новые возможности выпущенных расширений. По сути, целью данного гайдлайна будет реализация функционала, аналогичного написанному в прошлой статье, но на осях двух новых расширений. В конце статьи, после ознакомления, я выражу свой взгляд на область применения новых пакетов. Перед обновлением на вторую версию вспомним один момент из прошлой статьи. Локализация сообщений отладки, которую в тот раз мы безуспешно пытались сделать строчкой: Начиная с обновления v2.0 ядра проекта данный функционал доступен и позволяет локализовать отладку. В данном примере я воссоздал ту же ошибку, что возникла в прошлый раз - убрал один из обработчиков событий. В результате получил ошибку и предупреждение на русском языке: Прежде чем перейти к поставленным задачам, пробежимся по списку изменений, которые необходимо внести в написанный в прошлый раз код. Во-первых, придётся обновить некоторые неймспейсы. Дело, скорее, чисто механической обработки. using SKitLs.Bots.Telegram.ArgedInteractions.Argumenting.Prototype; using SKitLs.Bots.Telegram.ArgedInteractions.Argumentation.Prototype; using SKitLs.Bots.Telegram.Core.Prototypes; using SKitLs.Bots.Telegram.Core.Prototype; Во-вторых, начиная с версии v1.3 расширение ArgedInteractions обновило название интерфейса IArgsSerializeService (было: IArgsSerilalizerService). А v1.1 AdvancedMessages позволяет быстрее создавать Inline-меню с сериализатором, автоматически разрешая этот интерфейса через менеджера. В-третьих, обновился интерфейс IUsersManager. К методам были добавлены соответствующие маркеры Async. Изменение внутренних процессов это не влечёт, лишь необходимость обновления нейминга. Ну и по мелочи: IApplicant<T> теперь требует метод ApplyTo вместо ApplyFor; вся доставка исправлена с Delievery на Delivery. Также, не изменяя наш проект возможностями новых расширений, внесём некоторые правки, которым, на мой взгляд, я уделил недостаточно внимания в прошлый раз, и уделим время визуальной составляющей  проекта. Меня немного раздражает, что при запуске команды "Найти" присылается новое сообщение. Во-первых, это некрасиво: сообщение под меню - не эстетично, на мой взгляд. Во-вторых, это небезопасно: при более сложной логике лучше запрещать пользователю выполнять одни и те же действия дважды. Обновим код следующим образом: воспользуемся специальным классом-оболочкой EditWrapper, чтобы редактировать существующее сообщение, а не отправлять новое: Скриншот не прикрепляю, ибо выглядит он малопривлекательно, но суть передана кодом полностью: текст сообщения был переписан, а меню удалено. Единственное, что остаётся пользователю - ввести город или "Выйти". Следующий аспект. Визуализация. Фреймворк SKitLs, как уже говорилось, является своеобразной оболочкой вокруг библиотеки Telegram.Bot. Для организации отправки сообщений служат классы-наследники IDeliverySystem. Но как быть, если функционал реализаций по умолчанию не позволяют отправлять что-либо, кроме текстовых сообщений? Да, можно было бы написать свой класс типа WeatherDelivery : IDeliverySystem и прикрутить необходимый функционал, но есть вариант проще. Например, вместе с погодой в городе, я хочу отправить координаты города в виде геопозиции. На этот случай без фреймворка можно было бы обратиться к сущности ITelegramBotClient из библиотеки Telegram.Bot и вызвать соответствующий метод SendLocationAsync(). Фреймворк SKitLs точно так же позволяет обратиться к данной сущности через свойство Bot класса BotManager (BotManager.Bot). Отправив сообщение пользователю сохраним сущность отправленного сообщения и, получив Id отправленного сообщения, отправим геопозицию с ответом на отправленное сообщение. Выглядеть всё это будет следующим образом: Последний момент, который хотел бы продемонстрировать - это функционал вызова AnswerCallbackQuery классического Telegram клиента. Так же, как и в прошлом примере, доступ к этому методу осуществляется через BotManager.Bot, однако для его вызова требуется параметр callbackQueryId. Все существующие касты обновлений, наследованные от ICastedUpdate, предоставляют доступ как к оригинальному Telegram.Bot.Types.Update обновлению, пришедшему с сервера через CastedUpdate.OriginalSource, так и к распакованным и типизированным данным конкретного типа. В данном случае это свойство Telegram.Bot.Types.CallbackQuery Callback обновления типа SignedCallbackUpdate, которое и даёт доступ к Id данного CallbackQuery. К теме разговора относится слабо, но также отмечу этот момент: допишем пару методов для преобразования координат из double к красивому виду формата Axx°yy'zz''. В GeoCoderInfo добавим: И обновим Do_InputCityAsync Эти действия не особо влияют на способности бота, однако позволяют шире увидеть функционал фреймворка. Весь обновлённый функционал доступен в том же репозитории GitHub, в ветви [v1.0]-Updated. Далее в статье мы займёмся непосредственно изучением новых расширений и дополнением кода. Перед работой откроем менеджер NuGet, включим "Предварительные версии" и скачаем недостающие пакеты: SKitLs.Bots.Telegram.BotProcesses и SKitLs.Bots.Telegram.DataBases. Итак, в первую очередь обратимся к возможностями проекта *.DataBases и создадим метод сборки нашего менеджера данных (аккурат перед GetMenuManager()) Менеджер данных работает с данными типа IBotDataSet, которые несут функции хранения, изменения и отображения массивов отображаемых данных. По умолчанию этот интерфейс реализован в двух классах: BotDataSet<T> и UserContextDataSet<T>. Оба класса работают с классами, поддерживающими интерфейс IBotDisplayable (where T : class, IBotDisplayable). А их различия заключаются в реализации метода GetContextSubset(ISignedUpdate): если первый класс возвращает весь объём данных полностью, то второй позволяет из общего набора данных выделить те объекты данных, чей владелец является пользователем, запросившем открытие этого датасета. Поэтому второй класс вводит дополнительное ограничение на данные, с которыми он в состоянии работать: поддержка интерфейса IOwnedData. Если обобщать, то BotDataSet<T> будет отображать вообще все данные типа <T>, имеющиеся в боте, в то время как UserContextDataSet<T> автоматически сделает выборку данных, относящихся к пользователю, перед открытием. Поскольку в менеджере данных будет храниться список вообще всех городов, внесённых всеми пользователями в категорию "Избранное", определим этот датасет как UserContext, чтобы не путать пользователей и выводить каждому его список. Сразу же получим ошибку: для GeoCoderInfo не хватает двух интерфейсов. Перейдём в соответствующий класс и определим их. Что касается метода IsOwnedBy, использующегося для определения UserContextDataSet, то его можно было бы определить в лоб, с помощью введения свойства long UserOwnerId. Однако представьте 10000 москвичей, каждый из которых будет хранить в избранном Москву. Это 10000 записей, содержащих идентичную информацию! Крайне неэффективно. Оптимизируем следующим образом: мы точно знаем, что массивы геометок городов не изменяются - город и его координаты статичны. Поэтому будем хранить коллекцию владельцев данной метки и возвращать принадлежность отправителя к ней. В идеале, надо было бы разделить всё это на два класса типа: GeoCodeArgument и GeoCodeData, каждый из которых отвечал бы за свою сущность: один для хранения в БД, второй для использования в виде коллбэк-аргумента. Но, слава богу, задачи сделать идеально у нас не стоит. Теперь необходимо обновить функционал добавления в избранное. Поскольку класс самого пользователя больше не хранит в себе информации о его избранном, а класс геометки хранит информацию о владельцах, напишем следующее: В этом коде мы с вами получаем сервис менеджера данных, используемый в нашем боте, в котором выполняем поиск датасета с типом данных GeoCoderInfo. Далее выполняем поиск геометки по координатам и добавляем в её владельцев отправителя. Если же такая метка не нашлась, то создаём и добавляем её. Обновим аргумент коллбэка UnfollowGeocode с IntWrapper на GeoCoderInfo и аналогично обновим логику удаления. Обновим логику добавления коллбэка UnfollowGeocode в меню методов Do_OpenFollowAsync и Do_LoadWeatherAsync Для метода получения сохранённых пользователем геометок можно обновить класс BotUser, добавив метод получения геометок, которыми пользователь владеет. Этот метод даст нам доступ к избранному пользователя при любом обновлении. В частности, обновим GetSavedList() и SavedFavoriteMenu.Build Теперь, когда функционал подготовлен, необходим подключить менеджер данных к боту и организовать к доступ к сохранённым данным. Итак, мы добавили новую навигационную кнопку в наше меню, которая ведёт к нашей базе данных. Старую я пока убирать не стал, чтобы была возможность проверять функционал. В целом, всё работает. Но нас с вами не устраивают кнопки "Добавить" (функционал добавления происходит через процесс поиска) и "Изменить" (геометки не редактируются). Кроме того, тем или иным образом нам надо добавить коллбэк "Узнать погоду". Для решения первых двух нюансов просто запретим добавление и изменение через свойства датасета. Добавить же действие "Узнать погоду" (LoadWeather) можно через метод AddAction() нашего датасета. Единственное различие, которое возникает - метод требует коллбэк с аргументом DtoArg<T>, наш же метод работает с самим T. Обновим сигнатуру коллбэка и метода () и добавим его к доступным действиям датасета. По сути, мы сделали то же, о чём я писал ранее - разграничили объект данных GroCodeInfo и GeoCodeArgument с той лишь разницей, что для этих целей использовали универсальный тип DtoArg<T>. По-хорошему, было бы неплохо поменять все коллбэки, введя везде этот аргумент в целях сохранения логики обмена данными. Но мы просто насладимся результатом проделанной работы. Вроде бы, всё хорошо. Однако можно заметить, что после загрузки погоды и кнопки "Удалить" пропал крестик. Это связано с тем, что перед нами две принципиально разные кнопки: одна загружена из IDataManager, вторую мы прописывали сами. В принципе, это можно обнаружить, заглянув в логи: интересующие нас действия выделены белым и тх actionId различается. Возникает закономерный вопрос: если мы отказываемся от прежних действий (UnfollowGeocode) и переезжаем на рельсы *.DataBases, что же тогда произойдёт при нажатии кнопки удаления? А в этом случае сбываются самые худшие подозрения: по умолчанию датасет полностью стирает запрошенный объект. Нам же, напротив, необходимо всего лишь изменить данные подписчиков. Что же, чтобы это исправить необходимо переписать метод "Remove" датасета, используемый по умолчанию. TerminatorProcess - один из процессов, предоставляемых *.BotProcesses. Он является своеобразной связкой между процессами ввода и делегатами действий. Его метод действия прост: сразу после запуска он вызывает своё уничтожение TerminateAsync(), не дожидаясь повторного входящего обновления и сразу же вызывая действие, которое должно произойти в момент его завершения. Однако, как и для всех процессов TextInputsProcessBase<T>, ему необходимы аргументы, обёрнутые в TextInputsArguments<T>. Что касается внутрянки Do_UnfollowGeocodeAsync, то выглядит это следующим образом. Осталось только обновить процесс удаления в нашем ДС. Итоговая настройка датасета выглядит следующим образом. Проверим работу. Взглянем на данные отладки и убедимся, что город "Москва" не исчез из наших сохранений, однако мы-как-подписчик исчезли из списка. Отдельно стоит отметить, что ни IDataManager, ни его реализация DefaultDataManager не предоставляют способы сохранения данных, являясь лишь виртуальным хранилищем, предоставляющим API работы с вашей БД фреймрворку. Поэтому нам также необходимо реаизовать методы загрузки/сохранения данных. В качестве БД может выступать, например, SQL или любая другая система. Мы же пропишем этот фнкционал через примитивные Serizlize/Deserizlize методы библиотеки Newtonsoft.Json и хранить данные будем в JSON файле.  Будем собирать датасет на основе данных БД документа и подключим обновление базы данных к событиям изменения датасета. Немного поработав с ботом, подписался на пару городов и отписался от третьего. Полученные данные выглядят вот так и прекрасно загружаются после перезапуска клиента бота. Здесь же стоит добавить формальную приписку, что, по возможности, надо снять нагрузку с API геокодера и сначала пробовать подгружать данные геометок их нашего датасета. Кроме того, можно хранить последние данные о погоде и обращаться к Я.Погоде только если прогноз устарел. Может показаться, что для столь примитивной логики, используемой в данном боте, функционал новых расширений - излишество, а их использование в целом - нерационально. Никак не могу опровергнуть данный тезис, однако на данном примере были разобраны основные функции расширений. Что же касается области применения... Как и обещал, отвечаю на этот вопрос, поставленный в начале статьи. На мой взгляд, использование всех возможностей фреймворка позволит создавать АИС и CRM системы на базе телеграма. Что это и как это есть планирую расписать в следующей статье, подкрепляя доводы примерами конкретного кейса, над которым сейчас идёт работа. Как и остальные исходники, результаты работы доступны в репозитории GitHub, в ветве "v2.0-Release". Позволю себе отойти от профиссионального языка в этом небольшом PS. Вся работа: от написания кода и документации к нему до оформления этих статей - выполняется мною единолично от и до. Это невероятно ресурсоёмкий и энергозатратный процесс. Всё решение является опенсурсом без монетизации и держится сугубо на инициативе одного студента, а потому, дорогие читатели, мне как никогда нужна Ваша поддержка. Прошу Вас, не стесняйтесь одобрять и критиковать статьи, добавлять репо в вотч-лист и писать о возникающих ошибках. Ваша реакция очень важна для меня. Именно она позволяет мне видеть актуальность и не терять веру в то, что я делаю. Только лишь вместе можно сделать лучше.