Skip to content

ExpressApp/sharpbotx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

Express.SharpBotX

Библиотека для разработки SmartApp-ботов на платформе Express (BotX).
Предоставляет инфраструктуру для приёма и обработки сообщений от пользователей, отправки ответов и управления манифестом SmartApp.


Архитектура

Backend-часть (бот) представляет собой .NET Web API приложение, которое:

  • реализует интерфейсы, необходимые для взаимодействия с платформой Express;
  • обрабатывает запросы, поступающие от UI SmartApp.

Роутинг запросов

Маршрутизация входящих запросов от UI SmartApp к конкретному методу-обработчику реализована через атрибуты и рефлектор:

  • [SmartAppController] — помечает класс как контроллер SmartApp;
  • [SmartAppControllerMethod("regexp")] — помечает метод как обработчик и задаёт regex-паттерн для фильтрации.

Фильтрация производится по полю method из объекта command.data.data входящего сообщения от UI SmartApp (message.Command.Data.SmartAppData.Method). Библиотека автоматически сопоставляет значение этого поля с зарегистрированными паттернами через Regex.IsMatch без учёта регистра (RegexOptions.IgnoreCase) и вызывает соответствующий метод-обработчик.

Обнаружение контроллеров происходит один раз при старте приложения через Assembly.GetEntryAssembly() — сканируются все экспортированные типы с атрибутом [SmartAppController]. Для вызова обработчика через DI контроллер должен быть зарегистрирован в контейнере.

UI SmartApp  →  BotX  →  POST /command  →  CommandMiddleware
                                                  ↓
                                    Десериализация UserMessage
                                    Token ← заголовок OPEN_ID_ACCESS_TOKEN
                                                  ↓
                                         SmartAppMiddleware
                                    Regex.IsMatch(Method, pattern)
                                                  ↓
                                    [SmartAppController] + рефлексия
                                    [SmartAppControllerMethod("regexp")]
                                                  ↓
                                          Метод-обработчик
                                                  ↓
                               IBotMessageSender.SendSmartAppResponseAsync
                                                  ↓
                                          BotX  →  UI SmartApp

Содержание


Конфигурация

Библиотека читает конфигурацию ботов из appsettings.json.
Добавьте секцию BotConfigArr — массив объектов, по одному на каждый бот:

{
  "BotConfigArr": [
    {
      "ApiBaseUrl": "https://your-backend-api/",
      "CTS_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "BOT_CTS": "https://cts.example.com/",
      "BOT_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "BOT_SECRET": "your_bot_secret_here",
      "FILE_CTS": "https://cts.example.com/",
      "FILE_DIRECT_BASE_URL": "https://files.example.com/",
      "NGINX_ROUTE": "your_route"
    }
  ]
}
Поле Описание
ApiBaseUrl Базовый URL вашего backend-API (не является обязательным для взаимодействия с BotX)
CTS_ID ID корпоративного сервера CTS
BOT_CTS URL CTS-сервера
BOT_ID ID бота
BOT_SECRET Секрет бота для аутентификации
FILE_CTS URL CTS для файловых операций (может совпадать с BOT_CTS, не является обязательным для взаимодействия с BotX)
FILE_DIRECT_BASE_URL Базовый URL для прямого доступа к файлам (не является обязательным для взаимодействия с BotX)

Любые дополнительные ключи (например, NGINX_ROUTE) также можно хранить в BotConfig, поскольку он наследует Dictionary<string, string>, и обращаться к ним через индексатор: botConfig["NGINX_ROUTE"].

Замены строк (опционально)

Секция Replaces позволяет автоматически заменять подстроки в теле запросов и ответов (например, подменять прямые ссылки на файлы проксированными):

{
  "Replaces": [
    {
      "originalString": "/Api/Files/",
      "replacedString": "{BOT_CTS}api/v1/smartapp_proxy/botserv/{NGINX_ROUTE}/smartapp_files/static/content_holder/{BOT_ID}/api/Files/"
    }
  ]
}

Подключение к приложению

1. Регистрация конфигурации и сервисов

// Регистрация IConfigService и опций конфигурации
services.AddOptions();
services.Configure<List<BotConfig>>(configuration.GetSection("BotConfigArr"));
services.Configure<StaticFileConfig>(configuration.GetSection("StaticFileConfig"));
services.AddSingleton<ReplaceSettings>(provider =>
    new ReplaceSettings(configuration.GetSection("Replaces").Get<ReplaceItem[]>()));
