From 4910a225dc00bfc4a91c5d8a976f749436480f9e Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Wed, 15 Oct 2025 12:51:13 +0300 Subject: [PATCH 01/19] - bugfix: multiple event handler appointment in case of Reloading a telegram bot client --- Botticelli.Framework.Telegram/TelegramBot.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 38ff9c5b..a52ea281 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -32,6 +32,7 @@ public class TelegramBot : BaseBot private readonly IBotDataAccess _data; private readonly IBotUpdateHandler _handler; private readonly ITextTransformer _textTransformer; + private bool _handlerLoaded = false; protected readonly ITelegramBotClient Client; // ReSharper disable once MemberCanBeProtected.Global @@ -512,13 +513,16 @@ protected override Task InnerStartBotAsync(StartBotRequest req BotStatusKeeper.IsStarted = true; // Rethrowing events from BotUpdateHandler - _handler.MessageReceived += (sender, e) + if (!_handlerLoaded) + { + _handler.MessageReceived += (sender, e) => MessageReceived?.Invoke(sender, e); - _handler.ContactShared += (sender, e) - => ContactShared?.Invoke(sender, e); - _handler.NewChatMembers += (sender, e) - => NewChatMembers?.Invoke(sender, e); - + _handler.ContactShared += (sender, e) + => ContactShared?.Invoke(sender, e); + _handler.NewChatMembers += (sender, e) + => NewChatMembers?.Invoke(sender, e); + _handlerLoaded = true; + } Client.StartReceiving(_handler, cancellationToken: token); Logger.LogInformation($"{nameof(StartBotAsync)}: started"); From 1bd1f22aef66286fac2abb9b082200b6a4b3a087 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 2 Jan 2026 12:43:35 +0300 Subject: [PATCH 02/19] - hotfix fo DI (locations) --- .../Extensions/ServiceCollectionExtensions.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs index 042073b0..a59958dc 100644 --- a/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -32,18 +32,18 @@ public static IServiceCollection AddOsmLocations(this IServiceCollection service TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly()); return services.Configure(config) - .AddScoped, PassValidator>() - .AddScoped, PassValidator>() + .AddSingleton, PassValidator>() + .AddSingleton, PassValidator>() .AddScoped>() .AddScoped>() .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped, InlineTelegramLayoutSupplier>() - .AddScoped, ReplyTelegramLayoutSupplier>() - .AddScoped(sp => new ForwardGeocoder(sp.GetRequiredService(), + .AddSingleton() + .AddSingleton() + .AddSingleton, InlineTelegramLayoutSupplier>() + .AddSingleton, ReplyTelegramLayoutSupplier>() + .AddSingleton(sp => new ForwardGeocoder(sp.GetRequiredService(), Url.Combine(url, "search"))) - .AddScoped(sp => new ReverseGeocoder(sp.GetRequiredService(), + .AddSingleton(sp => new ReverseGeocoder(sp.GetRequiredService(), Url.Combine(url, "reverse"))); } } \ No newline at end of file From 252cdd0c38d0c4cc2c4835be41e61f35e5b6c2b2 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 8 Jan 2026 00:01:33 +0300 Subject: [PATCH 03/19] - osm id in address --- Botticelli.Controls/BasicControls/Button.cs | 9 +++++++++ Botticelli.Controls/Layouts/Item.cs | 9 +++++++++ Botticelli.Locations/Integration/OsmLocationProvider.cs | 1 + Botticelli.Locations/Models/Address.cs | 3 +++ 4 files changed, 22 insertions(+) diff --git a/Botticelli.Controls/BasicControls/Button.cs b/Botticelli.Controls/BasicControls/Button.cs index baf80ebc..f0dd57c9 100644 --- a/Botticelli.Controls/BasicControls/Button.cs +++ b/Botticelli.Controls/BasicControls/Button.cs @@ -2,6 +2,15 @@ public class Button : IControl { + public Button() + { + } + + public Button(string? content) + { + Content = content; + } + public string? Image { get; set; } public string? Content { get; set; } diff --git a/Botticelli.Controls/Layouts/Item.cs b/Botticelli.Controls/Layouts/Item.cs index 38ca98b1..6263bc02 100644 --- a/Botticelli.Controls/Layouts/Item.cs +++ b/Botticelli.Controls/Layouts/Item.cs @@ -4,6 +4,15 @@ namespace Botticelli.Controls.Layouts; public class Item { + public Item() + { + } + + public Item(IControl? control) + { + Control = control; + } + public IControl? Control { get; set; } public ItemParams? Params { get; set; } diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index 3491b7e3..8085efcd 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -53,6 +53,7 @@ public async Task> Search(string query, int maxPoints) })).Select(gr => { var address = gr.Address?.Adapt
() ?? new Address(); + address.ObjectId = gr.OSMID.ToString(); address.Longitude = gr.Longitude; address.Latitude = gr.Latitude; address.DisplayName = gr.DisplayName; diff --git a/Botticelli.Locations/Models/Address.cs b/Botticelli.Locations/Models/Address.cs index a989d9fd..8ad0d27f 100644 --- a/Botticelli.Locations/Models/Address.cs +++ b/Botticelli.Locations/Models/Address.cs @@ -5,6 +5,9 @@ namespace Botticelli.Locations.Models; public class Address { + [JsonPropertyName("ObjectId")] + public string ObjectId { get; set; } + [JsonPropertyName("country")] public string? Country { get; set; } From b4443665ec09a563a5614435cca7b630bd9ae574 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 8 Jan 2026 13:36:45 +0300 Subject: [PATCH 04/19] - NoneBus lock fixed --- Botticelli.Bus/Agent/PassAgent.cs | 12 ++++++++---- .../Commands/Processors/FluentCommandProcessor.cs | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Botticelli.Bus/Agent/PassAgent.cs b/Botticelli.Bus/Agent/PassAgent.cs index 646da275..4cba477e 100644 --- a/Botticelli.Bus/Agent/PassAgent.cs +++ b/Botticelli.Bus/Agent/PassAgent.cs @@ -43,7 +43,9 @@ public Task SendResponseAsync(SendMessageResponse response, public Task StartAsync(CancellationToken token) { - return Task.Run(async () => await InnerProcess(_handler, token)); + Task.Run(() => InnerProcess(_handler, token), token); + + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) @@ -51,12 +53,14 @@ public Task StopAsync(CancellationToken cancellationToken) throw new NotImplementedException(); } - private async Task InnerProcess(THandler handler, CancellationToken token) + private void InnerProcess(THandler handler, CancellationToken token) { while (!token.IsCancellationRequested) { - if (NoneBus.SendMessageRequests.TryDequeue(out var request)) await handler.Handle(request, token); - Thread.Sleep(5); + if (NoneBus.SendMessageRequests.TryDequeue(out var request)) + handler.Handle(request, token).Wait(token); + + Task.Delay(5, token).Wait(token); } } } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/FluentCommandProcessor.cs b/Botticelli.Framework/Commands/Processors/FluentCommandProcessor.cs index 9674ff43..e6d48a7f 100644 --- a/Botticelli.Framework/Commands/Processors/FluentCommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/FluentCommandProcessor.cs @@ -13,12 +13,12 @@ namespace Botticelli.Framework.Commands.Processors; public abstract class FluentCommandProcessor( ILogger logger, MetricsProcessor metricsProcessor, - ICommandValidator commandValidator, - IBot bot) + ICommandValidator commandValidator) : ICommandProcessor where TCommand : class, IFluentCommand { - protected IBot Bot = bot; + protected IBot? Bot; + public string CommandText { get; init; } public async Task ProcessAsync(Message message, CancellationToken token) From 5d712faebdd380dfe504700627683cd649c730d7 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 8 Jan 2026 14:42:17 +0300 Subject: [PATCH 05/19] - standalone bot DI fix --- .../Builders/TelegramBotBuilder.cs | 19 +++++++++++++++++-- .../Builders/TelegramStandaloneBotBuilder.cs | 10 ++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 6d1c3575..78e27075 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -1,3 +1,4 @@ +using System.Configuration; using Botticelli.Bot.Data; using Botticelli.Bot.Data.Repositories; using Botticelli.Bot.Data.Settings; @@ -18,6 +19,7 @@ using Botticelli.Framework.Telegram.Layout; using Botticelli.Framework.Telegram.Options; using Botticelli.Framework.Telegram.Utils; +using Botticelli.Interfaces; using Botticelli.Shared.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -171,8 +173,8 @@ public TelegramBotBuilder Prepare() #region Data Services.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); - Services.AddScoped(); + o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}"), ServiceLifetime.Singleton); + Services.AddSingleton(); #endregion @@ -197,6 +199,19 @@ public TelegramBotBuilder Prepare() .AddBotticelliFramework() .AddSingleton() .AddSingleton(client); + + if (_isStandalone) + { + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + + if (BotData == null) throw new ConfigurationErrorsException("BotData is null!"); + + Services.AddHostedService() + .AddSingleton(BotData); + + Services.AddSingleton(sp => this .Build(sp)!); + } return this; } diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index 7460b2bc..4fb291d0 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -45,19 +45,13 @@ public TelegramStandaloneBotBuilder AddBotData(BotDataSettingsBuilder() - .AddServerCertificates(BotSettings); - - if (BotData == null) throw new ConfigurationErrorsException("BotData is null!"); - - Services.AddHostedService() - .AddSingleton(BotData); - return base.InnerBuild(serviceProvider); } } \ No newline at end of file From 97b8464fae8d14671a134da9bf33c77822fcd05b Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 8 Jan 2026 14:57:04 +0300 Subject: [PATCH 06/19] - AddProcessor lifetime selection --- .../Extensions/CommandAddServices.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Botticelli.Framework/Extensions/CommandAddServices.cs b/Botticelli.Framework/Extensions/CommandAddServices.cs index 3f8e33f9..1260adc5 100644 --- a/Botticelli.Framework/Extensions/CommandAddServices.cs +++ b/Botticelli.Framework/Extensions/CommandAddServices.cs @@ -10,20 +10,34 @@ namespace Botticelli.Framework.Extensions; public class CommandAddServices(IServiceCollection services) where TCommand : class, ICommand { - public CommandAddServices AddProcessor(IConfiguration configuration) + public CommandAddServices AddProcessor(IConfiguration configuration, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) where TCommandProcessor : class, ICommandProcessor where TConfiguration : class { services.Configure(configuration.GetSection(typeof(TConfiguration).Name)); - AddProcessor(); + AddProcessor(serviceLifetime); return this; } - public CommandAddServices AddProcessor() + public CommandAddServices AddProcessor(ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) where TCommandProcessor : class, ICommandProcessor { - services.AddSingleton(); + switch (serviceLifetime) + { + case ServiceLifetime.Singleton: + services.AddSingleton(); + break; + case ServiceLifetime.Scoped: + services.AddScoped(); + break; + case ServiceLifetime.Transient: + services.AddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null); + } + ProcessorFactoryBuilder.AddProcessor(services); return this; From 5356474e2d435b60922bb17880f98de06fcbfd9b Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 8 Jan 2026 15:08:11 +0300 Subject: [PATCH 07/19] - special scope for process factory --- .../Extensions/CommandAddServices.cs | 25 +++++++++++++++---- .../Processors/ProcessorFactoryBuilder.cs | 7 +++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Botticelli.Framework/Extensions/CommandAddServices.cs b/Botticelli.Framework/Extensions/CommandAddServices.cs index 1260adc5..d532ec33 100644 --- a/Botticelli.Framework/Extensions/CommandAddServices.cs +++ b/Botticelli.Framework/Extensions/CommandAddServices.cs @@ -43,24 +43,39 @@ public CommandAddServices AddProcessor(ServiceLifet return this; } - public CommandAddServices AddValidator(IConfiguration configuration) + public CommandAddServices AddValidator(IConfiguration configuration, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) where TCommandValidator : class, ICommandValidator where TConfiguration : class { services.Configure(configuration.GetSection(typeof(TConfiguration).Name)); // validator chain needs to be implemented! - AddValidator(); + AddValidator(serviceLifetime); return this; } - public CommandAddServices AddValidator() + public CommandAddServices AddValidator(ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) where TCommandValidator : class, ICommandValidator { // validator chain needs to be implemented! - services.AddSingleton() - .AddSingleton, TCommandValidator>(); + switch (serviceLifetime) + { + case ServiceLifetime.Singleton: + services.AddSingleton() + .AddSingleton, TCommandValidator>(); + break; + case ServiceLifetime.Scoped: + services.AddScoped() + .AddScoped, TCommandValidator>(); + break; + case ServiceLifetime.Transient: + services.AddTransient() + .AddTransient, TCommandValidator>(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null); + } return this; } diff --git a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs index 884e2838..ced40a9a 100644 --- a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs +++ b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs @@ -18,15 +18,16 @@ public static void AddProcessor(IServiceCollection serviceCollection public static ProcessorFactory Build(IServiceProvider sp) { + var scope = sp.CreateScope(); if (_serviceCollection == null) return new ProcessorFactory([]); var processors = ProcessorTypes .Select(pt => { - var processor = sp.GetRequiredService(pt) as ICommandProcessor; - processor?.SetBot(sp.GetRequiredService()); - processor?.SetServiceProvider(sp); + var processor = scope.ServiceProvider.GetRequiredService(pt) as ICommandProcessor; + processor?.SetBot(scope.ServiceProvider.GetRequiredService()); + processor?.SetServiceProvider(scope.ServiceProvider); return processor; }) From a80dd7abe5241b7eb2c59529cafb3c0113a4bdd6 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 9 Jan 2026 01:41:12 +0300 Subject: [PATCH 08/19] - osm address lookup by object id --- .../Integration/ILocationProvider.cs | 3 ++- .../Integration/OsmLocationProvider.cs | 26 +++++++++++++++++++ Botticelli.Locations/Models/Address.cs | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Botticelli.Locations/Integration/ILocationProvider.cs b/Botticelli.Locations/Integration/ILocationProvider.cs index e29f26fb..160f61b0 100644 --- a/Botticelli.Locations/Integration/ILocationProvider.cs +++ b/Botticelli.Locations/Integration/ILocationProvider.cs @@ -10,8 +10,9 @@ public interface ILocationProvider public Task GetMapLink(Address address); - public Task> Search(string query, int maxPoints); + public Task> SearchByIds(string[] ids); + public Task GetTimeZone(Location location); } \ No newline at end of file diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index 8085efcd..69107d28 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -14,14 +14,17 @@ public class OsmLocationProvider : ILocationProvider private readonly IForwardGeocoder _forwardGeocoder; private readonly IOptionsSnapshot _options; private readonly IReverseGeocoder _reverseGeoCoder; + private readonly IAddressSearcher _addressSearcher; public OsmLocationProvider(IReverseGeocoder reverseGeoCoder, IForwardGeocoder forwardGeocoder, + IAddressSearcher addressSearcher, IOptionsSnapshot options) { _reverseGeoCoder = reverseGeoCoder; _forwardGeocoder = forwardGeocoder; _options = options; + _addressSearcher = addressSearcher; } public async Task GetAddress(Location location) @@ -54,17 +57,40 @@ public async Task> Search(string query, int maxPoints) { var address = gr.Address?.Adapt
() ?? new Address(); address.ObjectId = gr.OSMID.ToString(); + address.ObjectType = gr.OSMType; address.Longitude = gr.Longitude; address.Latitude = gr.Latitude; address.DisplayName = gr.DisplayName; return address; }) + .Take(maxPoints) .ToList(); return results; } + public async Task> SearchByIds(string[] ids) + { + var results = (await _addressSearcher.Lookup(new AddressSearchRequest + { + OSMIDs = ids + })).Select(gr => + { + var address = gr.Address?.Adapt
() ?? new Address(); + address.ObjectId = gr.OSMID.ToString(); + address.ObjectType = gr.OSMType; + address.Longitude = gr.Longitude; + address.Latitude = gr.Latitude; + address.DisplayName = gr.DisplayName; + + return address; + }) + .ToList(); + + return results; + } + public Task GetTimeZone(Location location) { var tz = TimeZoneLookup.GetTimeZone(location.Lat, location.Lng).Result; diff --git a/Botticelli.Locations/Models/Address.cs b/Botticelli.Locations/Models/Address.cs index 8ad0d27f..3ac3f0db 100644 --- a/Botticelli.Locations/Models/Address.cs +++ b/Botticelli.Locations/Models/Address.cs @@ -7,6 +7,8 @@ public class Address { [JsonPropertyName("ObjectId")] public string ObjectId { get; set; } + [JsonPropertyName("ObjectType")] + public string ObjectType { get; set; } [JsonPropertyName("country")] public string? Country { get; set; } From e83a03a1006f8872bea36bf60b6d978d19108675 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 Jan 2026 01:11:40 +0300 Subject: [PATCH 09/19] - yandex GPT request models fix --- Botticelli.AI.YaGpt/Message/YaGpt/YaGptInputMessage.cs | 4 ++-- Botticelli.AI.YaGpt/Provider/YaGptProvider.cs | 5 ++--- Botticelli.Shared/ValueObjects/Message.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Botticelli.AI.YaGpt/Message/YaGpt/YaGptInputMessage.cs b/Botticelli.AI.YaGpt/Message/YaGpt/YaGptInputMessage.cs index 90dfb141..a1677bce 100644 --- a/Botticelli.AI.YaGpt/Message/YaGpt/YaGptInputMessage.cs +++ b/Botticelli.AI.YaGpt/Message/YaGpt/YaGptInputMessage.cs @@ -4,7 +4,7 @@ namespace Botticelli.AI.YaGpt.Message.YaGpt; public class YaGptInputMessage { - [JsonPropertyName("modelUri")] + [JsonPropertyName("model_uri")] public string ModelUri { get; set; } [JsonPropertyName("completionOptions")] @@ -22,7 +22,7 @@ public class CompletionOptions [JsonPropertyName("temperature")] public double Temperature { get; set; } - [JsonPropertyName("maxTokens")] + [JsonPropertyName("max_tokens")] public int MaxTokens { get; set; } } diff --git a/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs b/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs index ed84c8f0..5dc6652d 100644 --- a/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs +++ b/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs @@ -96,11 +96,10 @@ protected override async Task GetGptResponse(AiMessage mess Role = SystemRole, Text = Settings.Value.Instruction }, - new() { Role = UserRole, - Text = message.Body + Text = message.Body ?? string.Empty } ], CompletionOptions = new CompletionOptions @@ -114,7 +113,7 @@ protected override async Task GetGptResponse(AiMessage mess yaGptMessage.Messages.AddRange(message.AdditionalMessages?.Select(m => new YaGptMessage { Role = UserRole, - Text = m.Body + Text = m.Body ?? string.Empty, }) ?? new List()); diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index e6cc7941..6b16cfce 100644 --- a/Botticelli.Shared/ValueObjects/Message.cs +++ b/Botticelli.Shared/ValueObjects/Message.cs @@ -70,7 +70,7 @@ public Message(string uid) : this() /// /// Message attachments /// - public List Attachments { get; set; } = []; + public List? Attachments { get; set; } = []; /// /// From user From 437c24b1d5667d4d1c7b68fa054821076b14c01d Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 Jan 2026 16:02:40 +0300 Subject: [PATCH 10/19] - json layout parsing - callback field parsing for inline buttons --- Botticelli.Controls/Parsers/JsonLayoutParser.cs | 5 ++++- .../Extensions/ServiceCollectionExtensions.cs | 2 ++ Botticelli.Framework.Telegram/TelegramBot.cs | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Botticelli.Controls/Parsers/JsonLayoutParser.cs b/Botticelli.Controls/Parsers/JsonLayoutParser.cs index 5f3f026b..9be2d781 100644 --- a/Botticelli.Controls/Parsers/JsonLayoutParser.cs +++ b/Botticelli.Controls/Parsers/JsonLayoutParser.cs @@ -68,9 +68,12 @@ private static void ResolveControlType(JsonElement itemElement, Item item) { if (itemElement.TryGetProperty("Button", out var buttonElement)) { + var hasCallback = buttonElement.TryGetProperty("Callback", out var callbackElement); var button = new Button { - Content = buttonElement.GetProperty("Content").GetString() + Content = buttonElement.GetProperty("Content") + .GetString(), + CallbackData = hasCallback ? callbackElement.GetString() : null }; item.Control = button; diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 84b71ae2..5aa1f464 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -217,6 +217,8 @@ public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollecti services.AddSingleton() .AddSingleton, ReplyTelegramLayoutSupplier>() .AddSingleton, InlineTelegramLayoutSupplier>() + .AddSingleton() + .AddSingleton() .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() .AddSingleton, LayoutLoader InnerSendMessageAsync Date: Mon, 12 Jan 2026 14:48:49 +0300 Subject: [PATCH 11/19] - IFluentCommandProcessor - was removed (unneeded) --- .../Processors/IFluentCommandProcessor.cs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 Botticelli.Bot.Interfaces/Processors/IFluentCommandProcessor.cs diff --git a/Botticelli.Bot.Interfaces/Processors/IFluentCommandProcessor.cs b/Botticelli.Bot.Interfaces/Processors/IFluentCommandProcessor.cs deleted file mode 100644 index e3f15bdd..00000000 --- a/Botticelli.Bot.Interfaces/Processors/IFluentCommandProcessor.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Botticelli.Interfaces; - -namespace Botticelli.Bot.Interfaces.Processors; - -public interface IFluentCommandProcessor : IClientMessageProcessor -{ -} \ No newline at end of file From bd87b13355e5a3aed2a9e9d3174f854182a2d585 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 12 Jan 2026 14:56:47 +0300 Subject: [PATCH 12/19] BOTTICELLI-73: - Message.Copy methods in order to copy all basic properties to a new message --- Botticelli.AI/Message/AIMessage.cs | 13 +++++++++++-- Botticelli.Shared/ValueObjects/Message.cs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Botticelli.AI/Message/AIMessage.cs b/Botticelli.AI/Message/AIMessage.cs index 4a06f106..2767813d 100644 --- a/Botticelli.AI/Message/AIMessage.cs +++ b/Botticelli.AI/Message/AIMessage.cs @@ -10,7 +10,16 @@ public AiMessage(string uid) : base(uid) { } - public string Instruction { get; set; } + public string Instruction { get; set; } = string.Empty; - public List AdditionalMessages { get; set; } + public List AdditionalMessages { get; set; } = new List(); + + public override Shared.ValueObjects.Message Copy() + { + var newMessage = (AiMessage)(base.Copy()); + newMessage.Instruction = Instruction; + newMessage.AdditionalMessages = AdditionalMessages; + + return newMessage; + } } \ No newline at end of file diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index 6b16cfce..631bfc89 100644 --- a/Botticelli.Shared/ValueObjects/Message.cs +++ b/Botticelli.Shared/ValueObjects/Message.cs @@ -121,4 +121,18 @@ public Message(string uid) : this() /// Chain id for chained command processing /// public Guid? ChainId { get; set; } + + public virtual Message Copy() + { + var newMessage = new Message(Uid!) + { + ChatIds = ChatIds, + ChainId = ChainId, + From = From, + ForwardedFrom = ForwardedFrom, + ChatIdInnerIdLinks = ChatIdInnerIdLinks + }; + + return newMessage; + } } \ No newline at end of file From 93867d376d3c8b67f26ab6d716e473034818c959 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 12 Jan 2026 15:22:06 +0300 Subject: [PATCH 13/19] - language selection support for OSM --- Botticelli.Locations/Integration/ILocationProvider.cs | 2 +- .../Integration/OsmLocationProvider.cs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Botticelli.Locations/Integration/ILocationProvider.cs b/Botticelli.Locations/Integration/ILocationProvider.cs index 160f61b0..5a76fc0f 100644 --- a/Botticelli.Locations/Integration/ILocationProvider.cs +++ b/Botticelli.Locations/Integration/ILocationProvider.cs @@ -10,7 +10,7 @@ public interface ILocationProvider public Task GetMapLink(Address address); - public Task> Search(string query, int maxPoints); + public Task> Search(string query, int maxPoints, string language = ""); public Task> SearchByIds(string[] ids); diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index 69107d28..6cd0a3d2 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -48,11 +48,12 @@ public async Task GetMapLink(Address address) $"{address.Longitude.ToString("0.00000", CultureInfo.InvariantCulture)}"; } - public async Task> Search(string query, int maxPoints) + public async Task> Search(string query, int maxPoints, string language = "") { var results = (await _forwardGeocoder.Geocode(new ForwardGeocodeRequest { - queryString = query + queryString = query, + PreferredLanguages = language })).Select(gr => { var address = gr.Address?.Adapt
() ?? new Address(); @@ -99,12 +100,13 @@ public async Task> SearchByIds(string[] ids) return Task.FromResult(tzi)!; } - private async Task InnerGetAddress(Location location) + private async Task InnerGetAddress(Location location, string language = "") { var response = await _reverseGeoCoder.ReverseGeocode(new ReverseGeocodeRequest { Latitude = location.Lat, - Longitude = location.Lng + Longitude = location.Lng, + PreferredLanguages = language }); var result = response.Address?.Adapt
(); From e3fbaa4a998fe64ba7d120c0286da0c94e0580c9 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 12 Jan 2026 17:50:32 +0300 Subject: [PATCH 14/19] - bonded-box OSM|location search --- .../Botticelli.Locations.csproj | 2 +- .../Integration/ILocationProvider.cs | 3 +- .../Integration/OsmLocationProvider.cs | 77 +++++++++++++------ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/Botticelli.Locations/Botticelli.Locations.csproj b/Botticelli.Locations/Botticelli.Locations.csproj index dc7225e1..578c21fc 100644 --- a/Botticelli.Locations/Botticelli.Locations.csproj +++ b/Botticelli.Locations/Botticelli.Locations.csproj @@ -14,7 +14,7 @@ - + diff --git a/Botticelli.Locations/Integration/ILocationProvider.cs b/Botticelli.Locations/Integration/ILocationProvider.cs index 5a76fc0f..eb72413d 100644 --- a/Botticelli.Locations/Integration/ILocationProvider.cs +++ b/Botticelli.Locations/Integration/ILocationProvider.cs @@ -10,7 +10,8 @@ public interface ILocationProvider public Task GetMapLink(Address address); - public Task> Search(string query, int maxPoints, string language = ""); + public Task> Search(string query, int maxPoints, double? latitude = null, + double? longitude = null, int? radiusInMeters = null, string? language = null); public Task> SearchByIds(string[] ids); diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index 6cd0a3d2..d1c9678a 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -11,15 +11,15 @@ namespace Botticelli.Locations.Integration; public class OsmLocationProvider : ILocationProvider { + private readonly IAddressSearcher _addressSearcher; private readonly IForwardGeocoder _forwardGeocoder; private readonly IOptionsSnapshot _options; private readonly IReverseGeocoder _reverseGeoCoder; - private readonly IAddressSearcher _addressSearcher; public OsmLocationProvider(IReverseGeocoder reverseGeoCoder, - IForwardGeocoder forwardGeocoder, - IAddressSearcher addressSearcher, - IOptionsSnapshot options) + IForwardGeocoder forwardGeocoder, + IAddressSearcher addressSearcher, + IOptionsSnapshot options) { _reverseGeoCoder = reverseGeoCoder; _forwardGeocoder = forwardGeocoder; @@ -35,7 +35,7 @@ public OsmLocationProvider(IReverseGeocoder reverseGeoCoder, public async Task GetMapLink(Location location) { return $"{_options.Value.ApiUrl}/" + - $"#map={(int) _options.Value.InitialZoom}/" + + $"#map={(int)_options.Value.InitialZoom}/" + $"{location.Lat.ToString("0.00000", CultureInfo.InvariantCulture)}/" + $"{location.Lng.ToString("0.00000", CultureInfo.InvariantCulture)}"; } @@ -43,30 +43,49 @@ public async Task GetMapLink(Location location) public async Task GetMapLink(Address address) { return $"{_options.Value.ApiUrl}/" + - $"#map={(int) _options.Value.InitialZoom}/" + + $"#map={(int)_options.Value.InitialZoom}/" + $"{address.Latitude.ToString("0.00000", CultureInfo.InvariantCulture)}/" + $"{address.Longitude.ToString("0.00000", CultureInfo.InvariantCulture)}"; } - public async Task> Search(string query, int maxPoints, string language = "") + public async Task> Search(string query, int maxPoints, double? latitude = null, + double? longitude = null, int? radiusInMeters = null, string? language = null) { + if (radiusInMeters != null && (!latitude.HasValue || !longitude.HasValue)) + throw new ArgumentException("Please provide a valid latitude and longitude!"); + + var deltaLat = radiusInMeters == null ? 0 : DeltaLat(radiusInMeters.Value); + var deltaLong = radiusInMeters == null ? 0 : DeltaLong(radiusInMeters.Value); + var results = (await _forwardGeocoder.Geocode(new ForwardGeocodeRequest - { - queryString = query, - PreferredLanguages = language - })).Select(gr => - { - var address = gr.Address?.Adapt
() ?? new Address(); - address.ObjectId = gr.OSMID.ToString(); - address.ObjectType = gr.OSMType; - address.Longitude = gr.Longitude; - address.Latitude = gr.Latitude; - address.DisplayName = gr.DisplayName; - - return address; - }) - .Take(maxPoints) - .ToList(); + { + queryString = query, + PreferredLanguages = language ?? string.Empty, + LimitResults = maxPoints, + DedupeResults = true, + ViewBox = radiusInMeters == null + ? null + : new BoundingBox + { + minLatitude = latitude!.Value - deltaLat, + minLongitude = longitude!.Value - deltaLong, + maxLatitude = latitude.Value + deltaLat, + maxLongitude = longitude.Value + deltaLong + } + })) + .OrderByDescending(gr => gr.PlaceRank) + .Select(gr => + { + var address = gr.Address?.Adapt
() ?? new Address(); + address.ObjectId = gr.OSMID.ToString(); + address.ObjectType = gr.OSMType; + address.Longitude = gr.Longitude; + address.Latitude = gr.Latitude; + address.DisplayName = gr.DisplayName; + + return address; + }) + .ToList(); return results; } @@ -91,7 +110,7 @@ public async Task> SearchByIds(string[] ids) return results; } - + public Task GetTimeZone(Location location) { var tz = TimeZoneLookup.GetTimeZone(location.Lat, location.Lng).Result; @@ -113,4 +132,14 @@ public async Task> SearchByIds(string[] ids) return result; } + + private static double DeltaLat(double radius) + { + return radius / 111312.0d; + } + + private static double DeltaLong(double radius) + { + return radius / 72386.1936d; + } } \ No newline at end of file From 04d2466cfa28bfde6dd39017f962f2ff2f07157b Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 15 Jan 2026 00:21:15 +0300 Subject: [PATCH 15/19] - DI bugfixes for chained comman proc --- .../CommandChainFirstElementProcessor.cs | 15 ++++++--- .../CommandChainProcessorBuilder.cs | 22 +++++++++++-- .../Commands/Processors/CommandProcessor.cs | 2 +- ...tForClientResponseCommandChainProcessor.cs | 6 +++- .../Extensions/StartupExtensions.cs | 33 +++++++++++++++---- 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs index f0da2b77..ea0ac646 100644 --- a/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs @@ -23,11 +23,18 @@ public CommandChainFirstElementProcessor(ILogger)} : no next step, returning" : - $"{nameof(CommandChainProcessor)} : next step is '{Next?.GetType().Name}'"); + Logger.LogDebug(Next == null + ? $"{nameof(CommandChainProcessor)} : no next step, returning" + : $"{nameof(CommandChainProcessor)} : next step is '{Next?.GetType().Name}'"); - if (Next != null) await Next.ProcessAsync(message, token)!; + if (_bot != null) + { + if (Next != null) + { + Next.SetBot(_bot); + await Next.ProcessAsync(message, token)!; + } + } } protected override Task InnerProcess(Message message, CancellationToken token) diff --git a/Botticelli.Framework/Commands/Processors/CommandChainProcessorBuilder.cs b/Botticelli.Framework/Commands/Processors/CommandChainProcessorBuilder.cs index e523517e..0217b5d4 100644 --- a/Botticelli.Framework/Commands/Processors/CommandChainProcessorBuilder.cs +++ b/Botticelli.Framework/Commands/Processors/CommandChainProcessorBuilder.cs @@ -15,14 +15,29 @@ public CommandChainProcessorBuilder(IServiceCollection services) _typesChain.Add(typeof(CommandChainFirstElementProcessor)); _services.AddSingleton>(); + ProcessorFactoryBuilder.AddProcessor>(_services); } - public CommandChainProcessorBuilder AddNext() + public CommandChainProcessorBuilder AddNext(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) where TNextProcessor : class, ICommandChainProcessor { _typesChain.Add(typeof(TNextProcessor)); - _services.AddSingleton(); + switch (serviceLifetime) + { + case ServiceLifetime.Singleton: + _services.AddSingleton(); + break; + case ServiceLifetime.Scoped: + _services.AddScoped(); + break; + case ServiceLifetime.Transient: + _services.AddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null); + } + ProcessorFactoryBuilder.AddProcessor(_services); return this; @@ -31,6 +46,7 @@ public CommandChainProcessorBuilder AddNext() public ICommandChainProcessor? Build(IServiceProvider sp) { if (_typesChain.Count == 0) return null; + var scope = sp.CreateScope(); // initializing chain processors... @@ -41,7 +57,7 @@ public CommandChainProcessorBuilder AddNext() foreach (var type in _typesChain.Skip(1)) { - var proc = sp.GetRequiredService(type) as ICommandChainProcessor; + var proc = scope.ServiceProvider.GetRequiredService(type) as ICommandChainProcessor; if (prev != null) prev.Next = proc; diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index e3e44e2f..b3c48ce4 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -21,7 +21,7 @@ public abstract class CommandProcessor : ICommandProcessor private readonly IValidator _messageValidator; private readonly MetricsProcessor? _metricsProcessor; protected readonly ILogger Logger; - private IBot? _bot; + protected IBot? _bot; protected CommandProcessor(ILogger logger, ICommandValidator commandValidator, diff --git a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs index f5ada24c..3f51bb85 100644 --- a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs @@ -58,7 +58,11 @@ public override async Task ProcessAsync(Message message, CancellationToken token if (Next != null) { Next.ChainIds.Add(message.ChainId.Value); - await Next.ProcessAsync(message, token); + if (_bot != null) + { + Next.SetBot(_bot); + await Next.ProcessAsync(message, token); + } } else { diff --git a/Botticelli.Framework/Extensions/StartupExtensions.cs b/Botticelli.Framework/Extensions/StartupExtensions.cs index b5b3893e..7d4075c2 100644 --- a/Botticelli.Framework/Extensions/StartupExtensions.cs +++ b/Botticelli.Framework/Extensions/StartupExtensions.cs @@ -62,21 +62,40 @@ public static CommandAddServices AddBotCommand(this IService } public static CommandChainProcessorBuilder AddBotChainProcessedCommand(this IServiceCollection services) - where TCommand : class, ICommand where TCommandValidator : class, ICommandValidator + TCommandValidator>(this IServiceCollection services, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + where TCommand : class, ICommand where TCommandValidator : class, ICommandValidator { var builder = new CommandChainProcessorBuilder(services); - services.AddSingleton() - .AddSingleton, TCommandValidator>() - .AddSingleton(_ => builder); + switch (serviceLifetime) + { + case ServiceLifetime.Singleton: + services.AddSingleton() + .AddSingleton, TCommandValidator>() + .AddSingleton(_ => builder); + break; + case ServiceLifetime.Scoped: + services.AddScoped() + .AddScoped, TCommandValidator>() + .AddScoped(_ => builder); + break; + case ServiceLifetime.Transient: + services.AddTransient() + .AddTransient, TCommandValidator>() + .AddTransient(_ => builder); + break; + default: + throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null); + } + return builder; } public static IServiceProvider RegisterBotChainedCommand(this IServiceProvider sp) - where TCommand : class, ICommand - where TBot : IBot + where TCommand : class, ICommand + where TBot : IBot { var commandChainProcessorBuilder = sp.GetRequiredService>(); commandChainProcessorBuilder.Build(sp); From 55c55dd9b007bc504c45dc6f1483635cc15fd0ae Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 15 Jan 2026 12:16:14 +0300 Subject: [PATCH 16/19] - several languages for location provider --- Botticelli.Locations/Integration/ILocationProvider.cs | 4 ++-- Botticelli.Locations/Integration/OsmLocationProvider.cs | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Botticelli.Locations/Integration/ILocationProvider.cs b/Botticelli.Locations/Integration/ILocationProvider.cs index eb72413d..6d5afdf9 100644 --- a/Botticelli.Locations/Integration/ILocationProvider.cs +++ b/Botticelli.Locations/Integration/ILocationProvider.cs @@ -11,9 +11,9 @@ public interface ILocationProvider public Task GetMapLink(Address address); public Task> Search(string query, int maxPoints, double? latitude = null, - double? longitude = null, int? radiusInMeters = null, string? language = null); + double? longitude = null, int? radiusInMeters = null, string[]? languages = null); - public Task> SearchByIds(string[] ids); + public Task> SearchByIds(string[] ids, string[]? languages = null); public Task GetTimeZone(Location location); } \ No newline at end of file diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index d1c9678a..f16dfe35 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -49,7 +49,7 @@ public async Task GetMapLink(Address address) } public async Task> Search(string query, int maxPoints, double? latitude = null, - double? longitude = null, int? radiusInMeters = null, string? language = null) + double? longitude = null, int? radiusInMeters = null, string[]? languages = null) { if (radiusInMeters != null && (!latitude.HasValue || !longitude.HasValue)) throw new ArgumentException("Please provide a valid latitude and longitude!"); @@ -60,7 +60,7 @@ public async Task> Search(string query, int maxPoints, doub var results = (await _forwardGeocoder.Geocode(new ForwardGeocodeRequest { queryString = query, - PreferredLanguages = language ?? string.Empty, + PreferredLanguages = languages != null ? string.Join(',', languages) : null, LimitResults = maxPoints, DedupeResults = true, ViewBox = radiusInMeters == null @@ -90,11 +90,12 @@ public async Task> Search(string query, int maxPoints, doub return results; } - public async Task> SearchByIds(string[] ids) + public async Task> SearchByIds(string[] ids, string[]? languages = null) { var results = (await _addressSearcher.Lookup(new AddressSearchRequest { - OSMIDs = ids + OSMIDs = ids, + PreferredLanguages = languages != null ? string.Join(',', languages) : null, })).Select(gr => { var address = gr.Address?.Adapt
() ?? new Address(); From b3fb014b7d8fb1f092763edf6f9a9bb7cba7b457 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 15 Jan 2026 16:16:56 +0300 Subject: [PATCH 17/19] - Auth Type choice is available for GPT --- Botticelli.AI.YaGpt/Settings/YaGptSettings.cs | 1 - Botticelli.AI/AIProvider/ChatGptProvider.cs | 2 +- Botticelli.AI/Settings/AISettings.cs | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Botticelli.AI.YaGpt/Settings/YaGptSettings.cs b/Botticelli.AI.YaGpt/Settings/YaGptSettings.cs index 7db13f80..e1333545 100644 --- a/Botticelli.AI.YaGpt/Settings/YaGptSettings.cs +++ b/Botticelli.AI.YaGpt/Settings/YaGptSettings.cs @@ -4,7 +4,6 @@ namespace Botticelli.AI.YaGpt.Settings; public class YaGptSettings : AiSettings { - public string ApiKey { get; set; } public string Model { get; set; } public double Temperature { get; set; } public string Instruction { get; set; } diff --git a/Botticelli.AI/AIProvider/ChatGptProvider.cs b/Botticelli.AI/AIProvider/ChatGptProvider.cs index 34171eaf..0745a17d 100644 --- a/Botticelli.AI/AIProvider/ChatGptProvider.cs +++ b/Botticelli.AI/AIProvider/ChatGptProvider.cs @@ -95,7 +95,7 @@ private HttpClient GetClient() client.BaseAddress = new Uri(Settings.Value.Url); client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", Settings.Value.ApiKey); + new AuthenticationHeaderValue(Settings.Value.AuthMethod, Settings.Value.ApiKey); return client; } diff --git a/Botticelli.AI/Settings/AISettings.cs b/Botticelli.AI/Settings/AISettings.cs index 485139ed..63f5b3dc 100644 --- a/Botticelli.AI/Settings/AISettings.cs +++ b/Botticelli.AI/Settings/AISettings.cs @@ -3,7 +3,8 @@ public class AiSettings : ProviderSettings { public string? Url { get; set; } - public string AiName { get; set; } + public required string AiName { get; set; } public bool StreamGeneration { get; set; } - public string ApiKey { get; set; } + public string AuthMethod { get; set; } = "Bearer"; + public string? ApiKey { get; set; } } \ No newline at end of file From b05f95f411d2aaab4c04c09bb01311b312d939a6 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 16 Jan 2026 17:23:37 +0300 Subject: [PATCH 18/19] - exception message fix --- Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs b/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs index b9e0b851..0d1a3ec0 100644 --- a/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs +++ b/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs @@ -10,8 +10,8 @@ public class InlineButtonMenu : ILayout public InlineButtonMenu(int rows, int columns) { - if (rows < 1) throw new InvalidDataException("rows count should be > 1!"); - if (columns < 1) throw new InvalidDataException("columns count should be > 1!"); + if (rows < 1) throw new InvalidDataException("rows count should be > 0!"); + if (columns < 1) throw new InvalidDataException("columns count should be > 0!"); _rows = rows; _columns = columns; From 3954bdac62236a8ae58a1e28ef657be35130f8ea Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 26 Jan 2026 00:49:11 +0300 Subject: [PATCH 19/19] - osm location: cancellation token --- Botticelli.Locations/Integration/OsmLocationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Locations/Integration/OsmLocationProvider.cs b/Botticelli.Locations/Integration/OsmLocationProvider.cs index f16dfe35..98497449 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -95,7 +95,7 @@ public async Task> SearchByIds(string[] ids, string[]? lang var results = (await _addressSearcher.Lookup(new AddressSearchRequest { OSMIDs = ids, - PreferredLanguages = languages != null ? string.Join(',', languages) : null, + PreferredLanguages = languages != null ? string.Join(',', languages) : null })).Select(gr => { var address = gr.Address?.Adapt
() ?? new Address();