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.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/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.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 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 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.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; 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.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/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 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)} : 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/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) 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/CommandAddServices.cs b/Botticelli.Framework/Extensions/CommandAddServices.cs index 3f8e33f9..d532ec33 100644 --- a/Botticelli.Framework/Extensions/CommandAddServices.cs +++ b/Botticelli.Framework/Extensions/CommandAddServices.cs @@ -10,43 +10,72 @@ 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; } - 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; }) 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); 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 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 e29f26fb..6d5afdf9 100644 --- a/Botticelli.Locations/Integration/ILocationProvider.cs +++ b/Botticelli.Locations/Integration/ILocationProvider.cs @@ -10,8 +10,10 @@ 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[]? languages = null); - public Task> Search(string query, int maxPoints); + 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 3491b7e3..98497449 100644 --- a/Botticelli.Locations/Integration/OsmLocationProvider.cs +++ b/Botticelli.Locations/Integration/OsmLocationProvider.cs @@ -11,17 +11,20 @@ namespace Botticelli.Locations.Integration; public class OsmLocationProvider : ILocationProvider { + private readonly IAddressSearcher _addressSearcher; private readonly IForwardGeocoder _forwardGeocoder; private readonly IOptionsSnapshot _options; private readonly IReverseGeocoder _reverseGeoCoder; public OsmLocationProvider(IReverseGeocoder reverseGeoCoder, - IForwardGeocoder forwardGeocoder, - IOptionsSnapshot options) + IForwardGeocoder forwardGeocoder, + IAddressSearcher addressSearcher, + IOptionsSnapshot options) { _reverseGeoCoder = reverseGeoCoder; _forwardGeocoder = forwardGeocoder; _options = options; + _addressSearcher = addressSearcher; } public async Task GetAddress(Location location) @@ -32,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)}"; } @@ -40,26 +43,71 @@ 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) + public async Task> Search(string query, int maxPoints, double? latitude = 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!"); + + 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 - })).Select(gr => - { - var address = gr.Address?.Adapt
() ?? new Address(); - address.Longitude = gr.Longitude; - address.Latitude = gr.Latitude; - address.DisplayName = gr.DisplayName; - - return address; - }) - .ToList(); + { + queryString = query, + PreferredLanguages = languages != null ? string.Join(',', languages) : null, + 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; + } + + public async Task> SearchByIds(string[] ids, string[]? languages = null) + { + var results = (await _addressSearcher.Lookup(new AddressSearchRequest + { + OSMIDs = ids, + PreferredLanguages = languages != null ? string.Join(',', languages) : null + })).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; } @@ -72,16 +120,27 @@ public async Task> Search(string query, int maxPoints) 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
(); 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 diff --git a/Botticelli.Locations/Models/Address.cs b/Botticelli.Locations/Models/Address.cs index a989d9fd..3ac3f0db 100644 --- a/Botticelli.Locations/Models/Address.cs +++ b/Botticelli.Locations/Models/Address.cs @@ -5,6 +5,11 @@ namespace Botticelli.Locations.Models; public class Address { + [JsonPropertyName("ObjectId")] + public string ObjectId { get; set; } + [JsonPropertyName("ObjectType")] + public string ObjectType { get; set; } + [JsonPropertyName("country")] public string? Country { get; set; } diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index e6cc7941..631bfc89 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 @@ -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