services.AddSingleton<IConfigService, ConfigService>();

2. Регистрация контроллеров со SmartApp-атрибутом

// Автоматически регистрирует все классы с атрибутом [SmartAppController] из текущей сборки
services.RegisterClassesWithAttribute<SmartAppControllerAttribute>();

Вспомогательный метод расширения:

public static IServiceCollection RegisterClassesWithAttribute<TAttribute>(this IServiceCollection services)
    where TAttribute : Attribute
{
    // Важно: использовать GetEntryAssembly() — именно так библиотека сканирует контроллеры
    var types = Assembly.GetEntryAssembly().GetExportedTypes()
        .Where(t => t.GetCustomAttributes(typeof(TAttribute), true).Any());

    foreach (var type in types)
        services.AddScoped(type);

    return services;
}

3. Инициализация Express-бота

services.AddExpressBot(
    new BotXConfig(
        botConfigService.GetBotConfig().Select(e =>
            new BotEntry(
                new Uri(e.Value.BOT_CTS),  // адрес CTS-сервера
                Guid.Parse(e.Value.BOT_ID), // ID бота 
                e.Value.BOT_SECRET          // Secret бота
            )
        ),
        inChatExceptions: false // true — выводить ошибки в чат пользователю
    )
);

4. Подключение middleware (Configure)

app.UseExpress();

Регистрирует следующие маршруты:

Метод Путь Назначение
GET /status используется BotX для проверки доступности бота
POST /command Приём входящих сообщений от BotX
GET /smartapp_files/static/{**filePath} Проксирование статических файлов

Создание контроллера

Контроллер — это обычный C#-класс с атрибутом [SmartAppController].

Маршрутизация входящих запросов работает через рефлектор: библиотека автоматически находит все методы с атрибутом [SmartAppControllerMethod] и сопоставляет их regex-паттерны с полем Method из входящего запроса UI SmartApp. При совпадении вызывается соответствующий метод-обработчик.

⚠️ Чтобы рефлексия работала, все контроллеры должны быть зарегистрированы в DI — см. шаг 2 подключения.

Атрибуты

[SmartAppController]

Помечает класс как контроллер SmartApp. Библиотека обнаруживает его через рефлексию при старте и регистрирует все методы-обработчики внутри него.

[SmartAppController]
public class MyController
{
    // ...
}

[SmartAppControllerMethod(method)]

Помечает метод как обработчик входящего сообщения. Параметр method — это регулярное выражение, которое сопоставляется с полем Method входящего SmartApp-запроса через Regex.IsMatch без учёта регистра.

Атрибут поддерживает AllowMultiple = true — один метод можно пометить несколькими атрибутами с разными паттернами.

// Срабатывает на конкретную команду (точное совпадение)
[SmartAppControllerMethod("GetOrders$")]

// Срабатывает на любой HTTP-проксируемый запрос вида "VERB /path"
[SmartAppControllerMethod("\\S+ \\S+")]

// С явным указанием типов входных и выходных данных (для документирования/интроспекции)
[SmartAppControllerMethod("GetOrders$", typeof(GetOrdersInput), typeof(GetOrdersOutput))]

// Один метод обрабатывает несколько паттернов
[SmartAppControllerMethod("CreateOrder$")]
[SmartAppControllerMethod("UpdateOrder$")]
public async Task SaveOrder(UserMessage message, IBotMessageSender sender) { ... }

Паттерны проверяются в порядке регистрации (порядок обхода типов через рефлексию). При необходимости точного совпадения используйте якорь $, иначе широкий паттерн "\\S+ \\S+" может перехватить специализированные команды.

Сигнатура метода-обработчика

Каждый обработчик должен принимать два параметра:

Параметр Тип Описание
message UserMessage Входящее сообщение от UI SmartApp
sender IBotMessageSender Интерфейс для отправки ответа обратно
using Express.SharpBotX.Abstract;
using Express.SharpBotX.Attributes;
using Express.SharpBotX.JsonModel.Request;

[SmartAppController]
public class MyController
{
    private readonly ILogger<MyController> _logger;

