Библиотека для разработки 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
- Архитектура
- Конфигурация
- Подключение к приложению
- Создание контроллера
- Отправка ответов
- Отправка манифеста
- Работа с IConfigService
- Полный пример
Библиотека читает конфигурацию ботов из 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/"
}
]
}// Регистрация 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>();// Автоматически регистрирует все классы с атрибутом [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;
}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 — выводить ошибки в чат пользователю
)
);app.UseExpress();Регистрирует следующие маршруты:
| Метод | Путь | Назначение |
|---|---|---|
GET |
/status |
используется BotX для проверки доступности бота |
POST |
/command |
Приём входящих сообщений от BotX |
GET |
/smartapp_files/static/{**filePath} |
Проксирование статических файлов |
Контроллер — это обычный C#-класс с атрибутом [SmartAppController].
Маршрутизация входящих запросов работает через рефлектор: библиотека автоматически находит все методы с атрибутом [SmartAppControllerMethod] и сопоставляет их regex-паттерны с полем Method из входящего запроса UI SmartApp. При совпадении вызывается соответствующий метод-обработчик.
⚠️ Чтобы рефлексия работала, все контроллеры должны быть зарегистрированы в DI — см. шаг 2 подключения.
Помечает класс как контроллер SmartApp. Библиотека обнаруживает его через рефлексию при старте и регистрирует все методы-обработчики внутри него.
[SmartAppController]
public class MyController
{
// ...
}Помечает метод как обработчик входящего сообщения. Параметр 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" });
}
}| Свойство | Описание |
|---|---|
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 предоставляет доступ к конфигурации ботов:
// Получить конфигурацию бота по 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, поэтому её получение привязано к идентификатору бота.
{
"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/"
}
]
}// 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();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 |