    public MyController(ILogger<MyController> logger)
    {
        _logger = logger;
    }

    [SmartAppControllerMethod("\\S+ \\S+")]
    public async Task HandleRequest(UserMessage message, IBotMessageSender sender)
    {
        _logger.LogInformation("Получено сообщение");
        await sender.SendSmartAppResponseAsync(message, new { status = "ok" });
    }
}

Доступные свойства UserMessage

Свойство Описание
message.Token JWT-токен пользователя — извлекается из HTTP-заголовка OPEN_ID_ACCESS_TOKEN (не из тела сообщения)
message.BotId ID бота-получателя
message.From Данные отправителя: Host (адрес CTS), UserHuid, GroupChatId и др.
message.Command.Body Тип команды; для SmartApp-запросов всегда "system:smartapp_event"
message.Command.Data.SmartAppData.Method Строка с именем метода — именно по ней производится фильтрация через Regex, например GET /api/orders или Authorize
message.Command.Data.SmartAppData.SmartAppParams.Body Тело запроса (JSON-строка)
message.Command.Data.SmartAppData.SmartAppParams.Token Токен из тела SmartApp-запроса

Базовый класс контроллера (опционально)

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

public class BaseController
{
    protected readonly ILogger _logger;

    public BaseController(ILogger logger)
    {
        _logger = logger;
    }

    protected async Task SendMessageAsync(
        IBotMessageSender sender,
        UserMessage message,
        Func<UserMessage, Task<object>> operation)
    {
        try
        {
            var result = await operation(message);
            await sender.SendSmartAppResponseAsync(message, result);
        }
        catch (Exception ex)
        {
            await sender.SendSmartAppResponseAsync(
                message,
                new List<ErrorResponseModel>
                {
                    new() { Id = "500", Meta = ex.Message, Reason = "INTERNAL_ERROR" }
                },
                statusCode: 500);
        }
    }
}

[SmartAppController]
public class OrdersController : BaseController
{
    public OrdersController(ILogger<OrdersController> logger) : base(logger) { }

    [SmartAppControllerMethod("GetOrders$", typeof(GetOrdersInput), typeof(GetOrdersOutput))]
    public async Task GetOrders(UserMessage message, IBotMessageSender sender)
    {
        await SendMessageAsync(sender, message, async msg =>
        {
            // бизнес-логика
            return new GetOrdersOutput { Orders = new[] { "Order #1" } };
        });
    }
}

Отправка ответов

Интерфейс IBotMessageSender используется для отправки ответов:

// Успешный ответ с данными
await sender.SendSmartAppResponseAsync(message, responseData);

// Ответ с кастомным HTTP-статусом
await sender.SendSmartAppResponseAsync(message, responseData, statusCode: 201);

// Ответ с ошибкой (автоматически формирует сообщение об ошибке)
await sender.SendSmartAppResponseAsync(message, exception);

Пример ответа с ошибкой (стандартизованный формат)

await sender.SendSmartAppResponseAsync(
    message,
    new List<ErrorResponseModel>
    {
        new ErrorResponseModel
        {
            Id = "500",
            Meta = errorDetails,
            Reason = "INTERNAL_ERROR"
        }
    },
    statusCode: 500
);

Отправка манифеста

Манифест описывает параметры отображения SmartApp на устройствах. Отправляется при старте приложения:

var sender = serviceProvider.GetRequiredService<IBotMessageSender>();

await sender.SendSmartappsManifest(botGuid, new ManifestRequest
{
    manifest = new Manifest
    {
        android = new Android
        {
            always_pinned = true,
            fullscreen_layout = true
        },
        ios = new Ios
        {
            always_pinned = true,
            fullscreen_layout = true
        }
    }
});

Работа с IConfigService

IConfigService предоставляет доступ к конфигурации ботов:

// Получить конфигурацию бота по botId
var botConfig = _configService.GetBotByKey(message.BotId.ToString());
string apiBaseUrl = botConfig.ApiBaseUrl;
string nginxRoute = botConfig["NGINX_ROUTE"];

// Получить настройки замен строк
var replaceSettings = _configService.GetReplaceByKey(message.BotId.ToString());

// Получить настройки статических файлов
var staticConfig = _configService.GetStaticByKey(botId);

// Получить весь массив конфигураций ботов
var allBots = _configService.GetBotConfig();

Конфигурация может содержать параметры для нескольких ботов, находящихся на разных CTS, поэтому её получение привязано к идентификатору бота.


Полный пример

appsettings.json

{
  "BotConfigArr": [
    {
      "ApiBaseUrl": "https://backend.example.com/",
      "CTS_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "BOT_CTS": "https://cts.example.com/",
      "BOT_ID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "BOT_SECRET": "your_secret",
      "FILE_CTS": "https://cts.example.com/",
      "FILE_DIRECT_BASE_URL": "https://files.example.com/",
      "NGINX_ROUTE": "myapp"
    }
  ],
  "StaticFileConfig": {
    "StaticFilesOrigin": "https://files.example.com/"
  },
  "Replaces": [
    {
      "originalString": "/Api/Files/",
      "replacedString": "{BOT_CTS}api/v1/smartapp_proxy/botserv/{NGINX_ROUTE}/smartapp_files/static/content_holder/{BOT_ID}/api/Files/"
    }
  ]
}

Program.cs / Startup.cs

// ConfigureServices
services.AddOptions();
services.Configure<List<BotConfig>>(configuration.GetSection("BotConfigArr"));
services.Configure<StaticFileConfig>(configuration.GetSection("StaticFileConfig"));
services.AddSingleton<ReplaceSettings>(provider =>
    new ReplaceSettings(configuration.GetSection("Replaces").Get<ReplaceItem[]>()));
services.AddSingleton<IConfigService, ConfigService>();

services.RegisterClassesWithAttribute<SmartAppControllerAttribute>();

var configService = services.BuildServiceProvider().GetRequiredService<IConfigService>();
services.AddExpressBot(
    new BotXConfig(
        configService.GetBotConfig().Select(e =>
            new BotEntry(new Uri(e.Value.BOT_CTS), Guid.Parse(e.Value.BOT_ID), e.Value.BOT_SECRET)
        ),
        inChatExceptions: false
    )
);

// Отправка манифеста
var sender = services.BuildServiceProvider().GetRequiredService<IBotMessageSender>();
await sender.SendSmartappsManifest(Guid.Parse("BOT_ID_HERE"), new ManifestRequest
{
    manifest = new Manifest
    {
        android = new Android { always_pinned = true, fullscreen_layout = true },
        ios = new Ios { always_pinned = true, fullscreen_layout = true }
    }
});

// Configure
app.UseExpress();
app.UseRouting();
app.MapControllers();

OrdersController.cs

using Express.SharpBotX.Abstract;
using Express.SharpBotX.Attributes;
using Express.SharpBotX.JsonModel.Request;

[SmartAppController]
public class OrdersController
{
    private readonly ILogger<OrdersController> _logger;

    public OrdersController(ILogger<OrdersController> logger)
    {
        _logger = logger;
    }

    // Срабатывает на сообщения вида "GET /api/orders", "POST /api/orders" и т.д.
    [SmartAppControllerMethod("\\S+ \\S+")]
    public async Task ProxyRequest(UserMessage message, IBotMessageSender sender)
    {
        _logger.LogInformation("ProxyRequest");
        // ... логика проксирования запросов к backend
        await sender.SendSmartAppResponseAsync(message, new { status = "ok" });
    }

    // Срабатывает на конкретное действие "GetOrders$"
    [SmartAppControllerMethod("GetOrders$", typeof(GetOrdersInput), typeof(GetOrdersOutput))]
    public async Task GetOrders(UserMessage message, IBotMessageSender sender)
    {
        _logger.LogInformation("GetOrders");
        var result = new GetOrdersOutput { Orders = new[] { "Order #1", "Order #2" } };
        await sender.SendSmartAppResponseAsync(message, result);
    }

    // Специальный метод авторизации
    [SmartAppControllerMethod("Authorize")]
    public async Task Authorize(UserMessage message, IBotMessageSender sender)
    {
        await sender.SendSmartAppResponseAsync(message, new
        {
            token = message.Token,
            cts = message.From.Host,
            botId = message.BotId
        });
    }
}

Зависимости

Библиотека совместима с .NET 6 и выше. Рекомендуемые сопутствующие пакеты:

Пакет Назначение
Serilog.AspNetCore Структурированное логирование
Newtonsoft.Json Сериализация JSON

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages