From f584951812ae9970a8579f4866ae02aa62e5f8c7 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 11 Apr 2025 22:45:36 +0300 Subject: [PATCH 001/101] - refactoring --- Botticelli.Framework.Telegram/TelegramBot.cs | 6 +- Botticelli.Framework.Vk/VkBot.cs | 6 +- Botticelli.Framework/BaseBot.cs | 89 +++++++------------- 3 files changed, 38 insertions(+), 63 deletions(-) diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index d71d9c87..a4f3773e 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -50,9 +50,9 @@ public TelegramBot(ITelegramBotClient client, } public override BotType Type => BotType.Telegram; - public override event MsgSentEventHandler? MessageSent; - public override event MsgReceivedEventHandler? MessageReceived; - public override event MsgRemovedEventHandler? MessageRemoved; + public virtual event MsgSentEventHandler? MessageSent; + public virtual event MsgReceivedEventHandler? MessageReceived; + public virtual event MsgRemovedEventHandler? MessageRemoved; /// /// Deletes a message diff --git a/Botticelli.Framework.Vk/VkBot.cs b/Botticelli.Framework.Vk/VkBot.cs index 380e746d..6ef4fb49 100644 --- a/Botticelli.Framework.Vk/VkBot.cs +++ b/Botticelli.Framework.Vk/VkBot.cs @@ -327,8 +327,8 @@ public override Task DeleteMessageAsync(RemoveMessageRequ throw new NotImplementedException(); } - public override event MsgSentEventHandler MessageSent; - public override event MsgReceivedEventHandler MessageReceived; - public override event MsgRemovedEventHandler MessageRemoved; + public virtual event MsgSentEventHandler MessageSent; + public virtual event MsgReceivedEventHandler MessageReceived; + public virtual event MsgRemovedEventHandler MessageRemoved; public virtual event MessengerSpecificEventHandler MessengerSpecificEvent; } \ No newline at end of file diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index 439e3280..61f33991 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -28,10 +28,6 @@ public abstract class BaseBot public delegate void StartedEventHandler(object sender, StartedBotEventArgs e); public delegate void StoppedEventHandler(object sender, StoppedBotEventArgs e); - - public virtual event MsgSentEventHandler? MessageSent; - public virtual event MsgReceivedEventHandler? MessageReceived; - public virtual event MsgRemovedEventHandler? MessageRemoved; } /// @@ -39,22 +35,24 @@ public abstract class BaseBot /// /// public abstract class BaseBot : BaseBot, IBot - where T : BaseBot, IBot + where T : BaseBot, IBot { public delegate void MessengerSpecificEventHandler(object sender, MessengerSpecificBotEventArgs e); private readonly MetricsProcessor _metrics; protected readonly ILogger Logger; - protected BaseBot(ILogger logger, MetricsProcessor metrics) + protected BaseBot(ILogger logger, MetricsProcessor metrics, string botUserId) { Logger = logger; _metrics = metrics; + BotUserId = botUserId; } public virtual async Task StartBotAsync(StartBotRequest request, CancellationToken token) { - if (BotStatusKeeper.IsStarted) return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); + if (BotStatusKeeper.IsStarted) + return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); _metrics.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); @@ -69,7 +67,8 @@ public virtual async Task StopBotAsync(StopBotRequest request, { _metrics.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); - if (!BotStatusKeeper.IsStarted) return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); + if (!BotStatusKeeper.IsStarted) + return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); var result = await InnerStopBotAsync(request, token); @@ -78,7 +77,7 @@ public virtual async Task StopBotAsync(StopBotRequest request, return result; } - public abstract Task SetBotContext(BotData.Entities.Bot.BotData? botData, CancellationToken token); + public abstract Task SetBotContext(BotData.Entities.Bot.BotData? context, CancellationToken token); /// /// Sends a message @@ -86,10 +85,8 @@ public virtual async Task StopBotAsync(StopBotRequest request, /// Request /// /// - public Task SendMessageAsync(SendMessageRequest request, CancellationToken token) - { - return SendMessageAsync(request, null, token); - } + public Task SendMessageAsync(SendMessageRequest request, CancellationToken token) => + SendMessageAsync(request, null, token); /// @@ -100,38 +97,36 @@ public Task SendMessageAsync(SendMessageRequest request, Ca /// /// public virtual async Task SendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - CancellationToken token) - where TSendOptions : class + ISendOptionsBuilder? optionsBuilder, + CancellationToken token) + where TSendOptions : class { _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, - optionsBuilder, - false, - token); + optionsBuilder, + false, + token); } - public Task UpdateMessageAsync(SendMessageRequest request, CancellationToken token) - { - return UpdateMessageAsync(request, null, token); - } + public Task UpdateMessageAsync(SendMessageRequest request, CancellationToken token) => + UpdateMessageAsync(request, null, token); public async Task UpdateMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - CancellationToken token) - where TSendOptions : class + ISendOptionsBuilder? optionsBuilder, + CancellationToken token) + where TSendOptions : class { _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, - optionsBuilder, - true, - token); + optionsBuilder, + true, + token); } public virtual async Task DeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token) + CancellationToken token) { _metrics.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); @@ -141,39 +136,19 @@ public virtual async Task DeleteMessageAsync(RemoveMessag public abstract BotType Type { get; } public string BotUserId { get; set; } - public Task PingAsync(PingRequest request) - { - return Task.FromResult(PingResponse.GetInstance(request.Uid)); - } - protected abstract Task InnerStartBotAsync(StartBotRequest request, CancellationToken token); protected abstract Task InnerStopBotAsync(StopBotRequest request, CancellationToken token); protected abstract Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) - where TSendOptions : class; + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) + where TSendOptions : class; protected abstract Task InnerDeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token); + CancellationToken token); - /// - /// Additional message processing while sending a message - /// - /// - /// - /// - /// - /// - /// - protected abstract Task AdditionalProcessing(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - string chatId, - CancellationToken token); - - public event StartedEventHandler Started; - public event StoppedEventHandler Stopped; + public event StartedEventHandler? Started; + public event StoppedEventHandler? Stopped; } \ No newline at end of file From 81538e7d13bd4e113b718a59a0d81017a87ddec1 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 11 Apr 2025 23:09:50 +0300 Subject: [PATCH 002/101] - refactoring --- .../MessageRemovedBotEventArgs.cs | 2 +- Botticelli.Framework.Telegram/TelegramBot.cs | 277 +++++++++--------- Botticelli.Framework.Vk/VkBot.cs | 130 ++++---- Botticelli.Framework/BaseBot.cs | 5 +- .../Global/BotStatusKeeper.cs | 2 +- 5 files changed, 193 insertions(+), 223 deletions(-) diff --git a/Botticelli.Framework.Common/MessageRemovedBotEventArgs.cs b/Botticelli.Framework.Common/MessageRemovedBotEventArgs.cs index a17b3852..2f1dd698 100644 --- a/Botticelli.Framework.Common/MessageRemovedBotEventArgs.cs +++ b/Botticelli.Framework.Common/MessageRemovedBotEventArgs.cs @@ -2,5 +2,5 @@ public class MessageRemovedBotEventArgs : BotEventArgs { - public required string MessageUid { get; set; } + public required string? MessageUid { get; set; } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index a4f3773e..2b4cd467 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -34,14 +34,13 @@ public class TelegramBot : BaseBot private readonly ITextTransformer _textTransformer; protected readonly ITelegramBotClient Client; - public TelegramBot(ITelegramBotClient client, - IBotUpdateHandler handler, - ILogger logger, - MetricsProcessor metrics, - ITextTransformer textTransformer, - IBotDataAccess data) : base(logger, metrics) + protected TelegramBot(ITelegramBotClient client, + IBotUpdateHandler handler, + ILogger logger, + MetricsProcessor metrics, + ITextTransformer textTransformer, + IBotDataAccess data) : base(logger, metrics) { - BotStatusKeeper.IsStarted = false; Client = client; _handler = handler; _textTransformer = textTransformer; @@ -50,9 +49,9 @@ public TelegramBot(ITelegramBotClient client, } public override BotType Type => BotType.Telegram; - public virtual event MsgSentEventHandler? MessageSent; - public virtual event MsgReceivedEventHandler? MessageReceived; - public virtual event MsgRemovedEventHandler? MessageRemoved; + public event MsgSentEventHandler? MessageSent; + public event MsgReceivedEventHandler? MessageReceived; + public event MsgRemovedEventHandler? MessageRemoved; /// /// Deletes a message @@ -62,7 +61,7 @@ public TelegramBot(ITelegramBotClient client, /// /// protected override async Task InnerDeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token) + CancellationToken token) { request.NotNull(); request.Uid.NotNull(); @@ -84,9 +83,10 @@ protected override async Task InnerDeleteMessageAsync(Rem { if (string.IsNullOrWhiteSpace(request.Uid)) throw new BotException("request/message is null!"); - await Client.DeleteMessage(request.ChatId, - int.Parse(request.Uid), - token); + if (request.ChatId != null) + await Client.DeleteMessage(request.ChatId, + int.Parse(request.Uid), + token); response.MessageRemovedStatus = MessageRemovedStatus.Ok; } catch @@ -106,15 +106,6 @@ await Client.DeleteMessage(request.ChatId, return response; } - protected override Task AdditionalProcessing(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - string chatId, - CancellationToken token) - { - return Task.CompletedTask; - } - /// /// Sends a message as a telegram bot /// @@ -126,9 +117,9 @@ protected override Task AdditionalProcessing(SendMessageRequest re /// /// protected override async Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) { request.NotNull(); request.Message.NotNull(); @@ -149,10 +140,13 @@ protected override async Task InnerSendMessageAsync InnerSendMessageAsync pairs = []; - foreach (var link in request.Message.ChatIdInnerIdLinks) pairs.AddRange(link.Value.Select(innerId => (link.Key, innerId))); + foreach (var link in request.Message.ChatIdInnerIdLinks) + pairs.AddRange(link.Value.Select(innerId => (link.Key, innerId))); var chatIdOnly = request.Message.ChatIds.Where(c => !request.Message.ChatIdInnerIdLinks.ContainsKey(c)); pairs.AddRange(chatIdOnly.Select(c => (c, string.Empty))); - Logger.LogInformation($"Pairs count: {pairs.Count}"); + Logger.LogInformation("Pairs count: {Count}", pairs.Count); for (var i = 0; i < pairs.Count; i++) { @@ -175,39 +170,33 @@ protected override async Task InnerSendMessageAsync(request, - isUpdate, - token, - retText, - replyMarkup, - link); + isUpdate, + token, + retText, + replyMarkup, + link); if (request.Message.Poll != null) message = await ProcessPoll(request, - token, - link, - replyMarkup, - response); + token, + link, + replyMarkup, + response); if (request.Message.Contact != null) await ProcessContact(request, - response, - token, - replyMarkup); - - await AdditionalProcessing(request, - optionsBuilder, - isUpdate, - link.chatId, - token); + response, + token, + replyMarkup); if (request.Message.Attachments == null) continue; message = await ProcessAttachments(request, - token, - link, - replyMarkup, - response, - message); + token, + link, + replyMarkup, + response, + message); message.NotNull(); AddChatIdInnerIdLink(response, link.chatId, message); @@ -233,11 +222,11 @@ await AdditionalProcessing(request, } protected virtual async Task ProcessText(SendMessageRequest request, - bool isUpdate, - CancellationToken token, - string retText, - ReplyMarkup? replyMarkup, - (string chatId, string innerId) link) + bool isUpdate, + CancellationToken token, + string retText, + ReplyMarkup? replyMarkup, + (string chatId, string innerId) link) { if (!string.IsNullOrWhiteSpace(retText)) { @@ -257,112 +246,114 @@ async Task SendText(string sendText) if (!isUpdate) { var sentMessage = await Client.SendMessage(link.chatId, - sendText, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + sendText, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); link.innerId = sentMessage.MessageId.ToString(); } else { await Client.EditMessageText(link.chatId, - int.Parse(link.innerId), - sendText, - ParseMode.MarkdownV2, - replyMarkup: replyMarkup as InlineKeyboardMarkup, - cancellationToken: token); + int.Parse(link.innerId), + sendText, + ParseMode.MarkdownV2, + replyMarkup: replyMarkup as InlineKeyboardMarkup, + cancellationToken: token); } } } } protected virtual async Task ProcessAttachments(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response, - Message? message) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response, + Message? message) { request.Message.NotNull(); request.Message.Attachments.NotNullOrEmpty(); foreach (var attachment in request.Message - .Attachments - .Where(a => a is BinaryBaseAttachment) - .Cast()) + .Attachments + .Where(a => a is BinaryBaseAttachment) + .Cast()) switch (attachment.MediaType) { case MediaType.Audio: var audio = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendAudio(link.chatId, - audio, - request.Message.Subject, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + audio, + request.Message.Subject, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Video: var video = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendVideo(link.chatId, - video, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + video, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Image: var image = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendPhoto(link.chatId, - image, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + image, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Voice: var voice = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendVoice(link.chatId, - voice, - request.Message.Subject, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + voice, + request.Message.Subject, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Sticker: - InputFile sticker = string.IsNullOrWhiteSpace(attachment.Url) ? new InputFileStream(attachment.Data.ToStream(), attachment.Name) : new InputFileUrl(attachment.Url); + InputFile sticker = string.IsNullOrWhiteSpace(attachment.Url) + ? new InputFileStream(attachment.Data.ToStream(), attachment.Name) + : new InputFileUrl(attachment.Url); message = await Client.SendSticker(link.chatId, - sticker, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + sticker, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Contact: await ProcessContact(request, - response, - token, - replyMarkup); + response, + token, + replyMarkup); break; case MediaType.Document: var doc = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendDocument(link.chatId, - doc, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + doc, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; @@ -378,10 +369,10 @@ await ProcessContact(request, } protected virtual async Task ProcessPoll(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response) { Message? message; @@ -391,20 +382,20 @@ await ProcessContact(request, var type = request.Message.Poll?.Type switch { - Poll.PollType.Quiz => PollType.Quiz, + Poll.PollType.Quiz => PollType.Quiz, Poll.PollType.Regular => PollType.Regular, - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException() }; message = await Client.SendPoll(link.chatId, - request.Message.Poll?.Question ?? "No question", - GetPollOptions(request), - request.Message.Poll?.IsAnonymous ?? false, - type, - correctOptionId: request.Message.Poll?.CorrectAnswerId, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + request.Message.Poll?.Question ?? "No question", + GetPollOptions(request), + request.Message.Poll?.IsAnonymous ?? false, + type, + correctOptionId: request.Message.Poll?.CorrectAnswerId, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); @@ -426,12 +417,12 @@ await ProcessContact(request, private static InputPollOption[] GetPollOptions(SendMessageRequest request) { return request.Message.Poll?.Variants?.Select(po => - new InputPollOption - { - Text = po.option, - TextParseMode = ParseMode.MarkdownV2 - }) - .ToArray() ?? + new InputPollOption + { + Text = po.option, + TextParseMode = ParseMode.MarkdownV2 + }) + .ToArray() ?? []; } @@ -444,9 +435,9 @@ private static void AddChatIdInnerIdLink(SendMessageResponse response, string ch } protected virtual async Task ProcessContact(SendMessageRequest request, - SendMessageResponse response, - CancellationToken token, - ReplyMarkup? replyMarkup) + SendMessageResponse response, + CancellationToken token, + ReplyMarkup? replyMarkup) { request.Message.NotNull(); request.Message.Contact.NotNull(); @@ -457,12 +448,12 @@ protected virtual async Task ProcessContact(SendMessageRequest request, try { var message = await Client.SendContact(chatId, - request.Message?.Contact?.Phone!, - request.Message?.Contact?.Name!, - request.Message?.Contact?.Surname, - replyParameters: GetReplyParameters(request, chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + request.Message?.Contact?.Phone!, + request.Message?.Contact?.Name!, + request.Message?.Contact?.Surname, + replyParameters: GetReplyParameters(request, chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, chatId, message); } @@ -472,14 +463,12 @@ protected virtual async Task ProcessContact(SendMessageRequest request, } } - private static ReplyParameters GetReplyParameters(SendMessageRequest request, string chatId) - { - return new ReplyParameters + private static ReplyParameters GetReplyParameters(SendMessageRequest request, string chatId) => + new() { ChatId = chatId, MessageId = request.Message.ReplyToMessageUid != null ? int.Parse(request.Message.ReplyToMessageUid) : 0 }; - } /// /// Starts a bot @@ -505,7 +494,7 @@ protected override Task InnerStartBotAsync(StartBotRequest req // Rethrowing an event from BotUpdateHandler _handler.MessageReceived += (sender, e) - => MessageReceived?.Invoke(sender, e); + => MessageReceived?.Invoke(sender, e); Client.StartReceiving(_handler, cancellationToken: token); @@ -554,7 +543,7 @@ protected override async Task InnerStopBotAsync(StopBotRequest private void RecreateClient(string token) { - ((TelegramClientDecorator) Client).ChangeBotToken(token); + ((TelegramClientDecorator)Client).ChangeBotToken(token); } private async Task StartBot(CancellationToken token) diff --git a/Botticelli.Framework.Vk/VkBot.cs b/Botticelli.Framework.Vk/VkBot.cs index 6ef4fb49..f78bfce4 100644 --- a/Botticelli.Framework.Vk/VkBot.cs +++ b/Botticelli.Framework.Vk/VkBot.cs @@ -29,12 +29,12 @@ public class VkBot : BaseBot private bool _eventsAttached; public VkBot(LongPollMessagesProvider messagesProvider, - MessagePublisher? messagePublisher, - VkStorageUploader? vkUploader, - IBotDataAccess data, - IBotUpdateHandler handler, - MetricsProcessor metrics, - ILogger logger) : base(logger, metrics) + MessagePublisher? messagePublisher, + VkStorageUploader? vkUploader, + IBotDataAccess data, + IBotUpdateHandler handler, + MetricsProcessor metrics, + ILogger logger) : base(logger, metrics) { _messagesProvider = messagesProvider; _messagePublisher = messagePublisher; @@ -141,48 +141,40 @@ private void SetApiKey(BotData.Entities.Bot.BotData? context) _vkUploader.SetApiKey(context.BotKey); } - private string CreateVkAttach(VkSendPhotoResponse fk, string type) - { - return $"{type}" + - $"{fk.Response?.FirstOrDefault()?.OwnerId.ToString()}" + - $"_{fk.Response?.FirstOrDefault()?.Id.ToString()}"; - } + private string CreateVkAttach(VkSendPhotoResponse fk, string type) => + $"{type}" + + $"{fk.Response?.FirstOrDefault()?.OwnerId.ToString()}" + + $"_{fk.Response?.FirstOrDefault()?.Id.ToString()}"; - private string CreateVkAttach(VkSendVideoResponse fk, string type) - { - return $"{type}" + - $"{fk.Response?.OwnerId.ToString()}" + - $"_{fk.Response?.VideoId.ToString()}"; - } + private string CreateVkAttach(VkSendVideoResponse fk, string type) => + $"{type}" + + $"{fk.Response?.OwnerId.ToString()}" + + $"_{fk.Response?.VideoId.ToString()}"; - private string CreateVkAttach(VkSendAudioResponse fk, string type) - { - return $"{type}" + - $"{fk.AudioResponseData.AudioMessage.OwnerId}" + - $"_{fk.AudioResponseData.AudioMessage.Id}"; - } + private string CreateVkAttach(VkSendAudioResponse fk, string type) => + $"{type}" + + $"{fk.AudioResponseData.AudioMessage.OwnerId}" + + $"_{fk.AudioResponseData.AudioMessage.Id}"; - private string CreateVkAttach(VkSendDocumentResponse fk, string type) - { - return $"{type}" + - $"{fk.DocumentResponseData.Document.OwnerId}" + - $"_{fk.DocumentResponseData.Document.Id}"; - } + private string CreateVkAttach(VkSendDocumentResponse fk, string type) => + $"{type}" + + $"{fk.DocumentResponseData.Document.OwnerId}" + + $"_{fk.DocumentResponseData.Document.Id}"; protected override async Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) { foreach (var peerId in request.Message.ChatIds) try { var requests = await CreateRequestsWithAttachments(request, - peerId, - token); + peerId, + token); foreach (var vkRequest in requests) await _messagePublisher.SendAsync(vkRequest, token); } @@ -192,32 +184,21 @@ protected override async Task InnerSendMessageAsync InnerDeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token) - { + CancellationToken token) => throw new NotImplementedException(); - } - - protected override async Task AdditionalProcessing(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - string chatId, - CancellationToken token) - { - Logger.LogError($"{nameof(AdditionalProcessing)} not implemented!"); - } private async Task> CreateRequestsWithAttachments(SendMessageRequest request, - string peerId, - CancellationToken token) + string peerId, + CancellationToken token) { var currentContext = _data.GetData(); var result = new List(100); @@ -264,16 +245,17 @@ private async Task> CreateRequestsWithAttachme { switch (ba) { - case {MediaType: MediaType.Image}: - case {MediaType: MediaType.Sticker}: + case { MediaType: MediaType.Image }: + case { MediaType: MediaType.Sticker }: var sendPhotoResponse = await _vkUploader.SendPhotoAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendPhotoResponse != null) vkRequest.Attachment = CreateVkAttach(sendPhotoResponse, "photo"); + ba.Name, + ba.Data, + token); + if (sendPhotoResponse != null) + vkRequest.Attachment = CreateVkAttach(sendPhotoResponse, "photo"); break; - case {MediaType: MediaType.Video}: + case { MediaType: MediaType.Video }: //var sendVideoResponse = await _vkUploader.SendVideoAsync(vkRequest, // ba.Name, // ba.Data, @@ -282,22 +264,24 @@ private async Task> CreateRequestsWithAttachme //if (sendVideoResponse != default) vkRequest.Attachment = CreateVkAttach(sendVideoResponse, currentContext, "video"); break; - case {MediaType: MediaType.Voice}: - case {MediaType: MediaType.Audio}: + case { MediaType: MediaType.Voice }: + case { MediaType: MediaType.Audio }: var sendAudioMessageResponse = await _vkUploader.SendAudioMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendAudioMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendAudioMessageResponse, "doc"); + ba.Name, + ba.Data, + token); + if (sendAudioMessageResponse != null) + vkRequest.Attachment = CreateVkAttach(sendAudioMessageResponse, "doc"); break; - case {MediaType: MediaType.Document}: + case { MediaType: MediaType.Document }: var sendDocMessageResponse = await _vkUploader.SendDocsMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendDocMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendDocMessageResponse, "doc"); + ba.Name, + ba.Data, + token); + if (sendDocMessageResponse != null) + vkRequest.Attachment = CreateVkAttach(sendDocMessageResponse, "doc"); break; @@ -322,10 +306,8 @@ private async Task> CreateRequestsWithAttachme } public override Task DeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token) - { + CancellationToken token) => throw new NotImplementedException(); - } public virtual event MsgSentEventHandler MessageSent; public virtual event MsgReceivedEventHandler MessageReceived; diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index 61f33991..c5fdc2c9 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -42,11 +42,10 @@ public abstract class BaseBot : BaseBot, IBot private readonly MetricsProcessor _metrics; protected readonly ILogger Logger; - protected BaseBot(ILogger logger, MetricsProcessor metrics, string botUserId) + protected BaseBot(ILogger logger, MetricsProcessor metrics) { Logger = logger; _metrics = metrics; - BotUserId = botUserId; } public virtual async Task StartBotAsync(StartBotRequest request, CancellationToken token) @@ -134,7 +133,7 @@ public virtual async Task DeleteMessageAsync(RemoveMessag } public abstract BotType Type { get; } - public string BotUserId { get; set; } + public string? BotUserId { get; set; } protected abstract Task InnerStartBotAsync(StartBotRequest request, CancellationToken token); diff --git a/Botticelli.Framework/Global/BotStatusKeeper.cs b/Botticelli.Framework/Global/BotStatusKeeper.cs index 085d239a..ec5bb150 100644 --- a/Botticelli.Framework/Global/BotStatusKeeper.cs +++ b/Botticelli.Framework/Global/BotStatusKeeper.cs @@ -2,5 +2,5 @@ public static class BotStatusKeeper { - public static bool IsStarted = false; + public static bool IsStarted { get; set; } } \ No newline at end of file From 1edd2baafdd0c2bb3395b07ad209e67f35169bfa Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 11 Apr 2025 23:22:49 +0300 Subject: [PATCH 003/101] - refactoring --- .../Builders/TelegramBotBuilder.cs | 1 - .../Handlers/BotUpdateHandler.cs | 68 +++++++++---------- Botticelli.Framework.Telegram/TelegramBot.cs | 11 ++- .../Services/BotActualizationService.cs | 2 +- .../Services/BotStatusService.cs | 2 +- .../Services/MarkAsReceivedService.cs | 33 --------- 6 files changed, 37 insertions(+), 80 deletions(-) delete mode 100644 Botticelli.Framework/Services/MarkAsReceivedService.cs diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index e25fca67..24d8ae72 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -88,7 +88,6 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder Services!.AddHttpClient>() .AddServerCertificates(BotSettings); Services!.AddHostedService>>(); - Services!.AddHostedService>>(); Services!.AddHostedService(); var botId = BotDataUtils.GetBotId(); diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index a15f7329..c6a26c21 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -12,12 +12,10 @@ namespace Botticelli.Framework.Telegram.Handlers; -public class BotUpdateHandler : IBotUpdateHandler +public class BotUpdateHandler(ILogger logger) : IBotUpdateHandler { private readonly MemoryCacheEntryOptions _entryOptions - = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); - - private readonly ILogger _logger; + = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); private readonly MemoryCache _memoryCache = new(new MemoryCacheOptions { @@ -26,14 +24,9 @@ private readonly MemoryCacheEntryOptions _entryOptions private readonly List _subHandlers = []; - public BotUpdateHandler(ILogger logger) - { - _logger = logger; - } - public async Task HandleUpdateAsync(ITelegramBotClient botClient, - Update update, - CancellationToken cancellationToken) + Update update, + CancellationToken cancellationToken) { try { @@ -42,7 +35,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, _memoryCache.Set(update.Id, update, _entryOptions); - _logger.LogDebug($"{nameof(HandleUpdateAsync)}() started..."); + logger.LogDebug($"{nameof(HandleUpdateAsync)}() started..."); var botMessage = update.Message; @@ -56,7 +49,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, if (botMessage == null) { - _logger.LogError($"{nameof(HandleUpdateAsync)}() {nameof(botMessage)} is null!"); + logger.LogError($"{nameof(HandleUpdateAsync)}() {nameof(botMessage)} is null!"); return; } @@ -97,7 +90,8 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, IsAnonymous = update.Poll.IsAnonymous, Question = update.Poll.Question, Type = update.Poll.Type.ToLower() == "regular" ? Poll.PollType.Regular : Poll.PollType.Quiz, - Variants = update.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), + Variants = update.Poll.Options.Select( + o => new ValueTuple(o.Text, o.VoterCount)), CorrectAnswerId = update.Poll.CorrectOptionId } }; @@ -107,7 +101,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, botticelliMessage = new Message(botMessage.MessageId.ToString()) { ChatIdInnerIdLinks = new Dictionary> - {{botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()]}}, + { { botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()] } }, ChatIds = [botMessage.Chat.Id.ToString()], Subject = string.Empty, Body = botMessage.Text ?? string.Empty, @@ -132,13 +126,13 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, IsBot = botMessage.ForwardFrom?.IsBot, NickName = botMessage.ForwardFrom?.Username }, - Location = botMessage.Location != null ? - new GeoLocation - { - Latitude = (decimal) botMessage.Location.Latitude, - Longitude = (decimal) botMessage.Location.Longitude - } : - null + Location = botMessage.Location != null + ? new GeoLocation + { + Latitude = (decimal)botMessage.Location.Latitude, + Longitude = (decimal)botMessage.Location.Longitude + } + : null }; } @@ -149,26 +143,26 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, await Process(botticelliMessage, cancellationToken); MessageReceived?.Invoke(this, - new MessageReceivedBotEventArgs - { - Message = botticelliMessage - }); + new MessageReceivedBotEventArgs + { + Message = botticelliMessage + }); } - _logger.LogDebug($"{nameof(HandleUpdateAsync)}() finished..."); + logger.LogDebug($"{nameof(HandleUpdateAsync)}() finished..."); } catch (Exception ex) { - _logger.LogError(ex, $"{nameof(HandleUpdateAsync)}() error"); + logger.LogError(ex, $"{nameof(HandleUpdateAsync)}() error"); } } public Task HandleErrorAsync(ITelegramBotClient botClient, - Exception exception, - HandleErrorSource source, - CancellationToken cancellationToken) + Exception exception, + HandleErrorSource source, + CancellationToken cancellationToken) { - _logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); + logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); return Task.CompletedTask; } @@ -187,22 +181,22 @@ public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler /// protected async Task Process(Message request, CancellationToken token) { - _logger.LogDebug($"{nameof(Process)}({request.Uid}) started..."); + logger.LogDebug($"{nameof(Process)}({request.Uid}) started..."); - if (token is {CanBeCanceled: true, IsCancellationRequested: true}) return; + if (token is { CanBeCanceled: true, IsCancellationRequested: true }) return; var processorFactory = ProcessorFactoryBuilder.Build(); var clientNonChainedTasks = processorFactory.GetProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientChainedTasks = processorFactory.GetCommandChainProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientTasks = clientNonChainedTasks.Concat(clientChainedTasks).ToArray(); await Parallel.ForEachAsync(clientTasks, token, async (t, ct) => await t.WaitAsync(ct)); - _logger.LogDebug($"{nameof(Process)}({request.Uid}) finished..."); + logger.LogDebug($"{nameof(Process)}({request.Uid}) finished..."); } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 2b4cd467..22256fbd 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -573,14 +573,11 @@ public override async Task SetBotContext(BotData.Entities.Bot.BotData? context, await StartBot(token); } - else if (currentContext != null) + else if (currentContext != null && Client.BotId == 0) { - if (Client.BotId == 0) - { - await StopBot(token); - RecreateClient(context.BotKey!); - await StartBot(token); - } + await StopBot(token); + RecreateClient(context.BotKey!); + await StartBot(token); } } } \ No newline at end of file diff --git a/Botticelli.Framework/Services/BotActualizationService.cs b/Botticelli.Framework/Services/BotActualizationService.cs index be8cd472..d23bf96b 100644 --- a/Botticelli.Framework/Services/BotActualizationService.cs +++ b/Botticelli.Framework/Services/BotActualizationService.cs @@ -66,7 +66,7 @@ public virtual Task StopAsync(CancellationToken cancellationToken) var content = JsonContent.Create(request); - Logger.LogDebug("InnerSend request: {request}", request); + Logger.LogDebug("InnerSend request: {Request}", request); var response = await httpClient.PostAsync(Url.Combine(ServerSettings.ServerUri, funcName), content, diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 08635e9a..41c131b3 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -88,7 +88,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) if (task.Exception != null) { - Logger.LogError($"GetRequiredStatus task error: {task.Exception?.Message}"); + Logger.LogError("GetRequiredStatus task error: {Message}", task.Exception?.Message); Bot.StopBotAsync(StopBotRequest.GetInstance(), cancellationToken); return task; diff --git a/Botticelli.Framework/Services/MarkAsReceivedService.cs b/Botticelli.Framework/Services/MarkAsReceivedService.cs deleted file mode 100644 index 46e8b0fb..00000000 --- a/Botticelli.Framework/Services/MarkAsReceivedService.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Botticelli.Framework.Options; -using Botticelli.Interfaces; -using Botticelli.Shared.API.Client.Requests; -using Botticelli.Shared.API.Client.Responses; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Services; - -/// -/// Marks a broadcast message as received -/// -/// -/// -/// -/// -/// -public class MarkAsReceivedService( - IHttpClientFactory httpClientFactory, - ServerSettings serverSettings, - IBot bot, - ILogger logger) - : PollActualizationService(httpClientFactory, - "broadcast", - serverSettings, - bot, - logger) -{ - private readonly IBot _bot = bot; - - protected override async Task InnerProcess(MarksAsReceivedResponse response, CancellationToken ct) - { - } -} \ No newline at end of file From 80010fb009498e0271eb214ce8518017ee440ba1 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 11 Apr 2025 23:31:58 +0300 Subject: [PATCH 004/101] - AdditionalProcessing returned for Telegram --- Botticelli.Framework.Telegram/TelegramBot.cs | 204 ++++++++++-------- Botticelli.Pay.Telegram/TelegramPaymentBot.cs | 6 - 2 files changed, 113 insertions(+), 97 deletions(-) diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 22256fbd..f8f4a74b 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -34,13 +34,14 @@ public class TelegramBot : BaseBot private readonly ITextTransformer _textTransformer; protected readonly ITelegramBotClient Client; - protected TelegramBot(ITelegramBotClient client, - IBotUpdateHandler handler, - ILogger logger, - MetricsProcessor metrics, - ITextTransformer textTransformer, - IBotDataAccess data) : base(logger, metrics) + public TelegramBot(ITelegramBotClient client, + IBotUpdateHandler handler, + ILogger logger, + MetricsProcessor metrics, + ITextTransformer textTransformer, + IBotDataAccess data) : base(logger, metrics) { + BotStatusKeeper.IsStarted = false; Client = client; _handler = handler; _textTransformer = textTransformer; @@ -61,7 +62,7 @@ protected TelegramBot(ITelegramBotClient client, /// /// protected override async Task InnerDeleteMessageAsync(RemoveMessageRequest request, - CancellationToken token) + CancellationToken token) { request.NotNull(); request.Uid.NotNull(); @@ -83,10 +84,9 @@ protected override async Task InnerDeleteMessageAsync(Rem { if (string.IsNullOrWhiteSpace(request.Uid)) throw new BotException("request/message is null!"); - if (request.ChatId != null) - await Client.DeleteMessage(request.ChatId, - int.Parse(request.Uid), - token); + await Client.DeleteMessage(request.ChatId, + int.Parse(request.Uid), + token); response.MessageRemovedStatus = MessageRemovedStatus.Ok; } catch @@ -106,6 +106,15 @@ await Client.DeleteMessage(request.ChatId, return response; } + protected virtual Task AdditionalProcessing(SendMessageRequest request, + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + string chatId, + CancellationToken token) + { + return Task.CompletedTask; + } + /// /// Sends a message as a telegram bot /// @@ -117,9 +126,9 @@ await Client.DeleteMessage(request.ChatId, /// /// protected override async Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) { request.NotNull(); request.Message.NotNull(); @@ -156,13 +165,12 @@ protected override async Task InnerSendMessageAsync pairs = []; - foreach (var link in request.Message.ChatIdInnerIdLinks) - pairs.AddRange(link.Value.Select(innerId => (link.Key, innerId))); + foreach (var link in request.Message.ChatIdInnerIdLinks) pairs.AddRange(link.Value.Select(innerId => (link.Key, innerId))); var chatIdOnly = request.Message.ChatIds.Where(c => !request.Message.ChatIdInnerIdLinks.ContainsKey(c)); pairs.AddRange(chatIdOnly.Select(c => (c, string.Empty))); - Logger.LogInformation("Pairs count: {Count}", pairs.Count); + Logger.LogInformation($"Pairs count: {pairs.Count}"); for (var i = 0; i < pairs.Count; i++) { @@ -170,33 +178,39 @@ protected override async Task InnerSendMessageAsync(request, - isUpdate, - token, - retText, - replyMarkup, - link); + isUpdate, + token, + retText, + replyMarkup, + link); if (request.Message.Poll != null) message = await ProcessPoll(request, - token, - link, - replyMarkup, - response); + token, + link, + replyMarkup, + response); if (request.Message.Contact != null) await ProcessContact(request, - response, - token, - replyMarkup); + response, + token, + replyMarkup); + + await AdditionalProcessing(request, + optionsBuilder, + isUpdate, + link.chatId, + token); if (request.Message.Attachments == null) continue; message = await ProcessAttachments(request, - token, - link, - replyMarkup, - response, - message); + token, + link, + replyMarkup, + response, + message); message.NotNull(); AddChatIdInnerIdLink(response, link.chatId, message); @@ -222,11 +236,11 @@ await ProcessContact(request, } protected virtual async Task ProcessText(SendMessageRequest request, - bool isUpdate, - CancellationToken token, - string retText, - ReplyMarkup? replyMarkup, - (string chatId, string innerId) link) + bool isUpdate, + CancellationToken token, + string retText, + ReplyMarkup? replyMarkup, + (string chatId, string innerId) link) { if (!string.IsNullOrWhiteSpace(retText)) { @@ -246,35 +260,38 @@ async Task SendText(string sendText) if (!isUpdate) { var sentMessage = await Client.SendMessage(link.chatId, - sendText, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + sendText, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); link.innerId = sentMessage.MessageId.ToString(); } else { await Client.EditMessageText(link.chatId, - int.Parse(link.innerId), - sendText, - ParseMode.MarkdownV2, - replyMarkup: replyMarkup as InlineKeyboardMarkup, - cancellationToken: token); + int.Parse(link.innerId), + sendText, + ParseMode.MarkdownV2, + replyMarkup: replyMarkup as InlineKeyboardMarkup, + cancellationToken: token); } } } } protected virtual async Task ProcessAttachments(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response, - Message? message) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response, + Message? message) { request.Message.NotNull(); + if (request.Message.Attachments == null) + return message; + request.Message.Attachments.NotNullOrEmpty(); foreach (var attachment in request.Message @@ -369,10 +386,10 @@ await ProcessContact(request, } protected virtual async Task ProcessPoll(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response) { Message? message; @@ -382,20 +399,20 @@ await ProcessContact(request, var type = request.Message.Poll?.Type switch { - Poll.PollType.Quiz => PollType.Quiz, + Poll.PollType.Quiz => PollType.Quiz, Poll.PollType.Regular => PollType.Regular, - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException() }; message = await Client.SendPoll(link.chatId, - request.Message.Poll?.Question ?? "No question", - GetPollOptions(request), - request.Message.Poll?.IsAnonymous ?? false, - type, - correctOptionId: request.Message.Poll?.CorrectAnswerId, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + request.Message.Poll?.Question ?? "No question", + GetPollOptions(request), + request.Message.Poll?.IsAnonymous ?? false, + type, + correctOptionId: request.Message.Poll?.CorrectAnswerId, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); @@ -417,12 +434,12 @@ await ProcessContact(request, private static InputPollOption[] GetPollOptions(SendMessageRequest request) { return request.Message.Poll?.Variants?.Select(po => - new InputPollOption - { - Text = po.option, - TextParseMode = ParseMode.MarkdownV2 - }) - .ToArray() ?? + new InputPollOption + { + Text = po.option, + TextParseMode = ParseMode.MarkdownV2 + }) + .ToArray() ?? []; } @@ -435,9 +452,9 @@ private static void AddChatIdInnerIdLink(SendMessageResponse response, string ch } protected virtual async Task ProcessContact(SendMessageRequest request, - SendMessageResponse response, - CancellationToken token, - ReplyMarkup? replyMarkup) + SendMessageResponse response, + CancellationToken token, + ReplyMarkup? replyMarkup) { request.Message.NotNull(); request.Message.Contact.NotNull(); @@ -448,12 +465,12 @@ protected virtual async Task ProcessContact(SendMessageRequest request, try { var message = await Client.SendContact(chatId, - request.Message?.Contact?.Phone!, - request.Message?.Contact?.Name!, - request.Message?.Contact?.Surname, - replyParameters: GetReplyParameters(request, chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + request.Message?.Contact?.Phone!, + request.Message?.Contact?.Name!, + request.Message?.Contact?.Surname, + replyParameters: GetReplyParameters(request, chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, chatId, message); } @@ -463,12 +480,14 @@ protected virtual async Task ProcessContact(SendMessageRequest request, } } - private static ReplyParameters GetReplyParameters(SendMessageRequest request, string chatId) => - new() + private static ReplyParameters GetReplyParameters(SendMessageRequest request, string chatId) + { + return new ReplyParameters { ChatId = chatId, MessageId = request.Message.ReplyToMessageUid != null ? int.Parse(request.Message.ReplyToMessageUid) : 0 }; + } /// /// Starts a bot @@ -494,7 +513,7 @@ protected override Task InnerStartBotAsync(StartBotRequest req // Rethrowing an event from BotUpdateHandler _handler.MessageReceived += (sender, e) - => MessageReceived?.Invoke(sender, e); + => MessageReceived?.Invoke(sender, e); Client.StartReceiving(_handler, cancellationToken: token); @@ -543,7 +562,7 @@ protected override async Task InnerStopBotAsync(StopBotRequest private void RecreateClient(string token) { - ((TelegramClientDecorator)Client).ChangeBotToken(token); + ((TelegramClientDecorator) Client).ChangeBotToken(token); } private async Task StartBot(CancellationToken token) @@ -573,11 +592,14 @@ public override async Task SetBotContext(BotData.Entities.Bot.BotData? context, await StartBot(token); } - else if (currentContext != null && Client.BotId == 0) + else if (currentContext != null) { - await StopBot(token); - RecreateClient(context.BotKey!); - await StartBot(token); + if (Client.BotId == default) + { + await StopBot(token); + RecreateClient(context.BotKey!); + await StartBot(token); + } } } } \ No newline at end of file diff --git a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs index f8b1c494..aa15b03a 100644 --- a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs +++ b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs @@ -35,12 +35,6 @@ protected override async Task AdditionalProcessing(SendMessageRequ string chatId, CancellationToken token) { - await base.AdditionalProcessing(request, - optionsBuilder, - isUpdate, - chatId, - token); - var invoice = (request.Message as PayInvoiceMessage)?.Invoice; if (invoice is not null) From cd05d793aa83e6c0db6b5706aa15f0cf59d3a936 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 12 Apr 2025 11:25:26 +0300 Subject: [PATCH 005/101] - sonar --- .../Builders/TelegramBotBuilder.cs | 72 +++++++++---------- .../TelegramClientDecoratorBuilder.cs | 2 +- Botticelli.Framework.Telegram/TelegramBot.cs | 2 +- .../Builders/VkBotBuilder.cs | 2 +- Botticelli.Framework/Builders/BotBuilder.cs | 19 +++-- .../Ai.DeepSeek.Sample.Telegram/Program.cs | 2 +- 6 files changed, 47 insertions(+), 52 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 24d8ae72..fe830a22 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -27,30 +27,27 @@ namespace Botticelli.Framework.Telegram.Builders; public class TelegramBotBuilder : BotBuilder, TBot> - where TBot : TelegramBot + where TBot : TelegramBot { private readonly List> _subHandlers = []; private TelegramClientDecoratorBuilder _builder = null!; - private TelegramClientDecorator _client = null!; private TelegramBotSettings? BotSettings { get; set; } public static TelegramBotBuilder Instance(IServiceCollection services, - ServerSettingsBuilder serverSettingsBuilder, - BotSettingsBuilder settingsBuilder, - DataAccessSettingsBuilder dataAccessSettingsBuilder, - AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) - { - return new TelegramBotBuilder() - .AddServices(services) - .AddServerSettings(serverSettingsBuilder) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddBotSettings(settingsBuilder); - } + ServerSettingsBuilder serverSettingsBuilder, + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder, + AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) => + new TelegramBotBuilder() + .AddServices(services) + .AddServerSettings(serverSettingsBuilder) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder) + .AddBotSettings(settingsBuilder); public TelegramBotBuilder AddSubHandler() - where T : class, IBotUpdateSubHandler + where T : class, IBotUpdateSubHandler { Services.NotNull(); Services!.AddSingleton(); @@ -78,15 +75,15 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder Services!.AddSingleton(ServerSettingsBuilder.Build()); Services!.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services!.AddHostedService(); Services!.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services!.AddHostedService(); Services!.AddHttpClient>() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services!.AddHostedService>>(); Services!.AddHostedService(); @@ -106,7 +103,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder #region Data Services!.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder.Build().ConnectionString}")); + o.UseSqlite($"Data source={BotDataAccessSettingsBuilder.Build().ConnectionString}")); Services!.AddScoped(); #endregion @@ -118,40 +115,41 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder #endregion if (BotSettings?.UseThrottling is true) _builder.AddThrottler(new Throttler()); - _client = _builder.Build(); - _client.Timeout = TimeSpan.FromMilliseconds(BotSettings?.Timeout ?? 10000); + + var client = _builder.Build(); + + client.NotNull(); + + client!.Timeout = TimeSpan.FromMilliseconds(BotSettings?.Timeout ?? 10000); Services!.AddSingleton, ReplyTelegramLayoutSupplier>() - .AddBotticelliFramework() - .AddSingleton(); + .AddBotticelliFramework() + .AddSingleton(); Services!.AddSingleton(ServerSettingsBuilder.Build()); var sp = Services!.BuildServiceProvider(); - foreach (var sh in _subHandlers) sh.Invoke(sp); - ApplyMigrations(sp); - var telegramBot = Activator.CreateInstance(typeof(TBot), - _client, - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService()) as TBot; + foreach (var sh in _subHandlers) sh.Invoke(sp); - return telegramBot; + return Activator.CreateInstance(typeof(TBot), + client, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService()) as TBot; } - public override TelegramBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + public virtual TelegramBotBuilder AddBotSettings( + BotSettingsBuilder settingsBuilder) { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); return this; } - private void ApplyMigrations(IServiceProvider sp) - { + private static void ApplyMigrations(IServiceProvider sp) => sp.GetRequiredService().Database.Migrate(); - } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs index 9f404eba..2f3a229d 100644 --- a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs +++ b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs @@ -41,7 +41,7 @@ public TelegramClientDecoratorBuilder AddToken(string? token) return this; } - public TelegramClientDecorator Build() + public TelegramClientDecorator? Build() { _token ??= "11111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index f8f4a74b..8988855b 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -34,7 +34,7 @@ public class TelegramBot : BaseBot private readonly ITextTransformer _textTransformer; protected readonly ITelegramBotClient Client; - public TelegramBot(ITelegramBotClient client, + protected TelegramBot(ITelegramBotClient client, IBotUpdateHandler handler, ILogger logger, MetricsProcessor metrics, diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs index b6db5559..59f8304d 100644 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs @@ -90,7 +90,7 @@ protected override VkBot InnerBuild() sp.GetRequiredService>()); } - public override VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + public virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) { BotSettings = settingsBuilder.Build() as VkBotSettings ?? throw new InvalidOperationException(); diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 4dbed717..09340754 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -20,12 +20,11 @@ public abstract class BotBuilder } public abstract class BotBuilder : BotBuilder - where TBotBuilder : BotBuilder + where TBotBuilder : BotBuilder { - private readonly ServerSettings _serverSettings; - protected AnalyticsClientSettingsBuilder AnalyticsClientSettingsBuilder; - protected DataAccessSettingsBuilder BotDataAccessSettingsBuilder; - protected ServerSettingsBuilder ServerSettingsBuilder; + protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; + protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; + protected ServerSettingsBuilder? ServerSettingsBuilder; protected IServiceCollection? Services; protected override void Assert() @@ -39,24 +38,22 @@ protected TBotBuilder AddServices(IServiceCollection services) return (this as TBotBuilder)!; } - public abstract TBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) - where TBotSettings : BotSettings, new(); - - public TBotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) + protected TBotBuilder AddAnalyticsSettings( + AnalyticsClientSettingsBuilder clientSettingsBuilder) { AnalyticsClientSettingsBuilder = clientSettingsBuilder; return (this as TBotBuilder)!; } - public TBotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) + protected TBotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) { ServerSettingsBuilder = settingsBuilder; return (this as TBotBuilder)!; } - public TBotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) + protected TBotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) { BotDataAccessSettingsBuilder = botDataAccessBuilder; diff --git a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs index d634cd12..5133159e 100644 --- a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs +++ b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs @@ -29,4 +29,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file From 12ae7ed60448f7c64c95de93fe18d492f5e8fc0c Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 12 Apr 2025 11:27:09 +0300 Subject: [PATCH 006/101] - sonar --- Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs | 3 ++- Botticelli.Framework.Vk/Builders/VkBotBuilder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index fe830a22..aed47e46 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -142,8 +142,9 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder sp.GetRequiredService()) as TBot; } - public virtual TelegramBotBuilder AddBotSettings( + protected virtual TelegramBotBuilder AddBotSettings( BotSettingsBuilder settingsBuilder) + where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs index 59f8304d..a689997a 100644 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs @@ -90,7 +90,8 @@ protected override VkBot InnerBuild() sp.GetRequiredService>()); } - public virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + protected virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as VkBotSettings ?? throw new InvalidOperationException(); From a5070174eca41716b1bab8796526070e07404b68 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 12 Apr 2025 12:06:16 +0300 Subject: [PATCH 007/101] - Bod data settings (won't build!) --- .../Builders/TelegramBotBuilder.cs | 50 ++++++++++++++----- .../Options/TelegramBotSettings.cs | 5 ++ Botticelli.Framework.Telegram/TelegramBot.cs | 22 ++++---- .../Options/BotDataSettings.cs | 10 ++++ .../Options/BotDataSettingsBuilder.cs | 24 +++++++++ .../Services/BotAutonomousService.cs | 27 ++++++++++ .../Services/BotStatusService.cs | 1 + 7 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 Botticelli.Framework/Options/BotDataSettings.cs create mode 100644 Botticelli.Framework/Options/BotDataSettingsBuilder.cs create mode 100644 Botticelli.Framework/Services/BotAutonomousService.cs diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index aed47e46..7d71a535 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; @@ -33,6 +34,7 @@ public class TelegramBotBuilder : BotBuilder, TBo private TelegramClientDecoratorBuilder _builder = null!; private TelegramBotSettings? BotSettings { get; set; } + private BotData.Entities.Bot.BotData? BotData { get; set; } public static TelegramBotBuilder Instance(IServiceCollection services, ServerSettingsBuilder serverSettingsBuilder, @@ -72,28 +74,42 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder protected override TBot? InnerBuild() { - Services!.AddSingleton(ServerSettingsBuilder.Build()); + Services!.AddSingleton(ServerSettingsBuilder!.Build()); - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); + if (BotSettings?.IsAutonomous is false) + { + Services!.AddHttpClient() + .AddServerCertificates(BotSettings); + Services!.AddHostedService(); + + Services!.AddHttpClient() + .AddServerCertificates(BotSettings); + Services!.AddHostedService(); + } + else + { + Services!.AddHttpClient() + .AddServerCertificates(BotSettings); - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); + if (BotData == null) + throw new ConfigurationErrorsException("BotData is null!"); + + Services!.AddHostedService() + .AddSingleton(BotData); + } Services!.AddHttpClient>() .AddServerCertificates(BotSettings); - Services!.AddHostedService>>(); - - Services!.AddHostedService(); + Services!.AddHostedService>>() + .AddHostedService(); + var botId = BotDataUtils.GetBotId(); if (botId == null) throw new InvalidDataException($"{nameof(botId)} shouldn't be null!"); #region Metrics - var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder.Build()); + var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); var metricsProcessor = new MetricsProcessor(metricsPublisher); Services!.AddSingleton(metricsPublisher); Services!.AddSingleton(metricsProcessor); @@ -103,7 +119,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder #region Data Services!.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder.Build().ConnectionString}")); + o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); Services!.AddScoped(); #endregion @@ -151,6 +167,16 @@ protected virtual TelegramBotBuilder AddBotSettings( return this; } + protected virtual TelegramBotBuilder AddBotData( + BotDataSettingsBuilder dataBuilder) + { + BotData = dataBuilder.Build(); + + + + return this; + } + private static void ApplyMigrations(IServiceProvider sp) => sp.GetRequiredService().Database.Migrate(); } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs index f5c45223..c2f8ccad 100644 --- a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs +++ b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs @@ -22,6 +22,11 @@ public class TelegramBotSettings : BotSettings /// public bool? UseThrottling { get; set; } = true; + /// + /// Is this bor autonomous? + /// + public bool? IsAutonomous { get; set; } = false; + /// /// Should we use test environment /// diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 8988855b..1b528f84 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -170,14 +170,14 @@ protected override async Task InnerSendMessageAsync !request.Message.ChatIdInnerIdLinks.ContainsKey(c)); pairs.AddRange(chatIdOnly.Select(c => (c, string.Empty))); - Logger.LogInformation($"Pairs count: {pairs.Count}"); + Logger.LogInformation("Pairs count: {Count}", pairs.Count); for (var i = 0; i < pairs.Count; i++) { var link = pairs[i]; Message? message = null; - await ProcessText(request, + await ProcessText(request, isUpdate, token, retText, @@ -235,12 +235,12 @@ await AdditionalProcessing(request, return response; } - protected virtual async Task ProcessText(SendMessageRequest request, - bool isUpdate, - CancellationToken token, - string retText, - ReplyMarkup? replyMarkup, - (string chatId, string innerId) link) + private async Task ProcessText(SendMessageRequest request, + bool isUpdate, + CancellationToken token, + string retText, + ReplyMarkup? replyMarkup, + (string chatId, string innerId) link) { if (!string.IsNullOrWhiteSpace(retText)) { @@ -281,7 +281,7 @@ await Client.EditMessageText(link.chatId, } } - protected virtual async Task ProcessAttachments(SendMessageRequest request, + private async Task ProcessAttachments(SendMessageRequest request, CancellationToken token, (string chatId, string innerId) link, ReplyMarkup? replyMarkup, @@ -385,7 +385,7 @@ await ProcessContact(request, return message; } - protected virtual async Task ProcessPoll(SendMessageRequest request, + protected async Task ProcessPoll(SendMessageRequest request, CancellationToken token, (string chatId, string innerId) link, ReplyMarkup? replyMarkup, @@ -451,7 +451,7 @@ private static void AddChatIdInnerIdLink(SendMessageResponse response, string ch response.Message.ChatIdInnerIdLinks[chatId].Add(message!.MessageId.ToString()); } - protected virtual async Task ProcessContact(SendMessageRequest request, + protected async Task ProcessContact(SendMessageRequest request, SendMessageResponse response, CancellationToken token, ReplyMarkup? replyMarkup) diff --git a/Botticelli.Framework/Options/BotDataSettings.cs b/Botticelli.Framework/Options/BotDataSettings.cs new file mode 100644 index 00000000..1e7317b8 --- /dev/null +++ b/Botticelli.Framework/Options/BotDataSettings.cs @@ -0,0 +1,10 @@ +namespace Botticelli.Framework.Options; + +/// +/// Bot data/context settings +/// +public abstract class BotDataSettings +{ + public required string BotId { get; set; } + public string? BotKey { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs new file mode 100644 index 00000000..e4a9acde --- /dev/null +++ b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs @@ -0,0 +1,24 @@ +namespace Botticelli.Framework.Options; + +public class BotDataSettingsBuilder + where T : BotDataSettings +{ + private T _settings = new(); + + public void Set(T settings) + { + _settings = settings; + } + + public BotDataSettingsBuilder Set(Action func) + { + func(_settings); + + return this; + } + + public T Build() + { + return _settings; + } +} \ No newline at end of file diff --git a/Botticelli.Framework/Services/BotAutonomousService.cs b/Botticelli.Framework/Services/BotAutonomousService.cs new file mode 100644 index 00000000..c77aa6c5 --- /dev/null +++ b/Botticelli.Framework/Services/BotAutonomousService.cs @@ -0,0 +1,27 @@ +using Botticelli.Framework.Options; +using Botticelli.Interfaces; +using Botticelli.Shared.API.Admin.Requests; +using Microsoft.Extensions.Logging; + +namespace Botticelli.Framework.Services; + +public class BotAutonomousService( + IHttpClientFactory httpClientFactory, + ServerSettings serverSettings, + BotData.Entities.Bot.BotData botData, + IBot bot, + ILogger logger) + : BotActualizationService(httpClientFactory, + serverSettings, + bot, + logger) +{ + public override async Task StartAsync(CancellationToken cancellationToken) + { + await Bot.SetBotContext(botData, cancellationToken); + await Bot.StartBotAsync(StartBotRequest.GetInstance(), cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + => await Bot.StopBotAsync(StopBotRequest.GetInstance(), cancellationToken); +} \ No newline at end of file diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 41c131b3..87548d2c 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -6,6 +6,7 @@ using Botticelli.Shared.API.Admin.Responses; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; +using Botticelli.Shared.ValueObjects; using Microsoft.Extensions.Logging; using Polly; From 5715653e0b4b68a8e55c7c8b8d69286626c40dc1 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 27 Apr 2025 13:53:15 +0300 Subject: [PATCH 008/101] BOTTICELLI-62: standalone bots: -initialization --- .../Builders/TelegramBotBuilder.cs | 98 ++++++++----------- .../Builders/TelegramStandaloneBotBuilder.cs | 57 +++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 75 +++++++++----- .../Options/TelegramBotSettings.cs | 4 +- Botticelli.Framework/Builders/BotBuilder.cs | 2 +- .../Options/BotDataSettings.cs | 4 +- .../Options/BotDataSettingsBuilder.cs | 2 +- ...mousService.cs => BotStandaloneService.cs} | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 46 +++++---- 9 files changed, 181 insertions(+), 109 deletions(-) create mode 100644 Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs rename Botticelli.Framework/Services/{BotAutonomousService.cs => BotStandaloneService.cs} (96%) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 7d71a535..20422087 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -1,4 +1,3 @@ -using System.Configuration; using Botticelli.Bot.Data; using Botticelli.Bot.Data.Repositories; using Botticelli.Bot.Data.Settings; @@ -27,32 +26,45 @@ namespace Botticelli.Framework.Telegram.Builders; -public class TelegramBotBuilder : BotBuilder, TBot> +/// +/// +/// +/// +public abstract class TelegramBotBuilder : TelegramBotBuilder> + where TBot : TelegramBot; + +/// +/// Builder for a non-standalone Telegram bot +/// +/// +/// +public class TelegramBotBuilder : BotBuilder where TBot : TelegramBot + where TBotBuilder : TelegramBotBuilder { private readonly List> _subHandlers = []; private TelegramClientDecoratorBuilder _builder = null!; - private TelegramBotSettings? BotSettings { get; set; } - private BotData.Entities.Bot.BotData? BotData { get; set; } + protected TelegramBotSettings? BotSettings { get; set; } + protected BotData.Entities.Bot.BotData? BotData { get; set; } - public static TelegramBotBuilder Instance(IServiceCollection services, + public static TBotBuilder Instance(IServiceCollection services, ServerSettingsBuilder serverSettingsBuilder, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) => - new TelegramBotBuilder() + new TelegramBotBuilder() .AddServices(services) .AddServerSettings(serverSettingsBuilder) .AddAnalyticsSettings(analyticsClientSettingsBuilder) .AddBotDataAccessSettings(dataAccessSettingsBuilder) .AddBotSettings(settingsBuilder); - public TelegramBotBuilder AddSubHandler() + public TBotBuilder AddSubHandler() where T : class, IBotUpdateSubHandler { Services.NotNull(); - Services!.AddSingleton(); + Services.AddSingleton(); _subHandlers.Add(sp => { @@ -62,45 +74,31 @@ public TelegramBotBuilder AddSubHandler() botHandler.AddSubHandler(subHandler); }); - return this; + return (TBotBuilder)this; } - public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder) + public TBotBuilder AddClient(TelegramClientDecoratorBuilder builder) { _builder = builder; - return this; + return (TBotBuilder)this; } protected override TBot? InnerBuild() { - Services!.AddSingleton(ServerSettingsBuilder!.Build()); + Services.AddSingleton(ServerSettingsBuilder!.Build()); - if (BotSettings?.IsAutonomous is false) - { - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); - - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); - } - else - { - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - if (BotData == null) - throw new ConfigurationErrorsException("BotData is null!"); - - Services!.AddHostedService() - .AddSingleton(BotData); - } + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - Services!.AddHttpClient>() + Services.AddHttpClient>() .AddServerCertificates(BotSettings); - Services!.AddHostedService>>() + Services.AddHostedService>>() .AddHostedService(); var botId = BotDataUtils.GetBotId(); @@ -111,22 +109,22 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); var metricsProcessor = new MetricsProcessor(metricsPublisher); - Services!.AddSingleton(metricsPublisher); - Services!.AddSingleton(metricsProcessor); + Services.AddSingleton(metricsPublisher); + Services.AddSingleton(metricsProcessor); #endregion #region Data - Services!.AddDbContext(o => + Services.AddDbContext(o => o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); - Services!.AddScoped(); + Services.AddScoped(); #endregion #region TextTransformer - Services!.AddTransient(); + Services.AddTransient(); #endregion @@ -138,13 +136,13 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder client!.Timeout = TimeSpan.FromMilliseconds(BotSettings?.Timeout ?? 10000); - Services!.AddSingleton, ReplyTelegramLayoutSupplier>() + Services.AddSingleton, ReplyTelegramLayoutSupplier>() .AddBotticelliFramework() .AddSingleton(); - Services!.AddSingleton(ServerSettingsBuilder.Build()); + Services.AddSingleton(ServerSettingsBuilder.Build()); - var sp = Services!.BuildServiceProvider(); + var sp = Services.BuildServiceProvider(); ApplyMigrations(sp); foreach (var sh in _subHandlers) sh.Invoke(sp); @@ -157,24 +155,14 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder sp.GetRequiredService(), sp.GetRequiredService()) as TBot; } - - protected virtual TelegramBotBuilder AddBotSettings( + + protected TBotBuilder AddBotSettings( BotSettingsBuilder settingsBuilder) where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); - return this; - } - - protected virtual TelegramBotBuilder AddBotData( - BotDataSettingsBuilder dataBuilder) - { - BotData = dataBuilder.Build(); - - - - return this; + return (TBotBuilder)this; } private static void ApplyMigrations(IServiceProvider sp) => diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs new file mode 100644 index 00000000..c7f102f0 --- /dev/null +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -0,0 +1,57 @@ +using System.Configuration; +using Botticelli.Bot.Data.Settings; +using Botticelli.Framework.Options; +using Botticelli.Framework.Security; +using Botticelli.Framework.Services; +using Botticelli.Framework.Telegram.Options; +using Botticelli.Shared.API.Admin.Responses; +using Botticelli.Shared.Constants; +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Telegram.Builders; + +/// +/// Builder for a standalone telegram bot +/// +/// +public class TelegramStandaloneBotBuilder : TelegramBotBuilder> + where TBot : TelegramBot +{ + public static TelegramStandaloneBotBuilder Instance(IServiceCollection services, + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder) => + new TelegramStandaloneBotBuilder() + .AddServices(services) + .AddBotDataAccessSettings(dataAccessSettingsBuilder) + .AddBotSettings(settingsBuilder); + + public TelegramStandaloneBotBuilder AddBotData( + BotDataSettingsBuilder dataBuilder) + { + var settings = dataBuilder.Build(); + + BotData = new BotData.Entities.Bot.BotData + { + BotId = settings.BotId ?? throw new ConfigurationErrorsException("No BotId in bot settings!"), + Status = BotStatus.Unlocked, + Type = BotType.Telegram, + BotKey = settings.BotKey ?? throw new ConfigurationErrorsException("No BotKey in bot settings!") + }; + + return this; + } + + protected override TBot? InnerBuild() + { + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + + if (BotData == null) + throw new ConfigurationErrorsException("BotData is null!"); + + Services.AddHostedService() + .AddSingleton(BotData); + + return base.InnerBuild(); + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 68dd2cd7..047e0319 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Configuration; using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; +using Botticelli.Framework.Builders; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Options; using Botticelli.Framework.Telegram.Builders; @@ -26,14 +27,14 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddTelegramBot(this IServiceCollection services, IConfiguration configuration, - Action>? telegramBotBuilderFunc = null) + Action>? telegramBotBuilderFunc = null) { return AddTelegramBot(services, configuration, telegramBotBuilderFunc); } public static IServiceCollection AddTelegramBot(this IServiceCollection services, IConfiguration configuration, - Action>? telegramBotBuilderFunc = null) + Action>? telegramBotBuilderFunc = null) where TBot : TelegramBot { var telegramBotSettings = configuration @@ -68,7 +69,7 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se AnalyticsClientSettings analyticsClientSettings, ServerSettings serverSettings, DataAccessSettings dataAccessSettings, - Action>? telegramBotBuilderFunc = null) + Action>? telegramBotBuilderFunc = null) where TBot : TelegramBot { return services.AddTelegramBot(o => o.Set(botSettings), @@ -89,12 +90,12 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se /// /// public static IServiceCollection AddTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); @@ -104,28 +105,48 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); var botBuilder = TelegramBotBuilder.Instance(services, - ServerSettingsBuilder, - SettingsBuilder, - DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder) - .AddClient(clientBuilder); + ServerSettingsBuilder, + SettingsBuilder, + DataAccessSettingsBuilder, + AnalyticsClientOptionsBuilder) + .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke(botBuilder); + TelegramBot? bot = botBuilder.Build(); - var bot = botBuilder.Build(); - - return services.AddSingleton(bot) - .AddTelegramLayoutsSupport(); + return services.AddSingleton(bot!) + .AddTelegramLayoutsSupport(); } - - public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) + + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, + Action> optionsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { - return services.AddSingleton() - .AddSingleton, ReplyTelegramLayoutSupplier>() - .AddSingleton, InlineTelegramLayoutSupplier>() - .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() - .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); + optionsBuilderFunc(SettingsBuilder); + dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); + + var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); + + var botBuilder = TelegramStandaloneBotBuilder.Instance(services, + SettingsBuilder, + DataAccessSettingsBuilder) + .AddClient(clientBuilder); + + telegramBotBuilderFunc?.Invoke(botBuilder); + TelegramBot? bot = botBuilder.Build(); + + return services.AddSingleton(bot!) + .AddTelegramLayoutsSupport(); } + + public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => + services.AddSingleton() + .AddSingleton, ReplyTelegramLayoutSupplier>() + .AddSingleton, InlineTelegramLayoutSupplier>() + .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() + .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs index c2f8ccad..1b4edac9 100644 --- a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs +++ b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs @@ -23,9 +23,9 @@ public class TelegramBotSettings : BotSettings public bool? UseThrottling { get; set; } = true; /// - /// Is this bor autonomous? + /// Is this bot standalone? /// - public bool? IsAutonomous { get; set; } = false; + public bool? IsStandalone { get; set; } = false; /// /// Should we use test environment diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 09340754..ea3d8227 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -25,7 +25,7 @@ public abstract class BotBuilder : BotBuilder protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; protected ServerSettingsBuilder? ServerSettingsBuilder; - protected IServiceCollection? Services; + protected IServiceCollection Services = null!; protected override void Assert() { diff --git a/Botticelli.Framework/Options/BotDataSettings.cs b/Botticelli.Framework/Options/BotDataSettings.cs index 1e7317b8..61b70097 100644 --- a/Botticelli.Framework/Options/BotDataSettings.cs +++ b/Botticelli.Framework/Options/BotDataSettings.cs @@ -3,8 +3,8 @@ namespace Botticelli.Framework.Options; /// /// Bot data/context settings /// -public abstract class BotDataSettings +public class BotDataSettings { - public required string BotId { get; set; } + public string? BotId { get; set; } public string? BotKey { get; set; } } \ No newline at end of file diff --git a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs index e4a9acde..77312b1a 100644 --- a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs +++ b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs @@ -1,7 +1,7 @@ namespace Botticelli.Framework.Options; public class BotDataSettingsBuilder - where T : BotDataSettings + where T : BotDataSettings, new() { private T _settings = new(); diff --git a/Botticelli.Framework/Services/BotAutonomousService.cs b/Botticelli.Framework/Services/BotStandaloneService.cs similarity index 96% rename from Botticelli.Framework/Services/BotAutonomousService.cs rename to Botticelli.Framework/Services/BotStandaloneService.cs index c77aa6c5..8ccd8c4a 100644 --- a/Botticelli.Framework/Services/BotAutonomousService.cs +++ b/Botticelli.Framework/Services/BotStandaloneService.cs @@ -5,7 +5,7 @@ namespace Botticelli.Framework.Services; -public class BotAutonomousService( +public class BotStandaloneService( IHttpClientFactory httpClientFactory, ServerSettings serverSettings, BotData.Entities.Bot.BotData botData, diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index 1c85d9c6..232258a2 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -8,7 +8,6 @@ using Botticelli.Pay.Models; using Botticelli.Pay.Processors; using Botticelli.Pay.Telegram.Handlers; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Botticelli.Pay.Telegram.Extensions; @@ -25,37 +24,44 @@ public static class ServiceCollectionExtensions /// /// public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc) - where THandler : IPreCheckoutHandler, new() - where TProcessor : IPayProcessor + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc) + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor { services.AddPayments(); return services.AddTelegramBot(optionsBuilderFunc, - analyticsOptionsBuilderFunc, - serverSettingsBuilderFunc, - dataAccessSettingsBuilderFunc, - o => o.AddSubHandler() - .AddSubHandler()); + analyticsOptionsBuilderFunc, + serverSettingsBuilderFunc, + dataAccessSettingsBuilderFunc, + o => o + .AddSubHandler() + .AddSubHandler()); } /// - /// Adds a Telegram bot with a payment function + /// Adds a standalone Telegram bot with a payment function /// /// - /// + /// + /// /// - public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, IConfiguration configuration) - where THandler : IPreCheckoutHandler, new() - where TProcessor : IPayProcessor + public static IServiceCollection AddStandaloneTelegramPayBot(this IServiceCollection services, + Action> optionsBuilderFunc, + Action> dataAccessSettingsBuilderFunc) + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor { services.AddPayments(); - return services.AddTelegramBot(configuration, - o => o.AddSubHandler() - .AddSubHandler()); + return services.AddStandaloneTelegramBot( + optionsBuilderFunc, + dataAccessSettingsBuilderFunc, + o => o + .AddSubHandler() + .AddSubHandler()); } } \ No newline at end of file From cd9f5d403589c0fb44537dbf560dd1a5b09f4348 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 2 May 2025 14:16:45 +0300 Subject: [PATCH 009/101] - startup fix --- .../Extensions/ServiceCollectionExtensions.cs | 6 +- .../Builders/TelegramBotBuilder.cs | 24 +++--- .../Builders/TelegramStandaloneBotBuilder.cs | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 81 ++++++++++--------- Botticelli.Framework.Telegram/TelegramBot.cs | 3 +- Botticelli.Framework/Builders/BotBuilder.cs | 21 ++--- .../Extensions/ServiceCollectionExtensions.cs | 17 ++++ .../Processors/InfoCommandProcessor.cs | 37 +++------ Pay.Sample.Telegram/Settings/PaySettings.cs | 2 +- 9 files changed, 103 insertions(+), 94 deletions(-) diff --git a/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs index d7f3291a..15f3a9f7 100644 --- a/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ public static CommandAddServices AddMonadsChain( var runner = chainBuilder.Build(); - services.AddScoped(_ => runner); + services.AddSingleton(_ => runner); return commandAddServices.AddProcessor>() .AddValidator(); @@ -38,8 +38,8 @@ public static CommandAddServices AddMonadsChain runner) - .AddScoped, TLayoutSupplier>(); + services.AddSingleton(_ => runner) + .AddSingleton, TLayoutSupplier>(); return commandAddServices.AddProcessor>() .AddValidator(); diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 20422087..ae648d5a 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -38,9 +38,9 @@ public abstract class TelegramBotBuilder : TelegramBotBuilder /// /// -public class TelegramBotBuilder : BotBuilder +public class TelegramBotBuilder : BotBuilder where TBot : TelegramBot - where TBotBuilder : TelegramBotBuilder + where TBotBuilder : BotBuilder { private readonly List> _subHandlers = []; private TelegramClientDecoratorBuilder _builder = null!; @@ -48,19 +48,19 @@ public class TelegramBotBuilder : BotBuilder Instance(IServiceCollection services, ServerSettingsBuilder serverSettingsBuilder, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) => - new TelegramBotBuilder() - .AddServices(services) + (TelegramBotBuilder)new TelegramBotBuilder() + .AddBotSettings(settingsBuilder) .AddServerSettings(serverSettingsBuilder) .AddAnalyticsSettings(analyticsClientSettingsBuilder) .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddBotSettings(settingsBuilder); + .AddServices(services); - public TBotBuilder AddSubHandler() + public TelegramBotBuilder AddSubHandler() where T : class, IBotUpdateSubHandler { Services.NotNull(); @@ -74,14 +74,14 @@ public TBotBuilder AddSubHandler() botHandler.AddSubHandler(subHandler); }); - return (TBotBuilder)this; + return this; } - public TBotBuilder AddClient(TelegramClientDecoratorBuilder builder) + public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder) { _builder = builder; - return (TBotBuilder)this; + return this; } protected override TBot? InnerBuild() @@ -156,13 +156,13 @@ public TBotBuilder AddClient(TelegramClientDecoratorBuilder builder) sp.GetRequiredService()) as TBot; } - protected TBotBuilder AddBotSettings( + protected TelegramBotBuilder AddBotSettings( BotSettingsBuilder settingsBuilder) where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); - return (TBotBuilder)this; + return this; } private static void ApplyMigrations(IServiceProvider sp) => diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index c7f102f0..d4f32782 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -20,10 +20,10 @@ public class TelegramStandaloneBotBuilder : TelegramBotBuilder Instance(IServiceCollection services, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder) => - new TelegramStandaloneBotBuilder() + (TelegramStandaloneBotBuilder)new TelegramStandaloneBotBuilder() + .AddBotSettings(settingsBuilder) .AddServices(services) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddBotSettings(settingsBuilder); + .AddBotDataAccessSettings(dataAccessSettingsBuilder); public TelegramStandaloneBotBuilder AddBotData( BotDataSettingsBuilder dataBuilder) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 047e0319..020de386 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using System.Configuration; using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; -using Botticelli.Framework.Builders; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Options; using Botticelli.Framework.Telegram.Builders; @@ -21,62 +20,64 @@ public static class ServiceCollectionExtensions private static readonly ServerSettingsBuilder ServerSettingsBuilder = new(); private static readonly AnalyticsClientSettingsBuilder AnalyticsClientOptionsBuilder = - new(); + new(); private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>? telegramBotBuilderFunc = null) - { - return AddTelegramBot(services, configuration, telegramBotBuilderFunc); - } + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) => + AddTelegramBot(services, configuration, telegramBotBuilderFunc); public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(TelegramBotSettings)}!"); var analyticsClientSettings = configuration - .GetSection(AnalyticsClientSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); + .GetSection(AnalyticsClientSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); var serverSettings = configuration - .GetSection(ServerSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(ServerSettings)}!"); + .GetSection(ServerSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(ServerSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(DataAccessSettings)}!"); return services.AddTelegramBot(telegramBotSettings, - analyticsClientSettings, - serverSettings, - dataAccessSettings, - telegramBotBuilderFunc); + analyticsClientSettings, + serverSettings, + dataAccessSettings, + telegramBotBuilderFunc); } public static IServiceCollection AddTelegramBot(this IServiceCollection services, - TelegramBotSettings botSettings, - AnalyticsClientSettings analyticsClientSettings, - ServerSettings serverSettings, - DataAccessSettings dataAccessSettings, - Action>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + TelegramBotSettings botSettings, + AnalyticsClientSettings analyticsClientSettings, + ServerSettings serverSettings, + DataAccessSettings dataAccessSettings, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { return services.AddTelegramBot(o => o.Set(botSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } /// @@ -117,7 +118,7 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se return services.AddSingleton(bot!) .AddTelegramLayoutsSupport(); } - + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, Action> optionsBuilderFunc, Action> dataAccessSettingsBuilderFunc, @@ -134,13 +135,13 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol DataAccessSettingsBuilder) .AddClient(clientBuilder); - telegramBotBuilderFunc?.Invoke(botBuilder); + telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); TelegramBot? bot = botBuilder.Build(); - + return services.AddSingleton(bot!) .AddTelegramLayoutsSupport(); } - + public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => services.AddSingleton() .AddSingleton, ReplyTelegramLayoutSupplier>() diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 1b528f84..a2ae9d6a 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -34,7 +34,8 @@ public class TelegramBot : BaseBot private readonly ITextTransformer _textTransformer; protected readonly ITelegramBotClient Client; - protected TelegramBot(ITelegramBotClient client, + // ReSharper disable once MemberCanBeProtected.Global + public TelegramBot(ITelegramBotClient client, IBotUpdateHandler handler, ILogger logger, MetricsProcessor metrics, diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index ea3d8227..67b1be60 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -19,8 +19,8 @@ public abstract class BotBuilder protected abstract TBot? InnerBuild(); } -public abstract class BotBuilder : BotBuilder - where TBotBuilder : BotBuilder +public abstract class BotBuilder : BotBuilder + where TBotBuilder : BotBuilder { protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; @@ -31,32 +31,33 @@ protected override void Assert() { } - protected TBotBuilder AddServices(IServiceCollection services) + public BotBuilder AddServices(IServiceCollection services) { Services = services; - return (this as TBotBuilder)!; + return this; } - protected TBotBuilder AddAnalyticsSettings( + public BotBuilder AddAnalyticsSettings( AnalyticsClientSettingsBuilder clientSettingsBuilder) { AnalyticsClientSettingsBuilder = clientSettingsBuilder; - return (this as TBotBuilder)!; + return this; } - protected TBotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) + protected BotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) { ServerSettingsBuilder = settingsBuilder; - return (this as TBotBuilder)!; + return this; } - protected TBotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) + public BotBuilder AddBotDataAccessSettings( + DataAccessSettingsBuilder botDataAccessBuilder) { BotDataAccessSettingsBuilder = botDataAccessBuilder; - return (this as TBotBuilder)!; + return this; } } \ No newline at end of file diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index 232258a2..6c465ffc 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Botticelli.Pay.Models; using Botticelli.Pay.Processors; using Botticelli.Pay.Telegram.Handlers; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Botticelli.Pay.Telegram.Extensions; @@ -42,6 +43,22 @@ public static IServiceCollection AddTelegramPayBot .AddSubHandler()); } + /// + /// Adds a Telegram bot with a payment function + /// + /// + /// + /// + public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, IConfiguration configuration) + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor + { + services.AddPayments(); + + return services.AddTelegramBot(configuration, o => o.AddSubHandler() + .AddSubHandler()); + } + /// /// Adds a standalone Telegram bot with a payment function /// diff --git a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 52b28a68..6d6200b1 100644 --- a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -7,33 +7,22 @@ namespace TelegramPayBot.Commands.Processors; -public class InfoCommandProcessor : CommandProcessor where TReplyMarkup : class +public class InfoCommandProcessor( + ILogger> logger, + ICommandValidator commandValidator, + MetricsProcessor metricsProcessor, + IValidator messageValidator) + : CommandProcessor(logger, + commandValidator, + metricsProcessor, + messageValidator) + where TReplyMarkup : class { - public InfoCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) - : base(logger, - commandValidator, - metricsProcessor, - messageValidator) - { - } + protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; - protected override Task InnerProcessContact(Message message, CancellationToken token) - { - return Task.CompletedTask; - } + protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; - protected override Task InnerProcessPoll(Message message, CancellationToken token) - { - return Task.CompletedTask; - } - - protected override Task InnerProcessLocation(Message message, CancellationToken token) - { - return Task.CompletedTask; - } + protected override Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; protected override async Task InnerProcess(Message message, CancellationToken token) { diff --git a/Pay.Sample.Telegram/Settings/PaySettings.cs b/Pay.Sample.Telegram/Settings/PaySettings.cs index 3ea3b55c..8e1eb845 100644 --- a/Pay.Sample.Telegram/Settings/PaySettings.cs +++ b/Pay.Sample.Telegram/Settings/PaySettings.cs @@ -5,5 +5,5 @@ namespace TelegramPayBot.Settings; public class PaySettings { [JsonPropertyName("ProviderToken")] - public string? ProviderToken { get; set; } + public string? ProviderToken { get; init; } } \ No newline at end of file From 00d92f02bb964294ce904fd6a03583bd03cff145 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 2 May 2025 22:09:00 +0300 Subject: [PATCH 010/101] BOTTICELLI-62: - telegram bot standalone sample --- .../Extensions/ServiceCollectionExtensions.cs | 28 +++++++++++++++++ .../Builders/VkBotBuilder.cs | 14 ++++----- Botticelli.sln | 7 +++++ ...essaging.Sample.Telegram.Standalone.csproj | 20 +++++++++++++ .../Program.cs | 30 +++++++++++++++++++ .../Properties/launchSettings.json | 15 ++++++++++ .../appsettings.json | 18 +++++++++++ 7 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj create mode 100644 Messaging.Sample.Telegram.Standalone/Program.cs create mode 100644 Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json create mode 100644 Messaging.Sample.Telegram.Standalone/appsettings.json diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 020de386..f3628437 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -119,6 +119,34 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se .AddTelegramLayoutsSupport(); } + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) => + AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); + + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot + { + var telegramBotSettings = configuration + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(TelegramBotSettings)}!"); + + var dataAccessSettings = configuration + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(DataAccessSettings)}!"); + + return services.AddStandaloneTelegramBot( + botSettings => botSettings.Set(telegramBotSettings), + dataSettings => dataSettings.Set(dataAccessSettings) + ); + } + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, Action> optionsBuilderFunc, Action> dataAccessSettingsBuilderFunc, diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs index a689997a..68d631ac 100644 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs @@ -21,7 +21,7 @@ namespace Botticelli.Framework.Vk.Messages.Builders; -public class VkBotBuilder : BotBuilder +public class VkBotBuilder : BotBuilder { private LongPollMessagesProvider? _longPollMessagesProvider; private LongPollMessagesProviderBuilder? _longPollMessagesProviderBuilder; @@ -111,11 +111,11 @@ public static VkBotBuilder Instance(IServiceCollection services, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) { - return new VkBotBuilder() - .AddServices(services) - .AddServerSettings(serverSettingsBuilder) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddBotSettings(settingsBuilder); + return (VkBotBuilder)new VkBotBuilder() + .AddBotSettings(settingsBuilder) + .AddServerSettings(serverSettingsBuilder) + .AddServices(services) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder); } } \ No newline at end of file diff --git a/Botticelli.sln b/Botticelli.sln index 2f9d8801..e848acde 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -251,6 +251,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Auth.Sample.Telegram", "Sam EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Auth.Data.Sqlite", "Botticelli.Auth.Data.Sqlite\Botticelli.Auth.Data.Sqlite.csproj", "{D27A078E-1409-4B0B-AD7E-DD4528206049}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Messaging.Sample.Telegram.Standalone", "Messaging.Sample.Telegram.Standalone\Messaging.Sample.Telegram.Standalone.csproj", "{E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -525,6 +527,10 @@ Global {D27A078E-1409-4B0B-AD7E-DD4528206049}.Debug|Any CPU.Build.0 = Debug|Any CPU {D27A078E-1409-4B0B-AD7E-DD4528206049}.Release|Any CPU.ActiveCfg = Release|Any CPU {D27A078E-1409-4B0B-AD7E-DD4528206049}.Release|Any CPU.Build.0 = Release|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -628,6 +634,7 @@ Global {49938A37-DA38-47A0-ADF7-DF2221F7F4A7} = {2AD59714-541D-4794-9216-6EAB25F271DA} {9D1C8062-6388-4713-A81D-8B2F2C2D336D} = {49938A37-DA38-47A0-ADF7-DF2221F7F4A7} {D27A078E-1409-4B0B-AD7E-DD4528206049} = {08C7BAAF-80FD-4AAE-8400-2A2D12D6CF06} + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8} = {DEC72475-9783-4BD9-B31E-F77758269476} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj new file mode 100644 index 00000000..6e75a7b1 --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs new file mode 100644 index 00000000..0d96b838 --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -0,0 +1,30 @@ +using Botticelli.Framework.Commands.Validators; +using Botticelli.Framework.Extensions; +using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Schedule.Quartz.Extensions; +using MessagingSample.Common.Commands; +using MessagingSample.Common.Commands.Processors; +using NLog.Extensions.Logging; +using Telegram.Bot.Types.ReplyMarkups; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddStandaloneTelegramBot(builder.Configuration) + .AddTelegramLayoutsSupport() + .AddLogging(cfg => cfg.AddNLog()) + .AddQuartzScheduler(builder.Configuration); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Build().Run(); \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json b/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json new file mode 100644 index 00000000..59fc630c --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "TelegramBotSample.Standalone": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "", + "applicationUrl": "http://localhost:5076", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/appsettings.json b/Messaging.Sample.Telegram.Standalone/appsettings.json new file mode 100644 index 00000000..ca73374c --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DataAccess": { + "ConnectionString": "database.db;Password=123" + }, + "TelegramBot": { + "timeout": 60, + "useThrottling": false, + "useTestEnvironment": false, + "name": "TestBot" + }, + "AllowedHosts": "*" +} \ No newline at end of file From aaa3d63e2fbe24cd3f0fb2a2795ea80a8e015eba Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 2 May 2025 22:09:00 +0300 Subject: [PATCH 011/101] BOTTICELLI-62: - telegram bot standalone sample --- .../Builders/TelegramStandaloneBotBuilder.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 41 ++++++++++++++++++- .../Builders/VkBotBuilder.cs | 14 +++---- .../Options/BotDataSettings.cs | 2 + .../Options/BotDataSettingsBuilder.cs | 11 +++-- Botticelli.sln | 7 ++++ ...essaging.Sample.Telegram.Standalone.csproj | 39 ++++++++++++++++++ .../Program.cs | 30 ++++++++++++++ .../Properties/launchSettings.json | 15 +++++++ .../appsettings.json | 22 ++++++++++ 10 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj create mode 100644 Messaging.Sample.Telegram.Standalone/Program.cs create mode 100644 Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json create mode 100644 Messaging.Sample.Telegram.Standalone/appsettings.json diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index d4f32782..33cbedc3 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -26,7 +26,7 @@ public static TelegramStandaloneBotBuilder Instance(IServiceCollection ser .AddBotDataAccessSettings(dataAccessSettingsBuilder); public TelegramStandaloneBotBuilder AddBotData( - BotDataSettingsBuilder dataBuilder) + BotDataSettingsBuilder dataBuilder) { var settings = dataBuilder.Build(); diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 020de386..774d5e55 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -18,7 +18,8 @@ public static class ServiceCollectionExtensions { private static readonly BotSettingsBuilder SettingsBuilder = new(); private static readonly ServerSettingsBuilder ServerSettingsBuilder = new(); - + private static readonly BotDataSettingsBuilder BotDataSettingsBuilder = new(); + private static readonly AnalyticsClientSettingsBuilder AnalyticsClientOptionsBuilder = new(); @@ -119,20 +120,58 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se .AddTelegramLayoutsSupport(); } + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) => + AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); + + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot + { + var telegramBotSettings = configuration + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(TelegramBotSettings)}!"); + + var dataAccessSettings = configuration + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(DataAccessSettings)}!"); + + var botDataSettings = configuration + .GetSection(BotDataSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(BotDataSettings)}!"); + + return services.AddStandaloneTelegramBot( + botSettingsBuilder => botSettingsBuilder.Set(telegramBotSettings), + dataAccessSettingsBuilder => dataAccessSettingsBuilder.Set(dataAccessSettings), + botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings) + ); + } + public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, Action> optionsBuilderFunc, Action> dataAccessSettingsBuilderFunc, + Action> botDataSettingsBuilderFunc, Action>? telegramBotBuilderFunc = null) where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); + botDataSettingsBuilderFunc(BotDataSettingsBuilder); var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); var botBuilder = TelegramStandaloneBotBuilder.Instance(services, SettingsBuilder, DataAccessSettingsBuilder) + .AddBotData(BotDataSettingsBuilder) .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs index a689997a..68d631ac 100644 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs @@ -21,7 +21,7 @@ namespace Botticelli.Framework.Vk.Messages.Builders; -public class VkBotBuilder : BotBuilder +public class VkBotBuilder : BotBuilder { private LongPollMessagesProvider? _longPollMessagesProvider; private LongPollMessagesProviderBuilder? _longPollMessagesProviderBuilder; @@ -111,11 +111,11 @@ public static VkBotBuilder Instance(IServiceCollection services, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) { - return new VkBotBuilder() - .AddServices(services) - .AddServerSettings(serverSettingsBuilder) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddBotSettings(settingsBuilder); + return (VkBotBuilder)new VkBotBuilder() + .AddBotSettings(settingsBuilder) + .AddServerSettings(serverSettingsBuilder) + .AddServices(services) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder); } } \ No newline at end of file diff --git a/Botticelli.Framework/Options/BotDataSettings.cs b/Botticelli.Framework/Options/BotDataSettings.cs index 61b70097..cae2877c 100644 --- a/Botticelli.Framework/Options/BotDataSettings.cs +++ b/Botticelli.Framework/Options/BotDataSettings.cs @@ -5,6 +5,8 @@ namespace Botticelli.Framework.Options; /// public class BotDataSettings { + public const string Section = "BotData"; + public string? BotId { get; set; } public string? BotKey { get; set; } } \ No newline at end of file diff --git a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs index 77312b1a..93d5ebc3 100644 --- a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs +++ b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs @@ -1,11 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + namespace Botticelli.Framework.Options; public class BotDataSettingsBuilder where T : BotDataSettings, new() { - private T _settings = new(); + private T? _settings; + + public static BotDataSettingsBuilder Instance() => new(); + - public void Set(T settings) + public void Set(T? settings) { _settings = settings; } @@ -17,7 +22,7 @@ public BotDataSettingsBuilder Set(Action func) return this; } - public T Build() + public T? Build() { return _settings; } diff --git a/Botticelli.sln b/Botticelli.sln index 2f9d8801..e848acde 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -251,6 +251,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Auth.Sample.Telegram", "Sam EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Auth.Data.Sqlite", "Botticelli.Auth.Data.Sqlite\Botticelli.Auth.Data.Sqlite.csproj", "{D27A078E-1409-4B0B-AD7E-DD4528206049}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Messaging.Sample.Telegram.Standalone", "Messaging.Sample.Telegram.Standalone\Messaging.Sample.Telegram.Standalone.csproj", "{E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -525,6 +527,10 @@ Global {D27A078E-1409-4B0B-AD7E-DD4528206049}.Debug|Any CPU.Build.0 = Debug|Any CPU {D27A078E-1409-4B0B-AD7E-DD4528206049}.Release|Any CPU.ActiveCfg = Release|Any CPU {D27A078E-1409-4B0B-AD7E-DD4528206049}.Release|Any CPU.Build.0 = Release|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -628,6 +634,7 @@ Global {49938A37-DA38-47A0-ADF7-DF2221F7F4A7} = {2AD59714-541D-4794-9216-6EAB25F271DA} {9D1C8062-6388-4713-A81D-8B2F2C2D336D} = {49938A37-DA38-47A0-ADF7-DF2221F7F4A7} {D27A078E-1409-4B0B-AD7E-DD4528206049} = {08C7BAAF-80FD-4AAE-8400-2A2D12D6CF06} + {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8} = {DEC72475-9783-4BD9-B31E-F77758269476} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj new file mode 100644 index 00000000..95c9a12c --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + 0.8.0 + Botticelli + Igor Evdokimov + https://github.com/devgopher/botticelli + logo.jpg + https://github.com/devgopher/botticelli + TelegramMessagingSample.Standalone + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + Never + true + Never + + + + + + + + + + + \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs new file mode 100644 index 00000000..0d96b838 --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -0,0 +1,30 @@ +using Botticelli.Framework.Commands.Validators; +using Botticelli.Framework.Extensions; +using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Schedule.Quartz.Extensions; +using MessagingSample.Common.Commands; +using MessagingSample.Common.Commands.Processors; +using NLog.Extensions.Logging; +using Telegram.Bot.Types.ReplyMarkups; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddStandaloneTelegramBot(builder.Configuration) + .AddTelegramLayoutsSupport() + .AddLogging(cfg => cfg.AddNLog()) + .AddQuartzScheduler(builder.Configuration); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +builder.Build().Run(); \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json b/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json new file mode 100644 index 00000000..59fc630c --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "TelegramBotSample.Standalone": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "", + "applicationUrl": "http://localhost:5076", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/appsettings.json b/Messaging.Sample.Telegram.Standalone/appsettings.json new file mode 100644 index 00000000..385e2420 --- /dev/null +++ b/Messaging.Sample.Telegram.Standalone/appsettings.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DataAccess": { + "ConnectionString": "database.db;Password=123" + }, + "TelegramBot": { + "timeout": 60, + "useThrottling": false, + "useTestEnvironment": false, + "name": "TestBot" + }, + "BotData": { + "BotId": "botId", + "BotKey": "botKey" + }, + "AllowedHosts": "*" +} \ No newline at end of file From b955ab9991630b48fda36a707eee46fff5d53671 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 3 May 2025 11:40:08 +0300 Subject: [PATCH 012/101] BOTTICELLI-62: - metrics processing is not mandatory now (for standalone bots) --- .../Builders/TelegramBotBuilder.cs | 44 ++++++++++--------- Botticelli.Framework.Telegram/TelegramBot.cs | 35 +++++++-------- Botticelli.Framework/BaseBot.cs | 14 +++--- Botticelli.Pay.Telegram/TelegramPaymentBot.cs | 8 +--- 4 files changed, 49 insertions(+), 52 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index ae648d5a..2ac3807e 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -86,31 +86,37 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu protected override TBot? InnerBuild() { - Services.AddSingleton(ServerSettingsBuilder!.Build()); + if (BotSettings?.IsStandalone is true) + { + Services.AddSingleton(ServerSettingsBuilder!.Build()); + + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - Services.AddHttpClient() - .AddServerCertificates(BotSettings); - Services.AddHostedService(); + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - Services.AddHttpClient() - .AddServerCertificates(BotSettings); - Services.AddHostedService(); + Services.AddHttpClient>() + .AddServerCertificates(BotSettings); + Services.AddHostedService>>() + .AddHostedService(); + } - Services.AddHttpClient>() - .AddServerCertificates(BotSettings); - Services.AddHostedService>>() - .AddHostedService(); - var botId = BotDataUtils.GetBotId(); if (botId == null) throw new InvalidDataException($"{nameof(botId)} shouldn't be null!"); #region Metrics - var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); - var metricsProcessor = new MetricsProcessor(metricsPublisher); - Services.AddSingleton(metricsPublisher); - Services.AddSingleton(metricsProcessor); + if (BotSettings?.IsStandalone is true) + { + var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); + var metricsProcessor = new MetricsProcessor(metricsPublisher); + Services.AddSingleton(metricsPublisher); + Services.AddSingleton(metricsProcessor); + } #endregion @@ -140,8 +146,6 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu .AddBotticelliFramework() .AddSingleton(); - Services.AddSingleton(ServerSettingsBuilder.Build()); - var sp = Services.BuildServiceProvider(); ApplyMigrations(sp); @@ -151,9 +155,9 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu client, sp.GetRequiredService(), sp.GetRequiredService>(), - sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService()) as TBot; + sp.GetRequiredService(), + sp.GetService()) as TBot; } protected TelegramBotBuilder AddBotSettings( diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index a2ae9d6a..7ac37a96 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -36,11 +36,11 @@ public class TelegramBot : BaseBot // ReSharper disable once MemberCanBeProtected.Global public TelegramBot(ITelegramBotClient client, - IBotUpdateHandler handler, - ILogger logger, - MetricsProcessor metrics, - ITextTransformer textTransformer, - IBotDataAccess data) : base(logger, metrics) + IBotUpdateHandler handler, + ILogger logger, + ITextTransformer textTransformer, + IBotDataAccess data, + MetricsProcessor? metrics) : base(logger, metrics) { BotStatusKeeper.IsStarted = false; Client = client; @@ -186,7 +186,7 @@ await ProcessText(request, link); if (request.Message.Poll != null) - message = await ProcessPoll(request, + message = await ProcessPoll(request, token, link, replyMarkup, @@ -386,11 +386,11 @@ await ProcessContact(request, return message; } - protected async Task ProcessPoll(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response) + protected async Task ProcessPoll(SendMessageRequest request, + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response) { Message? message; @@ -402,7 +402,7 @@ await ProcessContact(request, { Poll.PollType.Quiz => PollType.Quiz, Poll.PollType.Regular => PollType.Regular, - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(paramName: nameof(Type)) }; message = await Client.SendPoll(link.chatId, @@ -593,14 +593,11 @@ public override async Task SetBotContext(BotData.Entities.Bot.BotData? context, await StartBot(token); } - else if (currentContext != null) + else if (currentContext != null && Client.BotId == 0) { - if (Client.BotId == default) - { - await StopBot(token); - RecreateClient(context.BotKey!); - await StartBot(token); - } + await StopBot(token); + RecreateClient(context.BotKey!); + await StartBot(token); } } } \ No newline at end of file diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index c5fdc2c9..8ccd352d 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -39,10 +39,10 @@ public abstract class BaseBot : BaseBot, IBot { public delegate void MessengerSpecificEventHandler(object sender, MessengerSpecificBotEventArgs e); - private readonly MetricsProcessor _metrics; + private readonly MetricsProcessor? _metrics; protected readonly ILogger Logger; - protected BaseBot(ILogger logger, MetricsProcessor metrics) + protected BaseBot(ILogger logger, MetricsProcessor? metrics) { Logger = logger; _metrics = metrics; @@ -53,7 +53,7 @@ public virtual async Task StartBotAsync(StartBotRequest reques if (BotStatusKeeper.IsStarted) return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); - _metrics.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); var result = await InnerStartBotAsync(request, token); @@ -64,7 +64,7 @@ public virtual async Task StartBotAsync(StartBotRequest reques public virtual async Task StopBotAsync(StopBotRequest request, CancellationToken token) { - _metrics.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); if (!BotStatusKeeper.IsStarted) return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); @@ -100,7 +100,7 @@ public virtual async Task SendMessageAsync(Se CancellationToken token) where TSendOptions : class { - _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, optionsBuilder, @@ -116,7 +116,7 @@ public async Task UpdateMessageAsync(SendMess CancellationToken token) where TSendOptions : class { - _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, optionsBuilder, @@ -127,7 +127,7 @@ public async Task UpdateMessageAsync(SendMess public virtual async Task DeleteMessageAsync(RemoveMessageRequest request, CancellationToken token) { - _metrics.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); return await InnerDeleteMessageAsync(request, token); } diff --git a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs index aa15b03a..e690275d 100644 --- a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs +++ b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs @@ -23,9 +23,8 @@ public TelegramPaymentBot(ITelegramBotClient client, IBotDataAccess data) : base(client, handler, logger, - metrics, textTransformer, - data) + data, metrics) { } @@ -50,8 +49,5 @@ await Client.SendInvoice(chatId, cancellationToken: token); } - private int ConvertPrice(decimal price, Currency currency) - { - return Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); - } + private static int ConvertPrice(decimal price, Currency currency) => Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); } \ No newline at end of file From 49bc9bea38e036895682b00ab80d38b2bf925b73 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 3 May 2025 11:40:08 +0300 Subject: [PATCH 013/101] BOTTICELLI-62: - metrics processing is not mandatory now (for standalone bots) --- .../InlineCalendar/ICCommandProcessor.cs | 3 +- .../Commands/Processors/ChainRunProcessor.cs | 3 +- .../Builders/TelegramBotBuilder.cs | 44 ++--- Botticelli.Framework.Telegram/TelegramBot.cs | 35 ++-- Botticelli.Framework/BaseBot.cs | 14 +- .../Processors/CommandChainProcessor.cs | 3 +- .../Commands/Processors/CommandProcessor.cs | 33 ++-- ...tForClientResponseCommandChainProcessor.cs | 3 +- .../Services/BotActualizationService.cs | 26 ++- .../Services/BotStandaloneService.cs | 2 - .../Services/BotStatusService.cs | 3 +- .../Services/PollActualizationService.cs | 3 +- .../FindLocationsCommandProcessor.cs | 3 +- .../CommandProcessors/MapCommandProcessor.cs | 3 +- Botticelli.Pay.Telegram/TelegramPaymentBot.cs | 8 +- .../Program.cs | 2 +- .../appsettings.json | 2 +- .../Processors/InfoCommandProcessor.cs | 3 +- .../Processors/SendInvoiceCommandProcessor.cs | 3 +- .../Ai.Common.Sample/AiCommandProcessor.cs | 3 +- .../Processors/InfoCommandProcessor.cs | 3 +- .../Processors/RegisterCommandProcessor.cs | 3 +- .../Processors/StartCommandProcessor.cs | 3 +- .../Handlers/DateChosenCommandProcessor.cs | 3 +- .../Handlers/GetCalendarCommandProcessor.cs | 3 +- .../Processors/InfoCommandProcessor.cs | 32 +++- .../Processors/StartCommandProcessor.cs | 151 ++++++++++-------- .../Processors/StopCommandProcessor.cs | 37 +++-- 28 files changed, 244 insertions(+), 190 deletions(-) diff --git a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs index dcbf59d8..6de3a30b 100644 --- a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs +++ b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs @@ -31,8 +31,7 @@ public ICCommandProcessor(ILogger> lo IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { _layoutSupplier = layoutSupplier; } diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs index 0b1f815d..8c34a45f 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs @@ -17,8 +17,7 @@ public class ChainRunProcessor( IValidator messageValidator) : CommandProcessor(logger, validator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) where TCommand : class, IChainCommand, new() { protected override async Task InnerProcess(Message message, CancellationToken token) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index ae648d5a..2ac3807e 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -86,31 +86,37 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu protected override TBot? InnerBuild() { - Services.AddSingleton(ServerSettingsBuilder!.Build()); + if (BotSettings?.IsStandalone is true) + { + Services.AddSingleton(ServerSettingsBuilder!.Build()); + + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - Services.AddHttpClient() - .AddServerCertificates(BotSettings); - Services.AddHostedService(); + Services.AddHttpClient() + .AddServerCertificates(BotSettings); + Services.AddHostedService(); - Services.AddHttpClient() - .AddServerCertificates(BotSettings); - Services.AddHostedService(); + Services.AddHttpClient>() + .AddServerCertificates(BotSettings); + Services.AddHostedService>>() + .AddHostedService(); + } - Services.AddHttpClient>() - .AddServerCertificates(BotSettings); - Services.AddHostedService>>() - .AddHostedService(); - var botId = BotDataUtils.GetBotId(); if (botId == null) throw new InvalidDataException($"{nameof(botId)} shouldn't be null!"); #region Metrics - var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); - var metricsProcessor = new MetricsProcessor(metricsPublisher); - Services.AddSingleton(metricsPublisher); - Services.AddSingleton(metricsProcessor); + if (BotSettings?.IsStandalone is true) + { + var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); + var metricsProcessor = new MetricsProcessor(metricsPublisher); + Services.AddSingleton(metricsPublisher); + Services.AddSingleton(metricsProcessor); + } #endregion @@ -140,8 +146,6 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu .AddBotticelliFramework() .AddSingleton(); - Services.AddSingleton(ServerSettingsBuilder.Build()); - var sp = Services.BuildServiceProvider(); ApplyMigrations(sp); @@ -151,9 +155,9 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu client, sp.GetRequiredService(), sp.GetRequiredService>(), - sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService()) as TBot; + sp.GetRequiredService(), + sp.GetService()) as TBot; } protected TelegramBotBuilder AddBotSettings( diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index a2ae9d6a..7ac37a96 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -36,11 +36,11 @@ public class TelegramBot : BaseBot // ReSharper disable once MemberCanBeProtected.Global public TelegramBot(ITelegramBotClient client, - IBotUpdateHandler handler, - ILogger logger, - MetricsProcessor metrics, - ITextTransformer textTransformer, - IBotDataAccess data) : base(logger, metrics) + IBotUpdateHandler handler, + ILogger logger, + ITextTransformer textTransformer, + IBotDataAccess data, + MetricsProcessor? metrics) : base(logger, metrics) { BotStatusKeeper.IsStarted = false; Client = client; @@ -186,7 +186,7 @@ await ProcessText(request, link); if (request.Message.Poll != null) - message = await ProcessPoll(request, + message = await ProcessPoll(request, token, link, replyMarkup, @@ -386,11 +386,11 @@ await ProcessContact(request, return message; } - protected async Task ProcessPoll(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response) + protected async Task ProcessPoll(SendMessageRequest request, + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response) { Message? message; @@ -402,7 +402,7 @@ await ProcessContact(request, { Poll.PollType.Quiz => PollType.Quiz, Poll.PollType.Regular => PollType.Regular, - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(paramName: nameof(Type)) }; message = await Client.SendPoll(link.chatId, @@ -593,14 +593,11 @@ public override async Task SetBotContext(BotData.Entities.Bot.BotData? context, await StartBot(token); } - else if (currentContext != null) + else if (currentContext != null && Client.BotId == 0) { - if (Client.BotId == default) - { - await StopBot(token); - RecreateClient(context.BotKey!); - await StartBot(token); - } + await StopBot(token); + RecreateClient(context.BotKey!); + await StartBot(token); } } } \ No newline at end of file diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index c5fdc2c9..8ccd352d 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -39,10 +39,10 @@ public abstract class BaseBot : BaseBot, IBot { public delegate void MessengerSpecificEventHandler(object sender, MessengerSpecificBotEventArgs e); - private readonly MetricsProcessor _metrics; + private readonly MetricsProcessor? _metrics; protected readonly ILogger Logger; - protected BaseBot(ILogger logger, MetricsProcessor metrics) + protected BaseBot(ILogger logger, MetricsProcessor? metrics) { Logger = logger; _metrics = metrics; @@ -53,7 +53,7 @@ public virtual async Task StartBotAsync(StartBotRequest reques if (BotStatusKeeper.IsStarted) return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); - _metrics.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); var result = await InnerStartBotAsync(request, token); @@ -64,7 +64,7 @@ public virtual async Task StartBotAsync(StartBotRequest reques public virtual async Task StopBotAsync(StopBotRequest request, CancellationToken token) { - _metrics.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); if (!BotStatusKeeper.IsStarted) return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); @@ -100,7 +100,7 @@ public virtual async Task SendMessageAsync(Se CancellationToken token) where TSendOptions : class { - _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, optionsBuilder, @@ -116,7 +116,7 @@ public async Task UpdateMessageAsync(SendMess CancellationToken token) where TSendOptions : class { - _metrics.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, optionsBuilder, @@ -127,7 +127,7 @@ public async Task UpdateMessageAsync(SendMess public virtual async Task DeleteMessageAsync(RemoveMessageRequest request, CancellationToken token) { - _metrics.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); + _metrics?.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); return await InnerDeleteMessageAsync(request, token); } diff --git a/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs index e4e4f61e..60f06801 100644 --- a/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs @@ -21,8 +21,7 @@ public CommandChainProcessor(ILogger> logge IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { } diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index 51071553..6628aabd 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -18,14 +18,24 @@ public abstract class CommandProcessor : ICommandProcessor private readonly string _command; private readonly ICommandValidator _commandValidator; private readonly IValidator _messageValidator; - private readonly MetricsProcessor _metricsProcessor; + private readonly MetricsProcessor? _metricsProcessor; protected readonly ILogger Logger; protected IBot Bot; protected CommandProcessor(ILogger logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) + ICommandValidator commandValidator, + IValidator messageValidator) + { + Logger = logger; + _commandValidator = commandValidator; + _messageValidator = messageValidator; + _command = GetOldFashionedCommandName(typeof(TCommand).Name); + } + + protected CommandProcessor(ILogger logger, + ICommandValidator commandValidator, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) { Logger = logger; _commandValidator = commandValidator; @@ -42,8 +52,9 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) if (!messageValidationResult.IsValid) { - _metricsProcessor.Process(MetricNames.BotError, BotDataUtils.GetBotId()); - Logger.LogError($"Error in {GetType().Name} invalid input message: {messageValidationResult.Errors.Select(e => $"({e.PropertyName} : {e.ErrorCode} : {e.ErrorMessage})")}"); + _metricsProcessor?.Process(MetricNames.BotError, BotDataUtils.GetBotId()); + Logger.LogError($"Error in {GetType().Name} invalid input message:" + + $" {messageValidationResult.Errors.Select(e => $"({e.PropertyName} : {e.ErrorCode} : {e.ErrorMessage})")}"); return; } @@ -66,7 +77,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) message.Poll == null && message.CallbackData == null) { - Logger.LogWarning("Message {msgId} is empty! Skipping...", message.Uid); + Logger.LogWarning("Message {MsgId} is empty! Skipping...", message.Uid); return; } @@ -117,7 +128,7 @@ await ValidateAndProcess(message, } catch (Exception ex) { - _metricsProcessor.Process(MetricNames.BotError, BotDataUtils.GetBotId()); + _metricsProcessor?.Process(MetricNames.BotError, BotDataUtils.GetBotId()); Logger.LogError(ex, $"Error in {GetType().Name}: {ex.Message}"); await InnerProcessError(message, ex, token); @@ -134,7 +145,7 @@ public void SetServiceProvider(IServiceProvider sp) { } - protected void Classify(ref Message message) + protected static void Classify(ref Message message) { var body = GetBody(message); @@ -155,12 +166,12 @@ private static string GetBody(Message message) private void SendMetric(string metricName) { - _metricsProcessor.Process(metricName, BotDataUtils.GetBotId()!); + _metricsProcessor?.Process(metricName, BotDataUtils.GetBotId()!); } private void SendMetric() { - _metricsProcessor.Process(GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), BotDataUtils.GetBotId()!); + _metricsProcessor?.Process(GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), BotDataUtils.GetBotId()!); } private string GetOldFashionedCommandName(string fullCommand) diff --git a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs index 58cdef7d..9114c96e 100644 --- a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs @@ -21,8 +21,7 @@ protected WaitForClientResponseCommandChainProcessor(ILogger messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { } diff --git a/Botticelli.Framework/Services/BotActualizationService.cs b/Botticelli.Framework/Services/BotActualizationService.cs index d23bf96b..cccfefbe 100644 --- a/Botticelli.Framework/Services/BotActualizationService.cs +++ b/Botticelli.Framework/Services/BotActualizationService.cs @@ -19,19 +19,35 @@ public abstract class BotActualizationService : IHostedService protected readonly string? BotId = BotDataUtils.GetBotId(); protected readonly IHttpClientFactory HttpClientFactory; protected readonly ILogger Logger; - protected readonly ServerSettings ServerSettings; + protected readonly ServerSettings? ServerSettings; /// /// This service is intended for sending keepalive/hello messages /// to Botticelli Admin server and receiving status messages from it /// protected BotActualizationService(IHttpClientFactory httpClientFactory, - ServerSettings serverSettings, - IBot bot, - ILogger logger) + IBot bot, + ILogger logger) { HttpClientFactory = httpClientFactory; + Bot = bot; + Logger = logger; + + ActualizationEvent.Reset(); + } + + + /// + /// This service is intended for sending keepalive/hello messages + /// to Botticelli Admin server and receiving status messages from it + /// + protected BotActualizationService(IHttpClientFactory httpClientFactory, + IBot bot, + ILogger logger, + ServerSettings? serverSettings) + { ServerSettings = serverSettings; + HttpClientFactory = httpClientFactory; Bot = bot; Logger = logger; @@ -68,7 +84,7 @@ public virtual Task StopAsync(CancellationToken cancellationToken) Logger.LogDebug("InnerSend request: {Request}", request); - var response = await httpClient.PostAsync(Url.Combine(ServerSettings.ServerUri, funcName), + var response = await httpClient.PostAsync(Url.Combine(ServerSettings?.ServerUri, funcName), content, cancellationToken); diff --git a/Botticelli.Framework/Services/BotStandaloneService.cs b/Botticelli.Framework/Services/BotStandaloneService.cs index 8ccd8c4a..3015acd4 100644 --- a/Botticelli.Framework/Services/BotStandaloneService.cs +++ b/Botticelli.Framework/Services/BotStandaloneService.cs @@ -7,12 +7,10 @@ namespace Botticelli.Framework.Services; public class BotStandaloneService( IHttpClientFactory httpClientFactory, - ServerSettings serverSettings, BotData.Entities.Bot.BotData botData, IBot bot, ILogger logger) : BotActualizationService(httpClientFactory, - serverSettings, bot, logger) { diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 87548d2c..071cad0f 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -18,9 +18,8 @@ public class BotStatusService( IBot bot, ILogger logger) : BotActualizationService(httpClientFactory, - serverSettings, bot, - logger) + logger, serverSettings) { private const short GetStatusPeriod = 5000; private Task? _getRequiredStatusEventTask; diff --git a/Botticelli.Framework/Services/PollActualizationService.cs b/Botticelli.Framework/Services/PollActualizationService.cs index 8cdca0d0..e5057d07 100644 --- a/Botticelli.Framework/Services/PollActualizationService.cs +++ b/Botticelli.Framework/Services/PollActualizationService.cs @@ -16,9 +16,8 @@ public class PollActualizationService( IBot bot, ILogger logger) : BotActualizationService(httpClientFactory, - serverSettings, bot, - logger) + logger, serverSettings) where TRequest : IBotRequest, new() { private const short ActionPeriod = 5000; diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 9a78db31..6ae155c7 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -23,8 +23,7 @@ public class FindLocationsCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) where TReplyMarkup : class { protected override async Task InnerProcess(Message message, CancellationToken token) diff --git a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs index 292a1b29..f1d6ae1b 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs @@ -18,8 +18,7 @@ public MapCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { } diff --git a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs index aa15b03a..e690275d 100644 --- a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs +++ b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs @@ -23,9 +23,8 @@ public TelegramPaymentBot(ITelegramBotClient client, IBotDataAccess data) : base(client, handler, logger, - metrics, textTransformer, - data) + data, metrics) { } @@ -50,8 +49,5 @@ await Client.SendInvoice(chatId, cancellationToken: token); } - private int ConvertPrice(decimal price, Currency currency) - { - return Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); - } + private static int ConvertPrice(decimal price, Currency currency) => Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); } \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs index 0d96b838..71bc226a 100644 --- a/Messaging.Sample.Telegram.Standalone/Program.cs +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -27,4 +27,4 @@ .AddProcessor>() .AddValidator>(); -builder.Build().Run(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/appsettings.json b/Messaging.Sample.Telegram.Standalone/appsettings.json index 385e2420..259aa54d 100644 --- a/Messaging.Sample.Telegram.Standalone/appsettings.json +++ b/Messaging.Sample.Telegram.Standalone/appsettings.json @@ -16,7 +16,7 @@ }, "BotData": { "BotId": "botId", - "BotKey": "botKey" + "BotKey": "6302967551:AAHR7o8zELeLU5XK3brXQyyqtpMoYkvrAI0" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 6d6200b1..5349505e 100644 --- a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -14,8 +14,7 @@ public class InfoCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) where TReplyMarkup : class { protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; diff --git a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs index d3736d6c..6d6f49be 100644 --- a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs @@ -23,8 +23,7 @@ public SendInvoiceCommandProcessor(ILogger paySettingsAccessor) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { _paySettingsAccessor = paySettingsAccessor; } diff --git a/Samples/Ai.Common.Sample/AiCommandProcessor.cs b/Samples/Ai.Common.Sample/AiCommandProcessor.cs index 00753e7a..921b64c3 100644 --- a/Samples/Ai.Common.Sample/AiCommandProcessor.cs +++ b/Samples/Ai.Common.Sample/AiCommandProcessor.cs @@ -27,8 +27,7 @@ public AiCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { _bus = bus; var responseLayout = new AiLayout(); diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index a21deceb..2eb75d9a 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -18,8 +18,7 @@ public InfoCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { } diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs index fc79dad2..b773422f 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs @@ -29,8 +29,7 @@ public class RegisterCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) where TReplyMarkup : class { protected override Task InnerProcessContact(Message message, CancellationToken token) diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs index 93bb3609..4bf14ac4 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs @@ -30,8 +30,7 @@ public StartCommandProcessor(ILogger> logger IManager roleManager) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { _userInfo = userInfo; _roleManager = roleManager; diff --git a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs index 769766c7..3522b45b 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs @@ -18,8 +18,7 @@ public class DateChosenCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { protected override async Task InnerProcess(Message message, CancellationToken token) { diff --git a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs index c9e71657..f648d123 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs @@ -27,8 +27,7 @@ public GetCalendarCommandProcessor(IBot bot, IValidator messageValidator) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, metricsProcessor) { _bot = bot; diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index a0358201..e389aacf 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -16,15 +16,31 @@ public class InfoCommandProcessor : CommandProcessor private readonly SendOptionsBuilder? _options; public InfoCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) + ICommandValidator commandValidator, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) + { + var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; + var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); + var responseMarkup = layoutSupplier.GetMarkup(responseLayout); + + _options = SendOptionsBuilder.CreateBuilder(responseMarkup); + } + + public InfoCommandProcessor(ILogger> logger, + ICommandValidator commandValidator, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, + metricsProcessor) { var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); @@ -60,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot?.SendMessageAsync(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks + await Bot.SendMessageAsync(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index 5a9ebcda..0613e020 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -20,41 +20,54 @@ public class StartCommandProcessor : CommandProcessor? _options; public StartCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) - : base(logger, - commandValidator, - metricsProcessor, - messageValidator) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) { _jobManager = jobManager; - var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; - var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); - var responseMarkup = layoutSupplier.GetMarkup(responseLayout); + var responseMarkup = Init(layoutSupplier, layoutParser); _options = SendOptionsBuilder.CreateBuilder(responseMarkup); } - protected override Task InnerProcessContact(Message message, CancellationToken token) + public StartCommandProcessor(ILogger> logger, + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) + : base(logger, + commandValidator, + messageValidator, metricsProcessor) { - return Task.CompletedTask; - } + _jobManager = jobManager; - protected override Task InnerProcessPoll(Message message, CancellationToken token) - { - return Task.CompletedTask; + var responseMarkup = Init(layoutSupplier, layoutParser); + + _options = SendOptionsBuilder.CreateBuilder(responseMarkup); } - protected override Task InnerProcessLocation(Message message, CancellationToken token) + private static TReplyMarkup Init(ILayoutSupplier layoutSupplier, ILayoutParser layoutParser) { - return Task.CompletedTask; + var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; + var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); + var responseMarkup = layoutSupplier.GetMarkup(responseLayout); + return responseMarkup; } + protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; + + protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + + protected override Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; + protected override async Task InnerProcess(Message message, CancellationToken token) { var chatId = message.ChatIds.First(); @@ -73,53 +86,53 @@ protected override async Task InnerProcess(Message message, CancellationToken to var assemblyPath = Path.GetDirectoryName(typeof(StartCommandProcessor).Assembly.Location) ?? throw new FileNotFoundException(); _jobManager.AddJob(Bot, - new Reliability - { - IsEnabled = false, - Delay = TimeSpan.FromSeconds(3), - IsExponential = true, - MaxTries = 5 - }, - new Message - { - Body = "Now you see me!", - ChatIds = [chatId], - Contact = new Contact - { - Phone = "+9003289384923842343243243", - Name = "Test", - Surname = "Botticelli" - }, - Attachments = - [ - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "testpic.png", - MediaType.Image, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "voice.mp3", - MediaType.Voice, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "video.mp4", - MediaType.Video, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "document.odt", - MediaType.Document, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) - ] - }, - new Schedule - { - Cron = "*/30 * * ? * * *" - }); + new Reliability + { + IsEnabled = false, + Delay = TimeSpan.FromSeconds(3), + IsExponential = true, + MaxTries = 5 + }, + new Message + { + Body = "Now you see me!", + ChatIds = [chatId], + Contact = new Contact + { + Phone = "+9003289384923842343243243", + Name = "Test", + Surname = "Botticelli" + }, + Attachments = + [ + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "testpic.png", + MediaType.Image, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "voice.mp3", + MediaType.Voice, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "video.mp4", + MediaType.Video, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "document.odt", + MediaType.Document, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) + ] + }, + new Schedule + { + Cron = "*/30 * * ? * * *" + }); } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs index bf30433f..1019abed 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs @@ -16,21 +16,40 @@ public class StopCommandProcessor : CommandProcessor where TReplyMarkup : class { private readonly IJobManager _jobManager; - private readonly SendOptionsBuilder? _options; + private SendOptionsBuilder? _options; public StopCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) + { + _jobManager = jobManager; + Init(layoutSupplier, layoutParser); + } + + public StopCommandProcessor(ILogger> logger, + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) : base(logger, commandValidator, - metricsProcessor, - messageValidator) + messageValidator, + metricsProcessor) { _jobManager = jobManager; + Init(layoutSupplier, layoutParser); + } + + private void Init(ILayoutSupplier layoutSupplier, ILayoutParser layoutParser) + { var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "start_layout.json")); var responseMarkup = layoutSupplier.GetMarkup(responseLayout); From 9ba97b2473975a7f4f7af935bffaf385acbb74ee Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 3 May 2025 21:49:19 +0300 Subject: [PATCH 014/101] - fix --- .../Extensions/ServiceCollectionExtensions.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index 6c465ffc..b6df747a 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -58,25 +58,21 @@ public static IServiceCollection AddTelegramPayBot(this IS return services.AddTelegramBot(configuration, o => o.AddSubHandler() .AddSubHandler()); } - + /// /// Adds a standalone Telegram bot with a payment function /// /// - /// - /// + /// /// public static IServiceCollection AddStandaloneTelegramPayBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> dataAccessSettingsBuilderFunc) + IConfiguration configuration) where THandler : IPreCheckoutHandler, new() where TProcessor : IPayProcessor { services.AddPayments(); - return services.AddStandaloneTelegramBot( - optionsBuilderFunc, - dataAccessSettingsBuilderFunc, + return services.AddStandaloneTelegramBot(configuration, o => o .AddSubHandler() .AddSubHandler()); From 4eb1ee395c7adadf1ebfe7f25414a165cfbb942c Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 3 May 2025 22:54:00 +0300 Subject: [PATCH 015/101] CommandProcessor: Send and update messages were incapsulated --- .idea/.idea.Botticelli/.idea/vcs.xml | 1 + .../InlineCalendar/ICCommandProcessor.cs | 14 +-- .../Commands/Processors/CommandProcessor.cs | 114 ++++++++++++------ ...tForClientResponseCommandChainProcessor.cs | 22 ++-- .../FindLocationsCommandProcessor.cs | 2 +- .../CommandProcessors/MapCommandProcessor.cs | 2 +- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/SendInvoiceCommandProcessor.cs | 2 +- .../Ai.Common.Sample/AiCommandProcessor.cs | 18 +-- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/RegisterCommandProcessor.cs | 4 +- .../Processors/StartCommandProcessor.cs | 2 +- .../GetNameCommandProcessor.cs | 6 +- .../SayHelloFinalCommandProcessor.cs | 6 +- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/StartCommandProcessor.cs | 107 ++++++++-------- .../Processors/StopCommandProcessor.cs | 2 +- 17 files changed, 168 insertions(+), 140 deletions(-) diff --git a/.idea/.idea.Botticelli/.idea/vcs.xml b/.idea/.idea.Botticelli/.idea/vcs.xml index 3672fbf7..9d91c1f7 100644 --- a/.idea/.idea.Botticelli/.idea/vcs.xml +++ b/.idea/.idea.Botticelli/.idea/vcs.xml @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs index 6de3a30b..b0cedcba 100644 --- a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs +++ b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs @@ -56,18 +56,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to var responseMarkup = _layoutSupplier.GetMarkup(calendar); var options = SendOptionsBuilder.CreateBuilder(responseMarkup); - await Bot.UpdateMessageAsync(new SendMessageRequest - { - ExpectPartialResponse = false, - Message = new Message - { - Body = message.CallbackData, - Uid = message.Uid, - ChatIds = message.ChatIds, - ChatIdInnerIdLinks = message.ChatIdInnerIdLinks - } - }, - options, - token); + await UpdateMessage(message, options, token); } } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index 6628aabd..1b7dfa1e 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -4,6 +4,7 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; +using Botticelli.Framework.SendOptions; using Botticelli.Interfaces; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; @@ -13,14 +14,14 @@ namespace Botticelli.Framework.Commands.Processors; public abstract class CommandProcessor : ICommandProcessor - where TCommand : class, ICommand + where TCommand : class, ICommand { private readonly string _command; private readonly ICommandValidator _commandValidator; private readonly IValidator _messageValidator; private readonly MetricsProcessor? _metricsProcessor; protected readonly ILogger Logger; - protected IBot Bot; + private IBot? _bot; protected CommandProcessor(ILogger logger, ICommandValidator commandValidator, @@ -31,7 +32,7 @@ protected CommandProcessor(ILogger logger, _messageValidator = messageValidator; _command = GetOldFashionedCommandName(typeof(TCommand).Name); } - + protected CommandProcessor(ILogger logger, ICommandValidator commandValidator, IValidator messageValidator, @@ -66,7 +67,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) return; } - if (message.From!.Id!.Equals(Bot.BotUserId, StringComparison.InvariantCulture)) return; + if (message.From!.Id!.Equals(_bot?.BotUserId, StringComparison.InvariantCulture)) return; Classify(ref message); @@ -88,7 +89,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) if (CommandUtils.SimpleCommandRegex.IsMatch(body)) { var match = CommandUtils.SimpleCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -103,7 +104,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) else if (CommandUtils.ArgsCommandRegex.IsMatch(body)) { var match = CommandUtils.ArgsCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -119,7 +120,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) { if (GetType().IsAssignableTo(typeof(CommandChainProcessor))) await ValidateAndProcess(message, - token); + token); } if (message.Location != null) await InnerProcessLocation(message, token); @@ -138,7 +139,7 @@ await ValidateAndProcess(message, public virtual void SetBot(IBot bot) { - Bot = bot; + _bot = bot; } public void SetServiceProvider(IServiceProvider sp) @@ -149,20 +150,16 @@ protected static void Classify(ref Message message) { var body = GetBody(message); - if (CommandUtils.SimpleCommandRegex.IsMatch(body)) - message.Type = Message.MessageType.Command; - else if (CommandUtils.ArgsCommandRegex.IsMatch(body)) + if (CommandUtils.SimpleCommandRegex.IsMatch(body) || CommandUtils.ArgsCommandRegex.IsMatch(body)) message.Type = Message.MessageType.Command; else message.Type = Message.MessageType.Messaging; } - private static string GetBody(Message message) - { - return !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData - : !string.IsNullOrWhiteSpace(message.Body) ? message.Body - : string.Empty; - } + private static string GetBody(Message message) => + !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData + : !string.IsNullOrWhiteSpace(message.Body) ? message.Body + : string.Empty; private void SendMetric(string metricName) { @@ -171,17 +168,20 @@ private void SendMetric(string metricName) private void SendMetric() { - _metricsProcessor?.Process(GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), BotDataUtils.GetBotId()!); + _metricsProcessor?.Process( + GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), + BotDataUtils.GetBotId()!); } - private string GetOldFashionedCommandName(string fullCommand) - { - return fullCommand.ToLowerInvariant().Replace("command", ""); - } + private string GetOldFashionedCommandName(string fullCommand) => + fullCommand.ToLowerInvariant().Replace("command", ""); private async Task ValidateAndProcess(Message message, - CancellationToken token) + CancellationToken token) { + if (_bot == null) + return; + if (message.Type == Message.MessageType.Messaging) { SendMetric(); @@ -206,29 +206,73 @@ private async Task ValidateAndProcess(Message message, } }; - await Bot.SendMessageAsync(errMessageRequest, token); + await SendMessage(errMessageRequest, token); } } - protected virtual Task InnerProcessContact(Message message, CancellationToken token) + + protected async Task SendMessage(Message message, CancellationToken token) { - return Task.CompletedTask; - } + if (_bot == null) + return; - protected virtual Task InnerProcessPoll(Message message, CancellationToken token) + var request = new SendMessageRequest + { + Message = message + }; + + await SendMessage(request, token); + } + + protected async Task SendMessage(SendMessageRequest request, CancellationToken token) { - return Task.CompletedTask; + if (_bot == null) + return; + + await _bot.SendMessageAsync(request, token); } - protected virtual Task InnerProcessLocation(Message message, CancellationToken token) + protected async Task SendMessage(SendMessageRequest request, + SendOptionsBuilder? options, CancellationToken token) + where TReplyMarkup : class { - return Task.CompletedTask; + if (_bot == null) + return; + + await _bot.SendMessageAsync(request, options, token); } + + protected async Task UpdateMessage(Message message, ISendOptionsBuilder? options, + CancellationToken token) + where TSendOptions : class + { + if (_bot == null) + return; + + await _bot.UpdateMessageAsync(new SendMessageRequest + { + ExpectPartialResponse = false, + Message = new Message + { + Body = message.CallbackData, + Uid = message.Uid, + ChatIds = message.ChatIds, + ChatIdInnerIdLinks = message.ChatIdInnerIdLinks + } + }, + options, + token); + } + + + protected virtual Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; + + protected virtual Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + + protected virtual Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; protected abstract Task InnerProcess(Message message, CancellationToken token); - protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) - { - return Task.CompletedTask; - } + protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) => + Task.CompletedTask; } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs index 9114c96e..9fd65b07 100644 --- a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs @@ -1,6 +1,5 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Validators; -using Botticelli.Interfaces; using Botticelli.Shared.ValueObjects; using FluentValidation; using Microsoft.Extensions.Logging; @@ -12,26 +11,21 @@ namespace Botticelli.Framework.Commands.Processors; /// /// public abstract class WaitForClientResponseCommandChainProcessor : CommandProcessor, - ICommandChainProcessor - where TInputCommand : class, ICommand + ICommandChainProcessor + where TInputCommand : class, ICommand { protected WaitForClientResponseCommandChainProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator, metricsProcessor) + ICommandValidator commandValidator, + MetricsProcessor metricsProcessor, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator, metricsProcessor) { } private TimeSpan Timeout { get; } = TimeSpan.FromMinutes(10); - public virtual void SetBot(IBot bot) - { - Bot = bot; - } - public override async Task ProcessAsync(Message message, CancellationToken token) { // filters 'not our' chains diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 6ae155c7..98b08407 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -62,6 +62,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(request, replyOptions, token); + await SendMessage(request, replyOptions, token); } } \ No newline at end of file diff --git a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs index f1d6ae1b..a99b832c 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs @@ -34,6 +34,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(request, token); + await SendMessage(request, token); } } \ No newline at end of file diff --git a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 5349505e..efd4d830 100644 --- a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -35,6 +35,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, token); + await SendMessage(greetingMessageRequest, token); } } \ No newline at end of file diff --git a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs index 6d6f49be..8c4a854a 100644 --- a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs @@ -76,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot?.SendMessageAsync(sendInvoiceMessageRequest, token)!; // TODO: think about Bot mocks + await SendMessage(sendInvoiceMessageRequest, token); } } \ No newline at end of file diff --git a/Samples/Ai.Common.Sample/AiCommandProcessor.cs b/Samples/Ai.Common.Sample/AiCommandProcessor.cs index 921b64c3..1fb39715 100644 --- a/Samples/Ai.Common.Sample/AiCommandProcessor.cs +++ b/Samples/Ai.Common.Sample/AiCommandProcessor.cs @@ -37,15 +37,15 @@ public AiCommandProcessor(ILogger> logger, _bus.OnReceived += async (sender, response) => { - await Bot.SendMessageAsync(new SendMessageRequest(response.Uid) - { - Message = response.Message, - ExpectPartialResponse = response.IsPartial, - SequenceNumber = response.SequenceNumber, - IsFinal = response.IsFinal - }, - options, - CancellationToken.None); + await SendMessage(new SendMessageRequest(response.Uid) + { + Message = response.Message, + ExpectPartialResponse = response.IsPartial, + SequenceNumber = response.SequenceNumber, + IsFinal = response.IsFinal + }, + options, + CancellationToken.None); }; } diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 2eb75d9a..70f2d883 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -49,6 +49,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot?.SendMessageAsync(greetingMessageRequest, token)!; + await SendMessage(greetingMessageRequest, token); } } \ No newline at end of file diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs index b773422f..859f135c 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs @@ -68,7 +68,7 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(alreadyRegisteredRequest, token); + await SendMessage(alreadyRegisteredRequest, token); return; } @@ -102,7 +102,7 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(registeredRequest, token)!; + await SendMessage(registeredRequest, token); } private async Task GetUserRole() diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs index 4bf14ac4..56dee5df 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs @@ -71,6 +71,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token); + await SendMessage(greetingMessageRequest, _options, token); } } \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs index 6c144221..c19cac66 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs @@ -29,10 +29,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to Body = "Hello! What's your name?" }; - await Bot.SendMessageAsync(new SendMessageRequest - { - Message = responseMessage - }, - token); + await SendMessage(responseMessage, token); } } \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs index 93895818..9e3b4838 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs @@ -22,11 +22,7 @@ public SayHelloFinalCommandProcessor(ILogger())}!"; - await Bot.SendMessageAsync(new SendMessageRequest - { - Message = message - }, - token); + await SendMessage(message, token); } protected override Task InnerProcess(Message message, CancellationToken token) diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index e389aacf..4686b1d1 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -76,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks + await SendMessage(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index 0613e020..314b3cd9 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -4,6 +4,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; +using Botticelli.Interfaces; using Botticelli.Scheduler; using Botticelli.Scheduler.Interfaces; using Botticelli.Shared.API.Client.Requests; @@ -18,7 +19,8 @@ public class StartCommandProcessor : CommandProcessor? _options; - + private IBot? _bot; + public StartCommandProcessor(ILogger> logger, ICommandValidator commandValidator, IJobManager jobManager, @@ -62,6 +64,12 @@ private static TReplyMarkup Init(ILayoutSupplier layoutSupplier, I return responseMarkup; } + public override void SetBot(IBot bot) + { + base.SetBot(bot); + _bot = bot; + } + protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; @@ -81,58 +89,59 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token); + await SendMessage(greetingMessageRequest, _options, token); var assemblyPath = Path.GetDirectoryName(typeof(StartCommandProcessor).Assembly.Location) ?? throw new FileNotFoundException(); - _jobManager.AddJob(Bot, - new Reliability - { - IsEnabled = false, - Delay = TimeSpan.FromSeconds(3), - IsExponential = true, - MaxTries = 5 - }, - new Message - { - Body = "Now you see me!", - ChatIds = [chatId], - Contact = new Contact + if (_bot != null) + _jobManager.AddJob(_bot, + new Reliability { - Phone = "+9003289384923842343243243", - Name = "Test", - Surname = "Botticelli" + IsEnabled = false, + Delay = TimeSpan.FromSeconds(3), + IsExponential = true, + MaxTries = 5 }, - Attachments = - [ - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "testpic.png", - MediaType.Image, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "voice.mp3", - MediaType.Voice, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "video.mp4", - MediaType.Video, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "document.odt", - MediaType.Document, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) - ] - }, - new Schedule - { - Cron = "*/30 * * ? * * *" - }); + new Message + { + Body = "Now you see me!", + ChatIds = [chatId], + Contact = new Contact + { + Phone = "+9003289384923842343243243", + Name = "Test", + Surname = "Botticelli" + }, + Attachments = + [ + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "testpic.png", + MediaType.Image, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "voice.mp3", + MediaType.Voice, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "video.mp4", + MediaType.Video, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "document.odt", + MediaType.Document, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) + ] + }, + new Schedule + { + Cron = "*/30 * * ? * * *" + }); } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs index 1019abed..f706ddcb 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs @@ -87,6 +87,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(farewellMessageRequest, _options, token); + await SendMessage(farewellMessageRequest, _options, token); } } \ No newline at end of file From 6d42f57e966b0229e5c7613e12ba5fdf43630346 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 3 May 2025 22:54:00 +0300 Subject: [PATCH 016/101] CommandProcessor: Send, Delete and update messages were incapsulated --- .idea/.idea.Botticelli/.idea/vcs.xml | 1 + .../InlineCalendar/ICCommandProcessor.cs | 14 +- Botticelli.Framework.Telegram/TelegramBot.cs | 2 +- Botticelli.Framework.Vk/VkBot.cs | 4 +- Botticelli.Framework/BaseBot.cs | 4 +- .../Commands/Processors/CommandProcessor.cs | 128 +++++++++++++----- ...tForClientResponseCommandChainProcessor.cs | 22 ++- .../IEventBasedBotClientApi.cs | 2 +- .../FindLocationsCommandProcessor.cs | 2 +- .../CommandProcessors/MapCommandProcessor.cs | 2 +- ...sageRequest.cs => DeleteMessageRequest.cs} | 4 +- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/SendInvoiceCommandProcessor.cs | 2 +- .../Ai.Common.Sample/AiCommandProcessor.cs | 18 +-- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/RegisterCommandProcessor.cs | 4 +- .../Processors/StartCommandProcessor.cs | 2 +- .../GetNameCommandProcessor.cs | 6 +- .../SayHelloFinalCommandProcessor.cs | 6 +- .../Processors/InfoCommandProcessor.cs | 2 +- .../Processors/StartCommandProcessor.cs | 107 ++++++++------- .../Processors/StopCommandProcessor.cs | 2 +- 22 files changed, 191 insertions(+), 147 deletions(-) rename Botticelli.Shared/API/Client/Requests/{RemoveMessageRequest.cs => DeleteMessageRequest.cs} (50%) diff --git a/.idea/.idea.Botticelli/.idea/vcs.xml b/.idea/.idea.Botticelli/.idea/vcs.xml index 3672fbf7..9d91c1f7 100644 --- a/.idea/.idea.Botticelli/.idea/vcs.xml +++ b/.idea/.idea.Botticelli/.idea/vcs.xml @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs index 6de3a30b..b0cedcba 100644 --- a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs +++ b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs @@ -56,18 +56,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to var responseMarkup = _layoutSupplier.GetMarkup(calendar); var options = SendOptionsBuilder.CreateBuilder(responseMarkup); - await Bot.UpdateMessageAsync(new SendMessageRequest - { - ExpectPartialResponse = false, - Message = new Message - { - Body = message.CallbackData, - Uid = message.Uid, - ChatIds = message.ChatIds, - ChatIdInnerIdLinks = message.ChatIdInnerIdLinks - } - }, - options, - token); + await UpdateMessage(message, options, token); } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 9589bfde..f271c689 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -62,7 +62,7 @@ public TelegramBot(ITelegramBotClient client, /// /// /// - protected override async Task InnerDeleteMessageAsync(RemoveMessageRequest request, + protected override async Task InnerDeleteMessageAsync(DeleteMessageRequest request, CancellationToken token) { request.NotNull(); diff --git a/Botticelli.Framework.Vk/VkBot.cs b/Botticelli.Framework.Vk/VkBot.cs index f78bfce4..a711d17f 100644 --- a/Botticelli.Framework.Vk/VkBot.cs +++ b/Botticelli.Framework.Vk/VkBot.cs @@ -192,7 +192,7 @@ protected override async Task InnerSendMessageAsync InnerDeleteMessageAsync(RemoveMessageRequest request, + protected override Task InnerDeleteMessageAsync(DeleteMessageRequest request, CancellationToken token) => throw new NotImplementedException(); @@ -305,7 +305,7 @@ private async Task> CreateRequestsWithAttachme return result; } - public override Task DeleteMessageAsync(RemoveMessageRequest request, + public override Task DeleteMessageAsync(DeleteMessageRequest request, CancellationToken token) => throw new NotImplementedException(); diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index 8ccd352d..da428292 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -124,7 +124,7 @@ public async Task UpdateMessageAsync(SendMess token); } - public virtual async Task DeleteMessageAsync(RemoveMessageRequest request, + public virtual async Task DeleteMessageAsync(DeleteMessageRequest request, CancellationToken token) { _metrics?.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); @@ -145,7 +145,7 @@ protected abstract Task InnerSendMessageAsync CancellationToken token) where TSendOptions : class; - protected abstract Task InnerDeleteMessageAsync(RemoveMessageRequest request, + protected abstract Task InnerDeleteMessageAsync(DeleteMessageRequest request, CancellationToken token); public event StartedEventHandler? Started; diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index 6628aabd..258fa749 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -4,6 +4,7 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; +using Botticelli.Framework.SendOptions; using Botticelli.Interfaces; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; @@ -13,14 +14,14 @@ namespace Botticelli.Framework.Commands.Processors; public abstract class CommandProcessor : ICommandProcessor - where TCommand : class, ICommand + where TCommand : class, ICommand { private readonly string _command; private readonly ICommandValidator _commandValidator; private readonly IValidator _messageValidator; private readonly MetricsProcessor? _metricsProcessor; protected readonly ILogger Logger; - protected IBot Bot; + private IBot? _bot; protected CommandProcessor(ILogger logger, ICommandValidator commandValidator, @@ -31,7 +32,7 @@ protected CommandProcessor(ILogger logger, _messageValidator = messageValidator; _command = GetOldFashionedCommandName(typeof(TCommand).Name); } - + protected CommandProcessor(ILogger logger, ICommandValidator commandValidator, IValidator messageValidator, @@ -66,7 +67,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) return; } - if (message.From!.Id!.Equals(Bot.BotUserId, StringComparison.InvariantCulture)) return; + if (message.From!.Id!.Equals(_bot?.BotUserId, StringComparison.InvariantCulture)) return; Classify(ref message); @@ -88,7 +89,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) if (CommandUtils.SimpleCommandRegex.IsMatch(body)) { var match = CommandUtils.SimpleCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -103,7 +104,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) else if (CommandUtils.ArgsCommandRegex.IsMatch(body)) { var match = CommandUtils.ArgsCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -119,7 +120,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) { if (GetType().IsAssignableTo(typeof(CommandChainProcessor))) await ValidateAndProcess(message, - token); + token); } if (message.Location != null) await InnerProcessLocation(message, token); @@ -138,7 +139,7 @@ await ValidateAndProcess(message, public virtual void SetBot(IBot bot) { - Bot = bot; + _bot = bot; } public void SetServiceProvider(IServiceProvider sp) @@ -149,20 +150,16 @@ protected static void Classify(ref Message message) { var body = GetBody(message); - if (CommandUtils.SimpleCommandRegex.IsMatch(body)) - message.Type = Message.MessageType.Command; - else if (CommandUtils.ArgsCommandRegex.IsMatch(body)) + if (CommandUtils.SimpleCommandRegex.IsMatch(body) || CommandUtils.ArgsCommandRegex.IsMatch(body)) message.Type = Message.MessageType.Command; else message.Type = Message.MessageType.Messaging; } - private static string GetBody(Message message) - { - return !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData - : !string.IsNullOrWhiteSpace(message.Body) ? message.Body - : string.Empty; - } + private static string GetBody(Message message) => + !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData + : !string.IsNullOrWhiteSpace(message.Body) ? message.Body + : string.Empty; private void SendMetric(string metricName) { @@ -171,17 +168,20 @@ private void SendMetric(string metricName) private void SendMetric() { - _metricsProcessor?.Process(GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), BotDataUtils.GetBotId()!); + _metricsProcessor?.Process( + GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), + BotDataUtils.GetBotId()!); } - private string GetOldFashionedCommandName(string fullCommand) - { - return fullCommand.ToLowerInvariant().Replace("command", ""); - } + private string GetOldFashionedCommandName(string fullCommand) => + fullCommand.ToLowerInvariant().Replace("command", ""); private async Task ValidateAndProcess(Message message, - CancellationToken token) + CancellationToken token) { + if (_bot == null) + return; + if (message.Type == Message.MessageType.Messaging) { SendMetric(); @@ -206,29 +206,89 @@ private async Task ValidateAndProcess(Message message, } }; - await Bot.SendMessageAsync(errMessageRequest, token); + await SendMessage(errMessageRequest, token); } } - protected virtual Task InnerProcessContact(Message message, CancellationToken token) + protected async Task DeleteMessage(DeleteMessageRequest request, CancellationToken token) { - return Task.CompletedTask; + if (_bot == null) + return; + + await _bot.DeleteMessageAsync(request, token); } - - protected virtual Task InnerProcessPoll(Message message, CancellationToken token) + + protected async Task DeleteMessage(Message message, CancellationToken token) { - return Task.CompletedTask; + if (_bot == null) + return; + + foreach (var request in message.ChatIds.Select(chatId => new DeleteMessageRequest(message.Uid, chatId))) + await _bot.DeleteMessageAsync(request, token); } + + protected async Task SendMessage(Message message, CancellationToken token) + { + if (_bot == null) + return; - protected virtual Task InnerProcessLocation(Message message, CancellationToken token) + var request = new SendMessageRequest + { + Message = message + }; + + await SendMessage(request, token); + } + + protected async Task SendMessage(SendMessageRequest request, CancellationToken token) { - return Task.CompletedTask; + if (_bot == null) + return; + + await _bot.SendMessageAsync(request, token); } - protected abstract Task InnerProcess(Message message, CancellationToken token); + protected async Task SendMessage(SendMessageRequest request, + SendOptionsBuilder? options, CancellationToken token) + where TReplyMarkup : class + { + if (_bot == null) + return; - protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) + await _bot.SendMessageAsync(request, options, token); + } + + protected async Task UpdateMessage(Message message, ISendOptionsBuilder? options, + CancellationToken token) + where TSendOptions : class { - return Task.CompletedTask; + if (_bot == null) + return; + + await _bot.UpdateMessageAsync(new SendMessageRequest + { + ExpectPartialResponse = false, + Message = new Message + { + Body = message.CallbackData, + Uid = message.Uid, + ChatIds = message.ChatIds, + ChatIdInnerIdLinks = message.ChatIdInnerIdLinks + } + }, + options, + token); } + + + protected virtual Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; + + protected virtual Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + + protected virtual Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; + + protected abstract Task InnerProcess(Message message, CancellationToken token); + + protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) => + Task.CompletedTask; } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs index 9114c96e..9fd65b07 100644 --- a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs @@ -1,6 +1,5 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Validators; -using Botticelli.Interfaces; using Botticelli.Shared.ValueObjects; using FluentValidation; using Microsoft.Extensions.Logging; @@ -12,26 +11,21 @@ namespace Botticelli.Framework.Commands.Processors; /// /// public abstract class WaitForClientResponseCommandChainProcessor : CommandProcessor, - ICommandChainProcessor - where TInputCommand : class, ICommand + ICommandChainProcessor + where TInputCommand : class, ICommand { protected WaitForClientResponseCommandChainProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator, metricsProcessor) + ICommandValidator commandValidator, + MetricsProcessor metricsProcessor, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator, metricsProcessor) { } private TimeSpan Timeout { get; } = TimeSpan.FromMinutes(10); - public virtual void SetBot(IBot bot) - { - Bot = bot; - } - public override async Task ProcessAsync(Message message, CancellationToken token) { // filters 'not our' chains diff --git a/Botticelli.Interfaces/IEventBasedBotClientApi.cs b/Botticelli.Interfaces/IEventBasedBotClientApi.cs index accf23e6..ccdc58c3 100644 --- a/Botticelli.Interfaces/IEventBasedBotClientApi.cs +++ b/Botticelli.Interfaces/IEventBasedBotClientApi.cs @@ -45,5 +45,5 @@ public Task UpdateMessageAsync(SendMessageReq CancellationToken token) where TSendOptions : class; - public Task DeleteMessageAsync(RemoveMessageRequest request, CancellationToken token); + public Task DeleteMessageAsync(DeleteMessageRequest request, CancellationToken token); } \ No newline at end of file diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 6ae155c7..98b08407 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -62,6 +62,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(request, replyOptions, token); + await SendMessage(request, replyOptions, token); } } \ No newline at end of file diff --git a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs index f1d6ae1b..a99b832c 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs @@ -34,6 +34,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(request, token); + await SendMessage(request, token); } } \ No newline at end of file diff --git a/Botticelli.Shared/API/Client/Requests/RemoveMessageRequest.cs b/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs similarity index 50% rename from Botticelli.Shared/API/Client/Requests/RemoveMessageRequest.cs rename to Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs index 16e44757..75b2cb2c 100644 --- a/Botticelli.Shared/API/Client/Requests/RemoveMessageRequest.cs +++ b/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs @@ -1,8 +1,8 @@ using Botticelli.Shared.API; -public class RemoveMessageRequest : BaseRequest +public class DeleteMessageRequest : BaseRequest { - public RemoveMessageRequest(string? uid, string chatId) : base(uid) + public DeleteMessageRequest(string? uid, string chatId) : base(uid) { ChatId = chatId; } diff --git a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 5349505e..efd4d830 100644 --- a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -35,6 +35,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, token); + await SendMessage(greetingMessageRequest, token); } } \ No newline at end of file diff --git a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs index 6d6f49be..8c4a854a 100644 --- a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs @@ -76,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot?.SendMessageAsync(sendInvoiceMessageRequest, token)!; // TODO: think about Bot mocks + await SendMessage(sendInvoiceMessageRequest, token); } } \ No newline at end of file diff --git a/Samples/Ai.Common.Sample/AiCommandProcessor.cs b/Samples/Ai.Common.Sample/AiCommandProcessor.cs index 921b64c3..1fb39715 100644 --- a/Samples/Ai.Common.Sample/AiCommandProcessor.cs +++ b/Samples/Ai.Common.Sample/AiCommandProcessor.cs @@ -37,15 +37,15 @@ public AiCommandProcessor(ILogger> logger, _bus.OnReceived += async (sender, response) => { - await Bot.SendMessageAsync(new SendMessageRequest(response.Uid) - { - Message = response.Message, - ExpectPartialResponse = response.IsPartial, - SequenceNumber = response.SequenceNumber, - IsFinal = response.IsFinal - }, - options, - CancellationToken.None); + await SendMessage(new SendMessageRequest(response.Uid) + { + Message = response.Message, + ExpectPartialResponse = response.IsPartial, + SequenceNumber = response.SequenceNumber, + IsFinal = response.IsFinal + }, + options, + CancellationToken.None); }; } diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 2eb75d9a..70f2d883 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -49,6 +49,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot?.SendMessageAsync(greetingMessageRequest, token)!; + await SendMessage(greetingMessageRequest, token); } } \ No newline at end of file diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs index b773422f..859f135c 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs @@ -68,7 +68,7 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(alreadyRegisteredRequest, token); + await SendMessage(alreadyRegisteredRequest, token); return; } @@ -102,7 +102,7 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(registeredRequest, token)!; + await SendMessage(registeredRequest, token); } private async Task GetUserRole() diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs index 4bf14ac4..56dee5df 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs @@ -71,6 +71,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token); + await SendMessage(greetingMessageRequest, _options, token); } } \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs index 6c144221..c19cac66 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs @@ -29,10 +29,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to Body = "Hello! What's your name?" }; - await Bot.SendMessageAsync(new SendMessageRequest - { - Message = responseMessage - }, - token); + await SendMessage(responseMessage, token); } } \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs index 93895818..9e3b4838 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs @@ -22,11 +22,7 @@ public SayHelloFinalCommandProcessor(ILogger())}!"; - await Bot.SendMessageAsync(new SendMessageRequest - { - Message = message - }, - token); + await SendMessage(message, token); } protected override Task InnerProcess(Message message, CancellationToken token) diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index e389aacf..4686b1d1 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -76,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks + await SendMessage(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index 0613e020..314b3cd9 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -4,6 +4,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; +using Botticelli.Interfaces; using Botticelli.Scheduler; using Botticelli.Scheduler.Interfaces; using Botticelli.Shared.API.Client.Requests; @@ -18,7 +19,8 @@ public class StartCommandProcessor : CommandProcessor? _options; - + private IBot? _bot; + public StartCommandProcessor(ILogger> logger, ICommandValidator commandValidator, IJobManager jobManager, @@ -62,6 +64,12 @@ private static TReplyMarkup Init(ILayoutSupplier layoutSupplier, I return responseMarkup; } + public override void SetBot(IBot bot) + { + base.SetBot(bot); + _bot = bot; + } + protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; @@ -81,58 +89,59 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(greetingMessageRequest, _options, token); + await SendMessage(greetingMessageRequest, _options, token); var assemblyPath = Path.GetDirectoryName(typeof(StartCommandProcessor).Assembly.Location) ?? throw new FileNotFoundException(); - _jobManager.AddJob(Bot, - new Reliability - { - IsEnabled = false, - Delay = TimeSpan.FromSeconds(3), - IsExponential = true, - MaxTries = 5 - }, - new Message - { - Body = "Now you see me!", - ChatIds = [chatId], - Contact = new Contact + if (_bot != null) + _jobManager.AddJob(_bot, + new Reliability { - Phone = "+9003289384923842343243243", - Name = "Test", - Surname = "Botticelli" + IsEnabled = false, + Delay = TimeSpan.FromSeconds(3), + IsExponential = true, + MaxTries = 5 }, - Attachments = - [ - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "testpic.png", - MediaType.Image, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "voice.mp3", - MediaType.Voice, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "video.mp4", - MediaType.Video, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "document.odt", - MediaType.Document, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) - ] - }, - new Schedule - { - Cron = "*/30 * * ? * * *" - }); + new Message + { + Body = "Now you see me!", + ChatIds = [chatId], + Contact = new Contact + { + Phone = "+9003289384923842343243243", + Name = "Test", + Surname = "Botticelli" + }, + Attachments = + [ + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "testpic.png", + MediaType.Image, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "voice.mp3", + MediaType.Voice, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "video.mp4", + MediaType.Video, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "document.odt", + MediaType.Document, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) + ] + }, + new Schedule + { + Cron = "*/30 * * ? * * *" + }); } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs index 1019abed..f706ddcb 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs @@ -87,6 +87,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await Bot.SendMessageAsync(farewellMessageRequest, _options, token); + await SendMessage(farewellMessageRequest, _options, token); } } \ No newline at end of file From a7b54a019174eaf523ad0df10f1c40eaa74bcf5b Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 4 May 2025 11:44:44 +0300 Subject: [PATCH 017/101] - Is Standalone depends only on startup configuration now --- .../Builders/TelegramBotBuilder.cs | 18 +++++++++++------- .../Builders/TelegramStandaloneBotBuilder.cs | 6 ++++-- .../Extensions/ServiceCollectionExtensions.cs | 3 ++- .../Options/TelegramBotSettings.cs | 5 ----- .../Processors/InfoCommandProcessor.cs | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 14aa35a6..18d51f65 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -30,7 +30,8 @@ namespace Botticelli.Framework.Telegram.Builders; /// /// /// -public abstract class TelegramBotBuilder : TelegramBotBuilder> +public abstract class TelegramBotBuilder(bool isStandalone) + : TelegramBotBuilder>(isStandalone) where TBot : TelegramBot; /// @@ -44,9 +45,11 @@ public class TelegramBotBuilder : BotBuilder> _subHandlers = []; private TelegramClientDecoratorBuilder _builder = null!; + private readonly bool _isStandalone; + private string? _botToken; + + private protected TelegramBotBuilder(bool isStandalone) => _isStandalone = isStandalone; - public string? _botToken = null; - protected TelegramBotSettings? BotSettings { get; set; } protected BotData.Entities.Bot.BotData? BotData { get; set; } @@ -54,8 +57,9 @@ public static TelegramBotBuilder Instance(IServiceCollection ServerSettingsBuilder serverSettingsBuilder, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder, - AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) => - (TelegramBotBuilder)new TelegramBotBuilder() + AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, + bool isStandalone) => + (TelegramBotBuilder)new TelegramBotBuilder(isStandalone) .AddBotSettings(settingsBuilder) .AddServerSettings(serverSettingsBuilder) .AddAnalyticsSettings(analyticsClientSettingsBuilder) @@ -95,7 +99,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu protected override TBot? InnerBuild() { - if (BotSettings?.IsStandalone is false) + if (!_isStandalone) { Services.AddSingleton(ServerSettingsBuilder!.Build()); @@ -119,7 +123,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu #region Metrics - if (BotSettings?.IsStandalone is false) + if (!_isStandalone) { var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder!.Build()); var metricsProcessor = new MetricsProcessor(metricsPublisher); diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index 0de2f51f..c8c60b99 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -17,6 +17,10 @@ namespace Botticelli.Framework.Telegram.Builders; public class TelegramStandaloneBotBuilder : TelegramBotBuilder> where TBot : TelegramBot { + private TelegramStandaloneBotBuilder() : base(true) + { + } + public static TelegramStandaloneBotBuilder Instance(IServiceCollection services, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder) => @@ -50,8 +54,6 @@ public TelegramStandaloneBotBuilder AddBotData( if (BotData == null) throw new ConfigurationErrorsException("BotData is null!"); - - BotSettings!.IsStandalone = true; Services.AddHostedService() .AddSingleton(BotData); diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 774d5e55..4d69e9a3 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -110,7 +110,8 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se ServerSettingsBuilder, SettingsBuilder, DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder) + AnalyticsClientOptionsBuilder, + isStandalone: false) .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke(botBuilder); diff --git a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs index 1b4edac9..f5c45223 100644 --- a/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs +++ b/Botticelli.Framework.Telegram/Options/TelegramBotSettings.cs @@ -22,11 +22,6 @@ public class TelegramBotSettings : BotSettings /// public bool? UseThrottling { get; set; } = true; - /// - /// Is this bot standalone? - /// - public bool? IsStandalone { get; set; } = false; - /// /// Should we use test environment /// diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index 4686b1d1..102634bc 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -76,6 +76,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to } }; - await SendMessage(greetingMessageRequest, _options, token)!; // TODO: think about Bot mocks + await SendMessage(greetingMessageRequest, _options, token); } } \ No newline at end of file From 13dcbc68f5943193f1ce3f7c5edd1fa414fbfc8f Mon Sep 17 00:00:00 2001 From: stone1985 Date: Mon, 5 May 2025 18:12:51 +0300 Subject: [PATCH 018/101] - runasync - sonar --- Pay.Sample.Telegram/Program.cs | 2 +- Samples/Ai.ChatGpt.Sample.Vk/Program.cs | 6 +----- Samples/Ai.Messaging.Sample.Vk/Program.cs | 2 +- Samples/Ai.YaGpt.Sample.Telegram/Program.cs | 2 +- Samples/Ai.YaGpt.Sample.Vk/Program.cs | 6 +----- Samples/Auth.Sample.Telegram/Program.cs | 2 +- Samples/CommandChain.Sample.Telegram/Program.cs | 2 +- Samples/Layouts.Sample.Telegram/Program.cs | 2 +- Samples/Messaging.Sample.Telegram/Program.cs | 2 +- Samples/Monads.Sample.Telegram/Program.cs | 2 +- Samples/TelegramAiSample/Program.cs | 2 +- 11 files changed, 11 insertions(+), 19 deletions(-) diff --git a/Pay.Sample.Telegram/Program.cs b/Pay.Sample.Telegram/Program.cs index 0e9fd281..03239411 100644 --- a/Pay.Sample.Telegram/Program.cs +++ b/Pay.Sample.Telegram/Program.cs @@ -29,4 +29,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs b/Samples/Ai.ChatGpt.Sample.Vk/Program.cs index 3e3c345f..34f335da 100644 --- a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs +++ b/Samples/Ai.ChatGpt.Sample.Vk/Program.cs @@ -14,10 +14,6 @@ var builder = WebApplication.CreateBuilder(args); -var settings = builder.Configuration - .GetSection(nameof(SampleSettings)) - .Get(); - builder.Services.AddVkBot(builder.Configuration) .AddLogging(cfg => cfg.AddNLog()) .AddChatGptProvider(builder.Configuration) @@ -29,4 +25,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/Program.cs b/Samples/Ai.Messaging.Sample.Vk/Program.cs index 7466e643..3f5723a2 100644 --- a/Samples/Ai.Messaging.Sample.Vk/Program.cs +++ b/Samples/Ai.Messaging.Sample.Vk/Program.cs @@ -23,4 +23,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs index ccb6bada..beeb6b4b 100644 --- a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs +++ b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs @@ -28,4 +28,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.YaGpt.Sample.Vk/Program.cs b/Samples/Ai.YaGpt.Sample.Vk/Program.cs index 2a32783c..0421435c 100644 --- a/Samples/Ai.YaGpt.Sample.Vk/Program.cs +++ b/Samples/Ai.YaGpt.Sample.Vk/Program.cs @@ -14,10 +14,6 @@ var builder = WebApplication.CreateBuilder(args); -var settings = builder.Configuration - .GetSection(nameof(SampleSettings)) - .Get(); - builder.Services.AddVkBot(builder.Configuration) .AddLogging(cfg => cfg.AddNLog()) .AddYaGptProvider(builder.Configuration) @@ -29,4 +25,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Auth.Sample.Telegram/Program.cs b/Samples/Auth.Sample.Telegram/Program.cs index 894b4404..b978985d 100644 --- a/Samples/Auth.Sample.Telegram/Program.cs +++ b/Samples/Auth.Sample.Telegram/Program.cs @@ -27,4 +27,4 @@ .AddProcessor>() .AddValidator>(); -builder.Build().Run(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Program.cs b/Samples/CommandChain.Sample.Telegram/Program.cs index 32f6d488..bb83ebaf 100644 --- a/Samples/CommandChain.Sample.Telegram/Program.cs +++ b/Samples/CommandChain.Sample.Telegram/Program.cs @@ -33,4 +33,4 @@ app.Services.RegisterBotChainedCommand(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Layouts.Sample.Telegram/Program.cs b/Samples/Layouts.Sample.Telegram/Program.cs index ba096edc..d78d6c79 100644 --- a/Samples/Layouts.Sample.Telegram/Program.cs +++ b/Samples/Layouts.Sample.Telegram/Program.cs @@ -20,4 +20,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Messaging.Sample.Telegram/Program.cs b/Samples/Messaging.Sample.Telegram/Program.cs index cd116d42..369ffcb9 100644 --- a/Samples/Messaging.Sample.Telegram/Program.cs +++ b/Samples/Messaging.Sample.Telegram/Program.cs @@ -27,4 +27,4 @@ .AddProcessor>() .AddValidator>(); -builder.Build().Run(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index 7e55ccda..0c8d3d4d 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -32,4 +32,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/TelegramAiSample/Program.cs b/Samples/TelegramAiSample/Program.cs index 362be32d..fe5c6c3e 100644 --- a/Samples/TelegramAiSample/Program.cs +++ b/Samples/TelegramAiSample/Program.cs @@ -28,4 +28,4 @@ var app = builder.Build(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file From 1c157b6587b038c436166a737211158392c184cf Mon Sep 17 00:00:00 2001 From: stone1985 Date: Mon, 5 May 2025 18:18:49 +0300 Subject: [PATCH 019/101] - resharper --- .../Client/IBusClient.cs | 2 +- Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs | 9 +- Botticelli.Bus.Rabbit/Client/RabbitClient.cs | 19 +- .../Client/RabbitEventBusClient.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- Botticelli.Bus/Agent/PassAgent.cs | 5 +- Botticelli.Bus/Client/PassClient.cs | 6 +- .../InlineCalendar/ICCommandProcessor.cs | 4 +- .../BasicControls/Button.cs | 3 +- .../BasicControls/Text.cs | 5 +- .../Parsers/JsonLayoutParser.cs | 2 +- .../Commands/Processors/ChainRunProcessor.cs | 3 +- .../Builders/TelegramBotBuilder.cs | 93 ++++----- .../Builders/TelegramStandaloneBotBuilder.cs | 32 +-- .../Extensions/ServiceCollectionExtensions.cs | 185 +++++++++--------- .../Handlers/BotUpdateHandler.cs | 45 +++-- Botticelli.Framework.Telegram/TelegramBot.cs | 152 +++++++------- .../Builders/VkBotBuilder.cs | 16 +- Botticelli.Framework.Vk/VkBot.cs | 121 ++++++------ Botticelli.Framework/BaseBot.cs | 56 +++--- Botticelli.Framework/Builders/BotBuilder.cs | 10 +- .../Processors/CommandChainProcessor.cs | 3 +- .../Commands/Processors/CommandProcessor.cs | 140 ++++++------- ...tForClientResponseCommandChainProcessor.cs | 17 +- .../Options/BotDataSettings.cs | 2 +- .../Options/BotDataSettingsBuilder.cs | 9 +- .../Services/BotActualizationService.cs | 12 +- .../Services/BotStandaloneService.cs | 21 +- .../Services/BotStatusService.cs | 4 +- .../Services/PollActualizationService.cs | 3 +- .../ReverseGeocoderMock.cs | 9 +- .../FindLocationsCommandProcessor.cs | 3 +- .../CommandProcessors/MapCommandProcessor.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 45 ++--- Botticelli.Pay.Telegram/TelegramPaymentBot.cs | 8 +- .../Services/Auth/AdminAuthService.cs | 72 +++---- .../Services/BotManagementService.cs | 8 +- Botticelli.Talks/Botticelli.Talks.csproj | 6 +- ...essaging.Sample.Telegram.Standalone.csproj | 64 +++--- .../Program.cs | 20 +- .../Processors/InfoCommandProcessor.cs | 32 +-- .../Processors/SendInvoiceCommandProcessor.cs | 3 +- Samples/Ai.ChatGpt.Sample.Vk/Program.cs | 1 - .../Ai.Common.Sample/AiCommandProcessor.cs | 19 +- Samples/Ai.YaGpt.Sample.Vk/Program.cs | 1 - .../Processors/InfoCommandProcessor.cs | 3 +- .../Processors/RegisterCommandProcessor.cs | 3 +- .../Processors/StartCommandProcessor.cs | 3 +- .../GetNameCommandProcessor.cs | 1 - .../SayHelloFinalCommandProcessor.cs | 1 - .../Handlers/DateChosenCommandProcessor.cs | 3 +- .../Handlers/GetCalendarCommandProcessor.cs | 3 +- .../Processors/InfoCommandProcessor.cs | 26 +-- .../Processors/StartCommandProcessor.cs | 151 +++++++------- .../Processors/StopCommandProcessor.cs | 32 +-- .../MessagePublisherTests.cs | 2 +- .../MetricsRandomGenerator.csproj | 6 +- Viber.Api/ViberService.cs | 2 +- 58 files changed, 786 insertions(+), 729 deletions(-) diff --git a/Botticelli.Bot.Interfaces/Client/IBusClient.cs b/Botticelli.Bot.Interfaces/Client/IBusClient.cs index 47337bc6..353c64d2 100644 --- a/Botticelli.Bot.Interfaces/Client/IBusClient.cs +++ b/Botticelli.Bot.Interfaces/Client/IBusClient.cs @@ -12,7 +12,7 @@ public IAsyncEnumerable SendAndGetResponseSeries(SendMessag CancellationToken token); public Task SendAndGetResponse(SendMessageRequest request, - CancellationToken token); + CancellationToken token); public Task SendResponse(SendMessageResponse response, CancellationToken token); } \ No newline at end of file diff --git a/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs b/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs index e56e3c9d..fd286ce5 100644 --- a/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs +++ b/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs @@ -79,6 +79,7 @@ public Task StopAsync(CancellationToken cancellationToken) { _isActive = false; Thread.Sleep(3000); + return Task.CompletedTask; } @@ -91,6 +92,7 @@ public Task Subscribe(CancellationToken token) var handler = _sp.GetRequiredService(); ProcessSubscription(token, handler); + return Task.CompletedTask; } @@ -101,9 +103,7 @@ private void ProcessSubscription(CancellationToken token, THandler handler) var connection = _rabbitConnectionFactory.CreateConnection(); var channel = connection.CreateModel(); var queue = GetRequestQueueName(); - var declareResult = _settings.QueueSettings is { TryCreate: true } - ? channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false) - : channel.QueueDeclarePassive(queue); + var declareResult = _settings.QueueSettings is {TryCreate: true} ? channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false) : channel.QueueDeclarePassive(queue); _logger.LogDebug($"{nameof(Subscribe)}({typeof(THandler).Name}) queue declare: {declareResult.QueueName}"); @@ -125,8 +125,7 @@ private void ProcessSubscription(CancellationToken token, THandler handler) var policy = Policy.Handle() .WaitAndRetry(3, n => TimeSpan.FromSeconds(0.5 * Math.Exp(n))); - if (deserialized != null) - policy.Execute(() => handler.Handle(deserialized, token)); + if (deserialized != null) policy.Execute(() => handler.Handle(deserialized, token)); } catch (Exception ex) { diff --git a/Botticelli.Bus.Rabbit/Client/RabbitClient.cs b/Botticelli.Bus.Rabbit/Client/RabbitClient.cs index 94afdff2..9323c0f6 100644 --- a/Botticelli.Bus.Rabbit/Client/RabbitClient.cs +++ b/Botticelli.Bus.Rabbit/Client/RabbitClient.cs @@ -45,13 +45,11 @@ public async IAsyncEnumerable SendAndGetResponseSeries(Send Send(request, channel, GetRequestQueueName()); - if (request.Message.Uid == null) - yield break; - - if (!_responses.TryGetValue(request.Message.Uid, out var prevValue)) - yield break; + if (request.Message.Uid == null) yield break; - while (prevValue is { IsPartial: true, IsFinal: true }) + if (!_responses.TryGetValue(request.Message.Uid, out var prevValue)) yield break; + + while (prevValue is {IsPartial: true, IsFinal: true}) if (_responses.TryGetValue(request.Message.Uid, out var value)) { if (value.IsFinal) yield return value; @@ -65,7 +63,7 @@ public async IAsyncEnumerable SendAndGetResponseSeries(Send } public async Task SendAndGetResponse(SendMessageRequest request, - CancellationToken token) + CancellationToken token) { try { @@ -138,8 +136,8 @@ private void Init() _ = _settings .QueueSettings .TryCreate ? - channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false) : - channel.QueueDeclarePassive(queue); + channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false) : + channel.QueueDeclarePassive(queue); channel.BasicConsume(queue, true, _consumer); @@ -157,8 +155,7 @@ private void Init() response.Message.NotNull(); response.Message.Uid.NotNull(); - if (response.Message.Uid != null) - _responses.Add(response.Message.Uid, response); + if (response.Message.Uid != null) _responses.Add(response.Message.Uid, response); } catch (Exception ex) { diff --git a/Botticelli.Bus.Rabbit/Client/RabbitEventBusClient.cs b/Botticelli.Bus.Rabbit/Client/RabbitEventBusClient.cs index 9f6bddf5..c47067a1 100644 --- a/Botticelli.Bus.Rabbit/Client/RabbitEventBusClient.cs +++ b/Botticelli.Bus.Rabbit/Client/RabbitEventBusClient.cs @@ -20,7 +20,7 @@ public class RabbitEventBusClient : BasicFunctions, IEventBusClient public RabbitEventBusClient(IConnectionFactory rabbitConnectionFactory, RabbitBusSettings settings, - ILogger> logger, + ILogger> logger, EventingBasicConsumer consumer) { _rabbitConnectionFactory = rabbitConnectionFactory; @@ -66,7 +66,7 @@ private void Init() var exchange = _settings.Exchange; - if (_settings.QueueSettings is { TryCreate: true }) + if (_settings.QueueSettings is {TryCreate: true}) { channel.ExchangeDeclare(exchange, _settings.ExchangeType); channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false); diff --git a/Botticelli.Bus.Rabbit/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Bus.Rabbit/Extensions/ServiceCollectionExtensions.cs index 930c7978..6a4d860e 100644 --- a/Botticelli.Bus.Rabbit/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Bus.Rabbit/Extensions/ServiceCollectionExtensions.cs @@ -34,7 +34,7 @@ private static IServiceCollection AddConnectionFactory(this IServiceCollection s { settings.NotNull(); settings.Uri.NotNull(); - + if (!services.Any(s => s.ServiceType.IsAssignableFrom(typeof(IConnectionFactory)))) services.AddSingleton(s => new ConnectionFactory { diff --git a/Botticelli.Bus/Agent/PassAgent.cs b/Botticelli.Bus/Agent/PassAgent.cs index d063acbc..646da275 100644 --- a/Botticelli.Bus/Agent/PassAgent.cs +++ b/Botticelli.Bus/Agent/PassAgent.cs @@ -33,10 +33,11 @@ public Task Subscribe(CancellationToken token) /// /// public Task SendResponseAsync(SendMessageResponse response, - CancellationToken token, - int timeoutMs = 10000) + CancellationToken token, + int timeoutMs = 10000) { NoneBus.SendMessageResponses.Enqueue(response); + return Task.CompletedTask; } diff --git a/Botticelli.Bus/Client/PassClient.cs b/Botticelli.Bus/Client/PassClient.cs index 7bc40632..8b95e456 100644 --- a/Botticelli.Bus/Client/PassClient.cs +++ b/Botticelli.Bus/Client/PassClient.cs @@ -12,7 +12,7 @@ public class PassClient : IBusClient private static TimeSpan Timeout => TimeSpan.FromMinutes(5); public Task SendAndGetResponse(SendMessageRequest request, - CancellationToken token) + CancellationToken token) { NoneBus.SendMessageRequests.Enqueue(request); @@ -24,7 +24,8 @@ public Task SendAndGetResponse(SendMessageRequest request, while (period < Timeout.TotalMilliseconds) { if (NoneBus.SendMessageResponses.TryDequeue(out var response)) - if (response.Uid == request.Uid) return response; + if (response.Uid == request.Uid) + return response; Task.Delay(pause, token).Wait(token); period += pause; @@ -71,6 +72,7 @@ public async IAsyncEnumerable SendAndGetResponseSeries(Send public Task SendResponse(SendMessageResponse response, CancellationToken tokens) { NoneBus.SendMessageResponses.Enqueue(response); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs index b0cedcba..cf45b3af 100644 --- a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs +++ b/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs @@ -7,7 +7,6 @@ using Botticelli.Framework.Controls.Layouts.Inlines; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; -using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using FluentValidation; using Microsoft.Extensions.Logging; @@ -31,7 +30,8 @@ public ICCommandProcessor(ILogger> lo IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { _layoutSupplier = layoutSupplier; } diff --git a/Botticelli.Framework.Controls/BasicControls/Button.cs b/Botticelli.Framework.Controls/BasicControls/Button.cs index 6b5e3028..514badb8 100644 --- a/Botticelli.Framework.Controls/BasicControls/Button.cs +++ b/Botticelli.Framework.Controls/BasicControls/Button.cs @@ -15,8 +15,7 @@ public string? CallbackData set { Params ??= new Dictionary(); - if (value != null) - Params["CallbackData"] = value; + if (value != null) Params["CallbackData"] = value; } } } \ No newline at end of file diff --git a/Botticelli.Framework.Controls/BasicControls/Text.cs b/Botticelli.Framework.Controls/BasicControls/Text.cs index d833636f..2ddcfdfc 100644 --- a/Botticelli.Framework.Controls/BasicControls/Text.cs +++ b/Botticelli.Framework.Controls/BasicControls/Text.cs @@ -10,9 +10,8 @@ public string? CallbackData set { Params ??= new Dictionary(); - - if (value != null) - Params["CallbackData"] = value; + + if (value != null) Params["CallbackData"] = value; } } diff --git a/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs b/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs index 9f967725..f90c0c0e 100644 --- a/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs +++ b/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs @@ -48,7 +48,7 @@ public ILayout Parse(string jsonText) if (itemElement.TryGetProperty("Specials", out var messengerSpecific)) if (item.Control != null) item.Control.MessengerSpecificParams = - messengerSpecific.Deserialize>>(); + messengerSpecific.Deserialize>>(); row.AddItem(item); } diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs index 8c34a45f..58a1ce61 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs @@ -17,7 +17,8 @@ public class ChainRunProcessor( IValidator messageValidator) : CommandProcessor(logger, validator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) where TCommand : class, IChainCommand, new() { protected override async Task InnerProcess(Message message, CancellationToken token) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 18d51f65..1facc892 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -27,47 +27,52 @@ namespace Botticelli.Framework.Telegram.Builders; /// -/// +/// /// /// public abstract class TelegramBotBuilder(bool isStandalone) - : TelegramBotBuilder>(isStandalone) - where TBot : TelegramBot; + : TelegramBotBuilder>(isStandalone) + where TBot : TelegramBot; /// -/// Builder for a non-standalone Telegram bot +/// Builder for a non-standalone Telegram bot /// /// /// public class TelegramBotBuilder : BotBuilder - where TBot : TelegramBot - where TBotBuilder : BotBuilder + where TBot : TelegramBot + where TBotBuilder : BotBuilder { - private readonly List> _subHandlers = []; - private TelegramClientDecoratorBuilder _builder = null!; private readonly bool _isStandalone; + private readonly List> _subHandlers = []; private string? _botToken; + private TelegramClientDecoratorBuilder _builder = null!; - private protected TelegramBotBuilder(bool isStandalone) => _isStandalone = isStandalone; + private protected TelegramBotBuilder(bool isStandalone) + { + _isStandalone = isStandalone; + } protected TelegramBotSettings? BotSettings { get; set; } protected BotData.Entities.Bot.BotData? BotData { get; set; } public static TelegramBotBuilder Instance(IServiceCollection services, - ServerSettingsBuilder serverSettingsBuilder, - BotSettingsBuilder settingsBuilder, - DataAccessSettingsBuilder dataAccessSettingsBuilder, - AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, - bool isStandalone) => - (TelegramBotBuilder)new TelegramBotBuilder(isStandalone) - .AddBotSettings(settingsBuilder) - .AddServerSettings(serverSettingsBuilder) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddServices(services); + ServerSettingsBuilder serverSettingsBuilder, + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder, + AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, + bool isStandalone) + { + return (TelegramBotBuilder) new TelegramBotBuilder(isStandalone) + .AddBotSettings(settingsBuilder) + .AddServerSettings(serverSettingsBuilder) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder) + .AddServices(services); + } public TelegramBotBuilder AddSubHandler() - where T : class, IBotUpdateSubHandler + where T : class, IBotUpdateSubHandler { Services.NotNull(); Services.AddSingleton(); @@ -89,7 +94,7 @@ public TelegramBotBuilder AddToken(string botToken) return this; } - + public TelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder) { _builder = builder; @@ -104,17 +109,17 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu Services.AddSingleton(ServerSettingsBuilder!.Build()); Services.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService(); Services.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService(); Services.AddHttpClient>() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService>>() - .AddHostedService(); + .AddHostedService(); } var botId = BotDataUtils.GetBotId(); @@ -136,7 +141,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu #region Data Services.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); + o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); Services.AddScoped(); #endregion @@ -149,9 +154,8 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu if (BotSettings?.UseThrottling is false) _builder.AddThrottler(new Throttler()); - if (!string.IsNullOrWhiteSpace(_botToken)) - _builder.AddToken(_botToken); - + if (!string.IsNullOrWhiteSpace(_botToken)) _builder.AddToken(_botToken); + var client = _builder.Build(); client.NotNull(); @@ -159,8 +163,8 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu client!.Timeout = TimeSpan.FromMilliseconds(BotSettings?.Timeout ?? 10000); Services.AddSingleton, ReplyTelegramLayoutSupplier>() - .AddBotticelliFramework() - .AddSingleton(); + .AddBotticelliFramework() + .AddSingleton(); var sp = Services.BuildServiceProvider(); ApplyMigrations(sp); @@ -168,23 +172,24 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu foreach (var sh in _subHandlers) sh.Invoke(sp); return Activator.CreateInstance(typeof(TBot), - client, - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetService()) as TBot; + client, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService()) as TBot; } - - protected TelegramBotBuilder AddBotSettings( - BotSettingsBuilder settingsBuilder) - where TBotSettings : BotSettings, new() + + protected TelegramBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); return this; } - - private static void ApplyMigrations(IServiceProvider sp) => + + private static void ApplyMigrations(IServiceProvider sp) + { sp.GetRequiredService().Database.Migrate(); + } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index c8c60b99..775d1eb8 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -15,22 +15,23 @@ namespace Botticelli.Framework.Telegram.Builders; /// /// public class TelegramStandaloneBotBuilder : TelegramBotBuilder> - where TBot : TelegramBot + where TBot : TelegramBot { private TelegramStandaloneBotBuilder() : base(true) { } public static TelegramStandaloneBotBuilder Instance(IServiceCollection services, - BotSettingsBuilder settingsBuilder, - DataAccessSettingsBuilder dataAccessSettingsBuilder) => - (TelegramStandaloneBotBuilder)new TelegramStandaloneBotBuilder() - .AddBotSettings(settingsBuilder) - .AddServices(services) - .AddBotDataAccessSettings(dataAccessSettingsBuilder); - - public TelegramStandaloneBotBuilder AddBotData( - BotDataSettingsBuilder dataBuilder) + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder) + { + return (TelegramStandaloneBotBuilder) new TelegramStandaloneBotBuilder() + .AddBotSettings(settingsBuilder) + .AddServices(services) + .AddBotDataAccessSettings(dataAccessSettingsBuilder); + } + + public TelegramStandaloneBotBuilder AddBotData(BotDataSettingsBuilder dataBuilder) { var settings = dataBuilder.Build(); @@ -43,20 +44,19 @@ public TelegramStandaloneBotBuilder AddBotData( }; AddToken(BotData.BotKey); - + return this; } protected override TBot? InnerBuild() { Services.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); + + if (BotData == null) throw new ConfigurationErrorsException("BotData is null!"); - if (BotData == null) - throw new ConfigurationErrorsException("BotData is null!"); - Services.AddHostedService() - .AddSingleton(BotData); + .AddSingleton(BotData); return base.InnerBuild(); } diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 4d69e9a3..3ee7716a 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -19,66 +19,64 @@ public static class ServiceCollectionExtensions private static readonly BotSettingsBuilder SettingsBuilder = new(); private static readonly ServerSettingsBuilder ServerSettingsBuilder = new(); private static readonly BotDataSettingsBuilder BotDataSettingsBuilder = new(); - + private static readonly AnalyticsClientSettingsBuilder AnalyticsClientOptionsBuilder = - new(); + new(); private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) => - AddTelegramBot(services, configuration, telegramBotBuilderFunc); + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + { + return AddTelegramBot(services, configuration, telegramBotBuilderFunc); + } public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); var analyticsClientSettings = configuration - .GetSection(AnalyticsClientSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); + .GetSection(AnalyticsClientSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); var serverSettings = configuration - .GetSection(ServerSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(ServerSettings)}!"); + .GetSection(ServerSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(ServerSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(DataAccessSettings)}!"); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); return services.AddTelegramBot(telegramBotSettings, - analyticsClientSettings, - serverSettings, - dataAccessSettings, - telegramBotBuilderFunc); + analyticsClientSettings, + serverSettings, + dataAccessSettings, + telegramBotBuilderFunc); } public static IServiceCollection AddTelegramBot(this IServiceCollection services, - TelegramBotSettings botSettings, - AnalyticsClientSettings analyticsClientSettings, - ServerSettings serverSettings, - DataAccessSettings dataAccessSettings, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + TelegramBotSettings botSettings, + AnalyticsClientSettings analyticsClientSettings, + ServerSettings serverSettings, + DataAccessSettings dataAccessSettings, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { return services.AddTelegramBot(o => o.Set(botSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } /// @@ -92,12 +90,12 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se /// /// public static IServiceCollection AddTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); @@ -107,61 +105,58 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); var botBuilder = TelegramBotBuilder.Instance(services, - ServerSettingsBuilder, - SettingsBuilder, - DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder, - isStandalone: false) - .AddClient(clientBuilder); + ServerSettingsBuilder, + SettingsBuilder, + DataAccessSettingsBuilder, + AnalyticsClientOptionsBuilder, + false) + .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke(botBuilder); TelegramBot? bot = botBuilder.Build(); return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport(); } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) => - AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + { + return AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); + } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(DataAccessSettings)}!"); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); var botDataSettings = configuration - .GetSection(BotDataSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(BotDataSettings)}!"); - - return services.AddStandaloneTelegramBot( - botSettingsBuilder => botSettingsBuilder.Set(telegramBotSettings), - dataAccessSettingsBuilder => dataAccessSettingsBuilder.Set(dataAccessSettings), - botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings) - ); + .GetSection(BotDataSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(BotDataSettings)}!"); + + return services.AddStandaloneTelegramBot(botSettingsBuilder => botSettingsBuilder.Set(telegramBotSettings), + dataAccessSettingsBuilder => dataAccessSettingsBuilder.Set(dataAccessSettings), + botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings)); } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action> botDataSettingsBuilderFunc, - Action>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + Action> optionsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action> botDataSettingsBuilderFunc, + Action>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); @@ -170,24 +165,26 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); var botBuilder = TelegramStandaloneBotBuilder.Instance(services, - SettingsBuilder, - DataAccessSettingsBuilder) - .AddBotData(BotDataSettingsBuilder) - .AddClient(clientBuilder); + SettingsBuilder, + DataAccessSettingsBuilder) + .AddBotData(BotDataSettingsBuilder) + .AddClient(clientBuilder); - telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); + telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder) botBuilder); TelegramBot? bot = botBuilder.Build(); return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport(); } - public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => - services.AddSingleton() - .AddSingleton, ReplyTelegramLayoutSupplier>() - .AddSingleton, InlineTelegramLayoutSupplier>() - .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() - .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); + public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) + { + return services.AddSingleton() + .AddSingleton, ReplyTelegramLayoutSupplier>() + .AddSingleton, InlineTelegramLayoutSupplier>() + .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() + .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); + } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index c6a26c21..76dee60c 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -15,7 +15,7 @@ namespace Botticelli.Framework.Telegram.Handlers; public class BotUpdateHandler(ILogger logger) : IBotUpdateHandler { private readonly MemoryCacheEntryOptions _entryOptions - = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); + = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); private readonly MemoryCache _memoryCache = new(new MemoryCacheOptions { @@ -25,8 +25,8 @@ private readonly MemoryCacheEntryOptions _entryOptions private readonly List _subHandlers = []; public async Task HandleUpdateAsync(ITelegramBotClient botClient, - Update update, - CancellationToken cancellationToken) + Update update, + CancellationToken cancellationToken) { try { @@ -90,8 +90,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, IsAnonymous = update.Poll.IsAnonymous, Question = update.Poll.Question, Type = update.Poll.Type.ToLower() == "regular" ? Poll.PollType.Regular : Poll.PollType.Quiz, - Variants = update.Poll.Options.Select( - o => new ValueTuple(o.Text, o.VoterCount)), + Variants = update.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), CorrectAnswerId = update.Poll.CorrectOptionId } }; @@ -101,7 +100,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, botticelliMessage = new Message(botMessage.MessageId.ToString()) { ChatIdInnerIdLinks = new Dictionary> - { { botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()] } }, + {{botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()]}}, ChatIds = [botMessage.Chat.Id.ToString()], Subject = string.Empty, Body = botMessage.Text ?? string.Empty, @@ -126,13 +125,13 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, IsBot = botMessage.ForwardFrom?.IsBot, NickName = botMessage.ForwardFrom?.Username }, - Location = botMessage.Location != null - ? new GeoLocation - { - Latitude = (decimal)botMessage.Location.Latitude, - Longitude = (decimal)botMessage.Location.Longitude - } - : null + Location = botMessage.Location != null ? + new GeoLocation + { + Latitude = (decimal) botMessage.Location.Latitude, + Longitude = (decimal) botMessage.Location.Longitude + } : + null }; } @@ -143,10 +142,10 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, await Process(botticelliMessage, cancellationToken); MessageReceived?.Invoke(this, - new MessageReceivedBotEventArgs - { - Message = botticelliMessage - }); + new MessageReceivedBotEventArgs + { + Message = botticelliMessage + }); } logger.LogDebug($"{nameof(HandleUpdateAsync)}() finished..."); @@ -158,9 +157,9 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, } public Task HandleErrorAsync(ITelegramBotClient botClient, - Exception exception, - HandleErrorSource source, - CancellationToken cancellationToken) + Exception exception, + HandleErrorSource source, + CancellationToken cancellationToken) { logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); @@ -183,15 +182,15 @@ protected async Task Process(Message request, CancellationToken token) { logger.LogDebug($"{nameof(Process)}({request.Uid}) started..."); - if (token is { CanBeCanceled: true, IsCancellationRequested: true }) return; + if (token is {CanBeCanceled: true, IsCancellationRequested: true}) return; var processorFactory = ProcessorFactoryBuilder.Build(); var clientNonChainedTasks = processorFactory.GetProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientChainedTasks = processorFactory.GetCommandChainProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientTasks = clientNonChainedTasks.Concat(clientChainedTasks).ToArray(); diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index f271c689..e54fb5e1 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -36,11 +36,11 @@ public class TelegramBot : BaseBot // ReSharper disable once MemberCanBeProtected.Global public TelegramBot(ITelegramBotClient client, - IBotUpdateHandler handler, - ILogger logger, - ITextTransformer textTransformer, - IBotDataAccess data, - MetricsProcessor? metrics) : base(logger, metrics) + IBotUpdateHandler handler, + ILogger logger, + ITextTransformer textTransformer, + IBotDataAccess data, + MetricsProcessor? metrics) : base(logger, metrics) { BotStatusKeeper.IsStarted = false; Client = client; @@ -108,10 +108,10 @@ await Client.DeleteMessage(request.ChatId, } protected virtual Task AdditionalProcessing(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - string chatId, - CancellationToken token) + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + string chatId, + CancellationToken token) { return Task.CompletedTask; } @@ -149,7 +149,9 @@ protected override async Task InnerSendMessageAsync InnerSendMessageAsync ProcessAttachments(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response, - Message? message) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response, + Message? message) { request.Message.NotNull(); - if (request.Message.Attachments == null) - return message; - + + if (request.Message.Attachments == null) return message; + request.Message.Attachments.NotNullOrEmpty(); foreach (var attachment in request.Message - .Attachments - .Where(a => a is BinaryBaseAttachment) - .Cast()) + .Attachments + .Where(a => a is BinaryBaseAttachment) + .Cast()) switch (attachment.MediaType) { case MediaType.Audio: var audio = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendAudio(link.chatId, - audio, - request.Message.Subject, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + audio, + request.Message.Subject, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Video: var video = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendVideo(link.chatId, - video, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + video, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Image: var image = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendPhoto(link.chatId, - image, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + image, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Voice: var voice = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendVoice(link.chatId, - voice, - request.Message.Subject, - ParseMode.MarkdownV2, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + voice, + request.Message.Subject, + ParseMode.MarkdownV2, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Sticker: - InputFile sticker = string.IsNullOrWhiteSpace(attachment.Url) - ? new InputFileStream(attachment.Data.ToStream(), attachment.Name) - : new InputFileUrl(attachment.Url); + InputFile sticker = string.IsNullOrWhiteSpace(attachment.Url) ? new InputFileStream(attachment.Data.ToStream(), attachment.Name) : new InputFileUrl(attachment.Url); message = await Client.SendSticker(link.chatId, - sticker, - GetReplyParameters(request, link.chatId), - replyMarkup, - cancellationToken: token); + sticker, + GetReplyParameters(request, link.chatId), + replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; case MediaType.Contact: await ProcessContact(request, - response, - token, - replyMarkup); + response, + token, + replyMarkup); break; case MediaType.Document: var doc = new InputFileStream(attachment.Data.ToStream(), attachment.Name); message = await Client.SendDocument(link.chatId, - doc, - replyParameters: GetReplyParameters(request, link.chatId), - replyMarkup: replyMarkup, - cancellationToken: token); + doc, + replyParameters: GetReplyParameters(request, link.chatId), + replyMarkup: replyMarkup, + cancellationToken: token); AddChatIdInnerIdLink(response, link.chatId, message); break; @@ -387,10 +387,10 @@ await ProcessContact(request, } protected async Task ProcessPoll(SendMessageRequest request, - CancellationToken token, - (string chatId, string innerId) link, - ReplyMarkup? replyMarkup, - SendMessageResponse response) + CancellationToken token, + (string chatId, string innerId) link, + ReplyMarkup? replyMarkup, + SendMessageResponse response) { Message? message; @@ -402,7 +402,7 @@ await ProcessContact(request, { Poll.PollType.Quiz => PollType.Quiz, Poll.PollType.Regular => PollType.Regular, - _ => throw new ArgumentOutOfRangeException(paramName: nameof(Type)) + _ => throw new ArgumentOutOfRangeException(nameof(Type)) }; message = await Client.SendPoll(link.chatId, @@ -453,9 +453,9 @@ private static void AddChatIdInnerIdLink(SendMessageResponse response, string ch } protected async Task ProcessContact(SendMessageRequest request, - SendMessageResponse response, - CancellationToken token, - ReplyMarkup? replyMarkup) + SendMessageResponse response, + CancellationToken token, + ReplyMarkup? replyMarkup) { request.Message.NotNull(); request.Message.Contact.NotNull(); diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs index 68d631ac..95bbc6f1 100644 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs @@ -90,8 +90,8 @@ protected override VkBot InnerBuild() sp.GetRequiredService>()); } - protected virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) - where TBotSettings : BotSettings, new() + protected virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) + where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as VkBotSettings ?? throw new InvalidOperationException(); @@ -111,11 +111,11 @@ public static VkBotBuilder Instance(IServiceCollection services, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) { - return (VkBotBuilder)new VkBotBuilder() - .AddBotSettings(settingsBuilder) - .AddServerSettings(serverSettingsBuilder) - .AddServices(services) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder); + return (VkBotBuilder) new VkBotBuilder() + .AddBotSettings(settingsBuilder) + .AddServerSettings(serverSettingsBuilder) + .AddServices(services) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder); } } \ No newline at end of file diff --git a/Botticelli.Framework.Vk/VkBot.cs b/Botticelli.Framework.Vk/VkBot.cs index a711d17f..23bd10d9 100644 --- a/Botticelli.Framework.Vk/VkBot.cs +++ b/Botticelli.Framework.Vk/VkBot.cs @@ -29,12 +29,12 @@ public class VkBot : BaseBot private bool _eventsAttached; public VkBot(LongPollMessagesProvider messagesProvider, - MessagePublisher? messagePublisher, - VkStorageUploader? vkUploader, - IBotDataAccess data, - IBotUpdateHandler handler, - MetricsProcessor metrics, - ILogger logger) : base(logger, metrics) + MessagePublisher? messagePublisher, + VkStorageUploader? vkUploader, + IBotDataAccess data, + IBotUpdateHandler handler, + MetricsProcessor metrics, + ILogger logger) : base(logger, metrics) { _messagesProvider = messagesProvider; _messagePublisher = messagePublisher; @@ -141,40 +141,48 @@ private void SetApiKey(BotData.Entities.Bot.BotData? context) _vkUploader.SetApiKey(context.BotKey); } - private string CreateVkAttach(VkSendPhotoResponse fk, string type) => - $"{type}" + - $"{fk.Response?.FirstOrDefault()?.OwnerId.ToString()}" + - $"_{fk.Response?.FirstOrDefault()?.Id.ToString()}"; + private string CreateVkAttach(VkSendPhotoResponse fk, string type) + { + return $"{type}" + + $"{fk.Response?.FirstOrDefault()?.OwnerId.ToString()}" + + $"_{fk.Response?.FirstOrDefault()?.Id.ToString()}"; + } - private string CreateVkAttach(VkSendVideoResponse fk, string type) => - $"{type}" + - $"{fk.Response?.OwnerId.ToString()}" + - $"_{fk.Response?.VideoId.ToString()}"; + private string CreateVkAttach(VkSendVideoResponse fk, string type) + { + return $"{type}" + + $"{fk.Response?.OwnerId.ToString()}" + + $"_{fk.Response?.VideoId.ToString()}"; + } - private string CreateVkAttach(VkSendAudioResponse fk, string type) => - $"{type}" + - $"{fk.AudioResponseData.AudioMessage.OwnerId}" + - $"_{fk.AudioResponseData.AudioMessage.Id}"; + private string CreateVkAttach(VkSendAudioResponse fk, string type) + { + return $"{type}" + + $"{fk.AudioResponseData.AudioMessage.OwnerId}" + + $"_{fk.AudioResponseData.AudioMessage.Id}"; + } - private string CreateVkAttach(VkSendDocumentResponse fk, string type) => - $"{type}" + - $"{fk.DocumentResponseData.Document.OwnerId}" + - $"_{fk.DocumentResponseData.Document.Id}"; + private string CreateVkAttach(VkSendDocumentResponse fk, string type) + { + return $"{type}" + + $"{fk.DocumentResponseData.Document.OwnerId}" + + $"_{fk.DocumentResponseData.Document.Id}"; + } protected override async Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) { foreach (var peerId in request.Message.ChatIds) try { var requests = await CreateRequestsWithAttachments(request, - peerId, - token); + peerId, + token); foreach (var vkRequest in requests) await _messagePublisher.SendAsync(vkRequest, token); } @@ -184,21 +192,23 @@ protected override async Task InnerSendMessageAsync InnerDeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token) => + CancellationToken token) + { throw new NotImplementedException(); + } private async Task> CreateRequestsWithAttachments(SendMessageRequest request, - string peerId, - CancellationToken token) + string peerId, + CancellationToken token) { var currentContext = _data.GetData(); var result = new List(100); @@ -245,17 +255,16 @@ private async Task> CreateRequestsWithAttachme { switch (ba) { - case { MediaType: MediaType.Image }: - case { MediaType: MediaType.Sticker }: + case {MediaType: MediaType.Image}: + case {MediaType: MediaType.Sticker}: var sendPhotoResponse = await _vkUploader.SendPhotoAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendPhotoResponse != null) - vkRequest.Attachment = CreateVkAttach(sendPhotoResponse, "photo"); + ba.Name, + ba.Data, + token); + if (sendPhotoResponse != null) vkRequest.Attachment = CreateVkAttach(sendPhotoResponse, "photo"); break; - case { MediaType: MediaType.Video }: + case {MediaType: MediaType.Video}: //var sendVideoResponse = await _vkUploader.SendVideoAsync(vkRequest, // ba.Name, // ba.Data, @@ -264,24 +273,22 @@ private async Task> CreateRequestsWithAttachme //if (sendVideoResponse != default) vkRequest.Attachment = CreateVkAttach(sendVideoResponse, currentContext, "video"); break; - case { MediaType: MediaType.Voice }: - case { MediaType: MediaType.Audio }: + case {MediaType: MediaType.Voice}: + case {MediaType: MediaType.Audio}: var sendAudioMessageResponse = await _vkUploader.SendAudioMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendAudioMessageResponse != null) - vkRequest.Attachment = CreateVkAttach(sendAudioMessageResponse, "doc"); + ba.Name, + ba.Data, + token); + if (sendAudioMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendAudioMessageResponse, "doc"); break; - case { MediaType: MediaType.Document }: + case {MediaType: MediaType.Document}: var sendDocMessageResponse = await _vkUploader.SendDocsMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendDocMessageResponse != null) - vkRequest.Attachment = CreateVkAttach(sendDocMessageResponse, "doc"); + ba.Name, + ba.Data, + token); + if (sendDocMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendDocMessageResponse, "doc"); break; @@ -306,8 +313,10 @@ private async Task> CreateRequestsWithAttachme } public override Task DeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token) => + CancellationToken token) + { throw new NotImplementedException(); + } public virtual event MsgSentEventHandler MessageSent; public virtual event MsgReceivedEventHandler MessageReceived; diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index da428292..d1c341f4 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -35,7 +35,7 @@ public abstract class BaseBot /// /// public abstract class BaseBot : BaseBot, IBot - where T : BaseBot, IBot + where T : BaseBot, IBot { public delegate void MessengerSpecificEventHandler(object sender, MessengerSpecificBotEventArgs e); @@ -50,8 +50,7 @@ protected BaseBot(ILogger logger, MetricsProcessor? metrics) public virtual async Task StartBotAsync(StartBotRequest request, CancellationToken token) { - if (BotStatusKeeper.IsStarted) - return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); + if (BotStatusKeeper.IsStarted) return StartBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); _metrics?.Process(MetricNames.BotStarted, BotDataUtils.GetBotId()); @@ -66,8 +65,7 @@ public virtual async Task StopBotAsync(StopBotRequest request, { _metrics?.Process(MetricNames.BotStopped, BotDataUtils.GetBotId()); - if (!BotStatusKeeper.IsStarted) - return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); + if (!BotStatusKeeper.IsStarted) return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); var result = await InnerStopBotAsync(request, token); @@ -84,8 +82,10 @@ public virtual async Task StopBotAsync(StopBotRequest request, /// Request /// /// - public Task SendMessageAsync(SendMessageRequest request, CancellationToken token) => - SendMessageAsync(request, null, token); + public Task SendMessageAsync(SendMessageRequest request, CancellationToken token) + { + return SendMessageAsync(request, null, token); + } /// @@ -96,36 +96,38 @@ public Task SendMessageAsync(SendMessageRequest request, Ca /// /// public virtual async Task SendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - CancellationToken token) - where TSendOptions : class + ISendOptionsBuilder? optionsBuilder, + CancellationToken token) + where TSendOptions : class { _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, - optionsBuilder, - false, - token); + optionsBuilder, + false, + token); } - public Task UpdateMessageAsync(SendMessageRequest request, CancellationToken token) => - UpdateMessageAsync(request, null, token); + public Task UpdateMessageAsync(SendMessageRequest request, CancellationToken token) + { + return UpdateMessageAsync(request, null, token); + } public async Task UpdateMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - CancellationToken token) - where TSendOptions : class + ISendOptionsBuilder? optionsBuilder, + CancellationToken token) + where TSendOptions : class { _metrics?.Process(MetricNames.MessageSent, BotDataUtils.GetBotId()); return await InnerSendMessageAsync(request, - optionsBuilder, - true, - token); + optionsBuilder, + true, + token); } public virtual async Task DeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token) + CancellationToken token) { _metrics?.Process(MetricNames.MessageRemoved, BotDataUtils.GetBotId()); @@ -140,13 +142,13 @@ public virtual async Task DeleteMessageAsync(DeleteMessag protected abstract Task InnerStopBotAsync(StopBotRequest request, CancellationToken token); protected abstract Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) - where TSendOptions : class; + ISendOptionsBuilder? optionsBuilder, + bool isUpdate, + CancellationToken token) + where TSendOptions : class; protected abstract Task InnerDeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token); + CancellationToken token); public event StartedEventHandler? Started; public event StoppedEventHandler? Stopped; diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 67b1be60..cb55947b 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -19,8 +19,8 @@ public abstract class BotBuilder protected abstract TBot? InnerBuild(); } -public abstract class BotBuilder : BotBuilder - where TBotBuilder : BotBuilder +public abstract class BotBuilder : BotBuilder + where TBotBuilder : BotBuilder { protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; @@ -38,8 +38,7 @@ public BotBuilder AddServices(IServiceCollection services) return this; } - public BotBuilder AddAnalyticsSettings( - AnalyticsClientSettingsBuilder clientSettingsBuilder) + public BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) { AnalyticsClientSettingsBuilder = clientSettingsBuilder; @@ -53,8 +52,7 @@ protected BotBuilder AddServerSettings(ServerSettingsBuilder< return this; } - public BotBuilder AddBotDataAccessSettings( - DataAccessSettingsBuilder botDataAccessBuilder) + public BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) { BotDataAccessSettingsBuilder = botDataAccessBuilder; diff --git a/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs index 60f06801..c855e787 100644 --- a/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandChainProcessor.cs @@ -21,7 +21,8 @@ public CommandChainProcessor(ILogger> logge IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { } diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index 258fa749..7a8d4a0a 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -14,7 +14,7 @@ namespace Botticelli.Framework.Commands.Processors; public abstract class CommandProcessor : ICommandProcessor - where TCommand : class, ICommand + where TCommand : class, ICommand { private readonly string _command; private readonly ICommandValidator _commandValidator; @@ -24,8 +24,8 @@ public abstract class CommandProcessor : ICommandProcessor private IBot? _bot; protected CommandProcessor(ILogger logger, - ICommandValidator commandValidator, - IValidator messageValidator) + ICommandValidator commandValidator, + IValidator messageValidator) { Logger = logger; _commandValidator = commandValidator; @@ -34,9 +34,9 @@ protected CommandProcessor(ILogger logger, } protected CommandProcessor(ILogger logger, - ICommandValidator commandValidator, - IValidator messageValidator, - MetricsProcessor? metricsProcessor) + ICommandValidator commandValidator, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) { Logger = logger; _commandValidator = commandValidator; @@ -89,7 +89,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) if (CommandUtils.SimpleCommandRegex.IsMatch(body)) { var match = CommandUtils.SimpleCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -104,7 +104,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) else if (CommandUtils.ArgsCommandRegex.IsMatch(body)) { var match = CommandUtils.ArgsCommandRegex.Matches(body) - .FirstOrDefault(); + .FirstOrDefault(); if (match == null) return; @@ -120,7 +120,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) { if (GetType().IsAssignableTo(typeof(CommandChainProcessor))) await ValidateAndProcess(message, - token); + token); } if (message.Location != null) await InnerProcessLocation(message, token); @@ -156,10 +156,12 @@ protected static void Classify(ref Message message) message.Type = Message.MessageType.Messaging; } - private static string GetBody(Message message) => - !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData - : !string.IsNullOrWhiteSpace(message.Body) ? message.Body - : string.Empty; + private static string GetBody(Message message) + { + return !string.IsNullOrWhiteSpace(message.CallbackData) ? message.CallbackData + : !string.IsNullOrWhiteSpace(message.Body) ? message.Body + : string.Empty; + } private void SendMetric(string metricName) { @@ -168,20 +170,20 @@ private void SendMetric(string metricName) private void SendMetric() { - _metricsProcessor?.Process( - GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), - BotDataUtils.GetBotId()!); + _metricsProcessor?.Process(GetOldFashionedCommandName($"{GetType().Name.Replace("Processor", string.Empty)}Command"), + BotDataUtils.GetBotId()!); } - private string GetOldFashionedCommandName(string fullCommand) => - fullCommand.ToLowerInvariant().Replace("command", ""); + private string GetOldFashionedCommandName(string fullCommand) + { + return fullCommand.ToLowerInvariant().Replace("command", ""); + } private async Task ValidateAndProcess(Message message, - CancellationToken token) + CancellationToken token) { - if (_bot == null) - return; - + if (_bot == null) return; + if (message.Type == Message.MessageType.Messaging) { SendMetric(); @@ -212,83 +214,89 @@ private async Task ValidateAndProcess(Message message, protected async Task DeleteMessage(DeleteMessageRequest request, CancellationToken token) { - if (_bot == null) - return; - + if (_bot == null) return; + await _bot.DeleteMessageAsync(request, token); } - + protected async Task DeleteMessage(Message message, CancellationToken token) { - if (_bot == null) - return; + if (_bot == null) return; - foreach (var request in message.ChatIds.Select(chatId => new DeleteMessageRequest(message.Uid, chatId))) - await _bot.DeleteMessageAsync(request, token); + foreach (var request in message.ChatIds.Select(chatId => new DeleteMessageRequest(message.Uid, chatId))) await _bot.DeleteMessageAsync(request, token); } - + protected async Task SendMessage(Message message, CancellationToken token) { - if (_bot == null) - return; + if (_bot == null) return; var request = new SendMessageRequest { Message = message }; - + await SendMessage(request, token); } - + protected async Task SendMessage(SendMessageRequest request, CancellationToken token) { - if (_bot == null) - return; - + if (_bot == null) return; + await _bot.SendMessageAsync(request, token); } protected async Task SendMessage(SendMessageRequest request, - SendOptionsBuilder? options, CancellationToken token) - where TReplyMarkup : class + SendOptionsBuilder? options, + CancellationToken token) + where TReplyMarkup : class { - if (_bot == null) - return; + if (_bot == null) return; await _bot.SendMessageAsync(request, options, token); } - - protected async Task UpdateMessage(Message message, ISendOptionsBuilder? options, - CancellationToken token) - where TSendOptions : class + + protected async Task UpdateMessage(Message message, + ISendOptionsBuilder? options, + CancellationToken token) + where TSendOptions : class { - if (_bot == null) - return; - + if (_bot == null) return; + await _bot.UpdateMessageAsync(new SendMessageRequest - { - ExpectPartialResponse = false, - Message = new Message - { - Body = message.CallbackData, - Uid = message.Uid, - ChatIds = message.ChatIds, - ChatIdInnerIdLinks = message.ChatIdInnerIdLinks - } - }, - options, - token); + { + ExpectPartialResponse = false, + Message = new Message + { + Body = message.CallbackData, + Uid = message.Uid, + ChatIds = message.ChatIds, + ChatIdInnerIdLinks = message.ChatIdInnerIdLinks + } + }, + options, + token); } - protected virtual Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; + protected virtual Task InnerProcessContact(Message message, CancellationToken token) + { + return Task.CompletedTask; + } - protected virtual Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + protected virtual Task InnerProcessPoll(Message message, CancellationToken token) + { + return Task.CompletedTask; + } - protected virtual Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; + protected virtual Task InnerProcessLocation(Message message, CancellationToken token) + { + return Task.CompletedTask; + } protected abstract Task InnerProcess(Message message, CancellationToken token); - protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) => - Task.CompletedTask; + protected virtual Task InnerProcessError(Message message, Exception? ex, CancellationToken token) + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs index 9fd65b07..f5ada24c 100644 --- a/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/WaitForClientResponseCommandChainProcessor.cs @@ -11,16 +11,17 @@ namespace Botticelli.Framework.Commands.Processors; /// /// public abstract class WaitForClientResponseCommandChainProcessor : CommandProcessor, - ICommandChainProcessor - where TInputCommand : class, ICommand + ICommandChainProcessor + where TInputCommand : class, ICommand { protected WaitForClientResponseCommandChainProcessor(ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator, metricsProcessor) + ICommandValidator commandValidator, + MetricsProcessor metricsProcessor, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator, + metricsProcessor) { } diff --git a/Botticelli.Framework/Options/BotDataSettings.cs b/Botticelli.Framework/Options/BotDataSettings.cs index cae2877c..31b728b8 100644 --- a/Botticelli.Framework/Options/BotDataSettings.cs +++ b/Botticelli.Framework/Options/BotDataSettings.cs @@ -6,7 +6,7 @@ namespace Botticelli.Framework.Options; public class BotDataSettings { public const string Section = "BotData"; - + public string? BotId { get; set; } public string? BotKey { get; set; } } \ No newline at end of file diff --git a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs index 93d5ebc3..d474fad0 100644 --- a/Botticelli.Framework/Options/BotDataSettingsBuilder.cs +++ b/Botticelli.Framework/Options/BotDataSettingsBuilder.cs @@ -1,13 +1,14 @@ -using Microsoft.Extensions.DependencyInjection; - namespace Botticelli.Framework.Options; public class BotDataSettingsBuilder - where T : BotDataSettings, new() + where T : BotDataSettings, new() { private T? _settings; - public static BotDataSettingsBuilder Instance() => new(); + public static BotDataSettingsBuilder Instance() + { + return new BotDataSettingsBuilder(); + } public void Set(T? settings) diff --git a/Botticelli.Framework/Services/BotActualizationService.cs b/Botticelli.Framework/Services/BotActualizationService.cs index cccfefbe..c2edaf1e 100644 --- a/Botticelli.Framework/Services/BotActualizationService.cs +++ b/Botticelli.Framework/Services/BotActualizationService.cs @@ -26,8 +26,8 @@ public abstract class BotActualizationService : IHostedService /// to Botticelli Admin server and receiving status messages from it /// protected BotActualizationService(IHttpClientFactory httpClientFactory, - IBot bot, - ILogger logger) + IBot bot, + ILogger logger) { HttpClientFactory = httpClientFactory; Bot = bot; @@ -36,15 +36,15 @@ protected BotActualizationService(IHttpClientFactory httpClientFactory, ActualizationEvent.Reset(); } - + /// /// This service is intended for sending keepalive/hello messages /// to Botticelli Admin server and receiving status messages from it /// protected BotActualizationService(IHttpClientFactory httpClientFactory, - IBot bot, - ILogger logger, - ServerSettings? serverSettings) + IBot bot, + ILogger logger, + ServerSettings? serverSettings) { ServerSettings = serverSettings; HttpClientFactory = httpClientFactory; diff --git a/Botticelli.Framework/Services/BotStandaloneService.cs b/Botticelli.Framework/Services/BotStandaloneService.cs index 3015acd4..2882e9b2 100644 --- a/Botticelli.Framework/Services/BotStandaloneService.cs +++ b/Botticelli.Framework/Services/BotStandaloneService.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Options; using Botticelli.Interfaces; using Botticelli.Shared.API.Admin.Requests; using Microsoft.Extensions.Logging; @@ -6,13 +5,13 @@ namespace Botticelli.Framework.Services; public class BotStandaloneService( - IHttpClientFactory httpClientFactory, - BotData.Entities.Bot.BotData botData, - IBot bot, - ILogger logger) - : BotActualizationService(httpClientFactory, - bot, - logger) + IHttpClientFactory httpClientFactory, + BotData.Entities.Bot.BotData botData, + IBot bot, + ILogger logger) + : BotActualizationService(httpClientFactory, + bot, + logger) { public override async Task StartAsync(CancellationToken cancellationToken) { @@ -20,6 +19,8 @@ public override async Task StartAsync(CancellationToken cancellationToken) await Bot.StartBotAsync(StartBotRequest.GetInstance(), cancellationToken); } - public override async Task StopAsync(CancellationToken cancellationToken) - => await Bot.StopBotAsync(StopBotRequest.GetInstance(), cancellationToken); + public override async Task StopAsync(CancellationToken cancellationToken) + { + await Bot.StopBotAsync(StopBotRequest.GetInstance(), cancellationToken); + } } \ No newline at end of file diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 071cad0f..f26ddf70 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -6,7 +6,6 @@ using Botticelli.Shared.API.Admin.Responses; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; -using Botticelli.Shared.ValueObjects; using Microsoft.Extensions.Logging; using Polly; @@ -19,7 +18,8 @@ public class BotStatusService( ILogger logger) : BotActualizationService(httpClientFactory, bot, - logger, serverSettings) + logger, + serverSettings) { private const short GetStatusPeriod = 5000; private Task? _getRequiredStatusEventTask; diff --git a/Botticelli.Framework/Services/PollActualizationService.cs b/Botticelli.Framework/Services/PollActualizationService.cs index e5057d07..a05bba66 100644 --- a/Botticelli.Framework/Services/PollActualizationService.cs +++ b/Botticelli.Framework/Services/PollActualizationService.cs @@ -17,7 +17,8 @@ public class PollActualizationService( ILogger logger) : BotActualizationService(httpClientFactory, bot, - logger, serverSettings) + logger, + serverSettings) where TRequest : IBotRequest, new() { private const short ActionPeriod = 5000; diff --git a/Botticelli.Locations.Tests/ReverseGeocoderMock.cs b/Botticelli.Locations.Tests/ReverseGeocoderMock.cs index c11e9f2c..c4a66771 100644 --- a/Botticelli.Locations.Tests/ReverseGeocoderMock.cs +++ b/Botticelli.Locations.Tests/ReverseGeocoderMock.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Nominatim.API.Interfaces; using Nominatim.API.Models; @@ -8,8 +9,8 @@ public class ReverseGeocoderMock : IReverseGeocoder { public Task ReverseGeocode(ReverseGeocodeRequest req) { - if (req is { Latitude: not null, Longitude: not null }) - return Task.FromResult(new() + if (req is {Latitude: not null, Longitude: not null}) + return Task.FromResult(new GeocodeResponse { Latitude = req.Latitude.Value, Longitude = req.Longitude.Value, @@ -29,7 +30,7 @@ public Task ReverseGeocode(ReverseGeocodeRequest req) Name = string.Empty } }); - - throw new System.InvalidOperationException(); + + throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 98b08407..5efc65af 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -23,7 +23,8 @@ public class FindLocationsCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) where TReplyMarkup : class { protected override async Task InnerProcess(Message message, CancellationToken token) diff --git a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs index a99b832c..86effb00 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/MapCommandProcessor.cs @@ -18,7 +18,8 @@ public MapCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { } diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index b6df747a..e1cfb206 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -25,22 +25,22 @@ public static class ServiceCollectionExtensions /// /// public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc) - where THandler : IPreCheckoutHandler, new() - where TProcessor : IPayProcessor + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc) + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor { services.AddPayments(); return services.AddTelegramBot(optionsBuilderFunc, - analyticsOptionsBuilderFunc, - serverSettingsBuilderFunc, - dataAccessSettingsBuilderFunc, - o => o - .AddSubHandler() - .AddSubHandler()); + analyticsOptionsBuilderFunc, + serverSettingsBuilderFunc, + dataAccessSettingsBuilderFunc, + o => o + .AddSubHandler() + .AddSubHandler()); } /// @@ -50,13 +50,14 @@ public static IServiceCollection AddTelegramPayBot /// /// public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, IConfiguration configuration) - where THandler : IPreCheckoutHandler, new() - where TProcessor : IPayProcessor + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor { services.AddPayments(); - return services.AddTelegramBot(configuration, o => o.AddSubHandler() - .AddSubHandler()); + return services.AddTelegramBot(configuration, + o => o.AddSubHandler() + .AddSubHandler()); } /// @@ -66,15 +67,15 @@ public static IServiceCollection AddTelegramPayBot(this IS /// /// public static IServiceCollection AddStandaloneTelegramPayBot(this IServiceCollection services, - IConfiguration configuration) - where THandler : IPreCheckoutHandler, new() - where TProcessor : IPayProcessor + IConfiguration configuration) + where THandler : IPreCheckoutHandler, new() + where TProcessor : IPayProcessor { services.AddPayments(); return services.AddStandaloneTelegramBot(configuration, - o => o - .AddSubHandler() - .AddSubHandler()); + o => o + .AddSubHandler() + .AddSubHandler()); } } \ No newline at end of file diff --git a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs index e690275d..860bf183 100644 --- a/Botticelli.Pay.Telegram/TelegramPaymentBot.cs +++ b/Botticelli.Pay.Telegram/TelegramPaymentBot.cs @@ -24,7 +24,8 @@ public TelegramPaymentBot(ITelegramBotClient client, handler, logger, textTransformer, - data, metrics) + data, + metrics) { } @@ -49,5 +50,8 @@ await Client.SendInvoice(chatId, cancellationToken: token); } - private static int ConvertPrice(decimal price, Currency currency) => Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); + private static int ConvertPrice(decimal price, Currency currency) + { + return Convert.ToInt32(price * (decimal) Math.Pow(10, currency.Decimals ?? 2)); + } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs index f64a7e79..7d0afeba 100644 --- a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs +++ b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs @@ -25,10 +25,10 @@ public class AdminAuthService : IAdminAuthService private readonly ILogger _logger; public AdminAuthService(IConfiguration config, - IHttpContextAccessor httpContextAccessor, - ServerDataContext context, - ILogger logger, - IOptionsMonitor settings) + IHttpContextAccessor httpContextAccessor, + ServerDataContext context, + ILogger logger, + IOptionsMonitor settings) { _config = config; _httpContextAccessor = httpContextAccessor; @@ -41,10 +41,12 @@ public AdminAuthService(IConfiguration config, /// /// /// - public async Task HasUsersAsync() => - await _context - .ApplicationUsers - .AnyAsync(); + public async Task HasUsersAsync() + { + return await _context + .ApplicationUsers + .AnyAsync(); + } /// public async Task RegisterAsync(UserAddRequest userRegister) @@ -56,7 +58,7 @@ public async Task RegisterAsync(UserAddRequest userRegister) ValidateRequest(userRegister); if (_context.ApplicationUsers.AsQueryable() - .Any(u => u.NormalizedEmail == GetNormalized(userRegister.Email!))) + .Any(u => u.NormalizedEmail == GetNormalized(userRegister.Email!))) throw new DataException($"User with email {userRegister.Email} already exists!"); var user = new IdentityUser @@ -100,7 +102,7 @@ public async Task RegeneratePassword(UserAddRequest userRegister) ValidateRequest(userRegister); if (_context.ApplicationUsers.AsQueryable() - .Any(u => u.NormalizedEmail == GetNormalized(userRegister.Email!))) + .Any(u => u.NormalizedEmail == GetNormalized(userRegister.Email!))) throw new DataException($"User with email {userRegister.Email} already exists!"); await _context.SaveChangesAsync(); @@ -133,8 +135,8 @@ public async Task RegeneratePassword(UserAddRequest userRegister) } var user = _context.ApplicationUsers - .AsQueryable() - .FirstOrDefault(u => u.NormalizedEmail == GetNormalized(userLogin.Email)); + .AsQueryable() + .FirstOrDefault(u => u.NormalizedEmail == GetNormalized(userLogin.Email)); if (user == null) return new GetTokenResponse @@ -143,16 +145,16 @@ public async Task RegeneratePassword(UserAddRequest userRegister) }; var userRole = _context.ApplicationUserRoles - .AsQueryable() - .FirstOrDefault(ur => ur.UserId == user.Id); + .AsQueryable() + .FirstOrDefault(ur => ur.UserId == user.Id); var roleName = string.Empty; if (userRole != null) { var role = _context.ApplicationRoles - .AsQueryable() - .FirstOrDefault(r => r.Id == userRole.RoleId); + .AsQueryable() + .FirstOrDefault(r => r.Id == userRole.RoleId); roleName = role?.Name; } @@ -164,16 +166,15 @@ public async Task RegeneratePassword(UserAddRequest userRegister) new Claim("role", roleName ?? "no_role") }; - var key = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(_config["Authorization:Key"] ?? throw new InvalidOperationException())); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Authorization:Key"] ?? throw new InvalidOperationException())); var signCreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_config["Authorization:Issuer"], - _config["Authorization:Audience"], - claims, - expires: DateTime.Now.AddHours(24), // NOTE!!! Temporary! - //.AddMinutes(_settings.CurrentValue.TokenLifetimeMin), - signingCredentials: signCreds); + _config["Authorization:Audience"], + claims, + expires: DateTime.Now.AddHours(24), // NOTE!!! Temporary! + //.AddMinutes(_settings.CurrentValue.TokenLifetimeMin), + signingCredentials: signCreds); return new GetTokenResponse { @@ -200,13 +201,13 @@ public bool CheckToken(string token) var sign = _config["Authorization:Key"] ?? throw new InvalidOperationException(); var handler = new JwtSecurityTokenHandler(); handler.ValidateToken(token, - new TokenValidationParameters - { - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(sign)), - ValidIssuer = _config["Authorization:Issuer"], - ValidateAudience = false - }, - out var validatedToken); + new TokenValidationParameters + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(sign)), + ValidIssuer = _config["Authorization:Issuer"], + ValidateAudience = false + }, + out var validatedToken); _logger.LogInformation($"{nameof(CheckToken)}() validate token: {validatedToken != null}"); @@ -238,9 +239,9 @@ public bool CheckToken(string token) public string? GetCurrentUserId() { return _httpContextAccessor.HttpContext?.User - .Claims - .FirstOrDefault(c => c.Type == "applicationUserId") - ?.Value; + .Claims + .FirstOrDefault(c => c.Type == "applicationUserId") + ?.Value; } private static void ValidateRequest(UserAddRequest userRegister) @@ -258,5 +259,8 @@ private static void ValidateRequest(UserLoginRequest userLogin) userLogin.Password!.NotNullOrEmpty(); } - private static string GetNormalized(string input) => input.ToUpper(); + private static string GetNormalized(string input) + { + return input.ToUpper(); + } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Services/BotManagementService.cs b/Botticelli.Server.Back/Services/BotManagementService.cs index 6d784f5a..27d4935c 100644 --- a/Botticelli.Server.Back/Services/BotManagementService.cs +++ b/Botticelli.Server.Back/Services/BotManagementService.cs @@ -30,10 +30,10 @@ public BotManagementService(ServerDataContext context, /// /// public Task RegisterBot(string botId, - string? botKey, - string botName, - BotType botType, - Dictionary? additionalParams = null) + string? botKey, + string botName, + BotType botType, + Dictionary? additionalParams = null) { try { diff --git a/Botticelli.Talks/Botticelli.Talks.csproj b/Botticelli.Talks/Botticelli.Talks.csproj index a1bf21f7..78ca1e94 100644 --- a/Botticelli.Talks/Botticelli.Talks.csproj +++ b/Botticelli.Talks/Botticelli.Talks.csproj @@ -32,9 +32,9 @@ - - new_logo_compact.png - + + new_logo_compact.png + \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj index 95c9a12c..39b8acbc 100644 --- a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj +++ b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj @@ -1,39 +1,39 @@ - - net8.0 - enable - enable - 0.8.0 - Botticelli - Igor Evdokimov - https://github.com/devgopher/botticelli - logo.jpg - https://github.com/devgopher/botticelli - TelegramMessagingSample.Standalone - + + net8.0 + enable + enable + 0.8.0 + Botticelli + Igor Evdokimov + https://github.com/devgopher/botticelli + logo.jpg + https://github.com/devgopher/botticelli + TelegramMessagingSample.Standalone + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - Never - true - Never - - + + + Never + true + Never + + - - - - - - + + + + + + \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs index 71bc226a..2c063044 100644 --- a/Messaging.Sample.Telegram.Standalone/Program.cs +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -10,21 +10,21 @@ var builder = WebApplication.CreateBuilder(args); builder.Services - .AddStandaloneTelegramBot(builder.Configuration) - .AddTelegramLayoutsSupport() - .AddLogging(cfg => cfg.AddNLog()) - .AddQuartzScheduler(builder.Configuration); + .AddStandaloneTelegramBot(builder.Configuration) + .AddTelegramLayoutsSupport() + .AddLogging(cfg => cfg.AddNLog()) + .AddQuartzScheduler(builder.Configuration); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index efd4d830..6c4e99bd 100644 --- a/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -8,20 +8,30 @@ namespace TelegramPayBot.Commands.Processors; public class InfoCommandProcessor( - ILogger> logger, - ICommandValidator commandValidator, - MetricsProcessor metricsProcessor, - IValidator messageValidator) - : CommandProcessor(logger, - commandValidator, - messageValidator, metricsProcessor) - where TReplyMarkup : class + ILogger> logger, + ICommandValidator commandValidator, + MetricsProcessor metricsProcessor, + IValidator messageValidator) + : CommandProcessor(logger, + commandValidator, + messageValidator, + metricsProcessor) + where TReplyMarkup : class { - protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; + protected override Task InnerProcessContact(Message message, CancellationToken token) + { + return Task.CompletedTask; + } - protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + protected override Task InnerProcessPoll(Message message, CancellationToken token) + { + return Task.CompletedTask; + } - protected override Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; + protected override Task InnerProcessLocation(Message message, CancellationToken token) + { + return Task.CompletedTask; + } protected override async Task InnerProcess(Message message, CancellationToken token) { diff --git a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs index 8c4a854a..87137ab5 100644 --- a/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs +++ b/Pay.Sample.Telegram/Commands/Processors/SendInvoiceCommandProcessor.cs @@ -23,7 +23,8 @@ public SendInvoiceCommandProcessor(ILogger paySettingsAccessor) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { _paySettingsAccessor = paySettingsAccessor; } diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs b/Samples/Ai.ChatGpt.Sample.Vk/Program.cs index 34f335da..423dd8da 100644 --- a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs +++ b/Samples/Ai.ChatGpt.Sample.Vk/Program.cs @@ -1,7 +1,6 @@ using AiSample.Common; using AiSample.Common.Commands; using AiSample.Common.Handlers; -using AiSample.Common.Settings; using Botticelli.AI.ChatGpt.Extensions; using Botticelli.Bus.None.Extensions; using Botticelli.Framework.Commands.Validators; diff --git a/Samples/Ai.Common.Sample/AiCommandProcessor.cs b/Samples/Ai.Common.Sample/AiCommandProcessor.cs index 1fb39715..5e8849fb 100644 --- a/Samples/Ai.Common.Sample/AiCommandProcessor.cs +++ b/Samples/Ai.Common.Sample/AiCommandProcessor.cs @@ -27,7 +27,8 @@ public AiCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { _bus = bus; var responseLayout = new AiLayout(); @@ -38,14 +39,14 @@ public AiCommandProcessor(ILogger> logger, _bus.OnReceived += async (sender, response) => { await SendMessage(new SendMessageRequest(response.Uid) - { - Message = response.Message, - ExpectPartialResponse = response.IsPartial, - SequenceNumber = response.SequenceNumber, - IsFinal = response.IsFinal - }, - options, - CancellationToken.None); + { + Message = response.Message, + ExpectPartialResponse = response.IsPartial, + SequenceNumber = response.SequenceNumber, + IsFinal = response.IsFinal + }, + options, + CancellationToken.None); }; } diff --git a/Samples/Ai.YaGpt.Sample.Vk/Program.cs b/Samples/Ai.YaGpt.Sample.Vk/Program.cs index 0421435c..5f48415d 100644 --- a/Samples/Ai.YaGpt.Sample.Vk/Program.cs +++ b/Samples/Ai.YaGpt.Sample.Vk/Program.cs @@ -1,7 +1,6 @@ using AiSample.Common; using AiSample.Common.Commands; using AiSample.Common.Handlers; -using AiSample.Common.Settings; using Botticelli.AI.YaGpt.Extensions; using Botticelli.Bus.None.Extensions; using Botticelli.Framework.Commands.Validators; diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 70f2d883..83866531 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -18,7 +18,8 @@ public InfoCommandProcessor(ILogger> logger, IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { } diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs index 859f135c..5051c622 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/RegisterCommandProcessor.cs @@ -29,7 +29,8 @@ public class RegisterCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) where TReplyMarkup : class { protected override Task InnerProcessContact(Message message, CancellationToken token) diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs index 56dee5df..26302928 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs @@ -30,7 +30,8 @@ public StartCommandProcessor(ILogger> logger IManager roleManager) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { _userInfo = userInfo; _roleManager = roleManager; diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs index c19cac66..aa477b68 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/GetNameCommandProcessor.cs @@ -1,7 +1,6 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using FluentValidation; diff --git a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs index 9e3b4838..9021ab29 100644 --- a/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs +++ b/Samples/CommandChain.Sample.Telegram/Commands/CommandProcessors/SayHelloFinalCommandProcessor.cs @@ -1,7 +1,6 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using FluentValidation; diff --git a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs index 3522b45b..7b98eeaf 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs @@ -18,7 +18,8 @@ public class DateChosenCommandProcessor( IValidator messageValidator) : CommandProcessor(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { protected override async Task InnerProcess(Message message, CancellationToken token) { diff --git a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs index f648d123..cd8119b2 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs @@ -27,7 +27,8 @@ public GetCalendarCommandProcessor(IBot bot, IValidator messageValidator) : base(logger, commandValidator, - messageValidator, metricsProcessor) + messageValidator, + metricsProcessor) { _bot = bot; diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index 102634bc..6864981e 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -16,13 +16,13 @@ public class InfoCommandProcessor : CommandProcessor private readonly SendOptionsBuilder? _options; public InfoCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator) + ICommandValidator commandValidator, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) { var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); @@ -30,13 +30,13 @@ public InfoCommandProcessor(ILogger> logger, _options = SendOptionsBuilder.CreateBuilder(responseMarkup); } - + public InfoCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator, - MetricsProcessor? metricsProcessor) + ICommandValidator commandValidator, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) : base(logger, commandValidator, messageValidator, diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index 314b3cd9..5fb29564 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -20,16 +20,16 @@ public class StartCommandProcessor : CommandProcessor? _options; private IBot? _bot; - + public StartCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) { _jobManager = jobManager; @@ -39,15 +39,16 @@ public StartCommandProcessor(ILogger> logger } public StartCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator, - MetricsProcessor? metricsProcessor) - : base(logger, - commandValidator, - messageValidator, metricsProcessor) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) + : base(logger, + commandValidator, + messageValidator, + metricsProcessor) { _jobManager = jobManager; @@ -61,6 +62,7 @@ private static TReplyMarkup Init(ILayoutSupplier layoutSupplier, I var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; var responseLayout = layoutParser.ParseFromFile(Path.Combine(location, "main_layout.json")); var responseMarkup = layoutSupplier.GetMarkup(responseLayout); + return responseMarkup; } @@ -69,12 +71,21 @@ public override void SetBot(IBot bot) base.SetBot(bot); _bot = bot; } - - protected override Task InnerProcessContact(Message message, CancellationToken token) => Task.CompletedTask; - protected override Task InnerProcessPoll(Message message, CancellationToken token) => Task.CompletedTask; + protected override Task InnerProcessContact(Message message, CancellationToken token) + { + return Task.CompletedTask; + } - protected override Task InnerProcessLocation(Message message, CancellationToken token) => Task.CompletedTask; + protected override Task InnerProcessPoll(Message message, CancellationToken token) + { + return Task.CompletedTask; + } + + protected override Task InnerProcessLocation(Message message, CancellationToken token) + { + return Task.CompletedTask; + } protected override async Task InnerProcess(Message message, CancellationToken token) { @@ -95,53 +106,53 @@ protected override async Task InnerProcess(Message message, CancellationToken to throw new FileNotFoundException(); if (_bot != null) _jobManager.AddJob(_bot, - new Reliability - { - IsEnabled = false, - Delay = TimeSpan.FromSeconds(3), - IsExponential = true, - MaxTries = 5 - }, - new Message - { - Body = "Now you see me!", - ChatIds = [chatId], - Contact = new Contact - { - Phone = "+9003289384923842343243243", - Name = "Test", - Surname = "Botticelli" - }, - Attachments = - [ - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "testpic.png", - MediaType.Image, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "voice.mp3", - MediaType.Voice, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "video.mp4", - MediaType.Video, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), - - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - "document.odt", - MediaType.Document, - string.Empty, - await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) - ] - }, - new Schedule - { - Cron = "*/30 * * ? * * *" - }); + new Reliability + { + IsEnabled = false, + Delay = TimeSpan.FromSeconds(3), + IsExponential = true, + MaxTries = 5 + }, + new Message + { + Body = "Now you see me!", + ChatIds = [chatId], + Contact = new Contact + { + Phone = "+9003289384923842343243243", + Name = "Test", + Surname = "Botticelli" + }, + Attachments = + [ + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "testpic.png", + MediaType.Image, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/testpic.png"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "voice.mp3", + MediaType.Voice, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/voice.mp3"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "video.mp4", + MediaType.Video, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/video.mp4"), token)), + + new BinaryBaseAttachment(Guid.NewGuid().ToString(), + "document.odt", + MediaType.Document, + string.Empty, + await File.ReadAllBytesAsync(Path.Combine(assemblyPath, "Media/document.odt"), token)) + ] + }, + new Schedule + { + Cron = "*/30 * * ? * * *" + }); } } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs index f706ddcb..8072a500 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs @@ -19,29 +19,29 @@ public class StopCommandProcessor : CommandProcessor private SendOptionsBuilder? _options; public StopCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator) - : base(logger, - commandValidator, - messageValidator) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) { _jobManager = jobManager; Init(layoutSupplier, layoutParser); } - + public StopCommandProcessor(ILogger> logger, - ICommandValidator commandValidator, - IJobManager jobManager, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, - IValidator messageValidator, - MetricsProcessor? metricsProcessor) + ICommandValidator commandValidator, + IJobManager jobManager, + ILayoutSupplier layoutSupplier, + ILayoutParser layoutParser, + IValidator messageValidator, + MetricsProcessor? metricsProcessor) : base(logger, commandValidator, - messageValidator, + messageValidator, metricsProcessor) { _jobManager = jobManager; diff --git a/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs b/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs index 628c7c7c..f407233b 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs +++ b/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs @@ -15,7 +15,7 @@ public void Setup() _publisher = new MessagePublisher(new TestHttpClientFactory(), LoggerMocks.CreateConsoleLogger()); } - + private MessagePublisher _publisher = publisher; [Test] diff --git a/Tests/MetricsRandomGenerator/MetricsRandomGenerator.csproj b/Tests/MetricsRandomGenerator/MetricsRandomGenerator.csproj index 7435c120..d536bc17 100644 --- a/Tests/MetricsRandomGenerator/MetricsRandomGenerator.csproj +++ b/Tests/MetricsRandomGenerator/MetricsRandomGenerator.csproj @@ -12,9 +12,9 @@ - - appsettings.json - + + appsettings.json + diff --git a/Viber.Api/ViberService.cs b/Viber.Api/ViberService.cs index 0ec473e7..d29140c8 100644 --- a/Viber.Api/ViberService.cs +++ b/Viber.Api/ViberService.cs @@ -139,7 +139,7 @@ private async Task InnerSend(TReq request, if (!httpResponse.IsSuccessStatusCode) throw new ViberClientException($"Error sending request {nameof(SetWebHook)}: {httpResponse.StatusCode}!"); if (httpResponse.Content == null) throw new ViberClientException(""); - + return await httpResponse.Content.ReadFromJsonAsync(cancellationToken); } } From dad9f05338263330f1d12c6269fcf2b60bd52350 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 May 2025 11:51:39 +0300 Subject: [PATCH 020/101] - solution structure --- Botticelli.sln | 38 ++++++++++--------- .../Ai.ChatGpt.Sample.Vk.csproj | 4 -- .../Ai.Messaging.Sample.Vk.csproj | 4 -- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Botticelli.sln b/Botticelli.sln index e848acde..afcc59e5 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -177,14 +177,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ai.DeepSeek.Sample.Telegram EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.WeChat", "Botticelli.WeChat\Botticelli.WeChat.csproj", "{B8EE61E9-F029-49EA-81E5-D0BCBA265418}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{1BBE72D9-3509-4750-B79E-766B3122B999}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{94539590-4B6F-4500-B010-DED4D4A30529}" - ProjectSection(SolutionItems) = preProject - deploy\linux\bot_sample\Dockerfile = deploy\linux\bot_sample\Dockerfile - deploy\linux\bot_sample\push_docker.sh = deploy\linux\bot_sample\push_docker.sh - EndProjectSection -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Hangfire", "Botticelli.Scheduler.Hangfire\Botticelli.Scheduler.Hangfire.csproj", "{85C84F76-3730-4D17-923E-96B665544B57}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Quartz", "Botticelli.Scheduler.Quartz\Botticelli.Scheduler.Quartz.csproj", "{F958B922-0418-4AB8-B5C6-86510DB52F38}" @@ -253,6 +245,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Auth.Data.Sqlite EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Messaging.Sample.Telegram.Standalone", "Messaging.Sample.Telegram.Standalone\Messaging.Sample.Telegram.Standalone.csproj", "{E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Chained", "Botticelli.Chained", "{DB9DA043-4E18-433B-BD71-2CCC8A5E28C0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Bus", "Botticelli.Bus", "{EF3D6AA7-0334-4ECC-A1FD-C68856B7B954}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Pay", "Botticelli.Pay", "{34F34B4D-5BA2-45EC-8BC0-2507AF50A9A5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Schedule", "Botticelli.Schedule", "{2CC72293-C3FB-4A01-922A-E8C7CCD689A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -544,15 +544,11 @@ Global {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {CDFC4EAD-60BD-419A-ADF5-451766D22F29} = {A05D5A20-E0D3-4844-8311-C98E8B67DA7F} {9F4B5D3F-661C-433B-BC9F-CAC4048EF9EA} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {C010219B-6ECA-429F-98BA-839F6BAC0D6B} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {79DEC257-916B-402D-BBB9-BF4E6518FF8F} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {2851D399-D071-421A-B5B1-AB24883F82E0} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {1EE16F3E-B208-4211-9C77-9A24037668AC} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {BC2069C7-4808-4BF4-8D90-0160A588AD43} = {A05D5A20-E0D3-4844-8311-C98E8B67DA7F} {81FE9855-9884-49A9-A10D-61E7B709865E} = {A05D5A20-E0D3-4844-8311-C98E8B67DA7F} {B7B4B68F-7086-49F2-B401-C76B4EF0B320} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {2AD59714-541D-4794-9216-6EAB25F271DA} = {35480478-646E-45F1-96A3-EE7AC498ACE3} - {FD291718-B989-4660-AE04-70F7A456B7BB} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {A4F5B449-60D4-4959-9E0B-5EECFD2A317B} = {0530F522-2556-4D69-8275-86947E9D51EF} {9B590747-74F6-41C8-8733-B5B77A8FCAC1} = {A4F5B449-60D4-4959-9E0B-5EECFD2A317B} {2423A645-22D1-4736-946F-10A99353581C} = {A05D5A20-E0D3-4844-8311-C98E8B67DA7F} @@ -592,10 +588,6 @@ Global {252B974D-7725-44F5-BE69-330B85731B08} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} {1BC0B4EF-7C35-4BB7-B207-180FBC55681D} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} {B8EE61E9-F029-49EA-81E5-D0BCBA265418} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {1BBE72D9-3509-4750-B79E-766B3122B999} = {35480478-646E-45F1-96A3-EE7AC498ACE3} - {94539590-4B6F-4500-B010-DED4D4A30529} = {1BBE72D9-3509-4750-B79E-766B3122B999} - {85C84F76-3730-4D17-923E-96B665544B57} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {F958B922-0418-4AB8-B5C6-86510DB52F38} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {70EB3FE2-9EB5-4B62-9405-71D1A332B0FD} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {2DF28873-A172-42EB-8FBD-324DF8AD9323} = {2AD59714-541D-4794-9216-6EAB25F271DA} {9B731750-D28D-4DE0-AC8E-280E932BD301} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} @@ -611,7 +603,6 @@ Global {0530F522-2556-4D69-8275-86947E9D51EF} = {8C52EB41-3815-4B12-9E40-C0D1472361CA} {1A6572FD-D274-4700-901F-6F9F7A49F57B} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {8C96F00C-DD52-4808-9AC2-1C937257B47B} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {D159BBC6-8D73-44AB-A17B-B14AB7D56A0D} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73} = {2AD59714-541D-4794-9216-6EAB25F271DA} {ECCC27F2-F7ED-406C-90B8-BD53A8A59B47} = {8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73} {3D52F777-3A84-4238-974F-A5969085B472} = {8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73} @@ -623,7 +614,6 @@ Global {195344A3-DE14-4283-B7FB-3A5822C5C0FC} = {DEC72475-9783-4BD9-B31E-F77758269476} {8A5ECE73-1529-4B53-8907-735ADA05C20A} = {8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73} {CB97DC2A-EE66-43A6-977F-6CB82C030D7A} = {DEC72475-9783-4BD9-B31E-F77758269476} - {949C67AD-CF8F-4357-A3EF-D3DA407B525E} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {F4F6BA53-C40D-4168-9EE0-37DD9FC42665} = {2AD59714-541D-4794-9216-6EAB25F271DA} {FEA7D21C-E494-4E9C-A2F5-6D1F79FC07A9} = {F4F6BA53-C40D-4168-9EE0-37DD9FC42665} {08C7BAAF-80FD-4AAE-8400-2A2D12D6CF06} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} @@ -635,6 +625,18 @@ Global {9D1C8062-6388-4713-A81D-8B2F2C2D336D} = {49938A37-DA38-47A0-ADF7-DF2221F7F4A7} {D27A078E-1409-4B0B-AD7E-DD4528206049} = {08C7BAAF-80FD-4AAE-8400-2A2D12D6CF06} {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8} = {DEC72475-9783-4BD9-B31E-F77758269476} + {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {D159BBC6-8D73-44AB-A17B-B14AB7D56A0D} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {EF3D6AA7-0334-4ECC-A1FD-C68856B7B954} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {79DEC257-916B-402D-BBB9-BF4E6518FF8F} = {EF3D6AA7-0334-4ECC-A1FD-C68856B7B954} + {1EE16F3E-B208-4211-9C77-9A24037668AC} = {EF3D6AA7-0334-4ECC-A1FD-C68856B7B954} + {34F34B4D-5BA2-45EC-8BC0-2507AF50A9A5} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {C010219B-6ECA-429F-98BA-839F6BAC0D6B} = {34F34B4D-5BA2-45EC-8BC0-2507AF50A9A5} + {949C67AD-CF8F-4357-A3EF-D3DA407B525E} = {34F34B4D-5BA2-45EC-8BC0-2507AF50A9A5} + {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {FD291718-B989-4660-AE04-70F7A456B7BB} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} + {85C84F76-3730-4D17-923E-96B665544B57} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} + {F958B922-0418-4AB8-B5C6-86510DB52F38} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj b/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj index 31501d74..e8ec10e5 100644 --- a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj +++ b/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj @@ -23,10 +23,6 @@ - - - - PreserveNewest diff --git a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj b/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj index 8746e7e8..8e665623 100644 --- a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj +++ b/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj @@ -39,8 +39,4 @@ - - - - \ No newline at end of file From 54d9c6a5a2ff7110aba0190d2db50786f2ba95fa Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 May 2025 11:57:08 +0300 Subject: [PATCH 021/101] - solution structure --- .../Botticelli.Framework.Chained.Monads.csproj | 3 ++- .../Commands/Context/CommandContext.cs | 2 +- .../Commands/Context/IChainCommand.cs | 2 +- .../Commands/Context/ICommandContext.cs | 2 +- .../Commands/Names.cs | 2 +- .../Commands/Processors/ChainBuilder.cs | 4 ++-- .../Commands/Processors/ChainProcessor.cs | 6 +++--- .../Commands/Processors/ChainRunProcessor.cs | 4 ++-- .../Commands/Processors/ChainRunner.cs | 6 +++--- .../Commands/Processors/IChainProcessor.cs | 6 +++--- .../Commands/Processors/InputCommandProcessor.cs | 6 +++--- .../Commands/Processors/OutputCommandProcessor.cs | 6 +++--- .../Commands/Processors/TransformArgumentsProcessor.cs | 6 +++--- .../Commands/Processors/TransformProcessor.cs | 6 +++--- .../Commands/Result/BasicResult.cs | 2 +- .../Commands/Result/FailResult.cs | 2 +- .../Commands/Result/IResult.cs | 2 +- .../Commands/Result/SuccessResult.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 6 +++--- Botticelli.sln | 9 ++++++++- Samples/Monads.Sample.Telegram/Commands/MathCommand.cs | 2 +- .../Monads.Sample.Telegram/Monads.Sample.Telegram.csproj | 2 +- Samples/Monads.Sample.Telegram/Program.cs | 4 ++-- 23 files changed, 50 insertions(+), 42 deletions(-) rename Botticelli.Framework.Monads/Botticelli.Framework.Monads.csproj => Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj (95%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Context/CommandContext.cs (97%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Context/IChainCommand.cs (67%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Context/ICommandContext.cs (83%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Names.cs (74%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/ChainBuilder.cs (93%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/ChainProcessor.cs (89%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/ChainRunProcessor.cs (90%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/ChainRunner.cs (88%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/IChainProcessor.cs (71%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/InputCommandProcessor.cs (73%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/OutputCommandProcessor.cs (88%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/TransformArgumentsProcessor.cs (93%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Processors/TransformProcessor.cs (89%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Result/BasicResult.cs (84%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Result/FailResult.cs (91%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Result/IResult.cs (80%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Commands/Result/SuccessResult.cs (89%) rename {Botticelli.Framework.Monads => Botticelli.Framework.Chained.Monads}/Extensions/ServiceCollectionExtensions.cs (92%) diff --git a/Botticelli.Framework.Monads/Botticelli.Framework.Monads.csproj b/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj similarity index 95% rename from Botticelli.Framework.Monads/Botticelli.Framework.Monads.csproj rename to Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj index 2e7a8f09..ab9dbd06 100644 --- a/Botticelli.Framework.Monads/Botticelli.Framework.Monads.csproj +++ b/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Framework.Monads BotticelliBots new_logo_compact.png @@ -14,6 +14,7 @@ https://github.com/devgopher/botticelli telegram, bots, botticelli, vk, facebook, wechat, whatsapp true + Botticelli.Framework.Chained.Monads diff --git a/Botticelli.Framework.Monads/Commands/Context/CommandContext.cs b/Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs similarity index 97% rename from Botticelli.Framework.Monads/Commands/Context/CommandContext.cs rename to Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs index d65d68b3..b151a5f3 100644 --- a/Botticelli.Framework.Monads/Commands/Context/CommandContext.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -namespace Botticelli.Framework.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Monads.Commands.Context; /// /// Command context for transmitting data over command context chain diff --git a/Botticelli.Framework.Monads/Commands/Context/IChainCommand.cs b/Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs similarity index 67% rename from Botticelli.Framework.Monads/Commands/Context/IChainCommand.cs rename to Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs index d63f0e14..0c240841 100644 --- a/Botticelli.Framework.Monads/Commands/Context/IChainCommand.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs @@ -1,6 +1,6 @@ using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Monads.Commands.Context; public interface IChainCommand : ICommand { diff --git a/Botticelli.Framework.Monads/Commands/Context/ICommandContext.cs b/Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs similarity index 83% rename from Botticelli.Framework.Monads/Commands/Context/ICommandContext.cs rename to Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs index 70d54bb3..1a0ce00b 100644 --- a/Botticelli.Framework.Monads/Commands/Context/ICommandContext.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Monads.Commands.Context; /// /// A context for monad-based commands diff --git a/Botticelli.Framework.Monads/Commands/Names.cs b/Botticelli.Framework.Chained.Monads/Commands/Names.cs similarity index 74% rename from Botticelli.Framework.Monads/Commands/Names.cs rename to Botticelli.Framework.Chained.Monads/Commands/Names.cs index 3b86b585..6700dbf4 100644 --- a/Botticelli.Framework.Monads/Commands/Names.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Names.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Monads.Commands; +namespace Botticelli.Framework.Chained.Monads.Commands; public static class Names { diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainBuilder.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs similarity index 93% rename from Botticelli.Framework.Monads/Commands/Processors/ChainBuilder.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs index a43676db..2bd0c0bb 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainBuilder.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; public class ChainBuilder(IServiceCollection services) where TCommand : IChainCommand diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs similarity index 89% rename from Botticelli.Framework.Monads/Commands/Processors/ChainProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs index d8b931a1..c39ca12b 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs @@ -1,11 +1,11 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using Botticelli.Shared.ValueObjects; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; /// /// Chain processor diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs similarity index 90% rename from Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs index 58a1ce61..4c733242 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs @@ -1,13 +1,13 @@ using Botticelli.Client.Analytics; +using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Monads.Commands.Context; using Botticelli.Shared.ValueObjects; using FluentValidation; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; public class ChainRunProcessor( ILogger> logger, diff --git a/Botticelli.Framework.Monads/Commands/Processors/ChainRunner.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs similarity index 88% rename from Botticelli.Framework.Monads/Commands/Processors/ChainRunner.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs index c97ad6d7..c48e57a8 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/ChainRunner.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; public class ChainRunner(List> chain, ILogger> logger) where TCommand : IChainCommand diff --git a/Botticelli.Framework.Monads/Commands/Processors/IChainProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs similarity index 71% rename from Botticelli.Framework.Monads/Commands/Processors/IChainProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs index 54051535..94373f24 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/IChainProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using LanguageExt; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; /// /// Chain processor diff --git a/Botticelli.Framework.Monads/Commands/Processors/InputCommandProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs similarity index 73% rename from Botticelli.Framework.Monads/Commands/Processors/InputCommandProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs index d48d09d0..5e625d6d 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/InputCommandProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs @@ -1,8 +1,8 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; public class InputCommandProcessor(ILogger> logger) : ChainProcessor(logger) diff --git a/Botticelli.Framework.Monads/Commands/Processors/OutputCommandProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs similarity index 88% rename from Botticelli.Framework.Monads/Commands/Processors/OutputCommandProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs index adff9a4e..9e398867 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/OutputCommandProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs @@ -1,12 +1,12 @@ +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Framework.Controls.Parsers; -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; using Botticelli.Framework.SendOptions; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; public class OutputCommandProcessor : ChainProcessor where TReplyMarkup : class diff --git a/Botticelli.Framework.Monads/Commands/Processors/TransformArgumentsProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs similarity index 93% rename from Botticelli.Framework.Monads/Commands/Processors/TransformArgumentsProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs index 2825a284..e1a4ae17 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/TransformArgumentsProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; /// /// Func transform for command arguments processor diff --git a/Botticelli.Framework.Monads/Commands/Processors/TransformProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs similarity index 89% rename from Botticelli.Framework.Monads/Commands/Processors/TransformProcessor.cs rename to Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs index bbbfa87d..21bd8af0 100644 --- a/Botticelli.Framework.Monads/Commands/Processors/TransformProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Result; +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Monads.Commands.Processors; +namespace Botticelli.Framework.Chained.Monads.Commands.Processors; /// /// Func transform processor diff --git a/Botticelli.Framework.Monads/Commands/Result/BasicResult.cs b/Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs similarity index 84% rename from Botticelli.Framework.Monads/Commands/Result/BasicResult.cs rename to Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs index 743acb40..f2990366 100644 --- a/Botticelli.Framework.Monads/Commands/Result/BasicResult.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs @@ -1,6 +1,6 @@ using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Monads.Commands.Result; +namespace Botticelli.Framework.Chained.Monads.Commands.Result; public class BasicResult : IResult where TCommand : ICommand diff --git a/Botticelli.Framework.Monads/Commands/Result/FailResult.cs b/Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs similarity index 91% rename from Botticelli.Framework.Monads/Commands/Result/FailResult.cs rename to Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs index 80db04c9..163a1e59 100644 --- a/Botticelli.Framework.Monads/Commands/Result/FailResult.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs @@ -1,7 +1,7 @@ using Botticelli.Framework.Commands; using Botticelli.Shared.ValueObjects; -namespace Botticelli.Framework.Monads.Commands.Result; +namespace Botticelli.Framework.Chained.Monads.Commands.Result; /// /// Fail result diff --git a/Botticelli.Framework.Monads/Commands/Result/IResult.cs b/Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs similarity index 80% rename from Botticelli.Framework.Monads/Commands/Result/IResult.cs rename to Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs index 52c87116..ab86bae4 100644 --- a/Botticelli.Framework.Monads/Commands/Result/IResult.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Monads.Commands.Result; +namespace Botticelli.Framework.Chained.Monads.Commands.Result; public interface IResult where TCommand : ICommand diff --git a/Botticelli.Framework.Monads/Commands/Result/SuccessResult.cs b/Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs similarity index 89% rename from Botticelli.Framework.Monads/Commands/Result/SuccessResult.cs rename to Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs index 03a5eda6..3b66356f 100644 --- a/Botticelli.Framework.Monads/Commands/Result/SuccessResult.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs @@ -1,7 +1,7 @@ using Botticelli.Framework.Commands; using Botticelli.Shared.ValueObjects; -namespace Botticelli.Framework.Monads.Commands.Result; +namespace Botticelli.Framework.Chained.Monads.Commands.Result; /// /// Success result diff --git a/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs similarity index 92% rename from Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs index 15f3a9f7..7b6ed4a1 100644 --- a/Botticelli.Framework.Monads/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ +using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Processors; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Extensions; -using Botticelli.Framework.Monads.Commands.Context; -using Botticelli.Framework.Monads.Commands.Processors; using Microsoft.Extensions.DependencyInjection; -namespace Botticelli.Framework.Monads.Extensions; +namespace Botticelli.Framework.Chained.Monads.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.sln b/Botticelli.sln index afcc59e5..7af4de44 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -213,7 +213,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Bot.Data.Entitie EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monads.Sample.Telegram", "Samples\Monads.Sample.Telegram\Monads.Sample.Telegram.csproj", "{CBD02578-B208-4888-AB18-D9969A4082ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Monads", "Botticelli.Framework.Monads\Botticelli.Framework.Monads.csproj", "{D159BBC6-8D73-44AB-A17B-B14AB7D56A0D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Monads", "Botticelli.Framework.Chained.Monads\Botticelli.Framework.Chained.Monads.csproj", "{D159BBC6-8D73-44AB-A17B-B14AB7D56A0D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73}" EndProject @@ -253,6 +253,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Pay", "Botticell EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Schedule", "Botticelli.Schedule", "{2CC72293-C3FB-4A01-922A-E8C7CCD689A0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context", "Botticelli.Framework.Chained.Context\Botticelli.Framework.Chained.Context.csproj", "{3A9F8D81-DB62-43D3-841C-9F37D8238E36}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -531,6 +533,10 @@ Global {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9B52BA7-B0B3-4823-9BB3-8B4AA40CB2C8}.Release|Any CPU.Build.0 = Release|Any CPU + {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -637,6 +643,7 @@ Global {FD291718-B989-4660-AE04-70F7A456B7BB} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {85C84F76-3730-4D17-923E-96B665544B57} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {F958B922-0418-4AB8-B5C6-86510DB52F38} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} + {3A9F8D81-DB62-43D3-841C-9F37D8238E36} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs index 79a3f3a8..67e21e78 100644 --- a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs +++ b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands.Context; namespace TelegramMonadsBasedBot.Commands; diff --git a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj index f385c72e..4feade58 100644 --- a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj +++ b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj @@ -18,7 +18,7 @@ - + diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index 0c8d3d4d..8c6efdf5 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -1,7 +1,7 @@ +using Botticelli.Framework.Chained.Monads.Commands.Processors; +using Botticelli.Framework.Chained.Monads.Extensions; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; -using Botticelli.Framework.Monads.Commands.Processors; -using Botticelli.Framework.Monads.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Layout; using NLog.Extensions.Logging; From ea3997fdd73e51293f05368bce0f8dfd819d4ee5 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 May 2025 12:00:42 +0300 Subject: [PATCH 022/101] - Botticelli.Framework.Chained.Context --- .../Botticelli.Framework.Chained.Context.csproj | 9 +++++++++ .../CommandContext.cs | 2 +- .../ICommandContext.cs | 2 +- .../Names.cs | 2 +- .../Botticelli.Framework.Chained.Monads.csproj | 1 + .../Commands/{Context => }/IChainCommand.cs | 3 ++- .../Commands/Processors/ChainBuilder.cs | 1 - .../Commands/Processors/ChainProcessor.cs | 1 - .../Commands/Processors/ChainRunProcessor.cs | 2 +- .../Commands/Processors/ChainRunner.cs | 1 - .../Commands/Processors/IChainProcessor.cs | 1 - .../Commands/Processors/InputCommandProcessor.cs | 1 - .../Commands/Processors/OutputCommandProcessor.cs | 1 - .../Commands/Processors/TransformArgumentsProcessor.cs | 2 +- .../Commands/Processors/TransformProcessor.cs | 1 - .../Extensions/ServiceCollectionExtensions.cs | 2 +- Samples/Monads.Sample.Telegram/Commands/MathCommand.cs | 3 ++- 17 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj rename {Botticelli.Framework.Chained.Monads/Commands/Context => Botticelli.Framework.Chained.Context}/CommandContext.cs (97%) rename {Botticelli.Framework.Chained.Monads/Commands/Context => Botticelli.Framework.Chained.Context}/ICommandContext.cs (83%) rename {Botticelli.Framework.Chained.Monads/Commands => Botticelli.Framework.Chained.Context}/Names.cs (74%) rename Botticelli.Framework.Chained.Monads/Commands/{Context => }/IChainCommand.cs (57%) diff --git a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj new file mode 100644 index 00000000..3a635329 --- /dev/null +++ b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs b/Botticelli.Framework.Chained.Context/CommandContext.cs similarity index 97% rename from Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs rename to Botticelli.Framework.Chained.Context/CommandContext.cs index b151a5f3..48d9ec39 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Context/CommandContext.cs +++ b/Botticelli.Framework.Chained.Context/CommandContext.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -namespace Botticelli.Framework.Chained.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Context; /// /// Command context for transmitting data over command context chain diff --git a/Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs b/Botticelli.Framework.Chained.Context/ICommandContext.cs similarity index 83% rename from Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs rename to Botticelli.Framework.Chained.Context/ICommandContext.cs index 1a0ce00b..5fcb1243 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Context/ICommandContext.cs +++ b/Botticelli.Framework.Chained.Context/ICommandContext.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Chained.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Context; /// /// A context for monad-based commands diff --git a/Botticelli.Framework.Chained.Monads/Commands/Names.cs b/Botticelli.Framework.Chained.Context/Names.cs similarity index 74% rename from Botticelli.Framework.Chained.Monads/Commands/Names.cs rename to Botticelli.Framework.Chained.Context/Names.cs index 6700dbf4..cfb581ed 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Names.cs +++ b/Botticelli.Framework.Chained.Context/Names.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Chained.Monads.Commands; +namespace Botticelli.Framework.Chained.Context; public static class Names { diff --git a/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj b/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj index ab9dbd06..c2552b56 100644 --- a/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj +++ b/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj @@ -45,6 +45,7 @@ + diff --git a/Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs b/Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs similarity index 57% rename from Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs rename to Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs index 0c240841..2d7847ad 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Context/IChainCommand.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs @@ -1,6 +1,7 @@ +using Botticelli.Framework.Chained.Context; using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Chained.Monads.Commands.Context; +namespace Botticelli.Framework.Chained.Monads.Commands; public interface IChainCommand : ICommand { diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs index 2bd0c0bb..3a39461b 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs index c39ca12b..34cd60d8 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using Botticelli.Shared.ValueObjects; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs index 4c733242..32ec22c2 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs @@ -1,5 +1,5 @@ using Botticelli.Client.Analytics; -using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Context; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs index c48e57a8..0dd1b6e6 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs index 94373f24..c055485e 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using LanguageExt; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs index 5e625d6d..c69be169 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using Microsoft.Extensions.Logging; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs index 9e398867..e824c3c9 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs index e1a4ae17..682e533b 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs index 21bd8af0..ba99e809 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs @@ -1,4 +1,3 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; using Botticelli.Framework.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; diff --git a/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs index 7b6ed4a1..7cb3bd02 100644 --- a/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Monads.Commands; using Botticelli.Framework.Chained.Monads.Commands.Processors; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Controls.Parsers; diff --git a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs index 67e21e78..4ebb385a 100644 --- a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs +++ b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs @@ -1,4 +1,5 @@ -using Botticelli.Framework.Chained.Monads.Commands.Context; +using Botticelli.Framework.Chained.Context; +using Botticelli.Framework.Chained.Monads.Commands; namespace TelegramMonadsBasedBot.Commands; From 2c744ffc1613562de4b5b25db6e22a8520165863 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 May 2025 14:50:53 +0300 Subject: [PATCH 023/101] - redis storage --- ....Framework.Chained.Context.InMemory.csproj | 13 ++++ .../Extensions/ServiceCollectionExtensions.cs | 17 +++++ .../InMemoryContextStorageBuilder.cs | 19 +++++ .../InMemoryStorage.cs | 44 +++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 18 +++++ .../RedisStorage.cs | 76 +++++++++++++++++++ ...otticelli.Framework.Chained.Context.csproj | 4 + .../CommandContext.cs | 21 +++-- .../Extensions/ServiceCollectionExtensions.cs | 15 ++++ .../IStorage.cs | 56 ++++++++++++++ .../Settings/IContextStorageBuilder.cs | 10 +++ .../Commands/Processors/ChainRunProcessor.cs | 8 +- Botticelli.sln | 14 ++++ 13 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj create mode 100644 Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs create mode 100644 Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs create mode 100644 Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs create mode 100644 Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context/IStorage.cs create mode 100644 Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs diff --git a/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj b/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj new file mode 100644 index 00000000..49b5bd66 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..55735f42 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.InMemory.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedInMemoryStorage(this IServiceCollection services, Action> builderFunc) + where TKey : notnull + { + InMemoryContextStorageBuilder builder = new(); + builderFunc(builder); + + services.AddSingleton, InMemoryStorage>(_ => builder.Build()); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs b/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs new file mode 100644 index 00000000..18fb164e --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs @@ -0,0 +1,19 @@ +using Botticelli.Framework.Chained.Context.Settings; + +namespace Botticelli.Framework.Chained.Context.InMemory; + +public class InMemoryContextStorageBuilder : IContextStorageBuilder, TKey, TValue> + where TKey : notnull +{ + private int? _capacity; + + public InMemoryStorage Build() => + _capacity == null ? new InMemoryStorage() : new InMemoryStorage(_capacity.Value); + + public InMemoryContextStorageBuilder SetInitialCapacity(int capacity) + { + _capacity = capacity; + + return this; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs b/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs new file mode 100644 index 00000000..d848cd20 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Botticelli.Framework.Chained.Context.InMemory; + +/// +/// In-memory storage +/// +/// +/// +public class InMemoryStorage : IStorage + where TKey : notnull +{ + private readonly Dictionary _dictionary; + + public InMemoryStorage() + { + _dictionary = new Dictionary(); + } + + public InMemoryStorage(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + /// + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + public void Add(TKey key, TValue? value) => _dictionary.Add(key, value); + + /// + public bool Remove(TKey key) => _dictionary.Remove(key); + + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => + _dictionary.TryGetValue(key, out value); + + /// + public TValue? this[TKey key] + { + get => _dictionary[key]; + set => _dictionary[key] = value; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..02999349 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.Redis.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedRedisStorage(this IServiceCollection services, Action> builderFunc) + where TKey : notnull + where TValue : class + { + RedisContextStorageBuilder builder = new(); + builderFunc(builder); + + services.AddSingleton, RedisStorage>(_ => builder.Build()); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs b/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs new file mode 100644 index 00000000..bac587cb --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs @@ -0,0 +1,76 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using StackExchange.Redis; + +namespace Botticelli.Framework.Chained.Context.Redis; + +/// +/// RedisStorage storage +/// +/// +/// +public class RedisStorage : IStorage + where TKey : notnull where TValue : class +{ + private readonly IDatabase _database; + + public RedisStorage(string connectionString) + { + var redis = ConnectionMultiplexer.Connect(connectionString); + _database = redis.GetDatabase(); + } + + public bool ContainsKey(TKey key) => _database.KeyExists(key.ToString()); + + public void Add(TKey key, TValue? value) + { + _database.StringSet(key.ToString(), + value is not string ? JsonSerializer.Serialize(value?.ToString()) : value.ToString()); + } + + public bool Remove(TKey key) => _database.KeyDelete(key.ToString()); + + public bool TryGetValue(TKey key, out TValue? value) + { + value = default!; + + if (!ContainsKey(key)) + return false; + + InnerGet(key, ref value); + + return true; + } + + private void InnerGet(TKey key, [DisallowNull] ref TValue? value) + { + if (value is string) + { + value = _database.StringGet(key.ToString()) as TValue; + } + else + { + var text = _database.StringGet(key.ToString()); + + if (text is { HasValue: true }) + value = JsonSerializer.Deserialize(text!); + } + } + + public TValue? this[TKey key] + { + get + { + if (!ContainsKey(key)) + return null; + + TValue? value = default!; + + InnerGet(key, ref value); + + return value; + } + set => Add(key, value); + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj index 3a635329..b6e3fde8 100644 --- a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj +++ b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Botticelli.Framework.Chained.Context/CommandContext.cs b/Botticelli.Framework.Chained.Context/CommandContext.cs index 48d9ec39..3eb444b6 100644 --- a/Botticelli.Framework.Chained.Context/CommandContext.cs +++ b/Botticelli.Framework.Chained.Context/CommandContext.cs @@ -8,11 +8,16 @@ namespace Botticelli.Framework.Chained.Context; /// public class CommandContext : ICommandContext { - private readonly Dictionary _parameters = new(); + private readonly IStorage _storage; + + public CommandContext(IStorage storage) + { + _storage = storage; + } public T? Get(string name) { - if (!_parameters.TryGetValue(name, out var parameter)) return default; + if (!_storage.TryGetValue(name, out var parameter)) return default; var type = typeof(T); @@ -34,7 +39,7 @@ public class CommandContext : ICommandContext public string Get(string name) { - return _parameters[name]; + return _storage[name]; } public T Set(string name, T value) @@ -42,16 +47,16 @@ public T Set(string name, T value) if (value is null) throw new ArgumentNullException(nameof(value)); if (name == Names.Args) // args are always string - _parameters[name] = Stringify(value); + _storage[name] = Stringify(value); else - _parameters[name] = JsonSerializer.Serialize(value); + _storage[name] = JsonSerializer.Serialize(value); return value; } public string Set(string name, string value) { - _parameters[name] = Stringify(value); + _storage[name] = Stringify(value); return value; } @@ -59,11 +64,11 @@ public string Set(string name, string value) public T Transform(string name, Func func) { - if (!_parameters.TryGetValue(name, out var parameter)) throw new KeyNotFoundException(name); + if (!_storage.TryGetValue(name, out var parameter)) throw new KeyNotFoundException(name); var deserialized = JsonSerializer.Deserialize(parameter); - _parameters[name] = JsonSerializer.Serialize(func(deserialized)); + _storage[name] = JsonSerializer.Serialize(func(deserialized)); return deserialized; } diff --git a/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..dda2d29e --- /dev/null +++ b/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedStorage(this IServiceCollection services) + where TStorage : class, IStorage + where TKey : notnull + { + services.AddSingleton, TStorage>(); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/IStorage.cs b/Botticelli.Framework.Chained.Context/IStorage.cs new file mode 100644 index 00000000..584ac13a --- /dev/null +++ b/Botticelli.Framework.Chained.Context/IStorage.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Botticelli.Framework.Chained.Context; + +/// +/// Represents a generic key-value storage interface with basic CRUD-like operations. +/// Supports contravariant key types for flexible usage with inheritance hierarchies. +/// +/// The type of keys (contravariant - accepts the specified type or its base types). +/// The type of values to store. +public interface IStorage + where TKey : notnull +{ + /// + /// Determines whether the storage contains the specified key. + /// + /// The key to locate. + /// True if the key exists; otherwise, false. + bool ContainsKey(TKey key); + + /// + /// Adds a new key-value pair to the storage. + /// + /// The key of the element to add. + /// The value of the element to add. + /// Thrown if the key already exists. + void Add(TKey key, TValue? value); + + /// + /// Removes the key-value pair with the specified key from the storage. + /// + /// The key of the element to remove. + /// True if the element was successfully removed; otherwise, false. + bool Remove(TKey key); + + /// + /// Attempts to retrieve the value associated with the specified key. + /// + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, the default value for the type. + /// This parameter is marked as maybe null when the method returns false. + /// + /// True if the key exists; otherwise, false. + bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue? value); + + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key of the value to get or set. + /// The value associated with the specified key. + /// Thrown when getting a key that doesn't exist. + /// Thrown when setting a key that doesn't exist (if the storage requires explicit Add). + TValue? this[TKey key] { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs b/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs new file mode 100644 index 00000000..eef2d6de --- /dev/null +++ b/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs @@ -0,0 +1,10 @@ +namespace Botticelli.Framework.Chained.Context.Settings; + +public interface IContextStorageBuilder +where TStorage : IStorage +where TKey : notnull +{ + private TStorage Storage => throw new NotImplementedException(); + + public TStorage Build(); +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs index 32ec22c2..c220cc00 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs @@ -5,6 +5,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Shared.ValueObjects; using FluentValidation; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Botticelli.Framework.Chained.Monads.Commands.Processors; @@ -14,7 +15,8 @@ public class ChainRunProcessor( ICommandValidator validator, MetricsProcessor metricsProcessor, ChainRunner chainRunner, - IValidator messageValidator) + IValidator messageValidator, + IServiceProvider serviceProvider) : CommandProcessor(logger, validator, messageValidator, @@ -23,9 +25,11 @@ public class ChainRunProcessor( { protected override async Task InnerProcess(Message message, CancellationToken token) { + var storage = serviceProvider.GetRequiredService>(); + var command = new TCommand { - Context = new CommandContext() + Context = new CommandContext(storage) }; command.Context.Set(Names.Message, message); diff --git a/Botticelli.sln b/Botticelli.sln index 7af4de44..79240eb4 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -255,6 +255,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Schedule", "Bott EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context", "Botticelli.Framework.Chained.Context\Botticelli.Framework.Chained.Context.csproj", "{3A9F8D81-DB62-43D3-841C-9F37D8238E36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.InMemory", "Botticelli.Framework.Chained.Context.InMemory\Botticelli.Framework.Chained.Context.InMemory.csproj", "{1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.Redis", "Botticelli.Framework.Chained.Context.Redis\Botticelli.Framework.Chained.Context.Redis.csproj", "{4E5352E6-9F06-4A92-A6D9-E82DBA516E72}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -537,6 +541,14 @@ Global {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.Build.0 = Release|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Release|Any CPU.Build.0 = Release|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -644,6 +656,8 @@ Global {85C84F76-3730-4D17-923E-96B665544B57} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {F958B922-0418-4AB8-B5C6-86510DB52F38} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {3A9F8D81-DB62-43D3-841C-9F37D8238E36} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From abb5c053928057abf3ae79d194f34c3004d9c9b6 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 10 May 2025 14:50:53 +0300 Subject: [PATCH 024/101] - redis storage --- ....Framework.Chained.Context.InMemory.csproj | 13 ++++ .../Extensions/ServiceCollectionExtensions.cs | 17 +++++ .../InMemoryContextStorageBuilder.cs | 19 +++++ .../InMemoryStorage.cs | 44 +++++++++++ ...lli.Framework.Chained.Context.Redis.csproj | 18 +++++ .../Extensions/ServiceCollectionExtensions.cs | 18 +++++ .../RedisContextStorageBuilder.cs | 26 +++++++ .../RedisStorage.cs | 76 +++++++++++++++++++ ...otticelli.Framework.Chained.Context.csproj | 4 + .../CommandContext.cs | 21 +++-- .../Extensions/ServiceCollectionExtensions.cs | 15 ++++ .../IStorage.cs | 56 ++++++++++++++ .../Settings/IContextStorageBuilder.cs | 10 +++ .../Commands/Processors/ChainRunProcessor.cs | 8 +- Botticelli.sln | 14 ++++ 15 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj create mode 100644 Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs create mode 100644 Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs create mode 100644 Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj create mode 100644 Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs create mode 100644 Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs create mode 100644 Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Framework.Chained.Context/IStorage.cs create mode 100644 Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs diff --git a/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj b/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj new file mode 100644 index 00000000..49b5bd66 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..55735f42 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.InMemory.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedInMemoryStorage(this IServiceCollection services, Action> builderFunc) + where TKey : notnull + { + InMemoryContextStorageBuilder builder = new(); + builderFunc(builder); + + services.AddSingleton, InMemoryStorage>(_ => builder.Build()); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs b/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs new file mode 100644 index 00000000..18fb164e --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs @@ -0,0 +1,19 @@ +using Botticelli.Framework.Chained.Context.Settings; + +namespace Botticelli.Framework.Chained.Context.InMemory; + +public class InMemoryContextStorageBuilder : IContextStorageBuilder, TKey, TValue> + where TKey : notnull +{ + private int? _capacity; + + public InMemoryStorage Build() => + _capacity == null ? new InMemoryStorage() : new InMemoryStorage(_capacity.Value); + + public InMemoryContextStorageBuilder SetInitialCapacity(int capacity) + { + _capacity = capacity; + + return this; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs b/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs new file mode 100644 index 00000000..d848cd20 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Botticelli.Framework.Chained.Context.InMemory; + +/// +/// In-memory storage +/// +/// +/// +public class InMemoryStorage : IStorage + where TKey : notnull +{ + private readonly Dictionary _dictionary; + + public InMemoryStorage() + { + _dictionary = new Dictionary(); + } + + public InMemoryStorage(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + /// + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + public void Add(TKey key, TValue? value) => _dictionary.Add(key, value); + + /// + public bool Remove(TKey key) => _dictionary.Remove(key); + + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => + _dictionary.TryGetValue(key, out value); + + /// + public TValue? this[TKey key] + { + get => _dictionary[key]; + set => _dictionary[key] = value; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj b/Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj new file mode 100644 index 00000000..185c8e9a --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..02999349 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.Redis.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedRedisStorage(this IServiceCollection services, Action> builderFunc) + where TKey : notnull + where TValue : class + { + RedisContextStorageBuilder builder = new(); + builderFunc(builder); + + services.AddSingleton, RedisStorage>(_ => builder.Build()); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs b/Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs new file mode 100644 index 00000000..3877e318 --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs @@ -0,0 +1,26 @@ +using System.Configuration; +using Botticelli.Framework.Chained.Context.Settings; + +namespace Botticelli.Framework.Chained.Context.Redis; + +public class RedisContextStorageBuilder : IContextStorageBuilder, TKey, TValue> + where TKey : notnull + where TValue : class +{ + private string? _connectionString; + + public RedisContextStorageBuilder AddConnectionString(string connectionString) + { + _connectionString = connectionString; + + return this; + } + + public RedisStorage Build() + { + if (_connectionString != null) + return new(_connectionString); + + throw new ConfigurationErrorsException("No connection string for redis was given!"); + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs b/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs new file mode 100644 index 00000000..bac587cb --- /dev/null +++ b/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs @@ -0,0 +1,76 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using StackExchange.Redis; + +namespace Botticelli.Framework.Chained.Context.Redis; + +/// +/// RedisStorage storage +/// +/// +/// +public class RedisStorage : IStorage + where TKey : notnull where TValue : class +{ + private readonly IDatabase _database; + + public RedisStorage(string connectionString) + { + var redis = ConnectionMultiplexer.Connect(connectionString); + _database = redis.GetDatabase(); + } + + public bool ContainsKey(TKey key) => _database.KeyExists(key.ToString()); + + public void Add(TKey key, TValue? value) + { + _database.StringSet(key.ToString(), + value is not string ? JsonSerializer.Serialize(value?.ToString()) : value.ToString()); + } + + public bool Remove(TKey key) => _database.KeyDelete(key.ToString()); + + public bool TryGetValue(TKey key, out TValue? value) + { + value = default!; + + if (!ContainsKey(key)) + return false; + + InnerGet(key, ref value); + + return true; + } + + private void InnerGet(TKey key, [DisallowNull] ref TValue? value) + { + if (value is string) + { + value = _database.StringGet(key.ToString()) as TValue; + } + else + { + var text = _database.StringGet(key.ToString()); + + if (text is { HasValue: true }) + value = JsonSerializer.Deserialize(text!); + } + } + + public TValue? this[TKey key] + { + get + { + if (!ContainsKey(key)) + return null; + + TValue? value = default!; + + InnerGet(key, ref value); + + return value; + } + set => Add(key, value); + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj index 3a635329..b6e3fde8 100644 --- a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj +++ b/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Botticelli.Framework.Chained.Context/CommandContext.cs b/Botticelli.Framework.Chained.Context/CommandContext.cs index 48d9ec39..3eb444b6 100644 --- a/Botticelli.Framework.Chained.Context/CommandContext.cs +++ b/Botticelli.Framework.Chained.Context/CommandContext.cs @@ -8,11 +8,16 @@ namespace Botticelli.Framework.Chained.Context; /// public class CommandContext : ICommandContext { - private readonly Dictionary _parameters = new(); + private readonly IStorage _storage; + + public CommandContext(IStorage storage) + { + _storage = storage; + } public T? Get(string name) { - if (!_parameters.TryGetValue(name, out var parameter)) return default; + if (!_storage.TryGetValue(name, out var parameter)) return default; var type = typeof(T); @@ -34,7 +39,7 @@ public class CommandContext : ICommandContext public string Get(string name) { - return _parameters[name]; + return _storage[name]; } public T Set(string name, T value) @@ -42,16 +47,16 @@ public T Set(string name, T value) if (value is null) throw new ArgumentNullException(nameof(value)); if (name == Names.Args) // args are always string - _parameters[name] = Stringify(value); + _storage[name] = Stringify(value); else - _parameters[name] = JsonSerializer.Serialize(value); + _storage[name] = JsonSerializer.Serialize(value); return value; } public string Set(string name, string value) { - _parameters[name] = Stringify(value); + _storage[name] = Stringify(value); return value; } @@ -59,11 +64,11 @@ public string Set(string name, string value) public T Transform(string name, Func func) { - if (!_parameters.TryGetValue(name, out var parameter)) throw new KeyNotFoundException(name); + if (!_storage.TryGetValue(name, out var parameter)) throw new KeyNotFoundException(name); var deserialized = JsonSerializer.Deserialize(parameter); - _parameters[name] = JsonSerializer.Serialize(func(deserialized)); + _storage[name] = JsonSerializer.Serialize(func(deserialized)); return deserialized; } diff --git a/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..dda2d29e --- /dev/null +++ b/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Chained.Context.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddChainedStorage(this IServiceCollection services) + where TStorage : class, IStorage + where TKey : notnull + { + services.AddSingleton, TStorage>(); + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/IStorage.cs b/Botticelli.Framework.Chained.Context/IStorage.cs new file mode 100644 index 00000000..584ac13a --- /dev/null +++ b/Botticelli.Framework.Chained.Context/IStorage.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Botticelli.Framework.Chained.Context; + +/// +/// Represents a generic key-value storage interface with basic CRUD-like operations. +/// Supports contravariant key types for flexible usage with inheritance hierarchies. +/// +/// The type of keys (contravariant - accepts the specified type or its base types). +/// The type of values to store. +public interface IStorage + where TKey : notnull +{ + /// + /// Determines whether the storage contains the specified key. + /// + /// The key to locate. + /// True if the key exists; otherwise, false. + bool ContainsKey(TKey key); + + /// + /// Adds a new key-value pair to the storage. + /// + /// The key of the element to add. + /// The value of the element to add. + /// Thrown if the key already exists. + void Add(TKey key, TValue? value); + + /// + /// Removes the key-value pair with the specified key from the storage. + /// + /// The key of the element to remove. + /// True if the element was successfully removed; otherwise, false. + bool Remove(TKey key); + + /// + /// Attempts to retrieve the value associated with the specified key. + /// + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, the default value for the type. + /// This parameter is marked as maybe null when the method returns false. + /// + /// True if the key exists; otherwise, false. + bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue? value); + + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key of the value to get or set. + /// The value associated with the specified key. + /// Thrown when getting a key that doesn't exist. + /// Thrown when setting a key that doesn't exist (if the storage requires explicit Add). + TValue? this[TKey key] { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs b/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs new file mode 100644 index 00000000..eef2d6de --- /dev/null +++ b/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs @@ -0,0 +1,10 @@ +namespace Botticelli.Framework.Chained.Context.Settings; + +public interface IContextStorageBuilder +where TStorage : IStorage +where TKey : notnull +{ + private TStorage Storage => throw new NotImplementedException(); + + public TStorage Build(); +} \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs index 32ec22c2..c220cc00 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs @@ -5,6 +5,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Shared.ValueObjects; using FluentValidation; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Botticelli.Framework.Chained.Monads.Commands.Processors; @@ -14,7 +15,8 @@ public class ChainRunProcessor( ICommandValidator validator, MetricsProcessor metricsProcessor, ChainRunner chainRunner, - IValidator messageValidator) + IValidator messageValidator, + IServiceProvider serviceProvider) : CommandProcessor(logger, validator, messageValidator, @@ -23,9 +25,11 @@ public class ChainRunProcessor( { protected override async Task InnerProcess(Message message, CancellationToken token) { + var storage = serviceProvider.GetRequiredService>(); + var command = new TCommand { - Context = new CommandContext() + Context = new CommandContext(storage) }; command.Context.Set(Names.Message, message); diff --git a/Botticelli.sln b/Botticelli.sln index 7af4de44..79240eb4 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -255,6 +255,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Schedule", "Bott EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context", "Botticelli.Framework.Chained.Context\Botticelli.Framework.Chained.Context.csproj", "{3A9F8D81-DB62-43D3-841C-9F37D8238E36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.InMemory", "Botticelli.Framework.Chained.Context.InMemory\Botticelli.Framework.Chained.Context.InMemory.csproj", "{1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.Redis", "Botticelli.Framework.Chained.Context.Redis\Botticelli.Framework.Chained.Context.Redis.csproj", "{4E5352E6-9F06-4A92-A6D9-E82DBA516E72}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -537,6 +541,14 @@ Global {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A9F8D81-DB62-43D3-841C-9F37D8238E36}.Release|Any CPU.Build.0 = Release|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}.Release|Any CPU.Build.0 = Release|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -644,6 +656,8 @@ Global {85C84F76-3730-4D17-923E-96B665544B57} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {F958B922-0418-4AB8-B5C6-86510DB52F38} = {2CC72293-C3FB-4A01-922A-E8C7CCD689A0} {3A9F8D81-DB62-43D3-841C-9F37D8238E36} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {4E5352E6-9F06-4A92-A6D9-E82DBA516E72} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From b139df3a9b451f869b5c3a95c2d0105622f72b13 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sat, 10 May 2025 22:45:33 +0300 Subject: [PATCH 025/101] BOTTICELLI-63 Refactoring: simplifying & structuring --- .../Botticelli.Chained.Context.InMemory.csproj | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../InMemoryContextStorageBuilder.cs | 4 ++-- .../InMemoryStorage.cs | 2 +- .../Botticelli.Chained.Context.Redis.csproj | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../RedisContextStorageBuilder.cs | 4 ++-- .../RedisStorage.cs | 2 +- .../Botticelli.Chained.Context.csproj | 0 .../CommandContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../ICommandContext.cs | 2 +- .../IStorage.cs | 2 +- .../Names.cs | 2 +- .../Settings/IContextStorageBuilder.cs | 2 +- .../Botticelli.Chained.Monads.csproj | 5 ++--- .../Commands/IChainCommand.cs | 4 ++-- .../Commands/Processors/ChainBuilder.cs | 2 +- .../Commands/Processors/ChainProcessor.cs | 4 ++-- .../Commands/Processors/ChainRunProcessor.cs | 4 ++-- .../Commands/Processors/ChainRunner.cs | 4 ++-- .../Commands/Processors/IChainProcessor.cs | 4 ++-- .../Commands/Processors/InputCommandProcessor.cs | 4 ++-- .../Commands/Processors/OutputCommandProcessor.cs | 6 +++--- .../Processors/TransformArgumentsProcessor.cs | 6 +++--- .../Commands/Processors/TransformProcessor.cs | 4 ++-- .../Commands/Result/BasicResult.cs | 2 +- .../Commands/Result/FailResult.cs | 2 +- .../Commands/Result/IResult.cs | 2 +- .../Commands/Result/SuccessResult.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 8 ++++---- .../Botticelli.Controls.Layouts.csproj | 2 +- .../InlineCalendar/ICCommandProcessor.cs | 8 ++++---- .../InlineCalendar/BaseCalendarCommand.cs | 2 +- .../Commands/InlineCalendar/DateChosenCommand.cs | 2 +- .../InlineCalendar/MonthBackwardCommand.cs | 5 +++++ .../InlineCalendar/MonthForwardCommand.cs | 5 +++++ .../Commands/InlineCalendar/SetDateCommand.cs | 5 +++++ .../InlineCalendar/YearBackwardCommand.cs | 5 +++++ .../Commands/InlineCalendar/YearForwardCommand.cs | 5 +++++ .../Dialogs/yes_no.json | 0 .../Dialogs/yes_no_cancel.json | 0 .../Extensions/ServiceCollectionExtensions.cs | 8 ++++---- .../Inlines/CalendarFactory.cs | 2 +- .../Inlines/InlineButtonMenu.cs | 4 ++-- .../Inlines/InlineCalendar.cs | 4 ++-- .../Inlines/Table.cs | 2 +- .../Resources/Images/Common/check.xcf | Bin .../Resources/Images/Common/check_black.png | Bin .../Resources/Images/Common/check_green.png | Bin .../BasicControls/Button.cs | 2 +- .../BasicControls/IControl.cs | 2 +- .../BasicControls/Text.cs | 2 +- Botticelli.Controls/BasicControls/TextButton.cs | 5 +++++ .../Botticelli.Controls.csproj | 0 .../Exceptions/LayoutException.cs | 2 +- .../Extensions/DictionaryExtensions.cs | 2 +- .../Layouts/BaseLayout.cs | 2 +- .../Layouts/CellAlign.cs | 2 +- .../Layouts/ILayout.cs | 2 +- .../Layouts/Item.cs | 4 ++-- .../Layouts/ItemParams.cs | 2 +- .../Layouts/Row.cs | 2 +- .../Parsers/ILayoutLoader.cs | 2 +- .../Parsers/ILayoutParser.cs | 4 ++-- .../Parsers/ILayoutSupplier.cs | 4 ++-- .../Parsers/JsonLayoutParser.cs | 8 ++++---- .../Parsers/LayoutLoader.cs | 4 ++-- .../InlineCalendar/MonthBackwardCommand.cs | 5 ----- .../InlineCalendar/MonthForwardCommand.cs | 5 ----- .../Commands/InlineCalendar/SetDateCommand.cs | 5 ----- .../InlineCalendar/YearBackwardCommand.cs | 5 ----- .../Commands/InlineCalendar/YearForwardCommand.cs | 5 ----- .../BasicControls/TextButton.cs | 5 ----- .../Botticelli.Framework.Telegram.csproj | 2 +- .../Builders/TelegramBotBuilder.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Layout/IInlineTelegramLayoutSupplier.cs | 2 +- .../Layout/IReplyTelegramLayoutSupplier.cs | 2 +- .../Layout/InlineTelegramLayoutSupplier.cs | 4 ++-- .../Layout/ReplyTelegramLayoutSupplier.cs | 4 ++-- .../Botticelli.Framework.Vk.Messages.csproj | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Layout/IVkLayoutSupplier.cs | 2 +- .../Layout/VkLayoutSupplier.cs | 8 ++++---- .../Extensions/ServiceCollectionExtensions.cs | 12 +++++------- .../Extensions/ServiceCollectionExtensions.cs | 2 +- Botticelli.Locations/Botticelli.Locations.csproj | 4 ++-- .../FindLocationsCommandProcessor.cs | 8 ++++---- Botticelli.sln | 14 +++++++------- Pay.Sample.Telegram/Program.cs | 2 +- Samples/Ai.Common.Sample/Ai.Common.Sample.csproj | 2 +- Samples/Ai.Common.Sample/AiCommandProcessor.cs | 2 +- Samples/Ai.Common.Sample/Layouts/AiLayout.cs | 4 ++-- .../Commands/Processors/InfoCommandProcessor.cs | 3 --- .../Commands/Processors/StartCommandProcessor.cs | 2 +- Samples/CommandChain.Sample.Telegram/Program.cs | 4 ++-- .../Handlers/DateChosenCommandProcessor.cs | 2 +- .../Handlers/GetCalendarCommandProcessor.cs | 4 ++-- .../Layouts.Sample.Telegram.csproj | 2 +- Samples/Layouts.Sample.Telegram/Program.cs | 2 +- .../Commands/Processors/InfoCommandProcessor.cs | 2 +- .../Commands/Processors/StartCommandProcessor.cs | 2 +- .../Commands/Processors/StopCommandProcessor.cs | 2 +- .../Commands/MathCommand.cs | 4 ++-- .../Monads.Sample.Telegram.csproj | 2 +- Samples/Monads.Sample.Telegram/Program.cs | 4 ++-- .../Botticelli.Framework.Controls.Tests.csproj | 2 +- .../Layouts/JsonLayoutParserTest.cs | 4 ++-- .../Layouts/ReplyTelegramLayoutSupplierTest.cs | 2 +- 110 files changed, 177 insertions(+), 183 deletions(-) rename Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj => Botticelli.Chained.Context.InMemory/Botticelli.Chained.Context.InMemory.csproj (68%) rename {Botticelli.Framework.Chained.Context.InMemory => Botticelli.Chained.Context.InMemory}/Extensions/ServiceCollectionExtensions.cs (89%) rename {Botticelli.Framework.Chained.Context.InMemory => Botticelli.Chained.Context.InMemory}/InMemoryContextStorageBuilder.cs (82%) rename {Botticelli.Framework.Chained.Context.InMemory => Botticelli.Chained.Context.InMemory}/InMemoryStorage.cs (95%) rename Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj => Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj (79%) rename {Botticelli.Framework.Chained.Context.Redis => Botticelli.Chained.Context.Redis}/Extensions/ServiceCollectionExtensions.cs (90%) rename {Botticelli.Framework.Chained.Context.Redis => Botticelli.Chained.Context.Redis}/RedisContextStorageBuilder.cs (86%) rename {Botticelli.Framework.Chained.Context.Redis => Botticelli.Chained.Context.Redis}/RedisStorage.cs (97%) rename Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj => Botticelli.Chained.Context/Botticelli.Chained.Context.csproj (100%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/CommandContext.cs (97%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/Extensions/ServiceCollectionExtensions.cs (87%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/ICommandContext.cs (87%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/IStorage.cs (98%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/Names.cs (77%) rename {Botticelli.Framework.Chained.Context => Botticelli.Chained.Context}/Settings/IContextStorageBuilder.cs (80%) rename Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj => Botticelli.Chained.Monads/Botticelli.Chained.Monads.csproj (88%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/IChainCommand.cs (57%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/ChainBuilder.cs (96%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/ChainProcessor.cs (92%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/ChainRunProcessor.cs (92%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/ChainRunner.cs (92%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/IChainProcessor.cs (78%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/InputCommandProcessor.cs (80%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/OutputCommandProcessor.cs (89%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/TransformArgumentsProcessor.cs (93%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Processors/TransformProcessor.cs (92%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Result/BasicResult.cs (84%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Result/FailResult.cs (91%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Result/IResult.cs (80%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Commands/Result/SuccessResult.cs (89%) rename {Botticelli.Framework.Chained.Monads => Botticelli.Chained.Monads}/Extensions/ServiceCollectionExtensions.cs (90%) rename Botticelli.Framework.Controls.Layouts/Botticelli.Framework.Controls.Layouts.csproj => Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj (91%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/CommandProcessors/InlineCalendar/ICCommandProcessor.cs (90%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Commands/InlineCalendar/BaseCalendarCommand.cs (69%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Commands/InlineCalendar/DateChosenCommand.cs (69%) create mode 100644 Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs create mode 100644 Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs create mode 100644 Botticelli.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs create mode 100644 Botticelli.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs create mode 100644 Botticelli.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Dialogs/yes_no.json (100%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Dialogs/yes_no_cancel.json (100%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Extensions/ServiceCollectionExtensions.cs (87%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Inlines/CalendarFactory.cs (95%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Inlines/InlineButtonMenu.cs (95%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Inlines/InlineCalendar.cs (97%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Inlines/Table.cs (88%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Resources/Images/Common/check.xcf (100%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Resources/Images/Common/check_black.png (100%) rename {Botticelli.Framework.Controls.Layouts => Botticelli.Controls.Layouts}/Resources/Images/Common/check_green.png (100%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/BasicControls/Button.cs (90%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/BasicControls/IControl.cs (91%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/BasicControls/Text.cs (89%) create mode 100644 Botticelli.Controls/BasicControls/TextButton.cs rename Botticelli.Framework.Controls/Botticelli.Framework.Controls.csproj => Botticelli.Controls/Botticelli.Controls.csproj (100%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Exceptions/LayoutException.cs (80%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Extensions/DictionaryExtensions.cs (88%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/BaseLayout.cs (78%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/CellAlign.cs (52%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/ILayout.cs (65%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/Item.cs (51%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/ItemParams.cs (67%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Layouts/Row.cs (73%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Parsers/ILayoutLoader.cs (82%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Parsers/ILayoutParser.cs (57%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Parsers/ILayoutSupplier.cs (73%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Parsers/JsonLayoutParser.cs (93%) rename {Botticelli.Framework.Controls => Botticelli.Controls}/Parsers/LayoutLoader.cs (88%) delete mode 100644 Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs delete mode 100644 Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs delete mode 100644 Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs delete mode 100644 Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs delete mode 100644 Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs delete mode 100644 Botticelli.Framework.Controls/BasicControls/TextButton.cs diff --git a/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj b/Botticelli.Chained.Context.InMemory/Botticelli.Chained.Context.InMemory.csproj similarity index 68% rename from Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj rename to Botticelli.Chained.Context.InMemory/Botticelli.Chained.Context.InMemory.csproj index 49b5bd66..5b455fb5 100644 --- a/Botticelli.Framework.Chained.Context.InMemory/Botticelli.Framework.Chained.Context.InMemory.csproj +++ b/Botticelli.Chained.Context.InMemory/Botticelli.Chained.Context.InMemory.csproj @@ -7,7 +7,7 @@ - + diff --git a/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs similarity index 89% rename from Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs index 55735f42..2ee663b9 100644 --- a/Botticelli.Framework.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Context.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace Botticelli.Framework.Chained.Context.InMemory.Extensions; +namespace Botticelli.Chained.Context.InMemory.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs b/Botticelli.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs similarity index 82% rename from Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs rename to Botticelli.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs index 18fb164e..b5022953 100644 --- a/Botticelli.Framework.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs +++ b/Botticelli.Chained.Context.InMemory/InMemoryContextStorageBuilder.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Chained.Context.Settings; +using Botticelli.Chained.Context.Settings; -namespace Botticelli.Framework.Chained.Context.InMemory; +namespace Botticelli.Chained.Context.InMemory; public class InMemoryContextStorageBuilder : IContextStorageBuilder, TKey, TValue> where TKey : notnull diff --git a/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs b/Botticelli.Chained.Context.InMemory/InMemoryStorage.cs similarity index 95% rename from Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs rename to Botticelli.Chained.Context.InMemory/InMemoryStorage.cs index d848cd20..efc7d68c 100644 --- a/Botticelli.Framework.Chained.Context.InMemory/InMemoryStorage.cs +++ b/Botticelli.Chained.Context.InMemory/InMemoryStorage.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Botticelli.Framework.Chained.Context.InMemory; +namespace Botticelli.Chained.Context.InMemory; /// /// In-memory storage diff --git a/Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj b/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj similarity index 79% rename from Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj rename to Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj index 185c8e9a..e4c7e12b 100644 --- a/Botticelli.Framework.Chained.Context.Redis/Botticelli.Framework.Chained.Context.Redis.csproj +++ b/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj @@ -7,7 +7,7 @@ - + diff --git a/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs similarity index 90% rename from Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs index 02999349..9ca9ee75 100644 --- a/Botticelli.Framework.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace Botticelli.Framework.Chained.Context.Redis.Extensions; +namespace Botticelli.Chained.Context.Redis.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs b/Botticelli.Chained.Context.Redis/RedisContextStorageBuilder.cs similarity index 86% rename from Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs rename to Botticelli.Chained.Context.Redis/RedisContextStorageBuilder.cs index 3877e318..1f8f9cf1 100644 --- a/Botticelli.Framework.Chained.Context.Redis/RedisContextStorageBuilder.cs +++ b/Botticelli.Chained.Context.Redis/RedisContextStorageBuilder.cs @@ -1,7 +1,7 @@ using System.Configuration; -using Botticelli.Framework.Chained.Context.Settings; +using Botticelli.Chained.Context.Settings; -namespace Botticelli.Framework.Chained.Context.Redis; +namespace Botticelli.Chained.Context.Redis; public class RedisContextStorageBuilder : IContextStorageBuilder, TKey, TValue> where TKey : notnull diff --git a/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs b/Botticelli.Chained.Context.Redis/RedisStorage.cs similarity index 97% rename from Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs rename to Botticelli.Chained.Context.Redis/RedisStorage.cs index bac587cb..d4834b67 100644 --- a/Botticelli.Framework.Chained.Context.Redis/RedisStorage.cs +++ b/Botticelli.Chained.Context.Redis/RedisStorage.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; using StackExchange.Redis; -namespace Botticelli.Framework.Chained.Context.Redis; +namespace Botticelli.Chained.Context.Redis; /// /// RedisStorage storage diff --git a/Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj b/Botticelli.Chained.Context/Botticelli.Chained.Context.csproj similarity index 100% rename from Botticelli.Framework.Chained.Context/Botticelli.Framework.Chained.Context.csproj rename to Botticelli.Chained.Context/Botticelli.Chained.Context.csproj diff --git a/Botticelli.Framework.Chained.Context/CommandContext.cs b/Botticelli.Chained.Context/CommandContext.cs similarity index 97% rename from Botticelli.Framework.Chained.Context/CommandContext.cs rename to Botticelli.Chained.Context/CommandContext.cs index 3eb444b6..63e4b117 100644 --- a/Botticelli.Framework.Chained.Context/CommandContext.cs +++ b/Botticelli.Chained.Context/CommandContext.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -namespace Botticelli.Framework.Chained.Context; +namespace Botticelli.Chained.Context; /// /// Command context for transmitting data over command context chain diff --git a/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Context/Extensions/ServiceCollectionExtensions.cs similarity index 87% rename from Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Chained.Context/Extensions/ServiceCollectionExtensions.cs index dda2d29e..4ce26845 100644 --- a/Botticelli.Framework.Chained.Context/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Context/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace Botticelli.Framework.Chained.Context.Extensions; +namespace Botticelli.Chained.Context.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.Framework.Chained.Context/ICommandContext.cs b/Botticelli.Chained.Context/ICommandContext.cs similarity index 87% rename from Botticelli.Framework.Chained.Context/ICommandContext.cs rename to Botticelli.Chained.Context/ICommandContext.cs index 5fcb1243..50409f36 100644 --- a/Botticelli.Framework.Chained.Context/ICommandContext.cs +++ b/Botticelli.Chained.Context/ICommandContext.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Chained.Context; +namespace Botticelli.Chained.Context; /// /// A context for monad-based commands diff --git a/Botticelli.Framework.Chained.Context/IStorage.cs b/Botticelli.Chained.Context/IStorage.cs similarity index 98% rename from Botticelli.Framework.Chained.Context/IStorage.cs rename to Botticelli.Chained.Context/IStorage.cs index 584ac13a..cf8fd2bb 100644 --- a/Botticelli.Framework.Chained.Context/IStorage.cs +++ b/Botticelli.Chained.Context/IStorage.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Botticelli.Framework.Chained.Context; +namespace Botticelli.Chained.Context; /// /// Represents a generic key-value storage interface with basic CRUD-like operations. diff --git a/Botticelli.Framework.Chained.Context/Names.cs b/Botticelli.Chained.Context/Names.cs similarity index 77% rename from Botticelli.Framework.Chained.Context/Names.cs rename to Botticelli.Chained.Context/Names.cs index cfb581ed..86037287 100644 --- a/Botticelli.Framework.Chained.Context/Names.cs +++ b/Botticelli.Chained.Context/Names.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Chained.Context; +namespace Botticelli.Chained.Context; public static class Names { diff --git a/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs b/Botticelli.Chained.Context/Settings/IContextStorageBuilder.cs similarity index 80% rename from Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs rename to Botticelli.Chained.Context/Settings/IContextStorageBuilder.cs index eef2d6de..e91bd262 100644 --- a/Botticelli.Framework.Chained.Context/Settings/IContextStorageBuilder.cs +++ b/Botticelli.Chained.Context/Settings/IContextStorageBuilder.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Chained.Context.Settings; +namespace Botticelli.Chained.Context.Settings; public interface IContextStorageBuilder where TStorage : IStorage diff --git a/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj b/Botticelli.Chained.Monads/Botticelli.Chained.Monads.csproj similarity index 88% rename from Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj rename to Botticelli.Chained.Monads/Botticelli.Chained.Monads.csproj index c2552b56..b88aa27d 100644 --- a/Botticelli.Framework.Chained.Monads/Botticelli.Framework.Chained.Monads.csproj +++ b/Botticelli.Chained.Monads/Botticelli.Chained.Monads.csproj @@ -14,7 +14,6 @@ https://github.com/devgopher/botticelli telegram, bots, botticelli, vk, facebook, wechat, whatsapp true - Botticelli.Framework.Chained.Monads @@ -45,8 +44,8 @@ - - + + \ No newline at end of file diff --git a/Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs b/Botticelli.Chained.Monads/Commands/IChainCommand.cs similarity index 57% rename from Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs rename to Botticelli.Chained.Monads/Commands/IChainCommand.cs index 2d7847ad..85b2f5d7 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/IChainCommand.cs +++ b/Botticelli.Chained.Monads/Commands/IChainCommand.cs @@ -1,7 +1,7 @@ -using Botticelli.Framework.Chained.Context; +using Botticelli.Chained.Context; using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Chained.Monads.Commands; +namespace Botticelli.Chained.Monads.Commands; public interface IChainCommand : ICommand { diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs b/Botticelli.Chained.Monads/Commands/Processors/ChainBuilder.cs similarity index 96% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs rename to Botticelli.Chained.Monads/Commands/Processors/ChainBuilder.cs index 3a39461b..6461a181 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainBuilder.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/ChainBuilder.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; public class ChainBuilder(IServiceCollection services) where TCommand : IChainCommand diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/ChainProcessor.cs similarity index 92% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/ChainProcessor.cs index 34cd60d8..b4aabce0 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/ChainProcessor.cs @@ -1,10 +1,10 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using Botticelli.Shared.ValueObjects; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; /// /// Chain processor diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/ChainRunProcessor.cs similarity index 92% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/ChainRunProcessor.cs index c220cc00..c3866bf4 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/ChainRunProcessor.cs @@ -1,5 +1,5 @@ using Botticelli.Client.Analytics; -using Botticelli.Framework.Chained.Context; +using Botticelli.Chained.Context; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; @@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; public class ChainRunProcessor( ILogger> logger, diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs b/Botticelli.Chained.Monads/Commands/Processors/ChainRunner.cs similarity index 92% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs rename to Botticelli.Chained.Monads/Commands/Processors/ChainRunner.cs index 0dd1b6e6..c35c8d22 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/ChainRunner.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/ChainRunner.cs @@ -1,8 +1,8 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; public class ChainRunner(List> chain, ILogger> logger) where TCommand : IChainCommand diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/IChainProcessor.cs similarity index 78% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/IChainProcessor.cs index c055485e..c17070fe 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/IChainProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/IChainProcessor.cs @@ -1,8 +1,8 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Monads.Commands.Result; using Botticelli.Interfaces; using LanguageExt; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; /// /// Chain processor diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/InputCommandProcessor.cs similarity index 80% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/InputCommandProcessor.cs index c69be169..d1650641 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/InputCommandProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/InputCommandProcessor.cs @@ -1,7 +1,7 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Monads.Commands.Result; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; public class InputCommandProcessor(ILogger> logger) : ChainProcessor(logger) diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs similarity index 89% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs index e824c3c9..db84e5d8 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/OutputCommandProcessor.cs @@ -1,11 +1,11 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Chained.Monads.Commands.Result; +using Botticelli.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; public class OutputCommandProcessor : ChainProcessor where TReplyMarkup : class diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs similarity index 93% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs index 682e533b..3453c908 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/TransformArgumentsProcessor.cs @@ -1,9 +1,9 @@ -using Botticelli.Framework.Chained.Context; -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Context; +using Botticelli.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; /// /// Func transform for command arguments processor diff --git a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs b/Botticelli.Chained.Monads/Commands/Processors/TransformProcessor.cs similarity index 92% rename from Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs rename to Botticelli.Chained.Monads/Commands/Processors/TransformProcessor.cs index ba99e809..cc4a909a 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Processors/TransformProcessor.cs +++ b/Botticelli.Chained.Monads/Commands/Processors/TransformProcessor.cs @@ -1,8 +1,8 @@ -using Botticelli.Framework.Chained.Monads.Commands.Result; +using Botticelli.Chained.Monads.Commands.Result; using LanguageExt; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Chained.Monads.Commands.Processors; +namespace Botticelli.Chained.Monads.Commands.Processors; /// /// Func transform processor diff --git a/Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs b/Botticelli.Chained.Monads/Commands/Result/BasicResult.cs similarity index 84% rename from Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs rename to Botticelli.Chained.Monads/Commands/Result/BasicResult.cs index f2990366..3fc60fe1 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Result/BasicResult.cs +++ b/Botticelli.Chained.Monads/Commands/Result/BasicResult.cs @@ -1,6 +1,6 @@ using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Chained.Monads.Commands.Result; +namespace Botticelli.Chained.Monads.Commands.Result; public class BasicResult : IResult where TCommand : ICommand diff --git a/Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs b/Botticelli.Chained.Monads/Commands/Result/FailResult.cs similarity index 91% rename from Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs rename to Botticelli.Chained.Monads/Commands/Result/FailResult.cs index 163a1e59..1f8b4f5f 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Result/FailResult.cs +++ b/Botticelli.Chained.Monads/Commands/Result/FailResult.cs @@ -1,7 +1,7 @@ using Botticelli.Framework.Commands; using Botticelli.Shared.ValueObjects; -namespace Botticelli.Framework.Chained.Monads.Commands.Result; +namespace Botticelli.Chained.Monads.Commands.Result; /// /// Fail result diff --git a/Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs b/Botticelli.Chained.Monads/Commands/Result/IResult.cs similarity index 80% rename from Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs rename to Botticelli.Chained.Monads/Commands/Result/IResult.cs index ab86bae4..2f6ffdcd 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Result/IResult.cs +++ b/Botticelli.Chained.Monads/Commands/Result/IResult.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Chained.Monads.Commands.Result; +namespace Botticelli.Chained.Monads.Commands.Result; public interface IResult where TCommand : ICommand diff --git a/Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs b/Botticelli.Chained.Monads/Commands/Result/SuccessResult.cs similarity index 89% rename from Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs rename to Botticelli.Chained.Monads/Commands/Result/SuccessResult.cs index 3b66356f..13b9968a 100644 --- a/Botticelli.Framework.Chained.Monads/Commands/Result/SuccessResult.cs +++ b/Botticelli.Chained.Monads/Commands/Result/SuccessResult.cs @@ -1,7 +1,7 @@ using Botticelli.Framework.Commands; using Botticelli.Shared.ValueObjects; -namespace Botticelli.Framework.Chained.Monads.Commands.Result; +namespace Botticelli.Chained.Monads.Commands.Result; /// /// Success result diff --git a/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Monads/Extensions/ServiceCollectionExtensions.cs similarity index 90% rename from Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Chained.Monads/Extensions/ServiceCollectionExtensions.cs index 7cb3bd02..f9abac5f 100644 --- a/Botticelli.Framework.Chained.Monads/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Monads/Extensions/ServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ -using Botticelli.Framework.Chained.Monads.Commands; -using Botticelli.Framework.Chained.Monads.Commands.Processors; +using Botticelli.Chained.Monads.Commands; +using Botticelli.Chained.Monads.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Extensions; using Microsoft.Extensions.DependencyInjection; -namespace Botticelli.Framework.Chained.Monads.Extensions; +namespace Botticelli.Chained.Monads.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.Framework.Controls.Layouts/Botticelli.Framework.Controls.Layouts.csproj b/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj similarity index 91% rename from Botticelli.Framework.Controls.Layouts/Botticelli.Framework.Controls.Layouts.csproj rename to Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj index b8b967b8..a40d735c 100644 --- a/Botticelli.Framework.Controls.Layouts/Botticelli.Framework.Controls.Layouts.csproj +++ b/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj @@ -17,7 +17,7 @@ - + diff --git a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs b/Botticelli.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs similarity index 90% rename from Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs rename to Botticelli.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs index cf45b3af..0a617415 100644 --- a/Botticelli.Framework.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs +++ b/Botticelli.Controls.Layouts/CommandProcessors/InlineCalendar/ICCommandProcessor.cs @@ -3,15 +3,15 @@ using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; -using Botticelli.Framework.Controls.Layouts.Inlines; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Layouts.Commands.InlineCalendar; +using Botticelli.Controls.Layouts.Inlines; +using Botticelli.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Shared.ValueObjects; using FluentValidation; using Microsoft.Extensions.Logging; -namespace Botticelli.Framework.Controls.Layouts.CommandProcessors.InlineCalendar; +namespace Botticelli.Controls.Layouts.CommandProcessors.InlineCalendar; /// /// Calendar command processor diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs similarity index 69% rename from Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs rename to Botticelli.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs index bb051769..7bc019ce 100644 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/BaseCalendarCommand.cs @@ -1,6 +1,6 @@ using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; public abstract class BaseCalendarCommand : ICommand { diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs similarity index 69% rename from Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs rename to Botticelli.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs index e3de8b04..df3c7f2f 100644 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/DateChosenCommand.cs @@ -1,6 +1,6 @@ using Botticelli.Framework.Commands; -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; public abstract class DateChosenCommand : ICommand { diff --git a/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs new file mode 100644 index 00000000..05bfcef9 --- /dev/null +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; + +public class MonthBackwardCommand : BaseCalendarCommand +{ +} \ No newline at end of file diff --git a/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs new file mode 100644 index 00000000..0681bfbf --- /dev/null +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; + +public class MonthForwardCommand : BaseCalendarCommand +{ +} \ No newline at end of file diff --git a/Botticelli.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs new file mode 100644 index 00000000..03cf7d77 --- /dev/null +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; + +public class SetDateCommand : BaseCalendarCommand +{ +} \ No newline at end of file diff --git a/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs new file mode 100644 index 00000000..4bf8bd40 --- /dev/null +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; + +public class YearBackwardCommand : BaseCalendarCommand +{ +} \ No newline at end of file diff --git a/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs b/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs new file mode 100644 index 00000000..14060177 --- /dev/null +++ b/Botticelli.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.Layouts.Commands.InlineCalendar; + +public class YearForwardCommand : BaseCalendarCommand +{ +} \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/Dialogs/yes_no.json b/Botticelli.Controls.Layouts/Dialogs/yes_no.json similarity index 100% rename from Botticelli.Framework.Controls.Layouts/Dialogs/yes_no.json rename to Botticelli.Controls.Layouts/Dialogs/yes_no.json diff --git a/Botticelli.Framework.Controls.Layouts/Dialogs/yes_no_cancel.json b/Botticelli.Controls.Layouts/Dialogs/yes_no_cancel.json similarity index 100% rename from Botticelli.Framework.Controls.Layouts/Dialogs/yes_no_cancel.json rename to Botticelli.Controls.Layouts/Dialogs/yes_no_cancel.json diff --git a/Botticelli.Framework.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs similarity index 87% rename from Botticelli.Framework.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs rename to Botticelli.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs index ff169843..ebfaa68e 100644 --- a/Botticelli.Framework.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Controls.Layouts/Extensions/ServiceCollectionExtensions.cs @@ -1,12 +1,12 @@ using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Layouts.CommandProcessors.InlineCalendar; -using Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Layouts.CommandProcessors.InlineCalendar; +using Botticelli.Controls.Layouts.Commands.InlineCalendar; +using Botticelli.Controls.Parsers; using Microsoft.Extensions.DependencyInjection; using Telegram.Bot.Types.ReplyMarkups; -namespace Botticelli.Framework.Controls.Layouts.Extensions; +namespace Botticelli.Controls.Layouts.Extensions; public static class ServiceCollectionExtensions { diff --git a/Botticelli.Framework.Controls.Layouts/Inlines/CalendarFactory.cs b/Botticelli.Controls.Layouts/Inlines/CalendarFactory.cs similarity index 95% rename from Botticelli.Framework.Controls.Layouts/Inlines/CalendarFactory.cs rename to Botticelli.Controls.Layouts/Inlines/CalendarFactory.cs index 7374477a..21f17704 100644 --- a/Botticelli.Framework.Controls.Layouts/Inlines/CalendarFactory.cs +++ b/Botticelli.Controls.Layouts/Inlines/CalendarFactory.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using System.Globalization; -namespace Botticelli.Framework.Controls.Layouts.Inlines; +namespace Botticelli.Controls.Layouts.Inlines; public static class CalendarFactory { diff --git a/Botticelli.Framework.Controls.Layouts/Inlines/InlineButtonMenu.cs b/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs similarity index 95% rename from Botticelli.Framework.Controls.Layouts/Inlines/InlineButtonMenu.cs rename to Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs index 56ed883c..b9e0b851 100644 --- a/Botticelli.Framework.Controls.Layouts/Inlines/InlineButtonMenu.cs +++ b/Botticelli.Controls.Layouts/Inlines/InlineButtonMenu.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Controls.BasicControls; +using Botticelli.Controls.BasicControls; -namespace Botticelli.Framework.Controls.Layouts.Inlines; +namespace Botticelli.Controls.Layouts.Inlines; public class InlineButtonMenu : ILayout { diff --git a/Botticelli.Framework.Controls.Layouts/Inlines/InlineCalendar.cs b/Botticelli.Controls.Layouts/Inlines/InlineCalendar.cs similarity index 97% rename from Botticelli.Framework.Controls.Layouts/Inlines/InlineCalendar.cs rename to Botticelli.Controls.Layouts/Inlines/InlineCalendar.cs index 36558200..4880fee0 100644 --- a/Botticelli.Framework.Controls.Layouts/Inlines/InlineCalendar.cs +++ b/Botticelli.Controls.Layouts/Inlines/InlineCalendar.cs @@ -1,7 +1,7 @@ using System.Globalization; -using Botticelli.Framework.Controls.BasicControls; +using Botticelli.Controls.BasicControls; -namespace Botticelli.Framework.Controls.Layouts.Inlines; +namespace Botticelli.Controls.Layouts.Inlines; public class InlineCalendar : ILayout { diff --git a/Botticelli.Framework.Controls.Layouts/Inlines/Table.cs b/Botticelli.Controls.Layouts/Inlines/Table.cs similarity index 88% rename from Botticelli.Framework.Controls.Layouts/Inlines/Table.cs rename to Botticelli.Controls.Layouts/Inlines/Table.cs index c93dfbc9..0743aa2e 100644 --- a/Botticelli.Framework.Controls.Layouts/Inlines/Table.cs +++ b/Botticelli.Controls.Layouts/Inlines/Table.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts.Inlines; +namespace Botticelli.Controls.Layouts.Inlines; public class Table(int cols) : ILayout { diff --git a/Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check.xcf b/Botticelli.Controls.Layouts/Resources/Images/Common/check.xcf similarity index 100% rename from Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check.xcf rename to Botticelli.Controls.Layouts/Resources/Images/Common/check.xcf diff --git a/Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check_black.png b/Botticelli.Controls.Layouts/Resources/Images/Common/check_black.png similarity index 100% rename from Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check_black.png rename to Botticelli.Controls.Layouts/Resources/Images/Common/check_black.png diff --git a/Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check_green.png b/Botticelli.Controls.Layouts/Resources/Images/Common/check_green.png similarity index 100% rename from Botticelli.Framework.Controls.Layouts/Resources/Images/Common/check_green.png rename to Botticelli.Controls.Layouts/Resources/Images/Common/check_green.png diff --git a/Botticelli.Framework.Controls/BasicControls/Button.cs b/Botticelli.Controls/BasicControls/Button.cs similarity index 90% rename from Botticelli.Framework.Controls/BasicControls/Button.cs rename to Botticelli.Controls/BasicControls/Button.cs index 514badb8..baf80ebc 100644 --- a/Botticelli.Framework.Controls/BasicControls/Button.cs +++ b/Botticelli.Controls/BasicControls/Button.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.BasicControls; +namespace Botticelli.Controls.BasicControls; public class Button : IControl { diff --git a/Botticelli.Framework.Controls/BasicControls/IControl.cs b/Botticelli.Controls/BasicControls/IControl.cs similarity index 91% rename from Botticelli.Framework.Controls/BasicControls/IControl.cs rename to Botticelli.Controls/BasicControls/IControl.cs index 8ffcfe71..fe9286f1 100644 --- a/Botticelli.Framework.Controls/BasicControls/IControl.cs +++ b/Botticelli.Controls/BasicControls/IControl.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.BasicControls; +namespace Botticelli.Controls.BasicControls; /// /// A basic control interface diff --git a/Botticelli.Framework.Controls/BasicControls/Text.cs b/Botticelli.Controls/BasicControls/Text.cs similarity index 89% rename from Botticelli.Framework.Controls/BasicControls/Text.cs rename to Botticelli.Controls/BasicControls/Text.cs index 2ddcfdfc..1e4f528f 100644 --- a/Botticelli.Framework.Controls/BasicControls/Text.cs +++ b/Botticelli.Controls/BasicControls/Text.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.BasicControls; +namespace Botticelli.Controls.BasicControls; public class Text : IControl { diff --git a/Botticelli.Controls/BasicControls/TextButton.cs b/Botticelli.Controls/BasicControls/TextButton.cs new file mode 100644 index 00000000..fa77126d --- /dev/null +++ b/Botticelli.Controls/BasicControls/TextButton.cs @@ -0,0 +1,5 @@ +namespace Botticelli.Controls.BasicControls; + +public class TextButton : Button +{ +} \ No newline at end of file diff --git a/Botticelli.Framework.Controls/Botticelli.Framework.Controls.csproj b/Botticelli.Controls/Botticelli.Controls.csproj similarity index 100% rename from Botticelli.Framework.Controls/Botticelli.Framework.Controls.csproj rename to Botticelli.Controls/Botticelli.Controls.csproj diff --git a/Botticelli.Framework.Controls/Exceptions/LayoutException.cs b/Botticelli.Controls/Exceptions/LayoutException.cs similarity index 80% rename from Botticelli.Framework.Controls/Exceptions/LayoutException.cs rename to Botticelli.Controls/Exceptions/LayoutException.cs index 6ec00f0c..55a8134a 100644 --- a/Botticelli.Framework.Controls/Exceptions/LayoutException.cs +++ b/Botticelli.Controls/Exceptions/LayoutException.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Exceptions; +namespace Botticelli.Controls.Exceptions; public class LayoutException : Exception { diff --git a/Botticelli.Framework.Controls/Extensions/DictionaryExtensions.cs b/Botticelli.Controls/Extensions/DictionaryExtensions.cs similarity index 88% rename from Botticelli.Framework.Controls/Extensions/DictionaryExtensions.cs rename to Botticelli.Controls/Extensions/DictionaryExtensions.cs index 73850b5c..674a8b44 100644 --- a/Botticelli.Framework.Controls/Extensions/DictionaryExtensions.cs +++ b/Botticelli.Controls/Extensions/DictionaryExtensions.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace Botticelli.Framework.Controls.Extensions; +namespace Botticelli.Controls.Extensions; public static class DictionaryExtensions { diff --git a/Botticelli.Framework.Controls/Layouts/BaseLayout.cs b/Botticelli.Controls/Layouts/BaseLayout.cs similarity index 78% rename from Botticelli.Framework.Controls/Layouts/BaseLayout.cs rename to Botticelli.Controls/Layouts/BaseLayout.cs index 66998c7a..c9cc1d38 100644 --- a/Botticelli.Framework.Controls/Layouts/BaseLayout.cs +++ b/Botticelli.Controls/Layouts/BaseLayout.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public class BaseLayout : ILayout { diff --git a/Botticelli.Framework.Controls/Layouts/CellAlign.cs b/Botticelli.Controls/Layouts/CellAlign.cs similarity index 52% rename from Botticelli.Framework.Controls/Layouts/CellAlign.cs rename to Botticelli.Controls/Layouts/CellAlign.cs index 7e2d5c73..8d067f88 100644 --- a/Botticelli.Framework.Controls/Layouts/CellAlign.cs +++ b/Botticelli.Controls/Layouts/CellAlign.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public enum CellAlign { diff --git a/Botticelli.Framework.Controls/Layouts/ILayout.cs b/Botticelli.Controls/Layouts/ILayout.cs similarity index 65% rename from Botticelli.Framework.Controls/Layouts/ILayout.cs rename to Botticelli.Controls/Layouts/ILayout.cs index 2a18d35c..14c5d2f2 100644 --- a/Botticelli.Framework.Controls/Layouts/ILayout.cs +++ b/Botticelli.Controls/Layouts/ILayout.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public interface ILayout { diff --git a/Botticelli.Framework.Controls/Layouts/Item.cs b/Botticelli.Controls/Layouts/Item.cs similarity index 51% rename from Botticelli.Framework.Controls/Layouts/Item.cs rename to Botticelli.Controls/Layouts/Item.cs index 38fa562c..38ca98b1 100644 --- a/Botticelli.Framework.Controls/Layouts/Item.cs +++ b/Botticelli.Controls/Layouts/Item.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Controls.BasicControls; +using Botticelli.Controls.BasicControls; -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public class Item { diff --git a/Botticelli.Framework.Controls/Layouts/ItemParams.cs b/Botticelli.Controls/Layouts/ItemParams.cs similarity index 67% rename from Botticelli.Framework.Controls/Layouts/ItemParams.cs rename to Botticelli.Controls/Layouts/ItemParams.cs index 83f3e030..62e50b07 100644 --- a/Botticelli.Framework.Controls/Layouts/ItemParams.cs +++ b/Botticelli.Controls/Layouts/ItemParams.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public class ItemParams { diff --git a/Botticelli.Framework.Controls/Layouts/Row.cs b/Botticelli.Controls/Layouts/Row.cs similarity index 73% rename from Botticelli.Framework.Controls/Layouts/Row.cs rename to Botticelli.Controls/Layouts/Row.cs index d6af8a4f..b0d79295 100644 --- a/Botticelli.Framework.Controls/Layouts/Row.cs +++ b/Botticelli.Controls/Layouts/Row.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Layouts; +namespace Botticelli.Controls.Layouts; public class Row { diff --git a/Botticelli.Framework.Controls/Parsers/ILayoutLoader.cs b/Botticelli.Controls/Parsers/ILayoutLoader.cs similarity index 82% rename from Botticelli.Framework.Controls/Parsers/ILayoutLoader.cs rename to Botticelli.Controls/Parsers/ILayoutLoader.cs index d3671b27..31acf1da 100644 --- a/Botticelli.Framework.Controls/Parsers/ILayoutLoader.cs +++ b/Botticelli.Controls/Parsers/ILayoutLoader.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Controls.Parsers; +namespace Botticelli.Controls.Parsers; /// /// Gets layout from config file diff --git a/Botticelli.Framework.Controls/Parsers/ILayoutParser.cs b/Botticelli.Controls/Parsers/ILayoutParser.cs similarity index 57% rename from Botticelli.Framework.Controls/Parsers/ILayoutParser.cs rename to Botticelli.Controls/Parsers/ILayoutParser.cs index 2c2cda47..af108fb8 100644 --- a/Botticelli.Framework.Controls/Parsers/ILayoutParser.cs +++ b/Botticelli.Controls/Parsers/ILayoutParser.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.Layouts; -namespace Botticelli.Framework.Controls.Parsers; +namespace Botticelli.Controls.Parsers; public interface ILayoutParser { diff --git a/Botticelli.Framework.Controls/Parsers/ILayoutSupplier.cs b/Botticelli.Controls/Parsers/ILayoutSupplier.cs similarity index 73% rename from Botticelli.Framework.Controls/Parsers/ILayoutSupplier.cs rename to Botticelli.Controls/Parsers/ILayoutSupplier.cs index 918ddd83..c5540c43 100644 --- a/Botticelli.Framework.Controls/Parsers/ILayoutSupplier.cs +++ b/Botticelli.Controls/Parsers/ILayoutSupplier.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.Layouts; -namespace Botticelli.Framework.Controls.Parsers; +namespace Botticelli.Controls.Parsers; /// /// Supplier is responsible for conversion of Layout into messenger-specific controls (for example, ReplyMarkup in diff --git a/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs b/Botticelli.Controls/Parsers/JsonLayoutParser.cs similarity index 93% rename from Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs rename to Botticelli.Controls/Parsers/JsonLayoutParser.cs index f90c0c0e..5f3f026b 100644 --- a/Botticelli.Framework.Controls/Parsers/JsonLayoutParser.cs +++ b/Botticelli.Controls/Parsers/JsonLayoutParser.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using Botticelli.Framework.Controls.BasicControls; -using Botticelli.Framework.Controls.Exceptions; -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.BasicControls; +using Botticelli.Controls.Exceptions; +using Botticelli.Controls.Layouts; -namespace Botticelli.Framework.Controls.Parsers; +namespace Botticelli.Controls.Parsers; public class JsonLayoutParser : ILayoutParser { diff --git a/Botticelli.Framework.Controls/Parsers/LayoutLoader.cs b/Botticelli.Controls/Parsers/LayoutLoader.cs similarity index 88% rename from Botticelli.Framework.Controls/Parsers/LayoutLoader.cs rename to Botticelli.Controls/Parsers/LayoutLoader.cs index e8959611..5f1d6fc9 100644 --- a/Botticelli.Framework.Controls/Parsers/LayoutLoader.cs +++ b/Botticelli.Controls/Parsers/LayoutLoader.cs @@ -1,6 +1,6 @@ -using Botticelli.Framework.Controls.Exceptions; +using Botticelli.Controls.Exceptions; -namespace Botticelli.Framework.Controls.Parsers; +namespace Botticelli.Controls.Parsers; public class LayoutLoader(TLayoutParser parser, TLayoutSupplier supplier) : ILayoutLoader diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs b/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs deleted file mode 100644 index faa39bad..00000000 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthBackwardCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; - -public class MonthBackwardCommand : BaseCalendarCommand -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs b/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs deleted file mode 100644 index 76c637fa..00000000 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/MonthForwardCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; - -public class MonthForwardCommand : BaseCalendarCommand -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs b/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs deleted file mode 100644 index 97e09014..00000000 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/SetDateCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; - -public class SetDateCommand : BaseCalendarCommand -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs b/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs deleted file mode 100644 index 6534f391..00000000 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearBackwardCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; - -public class YearBackwardCommand : BaseCalendarCommand -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs b/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs deleted file mode 100644 index 5a98ffb3..00000000 --- a/Botticelli.Framework.Controls.Layouts/Commands/InlineCalendar/YearForwardCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; - -public class YearForwardCommand : BaseCalendarCommand -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Controls/BasicControls/TextButton.cs b/Botticelli.Framework.Controls/BasicControls/TextButton.cs deleted file mode 100644 index 41aaab8b..00000000 --- a/Botticelli.Framework.Controls/BasicControls/TextButton.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Botticelli.Framework.Controls.BasicControls; - -public class TextButton : Button -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index d3afe2b3..767c7749 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -24,7 +24,7 @@ - + diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 1facc892..44b8c6fe 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -6,7 +6,7 @@ using Botticelli.Client.Analytics; using Botticelli.Client.Analytics.Settings; using Botticelli.Framework.Builders; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Extensions; using Botticelli.Framework.Options; using Botticelli.Framework.Security; diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 3ee7716a..ce39f002 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using System.Configuration; using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Options; using Botticelli.Framework.Telegram.Builders; using Botticelli.Framework.Telegram.Decorators; diff --git a/Botticelli.Framework.Telegram/Layout/IInlineTelegramLayoutSupplier.cs b/Botticelli.Framework.Telegram/Layout/IInlineTelegramLayoutSupplier.cs index b8c63244..81c7dbd0 100644 --- a/Botticelli.Framework.Telegram/Layout/IInlineTelegramLayoutSupplier.cs +++ b/Botticelli.Framework.Telegram/Layout/IInlineTelegramLayoutSupplier.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Layout; diff --git a/Botticelli.Framework.Telegram/Layout/IReplyTelegramLayoutSupplier.cs b/Botticelli.Framework.Telegram/Layout/IReplyTelegramLayoutSupplier.cs index ea4a2b4c..08f75299 100644 --- a/Botticelli.Framework.Telegram/Layout/IReplyTelegramLayoutSupplier.cs +++ b/Botticelli.Framework.Telegram/Layout/IReplyTelegramLayoutSupplier.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Layout; diff --git a/Botticelli.Framework.Telegram/Layout/InlineTelegramLayoutSupplier.cs b/Botticelli.Framework.Telegram/Layout/InlineTelegramLayoutSupplier.cs index e666bacb..fae80e43 100644 --- a/Botticelli.Framework.Telegram/Layout/InlineTelegramLayoutSupplier.cs +++ b/Botticelli.Framework.Telegram/Layout/InlineTelegramLayoutSupplier.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Controls.Exceptions; -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.Exceptions; +using Botticelli.Controls.Layouts; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Layout; diff --git a/Botticelli.Framework.Telegram/Layout/ReplyTelegramLayoutSupplier.cs b/Botticelli.Framework.Telegram/Layout/ReplyTelegramLayoutSupplier.cs index 0c5261c9..90d29923 100644 --- a/Botticelli.Framework.Telegram/Layout/ReplyTelegramLayoutSupplier.cs +++ b/Botticelli.Framework.Telegram/Layout/ReplyTelegramLayoutSupplier.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Controls.Exceptions; -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.Exceptions; +using Botticelli.Controls.Layouts; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Layout; diff --git a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj index 4f8716e0..c84a43fc 100644 --- a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj +++ b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj @@ -30,7 +30,7 @@ - + diff --git a/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs index a7ed31ea..87dbbf4b 100644 --- a/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using System.Configuration; using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Options; using Botticelli.Framework.Vk.Messages.API.Markups; using Botticelli.Framework.Vk.Messages.Builders; diff --git a/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs b/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs index 20b08f50..6b82eac1 100644 --- a/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs +++ b/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Vk.Messages.API.Markups; namespace Botticelli.Framework.Vk.Messages.Layout; diff --git a/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs b/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs index 4cdf4837..a578d51c 100644 --- a/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs +++ b/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs @@ -1,7 +1,7 @@ -using Botticelli.Framework.Controls.BasicControls; -using Botticelli.Framework.Controls.Exceptions; -using Botticelli.Framework.Controls.Extensions; -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.BasicControls; +using Botticelli.Controls.Exceptions; +using Botticelli.Controls.Extensions; +using Botticelli.Controls.Layouts; using Botticelli.Framework.Vk.Messages.API.Markups; using Botticelli.Shared.Utils; using Action = Botticelli.Framework.Vk.Messages.API.Markups.Action; diff --git a/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs index 21cdc550..042073b0 100644 --- a/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Locations.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using System.Reflection; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Telegram.Layout; using Botticelli.Locations.Commands; using Botticelli.Locations.Commands.CommandProcessors; @@ -41,11 +41,9 @@ public static IServiceCollection AddOsmLocations(this IServiceCollection service .AddScoped() .AddScoped, InlineTelegramLayoutSupplier>() .AddScoped, ReplyTelegramLayoutSupplier>() - .AddScoped(sp => - new ForwardGeocoder(sp.GetRequiredService(), - Url.Combine(url, "search"))) - .AddScoped(sp => - new ReverseGeocoder(sp.GetRequiredService(), - Url.Combine(url, "reverse"))); + .AddScoped(sp => new ForwardGeocoder(sp.GetRequiredService(), + Url.Combine(url, "search"))) + .AddScoped(sp => new ReverseGeocoder(sp.GetRequiredService(), + Url.Combine(url, "reverse"))); } } \ No newline at end of file diff --git a/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs index b4569ba2..85291402 100644 --- a/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using System.Reflection; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Vk.Messages.API.Markups; using Botticelli.Framework.Vk.Messages.Layout; using Botticelli.Locations.Commands; diff --git a/Botticelli.Locations/Botticelli.Locations.csproj b/Botticelli.Locations/Botticelli.Locations.csproj index d2fd7909..dc7225e1 100644 --- a/Botticelli.Locations/Botticelli.Locations.csproj +++ b/Botticelli.Locations/Botticelli.Locations.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 5efc65af..40616fe9 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -1,10 +1,10 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.BasicControls; -using Botticelli.Framework.Controls.Layouts; -using Botticelli.Framework.Controls.Layouts.Inlines; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.BasicControls; +using Botticelli.Controls.Layouts; +using Botticelli.Controls.Layouts.Inlines; +using Botticelli.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Locations.Integration; using Botticelli.Shared.API.Client.Requests; diff --git a/Botticelli.sln b/Botticelli.sln index 79240eb4..4de45670 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -181,11 +181,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Hangfi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Quartz", "Botticelli.Scheduler.Quartz\Botticelli.Scheduler.Quartz.csproj", "{F958B922-0418-4AB8-B5C6-86510DB52F38}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Controls", "Botticelli.Framework.Controls\Botticelli.Framework.Controls.csproj", "{C556B6EE-DCE7-4ED2-85E1-5D5212E0C184}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Controls", "Botticelli.Controls\Botticelli.Controls.csproj", "{C556B6EE-DCE7-4ED2-85E1-5D5212E0C184}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Controls.Tests", "Tests\Botticelli.Framework.Controls.Tests\Botticelli.Framework.Controls.Tests.csproj", "{70EB3FE2-9EB5-4B62-9405-71D1A332B0FD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Controls.Layouts", "Botticelli.Framework.Controls.Layouts\Botticelli.Framework.Controls.Layouts.csproj", "{C693D17D-DEB1-43E9-9EE4-031D14696065}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Controls.Layouts", "Botticelli.Controls.Layouts\Botticelli.Controls.Layouts.csproj", "{C693D17D-DEB1-43E9-9EE4-031D14696065}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Layouts.Sample.Telegram", "Samples\Layouts.Sample.Telegram\Layouts.Sample.Telegram.csproj", "{2DF28873-A172-42EB-8FBD-324DF8AD9323}" EndProject @@ -213,7 +213,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Bot.Data.Entitie EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monads.Sample.Telegram", "Samples\Monads.Sample.Telegram\Monads.Sample.Telegram.csproj", "{CBD02578-B208-4888-AB18-D9969A4082ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Monads", "Botticelli.Framework.Chained.Monads\Botticelli.Framework.Chained.Monads.csproj", "{D159BBC6-8D73-44AB-A17B-B14AB7D56A0D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Monads", "Botticelli.Chained.Monads\Botticelli.Chained.Monads.csproj", "{D159BBC6-8D73-44AB-A17B-B14AB7D56A0D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{8300ACD9-51DA-4B69-B7DF-FB2E5CC63F73}" EndProject @@ -251,13 +251,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Bus", "Botticell EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Pay", "Botticelli.Pay", "{34F34B4D-5BA2-45EC-8BC0-2507AF50A9A5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Schedule", "Botticelli.Schedule", "{2CC72293-C3FB-4A01-922A-E8C7CCD689A0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Scheduler", "Botticelli.Scheduler", "{2CC72293-C3FB-4A01-922A-E8C7CCD689A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context", "Botticelli.Framework.Chained.Context\Botticelli.Framework.Chained.Context.csproj", "{3A9F8D81-DB62-43D3-841C-9F37D8238E36}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context", "Botticelli.Chained.Context\Botticelli.Chained.Context.csproj", "{3A9F8D81-DB62-43D3-841C-9F37D8238E36}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.InMemory", "Botticelli.Framework.Chained.Context.InMemory\Botticelli.Framework.Chained.Context.InMemory.csproj", "{1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context.InMemory", "Botticelli.Chained.Context.InMemory\Botticelli.Chained.Context.InMemory.csproj", "{1A8FB2E2-4272-402F-9301-9C75FEA2E9C1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Chained.Context.Redis", "Botticelli.Framework.Chained.Context.Redis\Botticelli.Framework.Chained.Context.Redis.csproj", "{4E5352E6-9F06-4A92-A6D9-E82DBA516E72}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context.Redis", "Botticelli.Chained.Context.Redis\Botticelli.Chained.Context.Redis.csproj", "{4E5352E6-9F06-4A92-A6D9-E82DBA516E72}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Pay.Sample.Telegram/Program.cs b/Pay.Sample.Telegram/Program.cs index 03239411..6dec2fb0 100644 --- a/Pay.Sample.Telegram/Program.cs +++ b/Pay.Sample.Telegram/Program.cs @@ -1,5 +1,5 @@ +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.Extensions; using Botticelli.Pay.Models; using Botticelli.Pay.Processors; diff --git a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj index 84aa9c9a..67132326 100644 --- a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj +++ b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj @@ -13,7 +13,7 @@ - + diff --git a/Samples/Ai.Common.Sample/AiCommandProcessor.cs b/Samples/Ai.Common.Sample/AiCommandProcessor.cs index 5e8849fb..d689959a 100644 --- a/Samples/Ai.Common.Sample/AiCommandProcessor.cs +++ b/Samples/Ai.Common.Sample/AiCommandProcessor.cs @@ -6,7 +6,7 @@ using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; diff --git a/Samples/Ai.Common.Sample/Layouts/AiLayout.cs b/Samples/Ai.Common.Sample/Layouts/AiLayout.cs index f0e7397d..654f50bc 100644 --- a/Samples/Ai.Common.Sample/Layouts/AiLayout.cs +++ b/Samples/Ai.Common.Sample/Layouts/AiLayout.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Controls.BasicControls; -using Botticelli.Framework.Controls.Layouts; +using Botticelli.Controls.BasicControls; +using Botticelli.Controls.Layouts; namespace AiSample.Common.Layouts; diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs index 83866531..7781b1c9 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/InfoCommandProcessor.cs @@ -1,7 +1,6 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; using FluentValidation; @@ -13,8 +12,6 @@ public class InfoCommandProcessor : CommandProcessor public InfoCommandProcessor(ILogger> logger, ICommandValidator commandValidator, MetricsProcessor metricsProcessor, - ILayoutSupplier layoutSupplier, - ILayoutParser layoutParser, IValidator messageValidator) : base(logger, commandValidator, diff --git a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs index 26302928..24947240 100644 --- a/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Auth.Sample.Telegram/Commands/Processors/StartCommandProcessor.cs @@ -4,9 +4,9 @@ using Botticelli.Auth.Dto.User; using Botticelli.Auth.Services; using Botticelli.Client.Analytics; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; diff --git a/Samples/CommandChain.Sample.Telegram/Program.cs b/Samples/CommandChain.Sample.Telegram/Program.cs index bb83ebaf..e04f0403 100644 --- a/Samples/CommandChain.Sample.Telegram/Program.cs +++ b/Samples/CommandChain.Sample.Telegram/Program.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; +using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram; using Botticelli.Framework.Telegram.Extensions; diff --git a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs index 7b98eeaf..8abdd2db 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/DateChosenCommandProcessor.cs @@ -2,7 +2,7 @@ using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Utils; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Layouts.Commands.InlineCalendar; +using Botticelli.Controls.Layouts.Commands.InlineCalendar; using Botticelli.Interfaces; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; diff --git a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs index cd8119b2..ea7682db 100644 --- a/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs +++ b/Samples/Layouts.Sample.Telegram/Handlers/GetCalendarCommandProcessor.cs @@ -2,8 +2,8 @@ using Botticelli.Client.Analytics; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Layouts.Inlines; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Layouts.Inlines; +using Botticelli.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Interfaces; using Botticelli.Shared.API.Client.Requests; diff --git a/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj b/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj index c80a66db..29ef38fe 100644 --- a/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj +++ b/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj @@ -20,7 +20,7 @@ - + diff --git a/Samples/Layouts.Sample.Telegram/Program.cs b/Samples/Layouts.Sample.Telegram/Program.cs index d78d6c79..848fee38 100644 --- a/Samples/Layouts.Sample.Telegram/Program.cs +++ b/Samples/Layouts.Sample.Telegram/Program.cs @@ -1,5 +1,5 @@ using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Layouts.Extensions; +using Botticelli.Controls.Layouts.Extensions; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Layout; diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs index 6864981e..53200a28 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/InfoCommandProcessor.cs @@ -1,8 +1,8 @@ using System.Reflection; using Botticelli.Client.Analytics; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.ValueObjects; diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index 5fb29564..d2d9dcea 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -1,8 +1,8 @@ using System.Reflection; using Botticelli.Client.Analytics; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Interfaces; using Botticelli.Scheduler; diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs index 8072a500..f780b5d4 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StopCommandProcessor.cs @@ -1,8 +1,8 @@ using System.Reflection; using Botticelli.Client.Analytics; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Processors; using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Controls.Parsers; using Botticelli.Framework.SendOptions; using Botticelli.Scheduler.Interfaces; using Botticelli.Shared.API.Client.Requests; diff --git a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs index 4ebb385a..3012a44b 100644 --- a/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs +++ b/Samples/Monads.Sample.Telegram/Commands/MathCommand.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Chained.Context; -using Botticelli.Framework.Chained.Monads.Commands; +using Botticelli.Chained.Context; +using Botticelli.Chained.Monads.Commands; namespace TelegramMonadsBasedBot.Commands; diff --git a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj index 4feade58..d8ec9fdb 100644 --- a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj +++ b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj @@ -18,7 +18,7 @@ - + diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index 8c6efdf5..e7970b50 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Chained.Monads.Commands.Processors; -using Botticelli.Framework.Chained.Monads.Extensions; +using Botticelli.Chained.Monads.Commands.Processors; +using Botticelli.Chained.Monads.Extensions; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; diff --git a/Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj b/Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj index 432636dd..3c694055 100644 --- a/Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj +++ b/Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj @@ -37,7 +37,7 @@ - + diff --git a/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs b/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs index a2ec571e..74bed7ec 100644 --- a/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs +++ b/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs @@ -1,5 +1,5 @@ -using Botticelli.Framework.Controls.Exceptions; -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Exceptions; +using Botticelli.Controls.Parsers; namespace Botticelli.Framework.Controls.Tests.Layouts; diff --git a/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs b/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs index 44be131d..d8412301 100644 --- a/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs +++ b/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs @@ -1,4 +1,4 @@ -using Botticelli.Framework.Controls.Parsers; +using Botticelli.Controls.Parsers; using Botticelli.Framework.Telegram.Layout; namespace Botticelli.Framework.Controls.Tests.Layouts; From 87fd1120061b8b99d478dbada0ac34e678c19b33 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sat, 10 May 2025 22:55:28 +0300 Subject: [PATCH 026/101] BOTTICELLI-63: - namespaces simplifying --- Botticelli.AI.Test/AIProvider/BaseAiProviderTest.cs | 2 +- Botticelli.AI.Test/AIProvider/ChatGptProviderTest.cs | 2 +- Botticelli.AI.Test/AIProvider/DeepSeekGptProviderTest.cs | 2 +- Botticelli.AI.Test/AIProvider/YaGptProviderTest.cs | 2 +- Botticelli.AI.Test/Botticelli.AI.Test.csproj | 2 +- .../Botticelli.Locations.Tests.csproj | 2 +- Botticelli.sln | 6 +++--- .../Botticelli.Controls.Tests.csproj} | 0 .../Layouts/JsonLayoutParserTest.cs | 2 +- .../Layouts/ReplyTelegramLayoutSupplierTest.cs | 2 +- .../TestCases/CorrectLayout.json | 0 .../TestCases/InvalidLayout.json | 0 .../Botticelli.Vk.Tests.csproj} | 2 +- .../EnvironmentDataProvider.cs | 2 +- .../LongPollMessagesProviderTests.cs | 4 ++-- .../MessagePublisherTests.cs | 4 ++-- .../TestHttpClientFactory.cs | 2 +- .../appsettings.json | 0 Tests/{Shared => Mocks}/HttpClientFactoryMock.cs | 2 +- Tests/{Shared => Mocks}/LoggerMocks.cs | 2 +- Tests/{Shared/Shared.csproj => Mocks/Mocks.csproj} | 0 Tests/{Shared => Mocks}/OptionsMock.cs | 2 +- Tests/{Shared => Mocks}/OptionsMonitorMock.cs | 2 +- 23 files changed, 22 insertions(+), 22 deletions(-) rename Tests/{Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj => Botticelli.Controls.Tests/Botticelli.Controls.Tests.csproj} (100%) rename Tests/{Botticelli.Framework.Controls.Tests => Botticelli.Controls.Tests}/Layouts/JsonLayoutParserTest.cs (92%) rename Tests/{Botticelli.Framework.Controls.Tests => Botticelli.Controls.Tests}/Layouts/ReplyTelegramLayoutSupplierTest.cs (91%) rename Tests/{Botticelli.Framework.Controls.Tests => Botticelli.Controls.Tests}/TestCases/CorrectLayout.json (100%) rename Tests/{Botticelli.Framework.Controls.Tests => Botticelli.Controls.Tests}/TestCases/InvalidLayout.json (100%) rename Tests/{Botticelli.Framework.Vk.Tests/Botticelli.Framework.Vk.Tests.csproj => Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj} (96%) rename Tests/{Botticelli.Framework.Vk.Tests => Botticelli.Vk.Tests}/EnvironmentDataProvider.cs (88%) rename Tests/{Botticelli.Framework.Vk.Tests => Botticelli.Vk.Tests}/LongPollMessagesProviderTests.cs (96%) rename Tests/{Botticelli.Framework.Vk.Tests => Botticelli.Vk.Tests}/MessagePublisherTests.cs (96%) rename Tests/{Botticelli.Framework.Vk.Tests => Botticelli.Vk.Tests}/TestHttpClientFactory.cs (96%) rename Tests/{Botticelli.Framework.Vk.Tests => Botticelli.Vk.Tests}/appsettings.json (100%) rename Tests/{Shared => Mocks}/HttpClientFactoryMock.cs (94%) rename Tests/{Shared => Mocks}/LoggerMocks.cs (93%) rename Tests/{Shared/Shared.csproj => Mocks/Mocks.csproj} (100%) rename Tests/{Shared => Mocks}/OptionsMock.cs (91%) rename Tests/{Shared => Mocks}/OptionsMonitorMock.cs (95%) diff --git a/Botticelli.AI.Test/AIProvider/BaseAiProviderTest.cs b/Botticelli.AI.Test/AIProvider/BaseAiProviderTest.cs index 682261a9..a292eb26 100644 --- a/Botticelli.AI.Test/AIProvider/BaseAiProviderTest.cs +++ b/Botticelli.AI.Test/AIProvider/BaseAiProviderTest.cs @@ -12,7 +12,7 @@ using FluentAssertions; using FluentValidation; using NUnit.Framework; -using Shared; +using Mocks; using WireMock.Server; namespace Botticelli.AI.Test.AIProvider; diff --git a/Botticelli.AI.Test/AIProvider/ChatGptProviderTest.cs b/Botticelli.AI.Test/AIProvider/ChatGptProviderTest.cs index bc9704a2..bce0821f 100644 --- a/Botticelli.AI.Test/AIProvider/ChatGptProviderTest.cs +++ b/Botticelli.AI.Test/AIProvider/ChatGptProviderTest.cs @@ -5,7 +5,7 @@ using Botticelli.AI.ChatGpt.Provider; using Botticelli.AI.ChatGpt.Settings; using NUnit.Framework; -using Shared; +using Mocks; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using Usage = Botticelli.AI.ChatGpt.Message.ChatGpt.Usage; diff --git a/Botticelli.AI.Test/AIProvider/DeepSeekGptProviderTest.cs b/Botticelli.AI.Test/AIProvider/DeepSeekGptProviderTest.cs index 0d00c1ee..b643ff5f 100644 --- a/Botticelli.AI.Test/AIProvider/DeepSeekGptProviderTest.cs +++ b/Botticelli.AI.Test/AIProvider/DeepSeekGptProviderTest.cs @@ -5,7 +5,7 @@ using Botticelli.AI.DeepSeekGpt.Provider; using Botticelli.AI.DeepSeekGpt.Settings; using NUnit.Framework; -using Shared; +using Mocks; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using Usage = Botticelli.AI.DeepSeekGpt.Message.DeepSeek.Usage; diff --git a/Botticelli.AI.Test/AIProvider/YaGptProviderTest.cs b/Botticelli.AI.Test/AIProvider/YaGptProviderTest.cs index 1f0c44af..94181674 100644 --- a/Botticelli.AI.Test/AIProvider/YaGptProviderTest.cs +++ b/Botticelli.AI.Test/AIProvider/YaGptProviderTest.cs @@ -4,7 +4,7 @@ using Botticelli.AI.YaGpt.Provider; using Botticelli.AI.YaGpt.Settings; using NUnit.Framework; -using Shared; +using Mocks; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; diff --git a/Botticelli.AI.Test/Botticelli.AI.Test.csproj b/Botticelli.AI.Test/Botticelli.AI.Test.csproj index 2af7dd51..93f89488 100644 --- a/Botticelli.AI.Test/Botticelli.AI.Test.csproj +++ b/Botticelli.AI.Test/Botticelli.AI.Test.csproj @@ -24,7 +24,7 @@ - + \ No newline at end of file diff --git a/Botticelli.Locations.Tests/Botticelli.Locations.Tests.csproj b/Botticelli.Locations.Tests/Botticelli.Locations.Tests.csproj index 0d1efa6d..2917f24b 100644 --- a/Botticelli.Locations.Tests/Botticelli.Locations.Tests.csproj +++ b/Botticelli.Locations.Tests/Botticelli.Locations.Tests.csproj @@ -20,7 +20,7 @@ - + \ No newline at end of file diff --git a/Botticelli.sln b/Botticelli.sln index 4de45670..395a103a 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -114,7 +114,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Scheduler", "Bot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Framework.Vk.Messages", "Botticelli.Framework.Vk\Botticelli.Framework.Vk.Messages.csproj", "{343922EC-2CF3-4C0E-B745-08F2B333B4E3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Framework.Vk.Tests", "Tests\Botticelli.Framework.Vk.Tests\Botticelli.Framework.Vk.Tests.csproj", "{2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Vk.Tests", "Tests\Botticelli.Vk.Tests\Botticelli.Vk.Tests.csproj", "{2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VK", "VK", "{5BB351E1-460B-48C1-912C-E13702968BB2}" EndProject @@ -183,7 +183,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Quartz EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Controls", "Botticelli.Controls\Botticelli.Controls.csproj", "{C556B6EE-DCE7-4ED2-85E1-5D5212E0C184}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Framework.Controls.Tests", "Tests\Botticelli.Framework.Controls.Tests\Botticelli.Framework.Controls.Tests.csproj", "{70EB3FE2-9EB5-4B62-9405-71D1A332B0FD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Controls.Tests", "Tests\Botticelli.Controls.Tests\Botticelli.Controls.Tests.csproj", "{70EB3FE2-9EB5-4B62-9405-71D1A332B0FD}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Controls.Layouts", "Botticelli.Controls.Layouts\Botticelli.Controls.Layouts.csproj", "{C693D17D-DEB1-43E9-9EE4-031D14696065}" EndProject @@ -193,7 +193,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Locations", "Bot EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Locations.Tests", "Botticelli.Locations.Tests\Botticelli.Locations.Tests.csproj", "{9B731750-D28D-4DE0-AC8E-280E932BD301}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Tests\Shared\Shared.csproj", "{00F8B86E-B3DC-413C-AA74-D3EB747FFCF4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocks", "Tests\Mocks\Mocks.csproj", "{00F8B86E-B3DC-413C-AA74-D3EB747FFCF4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Controls", "Botticelli.Controls", "{B71A5B64-3A0D-4FD6-9BB3-C2EAEA15B60F}" EndProject diff --git a/Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj b/Tests/Botticelli.Controls.Tests/Botticelli.Controls.Tests.csproj similarity index 100% rename from Tests/Botticelli.Framework.Controls.Tests/Botticelli.Framework.Controls.Tests.csproj rename to Tests/Botticelli.Controls.Tests/Botticelli.Controls.Tests.csproj diff --git a/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs b/Tests/Botticelli.Controls.Tests/Layouts/JsonLayoutParserTest.cs similarity index 92% rename from Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs rename to Tests/Botticelli.Controls.Tests/Layouts/JsonLayoutParserTest.cs index 74bed7ec..92d47951 100644 --- a/Tests/Botticelli.Framework.Controls.Tests/Layouts/JsonLayoutParserTest.cs +++ b/Tests/Botticelli.Controls.Tests/Layouts/JsonLayoutParserTest.cs @@ -1,7 +1,7 @@ using Botticelli.Controls.Exceptions; using Botticelli.Controls.Parsers; -namespace Botticelli.Framework.Controls.Tests.Layouts; +namespace Botticelli.Controls.Tests.Layouts; [TestFixture] [TestOf(typeof(JsonLayoutParser))] diff --git a/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs b/Tests/Botticelli.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs similarity index 91% rename from Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs rename to Tests/Botticelli.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs index d8412301..9c16165b 100644 --- a/Tests/Botticelli.Framework.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs +++ b/Tests/Botticelli.Controls.Tests/Layouts/ReplyTelegramLayoutSupplierTest.cs @@ -1,7 +1,7 @@ using Botticelli.Controls.Parsers; using Botticelli.Framework.Telegram.Layout; -namespace Botticelli.Framework.Controls.Tests.Layouts; +namespace Botticelli.Controls.Tests.Layouts; [TestFixture] [TestOf(typeof(ReplyTelegramLayoutSupplier))] diff --git a/Tests/Botticelli.Framework.Controls.Tests/TestCases/CorrectLayout.json b/Tests/Botticelli.Controls.Tests/TestCases/CorrectLayout.json similarity index 100% rename from Tests/Botticelli.Framework.Controls.Tests/TestCases/CorrectLayout.json rename to Tests/Botticelli.Controls.Tests/TestCases/CorrectLayout.json diff --git a/Tests/Botticelli.Framework.Controls.Tests/TestCases/InvalidLayout.json b/Tests/Botticelli.Controls.Tests/TestCases/InvalidLayout.json similarity index 100% rename from Tests/Botticelli.Framework.Controls.Tests/TestCases/InvalidLayout.json rename to Tests/Botticelli.Controls.Tests/TestCases/InvalidLayout.json diff --git a/Tests/Botticelli.Framework.Vk.Tests/Botticelli.Framework.Vk.Tests.csproj b/Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj similarity index 96% rename from Tests/Botticelli.Framework.Vk.Tests/Botticelli.Framework.Vk.Tests.csproj rename to Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj index 4634f82b..809ed791 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/Botticelli.Framework.Vk.Tests.csproj +++ b/Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj @@ -39,7 +39,7 @@ - + diff --git a/Tests/Botticelli.Framework.Vk.Tests/EnvironmentDataProvider.cs b/Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs similarity index 88% rename from Tests/Botticelli.Framework.Vk.Tests/EnvironmentDataProvider.cs rename to Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs index 678d4f9f..6766e403 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/EnvironmentDataProvider.cs +++ b/Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs @@ -1,4 +1,4 @@ -namespace Botticelli.Framework.Vk.Tests; +namespace Botticelli.Vk.Tests; internal static class EnvironmentDataProvider { diff --git a/Tests/Botticelli.Framework.Vk.Tests/LongPollMessagesProviderTests.cs b/Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs similarity index 96% rename from Tests/Botticelli.Framework.Vk.Tests/LongPollMessagesProviderTests.cs rename to Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs index 201b4c1f..3f786fa4 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/LongPollMessagesProviderTests.cs +++ b/Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs @@ -2,9 +2,9 @@ using Botticelli.Framework.Vk.Messages.Options; using Botticelli.Shared.Utils; using NUnit.Framework; -using Shared; +using Mocks; -namespace Botticelli.Framework.Vk.Tests; +namespace Botticelli.Vk.Tests; [TestFixture] public class LongPollMessagesProviderTests diff --git a/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs b/Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs similarity index 96% rename from Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs rename to Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs index f407233b..700e91db 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/MessagePublisherTests.cs +++ b/Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs @@ -2,9 +2,9 @@ using Botticelli.Framework.Vk.Messages; using Botticelli.Framework.Vk.Messages.API.Requests; using NUnit.Framework; -using Shared; +using Mocks; -namespace Botticelli.Framework.Vk.Tests; +namespace Botticelli.Vk.Tests; [TestFixture] public class MessagePublisherTests(MessagePublisher publisher) diff --git a/Tests/Botticelli.Framework.Vk.Tests/TestHttpClientFactory.cs b/Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs similarity index 96% rename from Tests/Botticelli.Framework.Vk.Tests/TestHttpClientFactory.cs rename to Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs index b3f187c1..a279bc46 100644 --- a/Tests/Botticelli.Framework.Vk.Tests/TestHttpClientFactory.cs +++ b/Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs @@ -3,7 +3,7 @@ using Botticelli.Framework.Vk.Messages.API.Utils; using RichardSzalay.MockHttp; -namespace Botticelli.Framework.Vk.Tests; +namespace Botticelli.Vk.Tests; internal class TestHttpClientFactory : IHttpClientFactory { diff --git a/Tests/Botticelli.Framework.Vk.Tests/appsettings.json b/Tests/Botticelli.Vk.Tests/appsettings.json similarity index 100% rename from Tests/Botticelli.Framework.Vk.Tests/appsettings.json rename to Tests/Botticelli.Vk.Tests/appsettings.json diff --git a/Tests/Shared/HttpClientFactoryMock.cs b/Tests/Mocks/HttpClientFactoryMock.cs similarity index 94% rename from Tests/Shared/HttpClientFactoryMock.cs rename to Tests/Mocks/HttpClientFactoryMock.cs index d4e59781..e063f502 100644 --- a/Tests/Shared/HttpClientFactoryMock.cs +++ b/Tests/Mocks/HttpClientFactoryMock.cs @@ -1,4 +1,4 @@ -namespace Shared; +namespace Mocks; public class HttpClientFactoryMock : IHttpClientFactory { diff --git a/Tests/Shared/LoggerMocks.cs b/Tests/Mocks/LoggerMocks.cs similarity index 93% rename from Tests/Shared/LoggerMocks.cs rename to Tests/Mocks/LoggerMocks.cs index 80df9969..a86b9f14 100644 --- a/Tests/Shared/LoggerMocks.cs +++ b/Tests/Mocks/LoggerMocks.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace Shared; +namespace Mocks; public static class LoggerMocks { diff --git a/Tests/Shared/Shared.csproj b/Tests/Mocks/Mocks.csproj similarity index 100% rename from Tests/Shared/Shared.csproj rename to Tests/Mocks/Mocks.csproj diff --git a/Tests/Shared/OptionsMock.cs b/Tests/Mocks/OptionsMock.cs similarity index 91% rename from Tests/Shared/OptionsMock.cs rename to Tests/Mocks/OptionsMock.cs index dada66f5..f7340d74 100644 --- a/Tests/Shared/OptionsMock.cs +++ b/Tests/Mocks/OptionsMock.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; -namespace Shared; +namespace Mocks; public class OptionsMock : IOptions where T : class { diff --git a/Tests/Shared/OptionsMonitorMock.cs b/Tests/Mocks/OptionsMonitorMock.cs similarity index 95% rename from Tests/Shared/OptionsMonitorMock.cs rename to Tests/Mocks/OptionsMonitorMock.cs index 58b13585..7b315c59 100644 --- a/Tests/Shared/OptionsMonitorMock.cs +++ b/Tests/Mocks/OptionsMonitorMock.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; -namespace Shared; +namespace Mocks; public class OptionsMonitorMock : IOptionsMonitor { From ee9b274277c7031e67ed80c921fa740c58318bdb Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 11 May 2025 22:58:48 +0300 Subject: [PATCH 027/101] BOTTICELLI-63: redis settings --- .../Botticelli.Chained.Context.Redis.csproj | 2 ++ .../Extensions/ServiceCollectionExtensions.cs | 12 ++++++++++++ .../Settings/RedisStorageSettings.cs | 7 +++++++ .../Monads.Sample.Telegram.csproj | 1 + Samples/Monads.Sample.Telegram/Program.cs | 7 +++++-- Samples/Monads.Sample.Telegram/appsettings.json | 3 +++ 6 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 Botticelli.Chained.Context.Redis/Settings/RedisStorageSettings.cs diff --git a/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj b/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj index e4c7e12b..0673dc27 100644 --- a/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj +++ b/Botticelli.Chained.Context.Redis/Botticelli.Chained.Context.Redis.csproj @@ -11,6 +11,8 @@ + + diff --git a/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs index 9ca9ee75..fea43b22 100644 --- a/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,6 @@ +using System.Configuration; +using Botticelli.Chained.Context.Redis.Settings; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Botticelli.Chained.Context.Redis.Extensions; @@ -15,4 +18,13 @@ public static IServiceCollection AddChainedRedisStorage(this IServ return services; } + + public static IServiceCollection AddChainedRedisStorage(this IServiceCollection services, IConfiguration configuration) + where TKey : notnull + where TValue : class + { + var settings = configuration.GetRequiredSection(nameof(RedisStorageSettings)).Get(); + + return AddChainedRedisStorage(services, opt => opt.AddConnectionString(settings.ConnectionString)); + } } \ No newline at end of file diff --git a/Botticelli.Chained.Context.Redis/Settings/RedisStorageSettings.cs b/Botticelli.Chained.Context.Redis/Settings/RedisStorageSettings.cs new file mode 100644 index 00000000..ce86a81f --- /dev/null +++ b/Botticelli.Chained.Context.Redis/Settings/RedisStorageSettings.cs @@ -0,0 +1,7 @@ +namespace Botticelli.Chained.Context.Redis.Settings; + +public class RedisStorageSettings +{ + + public string? ConnectionString { get; set; } +} \ No newline at end of file diff --git a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj index d8ec9fdb..f46f6445 100644 --- a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj +++ b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj @@ -18,6 +18,7 @@ + diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index e7970b50..ee3a0d86 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -1,3 +1,4 @@ +using Botticelli.Chained.Context.Redis.Extensions; using Botticelli.Chained.Monads.Commands.Processors; using Botticelli.Chained.Monads.Extensions; using Botticelli.Framework.Commands.Validators; @@ -15,8 +16,10 @@ .AddLogging(cfg => cfg.AddNLog()) .AddTelegramLayoutsSupport(); -builder.Services.AddBotCommand() - .AddMonadsChain, ReplyKeyboardMarkup, ReplyTelegramLayoutSupplier>(builder.Services, +builder.Services + .AddChainedRedisStorage(builder.Configuration) + .AddBotCommand() + .AddMonadsChain, ReplyKeyboardMarkup, ReplyTelegramLayoutSupplier>(builder.Services, cb => cb.Next>() .Next>(tp => tp.SuccessFunc = Math.Sqrt) diff --git a/Samples/Monads.Sample.Telegram/appsettings.json b/Samples/Monads.Sample.Telegram/appsettings.json index 7ed160ca..a9619f7a 100644 --- a/Samples/Monads.Sample.Telegram/appsettings.json +++ b/Samples/Monads.Sample.Telegram/appsettings.json @@ -28,5 +28,8 @@ "speed": 0.0, "compressionLevel": 0 }, + "RedisStorageSettings": { + "ConnectionString": "localhost:6379" + }, "AllowedHosts": "*" } \ No newline at end of file From f873c2dacd0c9a1064225a67f103ef4c7b8c1b64 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sat, 24 May 2025 13:30:08 +0300 Subject: [PATCH 028/101] - unit tests for chained storage --- .../RedisStorage.cs | 5 + Botticelli.sln | 17 +++ ...elli.Chained.Context.InMemory.Tests.csproj | 29 +++++ .../InMemoryStorageTests.cs | 82 ++++++++++++++ ...ticelli.Chained.Context.Redis.Tests.csproj | 21 ++++ .../RedisStorageTests.cs | 102 ++++++++++++++++++ 6 files changed, 256 insertions(+) create mode 100644 Tests/Botticelli.Chained.Context.InMemory.Tests/Botticelli.Chained.Context.InMemory.Tests.csproj create mode 100644 Tests/Botticelli.Chained.Context.InMemory.Tests/InMemoryStorageTests.cs create mode 100644 Tests/Botticelli.Chained.Context.Redis.Tests/Botticelli.Chained.Context.Redis.Tests.csproj create mode 100644 Tests/Botticelli.Chained.Context.Redis.Tests/RedisStorageTests.cs diff --git a/Botticelli.Chained.Context.Redis/RedisStorage.cs b/Botticelli.Chained.Context.Redis/RedisStorage.cs index d4834b67..26713a54 100644 --- a/Botticelli.Chained.Context.Redis/RedisStorage.cs +++ b/Botticelli.Chained.Context.Redis/RedisStorage.cs @@ -20,6 +20,11 @@ public RedisStorage(string connectionString) var redis = ConnectionMultiplexer.Connect(connectionString); _database = redis.GetDatabase(); } + + public RedisStorage(IDatabase database) + { + _database = database; + } public bool ContainsKey(TKey key) => _database.KeyExists(key.ToString()); diff --git a/Botticelli.sln b/Botticelli.sln index 395a103a..5ce7cbcf 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -259,6 +259,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context.Redis", "Botticelli.Chained.Context.Redis\Botticelli.Chained.Context.Redis.csproj", "{4E5352E6-9F06-4A92-A6D9-E82DBA516E72}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context.InMemory.Tests", "Tests\Botticelli.Chained.Context.InMemory.Tests\Botticelli.Chained.Context.InMemory.Tests.csproj", "{D9752FF9-AC66-4CC9-827E-65F16868BF0E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context.Redis.Tests", "Tests\Botticelli.Chained.Context.Redis.Tests\Botticelli.Chained.Context.Redis.Tests.csproj", "{2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Chained", "Botticelli.Chained", "{0D3F6F1E-D5A5-4E6A-8140-4FE03601E084}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -549,6 +555,14 @@ Global {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E5352E6-9F06-4A92-A6D9-E82DBA516E72}.Release|Any CPU.Build.0 = Release|Any CPU + {D9752FF9-AC66-4CC9-827E-65F16868BF0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9752FF9-AC66-4CC9-827E-65F16868BF0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9752FF9-AC66-4CC9-827E-65F16868BF0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9752FF9-AC66-4CC9-827E-65F16868BF0E}.Release|Any CPU.Build.0 = Release|Any CPU + {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -658,6 +672,9 @@ Global {3A9F8D81-DB62-43D3-841C-9F37D8238E36} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} {1A8FB2E2-4272-402F-9301-9C75FEA2E9C1} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} {4E5352E6-9F06-4A92-A6D9-E82DBA516E72} = {DB9DA043-4E18-433B-BD71-2CCC8A5E28C0} + {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} + {D9752FF9-AC66-4CC9-827E-65F16868BF0E} = {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} + {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C} = {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Tests/Botticelli.Chained.Context.InMemory.Tests/Botticelli.Chained.Context.InMemory.Tests.csproj b/Tests/Botticelli.Chained.Context.InMemory.Tests/Botticelli.Chained.Context.InMemory.Tests.csproj new file mode 100644 index 00000000..d7fc6702 --- /dev/null +++ b/Tests/Botticelli.Chained.Context.InMemory.Tests/Botticelli.Chained.Context.InMemory.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Botticelli.Chained.Context.InMemory.Tests/InMemoryStorageTests.cs b/Tests/Botticelli.Chained.Context.InMemory.Tests/InMemoryStorageTests.cs new file mode 100644 index 00000000..85863ac7 --- /dev/null +++ b/Tests/Botticelli.Chained.Context.InMemory.Tests/InMemoryStorageTests.cs @@ -0,0 +1,82 @@ +using FluentAssertions; + +namespace Botticelli.Chained.Context.InMemory.Tests; + +[TestFixture] +public class InMemoryStorageTests +{ + [SetUp] + public void SetUp() + { + _storage = new InMemoryStorage(); + } + + private InMemoryStorage _storage; + + [Test] + [TestCase("testKey", 128923)] + public void Add_ShouldAddValue_WhenKeyIsNew(string key, int value) + { + // Act + _storage.Add(key, value); + + // Assert + _storage.ContainsKey(key).Should().BeTrue(); + _storage[key].Should().Be(value); + } + + [Test] + [TestCase("testKey", 1343223)] + public void Remove_ShouldRemoveValue_WhenKeyExists(string key, int value) + { + // Arrange + _storage.Add(key, value); + + // Act + var result = _storage.Remove(key); + + // Assert + result.Should().BeTrue(); + _storage.ContainsKey(key).Should().BeFalse(); + } + + [Test] + [TestCase("testKey", 33432)] + public void TryGetValue_ShouldReturnTrueAndValue_WhenKeyExists(string key, int value) + { + // Arrange + _storage.Add(key, value); + + // Act + var result = _storage.TryGetValue(key, out var retrievedValue); + + // Assert + result.Should().BeTrue(); + retrievedValue.Should().Be(value); + } + + [Test] + public void TryGetValue_ShouldReturnFalse_WhenKeyDoesNotExist() + { + // Arrange + var key = "nonExistentKey"; + + // Act + var result = _storage.TryGetValue(key, out var retrievedValue); + + // Assert + result.Should().BeFalse(); + retrievedValue.Should().Be(default); + } + + [Test] + [TestCase("testKey", 33432)] + public void Indexer_ShouldGetAndSetValue(string key, int value) + { + // Act + _storage[key] = value; + + // Assert + _storage[key].Should().Be(value); + } +} \ No newline at end of file diff --git a/Tests/Botticelli.Chained.Context.Redis.Tests/Botticelli.Chained.Context.Redis.Tests.csproj b/Tests/Botticelli.Chained.Context.Redis.Tests/Botticelli.Chained.Context.Redis.Tests.csproj new file mode 100644 index 00000000..f313d82f --- /dev/null +++ b/Tests/Botticelli.Chained.Context.Redis.Tests/Botticelli.Chained.Context.Redis.Tests.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Botticelli.Chained.Context.Redis.Tests/RedisStorageTests.cs b/Tests/Botticelli.Chained.Context.Redis.Tests/RedisStorageTests.cs new file mode 100644 index 00000000..881baeb4 --- /dev/null +++ b/Tests/Botticelli.Chained.Context.Redis.Tests/RedisStorageTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using StackExchange.Redis; + +namespace Botticelli.Chained.Context.Redis.Tests; + +[TestFixture] +public class RedisStorageTests +{ + private Mock _mockDatabase; + private RedisStorage _redisStorage; + private Dictionary _storage; + + [SetUp] + public void SetUp() + { + _mockDatabase = new Mock(); + var mockConnection = new Mock(); + mockConnection.Setup(m => m.GetDatabase(It.IsAny(), It.IsAny())).Returns(_mockDatabase.Object); + + _redisStorage = new RedisStorage(_mockDatabase.Object); + + // Setup the mock to maintain state + _mockDatabase.Setup(m => m.StringSet(It.IsAny(), It.IsAny(), null, When.Always, CommandFlags.None)) + .Returns((key, + value, + _, + _, + _) => + { + _storage[key] = value.ToString(); + return true; + }); + + _mockDatabase.Setup(m => m.StringGet(It.IsAny(), It.IsAny())) + .Returns((string key, CommandFlags _) => _storage.TryGetValue(key, out var val) ? val : RedisValue.Null); + _mockDatabase.Setup(m => m.KeyExists(It.IsAny(), It.IsAny())) + .Returns((string key, CommandFlags _) => _storage.ContainsKey(key)); + } + + [Test] + public void Add_ShouldAddValue_WhenKeyIsNew() + { + // Arrange + var key = "testKey"; + var value = "Test"; + + // Act + Assert.DoesNotThrow(() => _redisStorage.Add(key, value)); + } + + [Test] + public void Remove_ShouldRemoveValue_WhenKeyExists() + { + // Arrange + var key = "testKey"; + _mockDatabase.Setup(m => m.KeyDelete(key, CommandFlags.None)).Returns(true); + + // Act + var result = _redisStorage.Remove(key); + + // Assert + result.Should().BeTrue(); + _mockDatabase.Verify(m => m.KeyDelete(key, CommandFlags.None), Times.Once); + } + + [Test] + public void TryGetValue_ShouldReturnTrueAndValue_WhenKeyExists() + { + // Arrange + var key = "testKey"; + var value = "Test"; + _mockDatabase.Setup(m => m.StringGet(key, CommandFlags.None)).Returns(JsonSerializer.Serialize(value)); + _mockDatabase.Setup(m => m.KeyExists(key, CommandFlags.None)).Returns(true); + + // Act + var result = _redisStorage.TryGetValue(key, out var retrievedValue); + + // Assert + result.Should().BeTrue(); + retrievedValue.Should().BeEquivalentTo(value); + } + + [Test] + public void TryGetValue_ShouldReturnFalse_WhenKeyDoesNotExist() + { + // Arrange + var key = "nonExistentKey"; + _mockDatabase.Setup(m => m.KeyExists(key, CommandFlags.None)).Returns(false); + + // Act + var result = _redisStorage.TryGetValue(key, out var retrievedValue); + + // Assert + result.Should().BeFalse(); + retrievedValue.Should().BeNull(); + } +} \ No newline at end of file From c74c90ec4a248385e8dbe08bf8f133000e46ef34 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 1 Jun 2025 22:37:15 +0300 Subject: [PATCH 029/101] - broadcasting draft --- .../Botticelli.Broadcasting.Dal.csproj | 18 +++ .../BroadcastSender.cs | 120 ++++++++++++++++++ .../BroadcastingContext.cs | 19 +++ Botticelli.Broadcasting.Dal/Models/Chat.cs | 22 ++++ .../Models/MessageCache.cs | 35 +++++ .../Models/MessageStatus.cs | 39 ++++++ .../Botticelli.Broadcasting.csproj | 22 ++++ Botticelli.Broadcasting/BroadcastReceiver.cs | 34 +++++ .../Exceptions/BroadcastingException.cs | 3 + .../Extensions/ServiceCollectionExtensions.cs | 29 +++++ .../Settings/BroadcastingSettings.cs | 6 + .../Extensions/ServiceCollectionExtensions.cs | 3 +- Botticelli.sln | 17 +++ 13 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj create mode 100644 Botticelli.Broadcasting.Dal/BroadcastSender.cs create mode 100644 Botticelli.Broadcasting.Dal/BroadcastingContext.cs create mode 100644 Botticelli.Broadcasting.Dal/Models/Chat.cs create mode 100644 Botticelli.Broadcasting.Dal/Models/MessageCache.cs create mode 100644 Botticelli.Broadcasting.Dal/Models/MessageStatus.cs create mode 100644 Botticelli.Broadcasting/Botticelli.Broadcasting.csproj create mode 100644 Botticelli.Broadcasting/BroadcastReceiver.cs create mode 100644 Botticelli.Broadcasting/Exceptions/BroadcastingException.cs create mode 100644 Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs create mode 100644 Botticelli.Broadcasting/Settings/BroadcastingSettings.cs diff --git a/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj new file mode 100644 index 00000000..f9bce5cf --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Botticelli.Broadcasting.Dal/BroadcastSender.cs b/Botticelli.Broadcasting.Dal/BroadcastSender.cs new file mode 100644 index 00000000..a7931daa --- /dev/null +++ b/Botticelli.Broadcasting.Dal/BroadcastSender.cs @@ -0,0 +1,120 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Botticelli.Interfaces; +using Botticelli.Shared.API.Client.Requests; +using Botticelli.Shared.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Botticelli.Broadcasting.Dal; + +/// +/// Represents a service that sends messages from the database to a bot. +/// This class retrieves messages and updates their statuses after sending. +/// +/// The type of the bot that implements the IBot interface. +public class BroadcastSender : IHostedService, IDisposable + where TBot : class, IBot +{ + private readonly TBot _bot; + private readonly BroadcastingContext _context; + private readonly IServiceScope _scope; + private CancellationTokenSource _cancellationTokenSource; + private Task _executingTask; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider used to create a scope and resolve dependencies. + public BroadcastSender(IServiceProvider serviceProvider) + { + _scope = serviceProvider.CreateScope(); + _bot = _scope.ServiceProvider.GetRequiredService(); + _context = _scope.ServiceProvider.GetRequiredService(); + } + + /// + /// Disposes of the resources used by the broadcast sender. + /// + public void Dispose() + { + _scope.Dispose(); + _cancellationTokenSource?.Dispose(); + } + + /// + /// Starts the broadcast sender service. + /// This method retrieves messages from the database and sends them to the bot in a loop. + /// + /// A cancellation token to signal the operation's cancellation. + /// A task representing the asynchronous operation. + public Task StartAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _executingTask = Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token), cancellationToken); + + return Task.CompletedTask; + } + + /// + /// Stops the broadcast sender service. + /// This method disposes of the service scope and cancels the executing task. + /// + /// A cancellation token to signal the operation's cancellation. + /// A task representing the asynchronous operation. + public Task StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource.Cancel(); + + return _executingTask; + } + + /// + /// The main execution loop that retrieves and sends messages. + /// + /// A cancellation token to signal the operation's cancellation. + private async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + // Retrieve messages from the database + var messageStatuses = await _context.MessageStatuses + .Where(ms => !ms.IsSent) // Get messages that have not been sent + .ToListAsync(cancellationToken).ConfigureAwait(false); + + foreach (var messageStatus in messageStatuses) + { + var messageSource = _context.MessageCaches.FirstOrDefault(m => m.Id == messageStatus.MessageId); + + if (messageSource == null) + continue; + + var deserializedMessage = JsonSerializer.Deserialize(messageSource.SerializedMessageObject); + + if (deserializedMessage == null) + continue; + + var request = new SendMessageRequest + { + Message = deserializedMessage + }; + + // Send the message to the bot + var sendResult = await _bot.SendMessageAsync(request, cancellationToken).ConfigureAwait(false); + + // Update the message status + messageStatus.IsSent = sendResult.Message != null; + messageStatus.SentDate = messageStatus.IsSent ? DateTime.UtcNow : null; + + // Save changes to the database + _context.MessageStatuses.Update(messageStatus); + } + + await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + // Wait for a specified interval before checking for new messages again + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken).ConfigureAwait(false); // Adjust the delay as needed + } + } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs new file mode 100644 index 00000000..d27810a0 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs @@ -0,0 +1,19 @@ +using Botticelli.Broadcasting.Dal.Models; +using Microsoft.EntityFrameworkCore; + +namespace Botticelli.Broadcasting.Dal; + +public class BroadcastingContext : DbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(k => new {k.ChatId, k.MessageId}); + } + + public DbSet Chats { get; set; } + public DbSet MessageCaches { get; set; } + public DbSet MessageStatuses { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting.Dal/Models/Chat.cs b/Botticelli.Broadcasting.Dal/Models/Chat.cs new file mode 100644 index 00000000..c6cf15ae --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Models/Chat.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Botticelli.Broadcasting.Dal.Models; + +/// +/// Represents a chat record in the system. +/// This record stores information about a specific chat session, including its status. +/// +public record Chat +{ + /// + /// Gets or sets the unique identifier for the chat. + /// This property serves as the primary key in the database. + /// + [Key] + public required string ChatId { get; set; } + + /// + /// Gets or sets a value indicating whether the chat is currently active. + /// + public bool IsActive { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting.Dal/Models/MessageCache.cs b/Botticelli.Broadcasting.Dal/Models/MessageCache.cs new file mode 100644 index 00000000..7037db42 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Models/MessageCache.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace Botticelli.Broadcasting.Dal.Models; + +/// +/// Represents a cache for serialized messages. +/// This class is used to store messages in a serialized format for efficient retrieval. +/// +public class MessageCache +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier for the message cache entry. + /// The serialized message object. + public MessageCache(string id, string serializedMessageObject) + { + Id = id; + SerializedMessageObject = serializedMessageObject; + } + + /// + /// Gets or sets the unique identifier for the message cache entry. + /// This property serves as the primary key in the database. + /// + [Key] + public required string Id { get; set; } + + /// + /// Gets or sets the serialized representation of the message object. + /// This property stores the message in a format such as JSON or XML. + /// + [MaxLength(100000)] + public required string SerializedMessageObject { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting.Dal/Models/MessageStatus.cs b/Botticelli.Broadcasting.Dal/Models/MessageStatus.cs new file mode 100644 index 00000000..29695b48 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Models/MessageStatus.cs @@ -0,0 +1,39 @@ +namespace Botticelli.Broadcasting.Dal.Models; + +/// +/// Represents the status of a message within a chat. +/// This class stores information about whether a message has been sent, +/// along with relevant timestamps and identifiers. +/// +public class MessageStatus +{ + /// + /// Gets or sets the unique identifier for the message. + /// This property is used to associate the status with a specific message. + /// + public required string MessageId { get; set; } + + /// + /// Gets or sets the unique identifier for the chat. + /// This property links the message status to a specific chat session. + /// + public required string ChatId { get; set; } + + /// + /// Gets or sets a value indicating whether the message has been sent. + /// This property indicates the delivery status of the message (true if sent, false otherwise). + /// + public required bool IsSent { get; set; } + + /// + /// Gets or sets the date and time when the message status was created. + /// This property records when the status entry was created in the system. + /// + public required DateTime CreatedDate { get; set; } + + /// + /// Gets or sets the date and time when the message was sent. + /// This property is nullable, as it may not be set if the message has not been sent yet. + /// + public DateTime? SentDate { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj new file mode 100644 index 00000000..7464487b --- /dev/null +++ b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs new file mode 100644 index 00000000..5f552c00 --- /dev/null +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -0,0 +1,34 @@ +using Botticelli.Broadcasting.Dal; +using Botticelli.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Botticelli.Broadcasting; + +public class BroadcastReceiver : IHostedService +where TBot : class, IBot +{ + private readonly IServiceScope _scope; + private readonly BroadcastingContext _context; + private readonly TBot _bot; + + public BroadcastReceiver(IServiceProvider serviceProvider) + { + _scope = serviceProvider.CreateScope(); + _bot = _scope.ServiceProvider.GetRequiredService(); + _context = _scope.ServiceProvider.GetRequiredService(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // TODO : polls admin API for new messages to send to our chats and adds them to a MessageCache/MessageStatus + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _scope.Dispose(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting/Exceptions/BroadcastingException.cs b/Botticelli.Broadcasting/Exceptions/BroadcastingException.cs new file mode 100644 index 00000000..4d657ef2 --- /dev/null +++ b/Botticelli.Broadcasting/Exceptions/BroadcastingException.cs @@ -0,0 +1,3 @@ +namespace Botticelli.Broadcasting.Exceptions; + +public class BroadcastingException(string message, Exception? ex = null) : Exception(message, ex); \ No newline at end of file diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..149b594c --- /dev/null +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,29 @@ +using System.Configuration; +using Botticelli.Broadcasting.Settings; +using Botticelli.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Broadcasting.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds broadcasting to a bot + /// + /// + /// + /// + /// + public static IServiceCollection AddBroadcasting(this IServiceCollection services, IConfiguration config) + where TBot : IBot + { + var settings = config.Get(); + + if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); + + // TODO: add broadcasting service (IHostedService which uses injected IServiceProvider and creates a new IBot??) + + return services; + } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs new file mode 100644 index 00000000..bb88c704 --- /dev/null +++ b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs @@ -0,0 +1,6 @@ +namespace Botticelli.Broadcasting.Settings; + +public class BroadcastingSettings +{ + public required string BroadcastingDbConnectionString { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs index fea43b22..734f9dbd 100644 --- a/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Chained.Context.Redis/Extensions/ServiceCollectionExtensions.cs @@ -7,7 +7,8 @@ namespace Botticelli.Chained.Context.Redis.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddChainedRedisStorage(this IServiceCollection services, Action> builderFunc) + public static IServiceCollection AddChainedRedisStorage(this IServiceCollection services, + Action> builderFunc) where TKey : notnull where TValue : class { diff --git a/Botticelli.sln b/Botticelli.sln index 5ce7cbcf..6ca5e8dc 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -265,6 +265,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Chained.Context. EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Chained", "Botticelli.Chained", "{0D3F6F1E-D5A5-4E6A-8140-4FE03601E084}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Broadcasting", "Botticelli.Broadcasting", "{39412AE4-B325-4827-B24E-C15415DCA7E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting", "Botticelli.Broadcasting\Botticelli.Broadcasting.csproj", "{88039BCA-7501-48C8-9F94-9F8C2C0A19A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Dal", "Botticelli.Broadcasting.Dal\Botticelli.Broadcasting.Dal.csproj", "{9F898466-739B-4DE0-BFD7-6B5203957236}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -563,6 +569,14 @@ Global {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C}.Release|Any CPU.Build.0 = Release|Any CPU + {88039BCA-7501-48C8-9F94-9F8C2C0A19A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88039BCA-7501-48C8-9F94-9F8C2C0A19A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88039BCA-7501-48C8-9F94-9F8C2C0A19A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88039BCA-7501-48C8-9F94-9F8C2C0A19A5}.Release|Any CPU.Build.0 = Release|Any CPU + {9F898466-739B-4DE0-BFD7-6B5203957236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F898466-739B-4DE0-BFD7-6B5203957236}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F898466-739B-4DE0-BFD7-6B5203957236}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F898466-739B-4DE0-BFD7-6B5203957236}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -675,6 +689,9 @@ Global {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {D9752FF9-AC66-4CC9-827E-65F16868BF0E} = {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} {2FEF33B2-11B2-4D6A-B568-69FCDF6F098C} = {0D3F6F1E-D5A5-4E6A-8140-4FE03601E084} + {39412AE4-B325-4827-B24E-C15415DCA7E9} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {88039BCA-7501-48C8-9F94-9F8C2C0A19A5} = {39412AE4-B325-4827-B24E-C15415DCA7E9} + {9F898466-739B-4DE0-BFD7-6B5203957236} = {39412AE4-B325-4827-B24E-C15415DCA7E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From e246ddc9ac1b18293d48fa67f52e4c79c79112cd Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 3 Jun 2025 13:49:57 +0300 Subject: [PATCH 030/101] - broadcasting on a client side --- .../Botticelli.Broadcasting.Shared.csproj | 13 ++++ .../Requests/GetBroadcastMessagesRequest.cs | 7 ++ .../Responses/GetBroadcastMessagesResponse.cs | 9 +++ .../Botticelli.Broadcasting.csproj | 2 + Botticelli.Broadcasting/BroadcastReceiver.cs | 74 +++++++++++++++++-- .../Settings/BroadcastingSettings.cs | 3 + Botticelli.sln | 7 ++ 7 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 Botticelli.Broadcasting.Shared/Botticelli.Broadcasting.Shared.csproj create mode 100644 Botticelli.Broadcasting.Shared/Requests/GetBroadcastMessagesRequest.cs create mode 100644 Botticelli.Broadcasting.Shared/Responses/GetBroadcastMessagesResponse.cs diff --git a/Botticelli.Broadcasting.Shared/Botticelli.Broadcasting.Shared.csproj b/Botticelli.Broadcasting.Shared/Botticelli.Broadcasting.Shared.csproj new file mode 100644 index 00000000..e1ea3929 --- /dev/null +++ b/Botticelli.Broadcasting.Shared/Botticelli.Broadcasting.Shared.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Botticelli.Broadcasting.Shared/Requests/GetBroadcastMessagesRequest.cs b/Botticelli.Broadcasting.Shared/Requests/GetBroadcastMessagesRequest.cs new file mode 100644 index 00000000..9680a1fe --- /dev/null +++ b/Botticelli.Broadcasting.Shared/Requests/GetBroadcastMessagesRequest.cs @@ -0,0 +1,7 @@ +namespace Botticelli.Broadcasting.Shared.Requests; + +public class GetBroadcastMessagesRequest +{ + public required string BotId { get; set; } + public required TimeSpan HowOld { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting.Shared/Responses/GetBroadcastMessagesResponse.cs b/Botticelli.Broadcasting.Shared/Responses/GetBroadcastMessagesResponse.cs new file mode 100644 index 00000000..80f8e539 --- /dev/null +++ b/Botticelli.Broadcasting.Shared/Responses/GetBroadcastMessagesResponse.cs @@ -0,0 +1,9 @@ +using Botticelli.Shared.ValueObjects; + +namespace Botticelli.Broadcasting.Shared.Responses; + +public class GetBroadcastMessagesResponse +{ + public required string Id { get; set; } + public required List Messages { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj index 7464487b..8727ff1a 100644 --- a/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj +++ b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj @@ -7,10 +7,12 @@ + + diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index 5f552c00..63c275dd 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -1,34 +1,92 @@ +using System.Net; +using System.Net.Http.Json; using Botticelli.Broadcasting.Dal; +using Botticelli.Broadcasting.Settings; using Botticelli.Interfaces; +using Botticelli.Shared.API.Client.Requests; +using Botticelli.Shared.API.Client.Responses; +using Flurl.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Polly; namespace Botticelli.Broadcasting; +/// +/// Long poll for broadcast messages +/// +/// public class BroadcastReceiver : IHostedService -where TBot : class, IBot + where TBot : class, IBot { - private readonly IServiceScope _scope; - private readonly BroadcastingContext _context; private readonly TBot _bot; - - public BroadcastReceiver(IServiceProvider serviceProvider) + private readonly BroadcastingContext _context; + private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); + private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); + private readonly IServiceScope _scope; + private readonly IOptionsSnapshot _settings; + + + public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot settings) { + _settings = settings; _scope = serviceProvider.CreateScope(); _bot = _scope.ServiceProvider.GetRequiredService(); _context = _scope.ServiceProvider.GetRequiredService(); } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { // TODO : polls admin API for new messages to send to our chats and adds them to a MessageCache/MessageStatus - return Task.CompletedTask; + var updatePolicy = Policy.Handle(ex => + ex.Call.Response.ResponseMessage.StatusCode == HttpStatusCode.RequestTimeout) + .WaitAndRetryForeverAsync((_, _) => _retryPause); + + await updatePolicy.ExecuteAsync(async () => + { + var updates = await GetUpdates(cancellationToken); + + if (updates?.Messages == null) return updates; + + foreach (var update in updates.Messages) + { + // if no chat were specified - broadcast on all chats, we've + if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); + + var request = new SendMessageRequest + { + Message = update + }; + + await _bot.SendMessageAsync(request, cancellationToken); + } + + return updates; + }); } public Task StopAsync(CancellationToken cancellationToken) { _scope.Dispose(); - + return Task.CompletedTask; } + + private async Task GetUpdates(CancellationToken cancellationToken) + { + var updatesResponse = await $"{_settings.Value.ServerUri}" + .WithTimeout(_longPollTimeout) + .PostJsonAsync(new GetBroadCastMessagesRequest + { + BotId = _settings.Value.BotId + }, cancellationToken: cancellationToken); + + if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) + return null; + + return await updatesResponse.ResponseMessage.Content + .ReadFromJsonAsync( + cancellationToken); + } } \ No newline at end of file diff --git a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs index bb88c704..1fa06a19 100644 --- a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs +++ b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs @@ -3,4 +3,7 @@ public class BroadcastingSettings { public required string BroadcastingDbConnectionString { get; set; } + public required string BotId { get; set; } + public TimeSpan? HowOld { get; set; } = TimeSpan.FromSeconds(60); + public required string ServerUri { get; set; } } \ No newline at end of file diff --git a/Botticelli.sln b/Botticelli.sln index 6ca5e8dc..4aecc61a 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -271,6 +271,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Dal", "Botticelli.Broadcasting.Dal\Botticelli.Broadcasting.Dal.csproj", "{9F898466-739B-4DE0-BFD7-6B5203957236}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Shared", "Botticelli.Broadcasting.Shared\Botticelli.Broadcasting.Shared.csproj", "{0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -577,6 +579,10 @@ Global {9F898466-739B-4DE0-BFD7-6B5203957236}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F898466-739B-4DE0-BFD7-6B5203957236}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F898466-739B-4DE0-BFD7-6B5203957236}.Release|Any CPU.Build.0 = Release|Any CPU + {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -692,6 +698,7 @@ Global {39412AE4-B325-4827-B24E-C15415DCA7E9} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {88039BCA-7501-48C8-9F94-9F8C2C0A19A5} = {39412AE4-B325-4827-B24E-C15415DCA7E9} {9F898466-739B-4DE0-BFD7-6B5203957236} = {39412AE4-B325-4827-B24E-C15415DCA7E9} + {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637} = {39412AE4-B325-4827-B24E-C15415DCA7E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From 3789875602f8966e1c2c48a98b049f09f49ae1f2 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 3 Jun 2025 15:04:26 +0300 Subject: [PATCH 031/101] - events processing in a bot builder - chat list actualization while receiving messages --- .../BroadcastSender.cs | 120 ------------------ .../Botticelli.Broadcasting.csproj | 1 + .../{BroadcastReceiver.cs => Broadcaster.cs} | 15 ++- .../Extensions/ServiceCollectionExtensions.cs | 28 +++- Botticelli.Framework.Common/BotEventArgs.cs | 14 -- .../Builders/TelegramBotBuilder.cs | 81 +++++++----- Botticelli.Framework.Telegram/TelegramBot.cs | 10 +- Botticelli.Framework/BaseBot.cs | 4 + Botticelli.Framework/Builders/BotBuilder.cs | 27 +++- 9 files changed, 115 insertions(+), 185 deletions(-) delete mode 100644 Botticelli.Broadcasting.Dal/BroadcastSender.cs rename Botticelli.Broadcasting/{BroadcastReceiver.cs => Broadcaster.cs} (84%) diff --git a/Botticelli.Broadcasting.Dal/BroadcastSender.cs b/Botticelli.Broadcasting.Dal/BroadcastSender.cs deleted file mode 100644 index a7931daa..00000000 --- a/Botticelli.Broadcasting.Dal/BroadcastSender.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Botticelli.Interfaces; -using Botticelli.Shared.API.Client.Requests; -using Botticelli.Shared.ValueObjects; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Botticelli.Broadcasting.Dal; - -/// -/// Represents a service that sends messages from the database to a bot. -/// This class retrieves messages and updates their statuses after sending. -/// -/// The type of the bot that implements the IBot interface. -public class BroadcastSender : IHostedService, IDisposable - where TBot : class, IBot -{ - private readonly TBot _bot; - private readonly BroadcastingContext _context; - private readonly IServiceScope _scope; - private CancellationTokenSource _cancellationTokenSource; - private Task _executingTask; - - /// - /// Initializes a new instance of the class. - /// - /// The service provider used to create a scope and resolve dependencies. - public BroadcastSender(IServiceProvider serviceProvider) - { - _scope = serviceProvider.CreateScope(); - _bot = _scope.ServiceProvider.GetRequiredService(); - _context = _scope.ServiceProvider.GetRequiredService(); - } - - /// - /// Disposes of the resources used by the broadcast sender. - /// - public void Dispose() - { - _scope.Dispose(); - _cancellationTokenSource?.Dispose(); - } - - /// - /// Starts the broadcast sender service. - /// This method retrieves messages from the database and sends them to the bot in a loop. - /// - /// A cancellation token to signal the operation's cancellation. - /// A task representing the asynchronous operation. - public Task StartAsync(CancellationToken cancellationToken) - { - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _executingTask = Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token), cancellationToken); - - return Task.CompletedTask; - } - - /// - /// Stops the broadcast sender service. - /// This method disposes of the service scope and cancels the executing task. - /// - /// A cancellation token to signal the operation's cancellation. - /// A task representing the asynchronous operation. - public Task StopAsync(CancellationToken cancellationToken) - { - _cancellationTokenSource.Cancel(); - - return _executingTask; - } - - /// - /// The main execution loop that retrieves and sends messages. - /// - /// A cancellation token to signal the operation's cancellation. - private async Task ExecuteAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - // Retrieve messages from the database - var messageStatuses = await _context.MessageStatuses - .Where(ms => !ms.IsSent) // Get messages that have not been sent - .ToListAsync(cancellationToken).ConfigureAwait(false); - - foreach (var messageStatus in messageStatuses) - { - var messageSource = _context.MessageCaches.FirstOrDefault(m => m.Id == messageStatus.MessageId); - - if (messageSource == null) - continue; - - var deserializedMessage = JsonSerializer.Deserialize(messageSource.SerializedMessageObject); - - if (deserializedMessage == null) - continue; - - var request = new SendMessageRequest - { - Message = deserializedMessage - }; - - // Send the message to the bot - var sendResult = await _bot.SendMessageAsync(request, cancellationToken).ConfigureAwait(false); - - // Update the message status - messageStatus.IsSent = sendResult.Message != null; - messageStatus.SentDate = messageStatus.IsSent ? DateTime.UtcNow : null; - - // Save changes to the database - _context.MessageStatuses.Update(messageStatus); - } - - await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - // Wait for a specified interval before checking for new messages again - await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken).ConfigureAwait(false); // Adjust the delay as needed - } - } -} \ No newline at end of file diff --git a/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj index 8727ff1a..812fb28f 100644 --- a/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj +++ b/Botticelli.Broadcasting/Botticelli.Broadcasting.csproj @@ -18,6 +18,7 @@ + diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/Broadcaster.cs similarity index 84% rename from Botticelli.Broadcasting/BroadcastReceiver.cs rename to Botticelli.Broadcasting/Broadcaster.cs index 63c275dd..8250bff2 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/Broadcaster.cs @@ -2,10 +2,12 @@ using System.Net.Http.Json; using Botticelli.Broadcasting.Dal; using Botticelli.Broadcasting.Settings; +using Botticelli.Framework; using Botticelli.Interfaces; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; using Flurl.Http; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -14,11 +16,13 @@ namespace Botticelli.Broadcasting; /// -/// Long poll for broadcast messages +/// Broadcaster is a service that polls an admin API for new messages +/// to send to various chat clients. It implements IHostedService to manage +/// the lifecycle of the service within a hosted environment. /// -/// -public class BroadcastReceiver : IHostedService - where TBot : class, IBot +/// The type of bot that implements IBot interface. +public class Broadcaster : IHostedService + where TBot : BaseBot, IBot { private readonly TBot _bot; private readonly BroadcastingContext _context; @@ -27,8 +31,7 @@ public class BroadcastReceiver : IHostedService private readonly IServiceScope _scope; private readonly IOptionsSnapshot _settings; - - public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot settings) + public Broadcaster(IServiceProvider serviceProvider, IOptionsSnapshot settings) { _settings = settings; _scope = serviceProvider.CreateScope(); diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index 149b594c..0a87ed1d 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using System.Configuration; +using Botticelli.Broadcasting.Dal; using Botticelli.Broadcasting.Settings; +using Botticelli.Framework; +using Botticelli.Framework.Builders; using Botticelli.Interfaces; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -12,18 +16,34 @@ public static class ServiceCollectionExtensions /// Adds broadcasting to a bot /// /// - /// + /// /// /// - public static IServiceCollection AddBroadcasting(this IServiceCollection services, IConfiguration config) - where TBot : IBot + public static BotBuilder AddBroadcasting(this BotBuilder botBuilder, IConfiguration config) + where TBot : BaseBot, IBot + where TBotBuilder : BotBuilder { var settings = config.Get(); if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); // TODO: add broadcasting service (IHostedService which uses injected IServiceProvider and creates a new IBot??) + botBuilder.Services.AddHostedService>(); + + botBuilder.AddOnMessageReceived(async (sender, args) => + { + var context = botBuilder.Services.BuildServiceProvider().GetRequiredService(); + + var disabledChats = context.Chats.Where(c => !c.IsActive && args.Message.ChatIds.Contains(c.ChatId)).AsQueryable(); + var nonExistingChats = context.Chats.Where(c => !args.Message.ChatIds.Contains(c.ChatId)).ToArray(); - return services; + await context.Chats.AddRangeAsync(nonExistingChats); + await context.SaveChangesAsync(); + + await disabledChats.ExecuteUpdateAsync(c => c.SetProperty(chat => chat.IsActive, true)); + await context.SaveChangesAsync(); + }); + + return botBuilder; } } \ No newline at end of file diff --git a/Botticelli.Framework.Common/BotEventArgs.cs b/Botticelli.Framework.Common/BotEventArgs.cs index c478bbce..e281cd84 100644 --- a/Botticelli.Framework.Common/BotEventArgs.cs +++ b/Botticelli.Framework.Common/BotEventArgs.cs @@ -1,21 +1,7 @@ using System; -using Botticelli.Interfaces; namespace Botticelli.Framework.Events; -public delegate void MessengerSpecificEventHandler(object sender, MessengerSpecificBotEventArgs e) - where T : IBot; - -public delegate void MsgReceivedEventHandler(object sender, MessageReceivedBotEventArgs e); - -public delegate void MsgRemovedEventHandler(object sender, MessageRemovedBotEventArgs e); - -public delegate void MsgSentEventHandler(object sender, MessageSentBotEventArgs e); - -public delegate void StartedEventHandler(object sender, StartedBotEventArgs e); - -public delegate void StoppedEventHandler(object sender, StoppedBotEventArgs e); - public class BotEventArgs : EventArgs { public DateTime DateTime { get; } = DateTime.Now; diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 44b8c6fe..58dafec8 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -5,8 +5,8 @@ using Botticelli.Bot.Utils.TextUtils; using Botticelli.Client.Analytics; using Botticelli.Client.Analytics.Settings; -using Botticelli.Framework.Builders; using Botticelli.Controls.Parsers; +using Botticelli.Framework.Builders; using Botticelli.Framework.Extensions; using Botticelli.Framework.Options; using Botticelli.Framework.Security; @@ -31,8 +31,8 @@ namespace Botticelli.Framework.Telegram.Builders; /// /// public abstract class TelegramBotBuilder(bool isStandalone) - : TelegramBotBuilder>(isStandalone) - where TBot : TelegramBot; + : TelegramBotBuilder>(isStandalone) + where TBot : TelegramBot; /// /// Builder for a non-standalone Telegram bot @@ -40,8 +40,8 @@ public abstract class TelegramBotBuilder(bool isStandalone) /// /// public class TelegramBotBuilder : BotBuilder - where TBot : TelegramBot - where TBotBuilder : BotBuilder + where TBot : TelegramBot + where TBotBuilder : BotBuilder { private readonly bool _isStandalone; private readonly List> _subHandlers = []; @@ -57,22 +57,20 @@ private protected TelegramBotBuilder(bool isStandalone) protected BotData.Entities.Bot.BotData? BotData { get; set; } public static TelegramBotBuilder Instance(IServiceCollection services, - ServerSettingsBuilder serverSettingsBuilder, - BotSettingsBuilder settingsBuilder, - DataAccessSettingsBuilder dataAccessSettingsBuilder, - AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, - bool isStandalone) - { - return (TelegramBotBuilder) new TelegramBotBuilder(isStandalone) - .AddBotSettings(settingsBuilder) - .AddServerSettings(serverSettingsBuilder) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder) - .AddServices(services); - } + ServerSettingsBuilder serverSettingsBuilder, + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder, + AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, + bool isStandalone) => + (TelegramBotBuilder)new TelegramBotBuilder(isStandalone) + .AddBotSettings(settingsBuilder) + .AddServerSettings(serverSettingsBuilder) + .AddAnalyticsSettings(analyticsClientSettingsBuilder) + .AddBotDataAccessSettings(dataAccessSettingsBuilder) + .AddServices(services); public TelegramBotBuilder AddSubHandler() - where T : class, IBotUpdateSubHandler + where T : class, IBotUpdateSubHandler { Services.NotNull(); Services.AddSingleton(); @@ -109,17 +107,17 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu Services.AddSingleton(ServerSettingsBuilder!.Build()); Services.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService(); Services.AddHttpClient() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService(); Services.AddHttpClient>() - .AddServerCertificates(BotSettings); + .AddServerCertificates(BotSettings); Services.AddHostedService>>() - .AddHostedService(); + .AddHostedService(); } var botId = BotDataUtils.GetBotId(); @@ -141,7 +139,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu #region Data Services.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); + o.UseSqlite($"Data source={BotDataAccessSettingsBuilder!.Build().ConnectionString}")); Services.AddScoped(); #endregion @@ -163,25 +161,38 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu client!.Timeout = TimeSpan.FromMilliseconds(BotSettings?.Timeout ?? 10000); Services.AddSingleton, ReplyTelegramLayoutSupplier>() - .AddBotticelliFramework() - .AddSingleton(); + .AddBotticelliFramework() + .AddSingleton(); var sp = Services.BuildServiceProvider(); ApplyMigrations(sp); foreach (var sh in _subHandlers) sh.Invoke(sp); - return Activator.CreateInstance(typeof(TBot), - client, - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetService()) as TBot; + if (Activator.CreateInstance(typeof(TBot), + client, + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService()) is not TBot bot) + throw new InvalidDataException($"{nameof(bot)} shouldn't be null!"); + + AddEvents(bot); + + return bot; + } + + private void AddEvents(TBot bot) + { + bot.MessageSent += MessageSent; + bot.MessageReceived += MessageReceived; + bot.MessageRemoved += MessageRemoved; } - protected TelegramBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) - where TBotSettings : BotSettings, new() + protected TelegramBotBuilder AddBotSettings( + BotSettingsBuilder settingsBuilder) + where TBotSettings : BotSettings, new() { BotSettings = settingsBuilder.Build() as TelegramBotSettings ?? throw new InvalidOperationException(); diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index e54fb5e1..d49818c2 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -51,10 +51,10 @@ public TelegramBot(ITelegramBotClient client, } public override BotType Type => BotType.Telegram; - public event MsgSentEventHandler? MessageSent; - public event MsgReceivedEventHandler? MessageReceived; - public event MsgRemovedEventHandler? MessageRemoved; - + public override event MsgSentEventHandler? MessageSent; + public override event MsgReceivedEventHandler? MessageReceived; + public override event MsgRemovedEventHandler? MessageRemoved; + /// /// Deletes a message /// @@ -101,7 +101,7 @@ await Client.DeleteMessage(request.ChatId, { MessageUid = request.Uid }; - + MessageRemoved?.Invoke(this, eventArgs); return response; diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index d1c341f4..5164af55 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -28,6 +28,10 @@ public abstract class BaseBot public delegate void StartedEventHandler(object sender, StartedBotEventArgs e); public delegate void StoppedEventHandler(object sender, StoppedBotEventArgs e); + + public virtual event MsgSentEventHandler? MessageSent; + public virtual event MsgReceivedEventHandler? MessageReceived; + public virtual event MsgRemovedEventHandler? MessageRemoved; } /// diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index cb55947b..0de6bfd0 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -25,7 +25,11 @@ public abstract class BotBuilder : BotBuilder protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; protected ServerSettingsBuilder? ServerSettingsBuilder; - protected IServiceCollection Services = null!; + public IServiceCollection Services = null!; + + protected BaseBot.MsgSentEventHandler? MessageSent; + protected BaseBot.MsgReceivedEventHandler? MessageReceived; + protected BaseBot.MsgRemovedEventHandler? MessageRemoved; protected override void Assert() { @@ -58,4 +62,25 @@ public BotBuilder AddBotDataAccessSettings(DataAccessSettings return this; } + + public BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler) + { + MessageSent += handler; + + return this; + } + + public BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler) + { + MessageReceived += handler; + + return this; + } + + public BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler) + { + MessageRemoved += handler; + + return this; + } } \ No newline at end of file From 090c720635ada599edfc76dbf4c81067d9159b4e Mon Sep 17 00:00:00 2001 From: stone1985 Date: Tue, 3 Jun 2025 16:22:01 +0300 Subject: [PATCH 032/101] - telegram.bot upgrade --- Botticelli.Controls/Botticelli.Controls.csproj | 2 +- .../Botticelli.Framework.Telegram.csproj | 2 +- Botticelli.Pay/Botticelli.Pay.csproj | 2 +- Samples/Ai.Common.Sample/Ai.Common.Sample.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Botticelli.Controls/Botticelli.Controls.csproj b/Botticelli.Controls/Botticelli.Controls.csproj index 508345e6..86ab4d02 100644 --- a/Botticelli.Controls/Botticelli.Controls.csproj +++ b/Botticelli.Controls/Botticelli.Controls.csproj @@ -9,7 +9,7 @@ - + diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index 767c7749..ac4c452a 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -29,7 +29,7 @@ - + diff --git a/Botticelli.Pay/Botticelli.Pay.csproj b/Botticelli.Pay/Botticelli.Pay.csproj index e9cb48e8..e5d9cfc4 100644 --- a/Botticelli.Pay/Botticelli.Pay.csproj +++ b/Botticelli.Pay/Botticelli.Pay.csproj @@ -13,7 +13,7 @@ True - + diff --git a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj index 67132326..09fb90cf 100644 --- a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj +++ b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj @@ -8,7 +8,7 @@ - + From c62df4fd91a040402d7710d83446cd6ec361ee51 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Tue, 3 Jun 2025 16:22:01 +0300 Subject: [PATCH 033/101] - telegram.bot upgrade --- Botticelli.Controls/Botticelli.Controls.csproj | 2 +- .../Botticelli.Framework.Telegram.csproj | 2 +- Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs | 3 ++- Botticelli.Framework.Telegram/TelegramBot.cs | 2 +- Botticelli.Pay/Botticelli.Pay.csproj | 2 +- Samples/Ai.Common.Sample/Ai.Common.Sample.csproj | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Botticelli.Controls/Botticelli.Controls.csproj b/Botticelli.Controls/Botticelli.Controls.csproj index 508345e6..86ab4d02 100644 --- a/Botticelli.Controls/Botticelli.Controls.csproj +++ b/Botticelli.Controls/Botticelli.Controls.csproj @@ -9,7 +9,7 @@ - + diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index 767c7749..ac4c452a 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -29,7 +29,7 @@ - + diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index 76dee60c..8dc1eda7 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -6,6 +6,7 @@ using Telegram.Bot; using Telegram.Bot.Polling; using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; using Message = Botticelli.Shared.ValueObjects.Message; using Poll = Botticelli.Shared.ValueObjects.Poll; using User = Botticelli.Shared.ValueObjects.User; @@ -89,7 +90,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, Id = update.Poll.Id, IsAnonymous = update.Poll.IsAnonymous, Question = update.Poll.Question, - Type = update.Poll.Type.ToLower() == "regular" ? Poll.PollType.Regular : Poll.PollType.Quiz, + Type = update.Poll.Type == PollType.Regular ? Poll.PollType.Regular : Poll.PollType.Quiz, Variants = update.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), CorrectAnswerId = update.Poll.CorrectOptionId } diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index d49818c2..5b66ae5a 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -424,7 +424,7 @@ await ProcessContact(request, Id = message.Poll.Id, IsAnonymous = message.Poll.IsAnonymous, Question = message.Poll.Question, - Type = message.Poll.Type.ToLower() == "regular" ? Poll.PollType.Regular : Poll.PollType.Quiz, + Type = message.Poll.Type == PollType.Regular ? Poll.PollType.Regular : Poll.PollType.Quiz, Variants = message.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), CorrectAnswerId = message.Poll.CorrectOptionId }; diff --git a/Botticelli.Pay/Botticelli.Pay.csproj b/Botticelli.Pay/Botticelli.Pay.csproj index e9cb48e8..e5d9cfc4 100644 --- a/Botticelli.Pay/Botticelli.Pay.csproj +++ b/Botticelli.Pay/Botticelli.Pay.csproj @@ -13,7 +13,7 @@ True - + diff --git a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj index 67132326..09fb90cf 100644 --- a/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj +++ b/Samples/Ai.Common.Sample/Ai.Common.Sample.csproj @@ -8,7 +8,7 @@ - + From 56cee32308e1cb3f1f493cf56f89e6cf2b09dba4 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Tue, 3 Jun 2025 16:40:06 +0300 Subject: [PATCH 034/101] - telegram msg processing: reduced nesting & complexity --- .../Handlers/BotUpdateHandler.cs | 202 ++++++++++-------- 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index 8dc1eda7..b6da6271 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -44,103 +44,19 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, if (botMessage == null) { - if (update.CallbackQuery != null) - { - botMessage = update.CallbackQuery?.Message; - - if (botMessage == null) - { - logger.LogError($"{nameof(HandleUpdateAsync)}() {nameof(botMessage)} is null!"); - - return; - } - - botticelliMessage = new Message - { - ChatIdInnerIdLinks = new Dictionary> - { - { - update.CallbackQuery?.Message.Chat?.Id.ToString(), - [update.CallbackQuery.Message?.MessageId.ToString()] - } - }, - ChatIds = [update.CallbackQuery?.Message.Chat.Id.ToString()], - CallbackData = update.CallbackQuery?.Data ?? string.Empty, - CreatedAt = update.Message?.Date ?? DateTime.Now, - LastModifiedAt = update.Message?.Date ?? DateTime.Now, - From = new User - { - Id = update.CallbackQuery?.From.Id.ToString(), - Name = update.CallbackQuery?.From.FirstName, - Surname = update.CallbackQuery?.From.LastName, - Info = string.Empty, - IsBot = update.CallbackQuery?.From.IsBot, - NickName = update.CallbackQuery?.From.Username - } - }; - } - - if (update.Poll != null) - botticelliMessage = new Message - { - Subject = string.Empty, - Body = string.Empty, - Poll = new Poll - { - Id = update.Poll.Id, - IsAnonymous = update.Poll.IsAnonymous, - Question = update.Poll.Question, - Type = update.Poll.Type == PollType.Regular ? Poll.PollType.Regular : Poll.PollType.Quiz, - Variants = update.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), - CorrectAnswerId = update.Poll.CorrectOptionId - } - }; + if (ProcessCallback(update, ref botticelliMessage)) + return; + + botticelliMessage = ProcessPoll(update, botticelliMessage); } else - { - botticelliMessage = new Message(botMessage.MessageId.ToString()) - { - ChatIdInnerIdLinks = new Dictionary> - {{botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()]}}, - ChatIds = [botMessage.Chat.Id.ToString()], - Subject = string.Empty, - Body = botMessage.Text ?? string.Empty, - LastModifiedAt = botMessage.Date, - Attachments = new List(5), - CreatedAt = botMessage.Date, - From = new User - { - Id = botMessage.From?.Id.ToString(), - Name = botMessage.From?.FirstName, - Surname = botMessage.From?.LastName, - Info = string.Empty, - IsBot = botMessage.From?.IsBot, - NickName = botMessage.From?.Username - }, - ForwardedFrom = new User - { - Id = botMessage.ForwardFrom?.Id.ToString(), - Name = botMessage.ForwardFrom?.FirstName, - Surname = botMessage.ForwardFrom?.LastName, - Info = string.Empty, - IsBot = botMessage.ForwardFrom?.IsBot, - NickName = botMessage.ForwardFrom?.Username - }, - Location = botMessage.Location != null ? - new GeoLocation - { - Latitude = (decimal) botMessage.Location.Latitude, - Longitude = (decimal) botMessage.Location.Longitude - } : - null - }; - } + botticelliMessage = ProcessOrdinaryMessage(botMessage); foreach (var subHandler in _subHandlers) await subHandler.Process(botClient, update, cancellationToken); if (botticelliMessage != null) { - await Process(botticelliMessage, cancellationToken); + await ProcessInProcessors(botticelliMessage, cancellationToken); MessageReceived?.Invoke(this, new MessageReceivedBotEventArgs @@ -157,6 +73,106 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, } } + private static Message ProcessOrdinaryMessage(global::Telegram.Bot.Types.Message botMessage) + { + var botticelliMessage = new Message(botMessage.MessageId.ToString()) + { + ChatIdInnerIdLinks = new System.Collections.Generic.Dictionary> + {{botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()]}}, + ChatIds = [botMessage.Chat.Id.ToString()], + Subject = string.Empty, + Body = botMessage.Text ?? string.Empty, + LastModifiedAt = botMessage.Date, + Attachments = new System.Collections.Generic.List(5), + CreatedAt = botMessage.Date, + From = new User + { + Id = botMessage.From?.Id.ToString(), + Name = botMessage.From?.FirstName, + Surname = botMessage.From?.LastName, + Info = string.Empty, + IsBot = botMessage.From?.IsBot, + NickName = botMessage.From?.Username + }, + ForwardedFrom = new User + { + Id = botMessage.ForwardFrom?.Id.ToString(), + Name = botMessage.ForwardFrom?.FirstName, + Surname = botMessage.ForwardFrom?.LastName, + Info = string.Empty, + IsBot = botMessage.ForwardFrom?.IsBot, + NickName = botMessage.ForwardFrom?.Username + }, + Location = botMessage.Location != null ? + new GeoLocation + { + Latitude = (decimal) botMessage.Location.Latitude, + Longitude = (decimal) botMessage.Location.Longitude + } : + null + }; + return botticelliMessage; + } + + private static Message? ProcessPoll(Update update, Message? botticelliMessage) + { + if (update.Poll != null) + botticelliMessage = new Message + { + Subject = string.Empty, + Body = string.Empty, + Poll = new Poll + { + Id = update.Poll.Id, + IsAnonymous = update.Poll.IsAnonymous, + Question = update.Poll.Question, + Type = update.Poll.Type == PollType.Regular ? Poll.PollType.Regular : Poll.PollType.Quiz, + Variants = update.Poll.Options.Select(o => new ValueTuple(o.Text, o.VoterCount)), + CorrectAnswerId = update.Poll.CorrectOptionId + } + }; + return botticelliMessage; + } + + private bool ProcessCallback(Update update, ref Message? botticelliMessage) + { + if (update.CallbackQuery == null) + return false; + + if (update.CallbackQuery?.Message == null) + { + logger.LogError($"{nameof(HandleUpdateAsync)}() callback message is null!"); + + return true; + } + + botticelliMessage = new Message + { + ChatIdInnerIdLinks = new Dictionary> + { + { + update.CallbackQuery?.Message.Chat.Id.ToString(), + [update.CallbackQuery.Message?.MessageId.ToString()] + } + }, + ChatIds = [update.CallbackQuery?.Message.Chat.Id.ToString()], + CallbackData = update.CallbackQuery?.Data ?? string.Empty, + CreatedAt = update.Message?.Date ?? DateTime.Now, + LastModifiedAt = update.Message?.Date ?? DateTime.Now, + From = new User + { + Id = update.CallbackQuery?.From.Id.ToString(), + Name = update.CallbackQuery?.From.FirstName, + Surname = update.CallbackQuery?.From.LastName, + Info = string.Empty, + IsBot = update.CallbackQuery?.From.IsBot, + NickName = update.CallbackQuery?.From.Username + } + }; + + return false; + } + public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, @@ -179,9 +195,9 @@ public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler /// /// /// - protected async Task Process(Message request, CancellationToken token) + protected async Task ProcessInProcessors(Message request, CancellationToken token) { - logger.LogDebug($"{nameof(Process)}({request.Uid}) started..."); + logger.LogDebug($"{nameof(ProcessInProcessors)}({request.Uid}) started..."); if (token is {CanBeCanceled: true, IsCancellationRequested: true}) return; @@ -197,6 +213,6 @@ protected async Task Process(Message request, CancellationToken token) await Parallel.ForEachAsync(clientTasks, token, async (t, ct) => await t.WaitAsync(ct)); - logger.LogDebug($"{nameof(Process)}({request.Uid}) finished..."); + logger.LogDebug($"{nameof(ProcessInProcessors)}({request.Uid}) finished..."); } } \ No newline at end of file From 4b556fa7ce203562449a5318f22d2c0ab47c9ce5 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 3 Jun 2025 23:26:29 +0300 Subject: [PATCH 035/101] - DAL for broadcasting purposes --- .../Botticelli.Broadcasting.Dal.csproj | 1 + .../BroadcastingContext.cs | 10 +-- .../20250603202442_initial.Designer.cs | 75 +++++++++++++++++++ .../Migrations/20250603202442_initial.cs | 67 +++++++++++++++++ .../BroadcastingContextModelSnapshot.cs | 72 ++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 28 ++++--- .../Settings/BroadcastingSettings.cs | 1 - .../Messaging.Sample.Telegram.csproj | 1 + Samples/Messaging.Sample.Telegram/Program.cs | 5 +- .../appsettings.json | 6 ++ 10 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs create mode 100644 Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs create mode 100644 Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs diff --git a/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj index f9bce5cf..6a195d30 100644 --- a/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj +++ b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj @@ -8,6 +8,7 @@ + diff --git a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs index d27810a0..a1e1cf45 100644 --- a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs +++ b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs @@ -5,15 +5,15 @@ namespace Botticelli.Broadcasting.Dal; public class BroadcastingContext : DbContext { + public DbSet Chats { get; set; } + public DbSet MessageCaches { get; set; } + public DbSet MessageStatuses { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity() - .HasKey(k => new {k.ChatId, k.MessageId}); + .HasKey(k => new { k.ChatId, k.MessageId }); } - - public DbSet Chats { get; set; } - public DbSet MessageCaches { get; set; } - public DbSet MessageStatuses { get; set; } } \ No newline at end of file diff --git a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs new file mode 100644 index 00000000..be5edef3 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs @@ -0,0 +1,75 @@ +// +using System; +using Botticelli.Broadcasting.Dal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Botticelli.Broadcasting.Dal.Migrations +{ + [DbContext(typeof(BroadcastingContext))] + [Migration("20250603202442_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.13"); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.Chat", b => + { + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.HasKey("ChatId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageCache", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("SerializedMessageObject") + .IsRequired() + .HasMaxLength(100000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MessageCaches"); + }); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageStatus", b => + { + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("MessageId") + .HasColumnType("TEXT"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("IsSent") + .HasColumnType("INTEGER"); + + b.Property("SentDate") + .HasColumnType("TEXT"); + + b.HasKey("ChatId", "MessageId"); + + b.ToTable("MessageStatuses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs new file mode 100644 index 00000000..a2f29479 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Botticelli.Broadcasting.Dal.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Chats", + columns: table => new + { + ChatId = table.Column(type: "TEXT", nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Chats", x => x.ChatId); + }); + + migrationBuilder.CreateTable( + name: "MessageCaches", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + SerializedMessageObject = table.Column(type: "TEXT", maxLength: 100000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MessageCaches", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "MessageStatuses", + columns: table => new + { + MessageId = table.Column(type: "TEXT", nullable: false), + ChatId = table.Column(type: "TEXT", nullable: false), + IsSent = table.Column(type: "INTEGER", nullable: false), + CreatedDate = table.Column(type: "TEXT", nullable: false), + SentDate = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MessageStatuses", x => new { x.ChatId, x.MessageId }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Chats"); + + migrationBuilder.DropTable( + name: "MessageCaches"); + + migrationBuilder.DropTable( + name: "MessageStatuses"); + } + } +} diff --git a/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs new file mode 100644 index 00000000..e9d32593 --- /dev/null +++ b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs @@ -0,0 +1,72 @@ +// +using System; +using Botticelli.Broadcasting.Dal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Botticelli.Broadcasting.Dal.Migrations +{ + [DbContext(typeof(BroadcastingContext))] + partial class BroadcastingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.13"); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.Chat", b => + { + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.HasKey("ChatId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageCache", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("SerializedMessageObject") + .IsRequired() + .HasMaxLength(100000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MessageCaches"); + }); + + modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageStatus", b => + { + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("MessageId") + .HasColumnType("TEXT"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("IsSent") + .HasColumnType("INTEGER"); + + b.Property("SentDate") + .HasColumnType("TEXT"); + + b.HasKey("ChatId", "MessageId"); + + b.ToTable("MessageStatuses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index 0a87ed1d..a8de63d1 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -17,9 +17,13 @@ public static class ServiceCollectionExtensions /// /// /// + /// /// + /// /// - public static BotBuilder AddBroadcasting(this BotBuilder botBuilder, IConfiguration config) + public static BotBuilder AddBroadcasting(this BotBuilder botBuilder, + IConfiguration config, + Action? dbOptionsBuilder = null) where TBot : BaseBot, IBot where TBotBuilder : BotBuilder { @@ -27,23 +31,27 @@ public static BotBuilder AddBroadcasting(t if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); - // TODO: add broadcasting service (IHostedService which uses injected IServiceProvider and creates a new IBot??) - botBuilder.Services.AddHostedService>(); + botBuilder.Services + .AddHostedService>() + .AddDbContext(dbOptionsBuilder); + + ApplyMigrations(botBuilder.Services); - botBuilder.AddOnMessageReceived(async (sender, args) => + return botBuilder.AddOnMessageReceived((_, args) => { var context = botBuilder.Services.BuildServiceProvider().GetRequiredService(); var disabledChats = context.Chats.Where(c => !c.IsActive && args.Message.ChatIds.Contains(c.ChatId)).AsQueryable(); var nonExistingChats = context.Chats.Where(c => !args.Message.ChatIds.Contains(c.ChatId)).ToArray(); - await context.Chats.AddRangeAsync(nonExistingChats); - await context.SaveChangesAsync(); + context.Chats.AddRange(nonExistingChats); + disabledChats.ExecuteUpdate(c => c.SetProperty(chat => chat.IsActive, true)); - await disabledChats.ExecuteUpdateAsync(c => c.SetProperty(chat => chat.IsActive, true)); - await context.SaveChangesAsync(); + context.SaveChanges(); }); - - return botBuilder; } + + private static void ApplyMigrations(IServiceCollection services) => + services.BuildServiceProvider() + .GetRequiredService().Database.Migrate(); } \ No newline at end of file diff --git a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs index 1fa06a19..c753ac4a 100644 --- a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs +++ b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs @@ -2,7 +2,6 @@ public class BroadcastingSettings { - public required string BroadcastingDbConnectionString { get; set; } public required string BotId { get; set; } public TimeSpan? HowOld { get; set; } = TimeSpan.FromSeconds(60); public required string ServerUri { get; set; } diff --git a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj index 802e34c6..a55938c2 100644 --- a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj +++ b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj @@ -24,6 +24,7 @@ + diff --git a/Samples/Messaging.Sample.Telegram/Program.cs b/Samples/Messaging.Sample.Telegram/Program.cs index 369ffcb9..d4fb93f4 100644 --- a/Samples/Messaging.Sample.Telegram/Program.cs +++ b/Samples/Messaging.Sample.Telegram/Program.cs @@ -1,16 +1,19 @@ +using Botticelli.Broadcasting.Extensions; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Schedule.Quartz.Extensions; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; +using Microsoft.EntityFrameworkCore; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; var builder = WebApplication.CreateBuilder(args); builder.Services - .AddTelegramBot(builder.Configuration) + .AddTelegramBot(builder.Configuration, botBuilder => botBuilder.AddBroadcasting(builder.Configuration, + optionsBuilder => optionsBuilder.UseSqlite())) .AddTelegramLayoutsSupport() .AddLogging(cfg => cfg.AddNLog()) .AddQuartzScheduler(builder.Configuration); diff --git a/Samples/Messaging.Sample.Telegram/appsettings.json b/Samples/Messaging.Sample.Telegram/appsettings.json index ec02b8a1..057b1f17 100644 --- a/Samples/Messaging.Sample.Telegram/appsettings.json +++ b/Samples/Messaging.Sample.Telegram/appsettings.json @@ -20,5 +20,11 @@ "useTestEnvironment": false, "name": "TestBot" }, + "BroadcastingSettings": { + "BroadcastingDbConnectionString": "YourConnectionStringHere", + "BotId": "XuS3Ok5dP37v06vrqvfsfXT5G0LVq0nyD68Y", + "HowOld": "00:10:00", + "ServerUri": "http://103.252.116.18:5042/v1" + }, "AllowedHosts": "*" } \ No newline at end of file From f138ca4faf12021f40e726a6f94b3293353ac81a Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 3 Jun 2025 23:33:52 +0300 Subject: [PATCH 036/101] - ef core & polly update to 8.0.16 --- .../Botticelli.Auth.Data.Postgres.csproj | 4 ++-- .../Migrations/20250219145627_auth.Designer.cs | 2 +- .../Migrations/AuthDefaultDbContextModelSnapshot.cs | 2 +- .../Botticelli.Auth.Data.Sqlite.csproj | 2 +- .../Migrations/20250308203827_auth.Designer.cs | 2 +- .../Migrations/AuthDefaultDbContextModelSnapshot.cs | 2 +- Botticelli.Auth.Data/Botticelli.Auth.Data.csproj | 4 ++-- Botticelli.Auth/Botticelli.Auth.csproj | 4 ++-- Botticelli.Bot.Dal/Botticelli.Bot.Dal.csproj | 8 ++++---- .../Migrations/20241021193257_botdata.Designer.cs | 2 +- .../20241128210954_BroadCasting.Designer.cs | 2 +- .../Migrations/BotInfoContextModelSnapshot.cs | 2 +- .../Botticelli.Broadcasting.Dal.csproj | 4 ++-- .../Migrations/20250603202442_initial.Designer.cs | 2 +- .../Migrations/BroadcastingContextModelSnapshot.cs | 2 +- .../Botticelli.Framework.Vk.Messages.csproj | 2 +- .../Botticelli.Server.Analytics.csproj | 12 ++++++------ Botticelli.Server.Back/Botticelli.Server.Back.csproj | 8 ++++---- Botticelli.Server.Data/Botticelli.Server.Data.csproj | 10 +++++----- .../20241014112906_OneDataSource.Designer.cs | 2 +- .../20241201155452_BroadCastingServer.Designer.cs | 2 +- .../20250310193726_broadcasting.Designer.cs | 2 +- .../Migrations/BotInfoContextModelSnapshot.cs | 2 +- .../Botticelli.Server.FrontNew.csproj | 4 ++-- Botticelli.Shared/Botticelli.Shared.csproj | 2 +- .../Messaging.Sample.Telegram.Standalone.csproj | 2 +- .../Auth.Sample.Telegram/Auth.Sample.Telegram.csproj | 4 ++-- .../Messaging.Sample.Telegram.csproj | 2 +- .../Monads.Sample.Telegram.csproj | 2 +- Viber.Api/Viber.Api.csproj | 2 +- 30 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj b/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj index 2a49e8d4..f093aa48 100644 --- a/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj +++ b/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj @@ -7,10 +7,10 @@ - + - + diff --git a/Botticelli.Auth.Data.Postgres/Migrations/20250219145627_auth.Designer.cs b/Botticelli.Auth.Data.Postgres/Migrations/20250219145627_auth.Designer.cs index 4047640a..1bb7087f 100644 --- a/Botticelli.Auth.Data.Postgres/Migrations/20250219145627_auth.Designer.cs +++ b/Botticelli.Auth.Data.Postgres/Migrations/20250219145627_auth.Designer.cs @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Botticelli.Auth.Sample.Telegram") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "8.0.16") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/Botticelli.Auth.Data.Postgres/Migrations/AuthDefaultDbContextModelSnapshot.cs b/Botticelli.Auth.Data.Postgres/Migrations/AuthDefaultDbContextModelSnapshot.cs index b8417113..ca08ef9c 100644 --- a/Botticelli.Auth.Data.Postgres/Migrations/AuthDefaultDbContextModelSnapshot.cs +++ b/Botticelli.Auth.Data.Postgres/Migrations/AuthDefaultDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Botticelli.Auth.Sample.Telegram") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "8.0.16") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/Botticelli.Auth.Data.Sqlite/Botticelli.Auth.Data.Sqlite.csproj b/Botticelli.Auth.Data.Sqlite/Botticelli.Auth.Data.Sqlite.csproj index 92355e04..74200a0f 100644 --- a/Botticelli.Auth.Data.Sqlite/Botticelli.Auth.Data.Sqlite.csproj +++ b/Botticelli.Auth.Data.Sqlite/Botticelli.Auth.Data.Sqlite.csproj @@ -7,7 +7,7 @@ - + diff --git a/Botticelli.Auth.Data.Sqlite/Migrations/20250308203827_auth.Designer.cs b/Botticelli.Auth.Data.Sqlite/Migrations/20250308203827_auth.Designer.cs index 19efc2e2..bd98dc50 100644 --- a/Botticelli.Auth.Data.Sqlite/Migrations/20250308203827_auth.Designer.cs +++ b/Botticelli.Auth.Data.Sqlite/Migrations/20250308203827_auth.Designer.cs @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Botticelli.Auth.Sample.Telegram") - .HasAnnotation("ProductVersion", "8.0.13"); + .HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Auth.Data.Models.AccessHistory", b => { diff --git a/Botticelli.Auth.Data.Sqlite/Migrations/AuthDefaultDbContextModelSnapshot.cs b/Botticelli.Auth.Data.Sqlite/Migrations/AuthDefaultDbContextModelSnapshot.cs index 1dde8221..6280387b 100644 --- a/Botticelli.Auth.Data.Sqlite/Migrations/AuthDefaultDbContextModelSnapshot.cs +++ b/Botticelli.Auth.Data.Sqlite/Migrations/AuthDefaultDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Botticelli.Auth.Sample.Telegram") - .HasAnnotation("ProductVersion", "8.0.13"); + .HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Auth.Data.Models.AccessHistory", b => { diff --git a/Botticelli.Auth.Data/Botticelli.Auth.Data.csproj b/Botticelli.Auth.Data/Botticelli.Auth.Data.csproj index c02925c0..9f5909ed 100644 --- a/Botticelli.Auth.Data/Botticelli.Auth.Data.csproj +++ b/Botticelli.Auth.Data/Botticelli.Auth.Data.csproj @@ -8,11 +8,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Botticelli.Auth/Botticelli.Auth.csproj b/Botticelli.Auth/Botticelli.Auth.csproj index 3cedcc3f..44ac59cf 100644 --- a/Botticelli.Auth/Botticelli.Auth.csproj +++ b/Botticelli.Auth/Botticelli.Auth.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/Botticelli.Bot.Dal/Botticelli.Bot.Dal.csproj b/Botticelli.Bot.Dal/Botticelli.Bot.Dal.csproj index 977b158f..ce2bf8c7 100644 --- a/Botticelli.Bot.Dal/Botticelli.Bot.Dal.csproj +++ b/Botticelli.Bot.Dal/Botticelli.Bot.Dal.csproj @@ -8,13 +8,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Botticelli.Bot.Dal/Migrations/20241021193257_botdata.Designer.cs b/Botticelli.Bot.Dal/Migrations/20241021193257_botdata.Designer.cs index b7e92929..d7913e2e 100644 --- a/Botticelli.Bot.Dal/Migrations/20241021193257_botdata.Designer.cs +++ b/Botticelli.Bot.Dal/Migrations/20241021193257_botdata.Designer.cs @@ -17,7 +17,7 @@ partial class botdata protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Bot.Data.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Bot.Dal/Migrations/20241128210954_BroadCasting.Designer.cs b/Botticelli.Bot.Dal/Migrations/20241128210954_BroadCasting.Designer.cs index 8a38881d..0c86605a 100644 --- a/Botticelli.Bot.Dal/Migrations/20241128210954_BroadCasting.Designer.cs +++ b/Botticelli.Bot.Dal/Migrations/20241128210954_BroadCasting.Designer.cs @@ -17,7 +17,7 @@ partial class BroadCasting protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.BotData.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Bot.Dal/Migrations/BotInfoContextModelSnapshot.cs b/Botticelli.Bot.Dal/Migrations/BotInfoContextModelSnapshot.cs index 37a4877d..46dd44c5 100644 --- a/Botticelli.Bot.Dal/Migrations/BotInfoContextModelSnapshot.cs +++ b/Botticelli.Bot.Dal/Migrations/BotInfoContextModelSnapshot.cs @@ -14,7 +14,7 @@ partial class BotInfoContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.BotData.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj index 6a195d30..1bfdde6d 100644 --- a/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj +++ b/Botticelli.Broadcasting.Dal/Botticelli.Broadcasting.Dal.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs index be5edef3..22956f56 100644 --- a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs +++ b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.Designer.cs @@ -18,7 +18,7 @@ partial class initial protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.13"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.Chat", b => { diff --git a/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs index e9d32593..af4a9644 100644 --- a/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs +++ b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class BroadcastingContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.13"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.Chat", b => { diff --git a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj index c84a43fc..a98872e7 100644 --- a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj +++ b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj @@ -35,7 +35,7 @@ - + diff --git a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj index ae657a1c..b7ee967b 100644 --- a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj +++ b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj @@ -47,16 +47,16 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + diff --git a/Botticelli.Server.Back/Botticelli.Server.Back.csproj b/Botticelli.Server.Back/Botticelli.Server.Back.csproj index 4abafdf0..d3ed9793 100644 --- a/Botticelli.Server.Back/Botticelli.Server.Back.csproj +++ b/Botticelli.Server.Back/Botticelli.Server.Back.csproj @@ -63,13 +63,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Botticelli.Server.Data/Botticelli.Server.Data.csproj b/Botticelli.Server.Data/Botticelli.Server.Data.csproj index 9b390c27..beb1d44d 100644 --- a/Botticelli.Server.Data/Botticelli.Server.Data.csproj +++ b/Botticelli.Server.Data/Botticelli.Server.Data.csproj @@ -14,14 +14,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs b/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs index 77882574..f80a0045 100644 --- a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs +++ b/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs @@ -18,7 +18,7 @@ partial class OneDataSource protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs b/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs index d6080461..cc5598a6 100644 --- a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs +++ b/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs @@ -18,7 +18,7 @@ partial class BroadCastingServer protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs b/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs index 76e068f8..d0738c2e 100644 --- a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs +++ b/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs @@ -18,7 +18,7 @@ partial class broadcasting protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs b/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs index 2f053004..cad5c226 100644 --- a/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs +++ b/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class BotInfoContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => { diff --git a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj index ebab3772..5af27db1 100644 --- a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj +++ b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/Botticelli.Shared/Botticelli.Shared.csproj b/Botticelli.Shared/Botticelli.Shared.csproj index 27705e02..bc7053b8 100644 --- a/Botticelli.Shared/Botticelli.Shared.csproj +++ b/Botticelli.Shared/Botticelli.Shared.csproj @@ -21,7 +21,7 @@ - + \ No newline at end of file diff --git a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj index 39b8acbc..9c561d64 100644 --- a/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj +++ b/Messaging.Sample.Telegram.Standalone/Messaging.Sample.Telegram.Standalone.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Samples/Auth.Sample.Telegram/Auth.Sample.Telegram.csproj b/Samples/Auth.Sample.Telegram/Auth.Sample.Telegram.csproj index 2f8e7bb2..ab014cde 100644 --- a/Samples/Auth.Sample.Telegram/Auth.Sample.Telegram.csproj +++ b/Samples/Auth.Sample.Telegram/Auth.Sample.Telegram.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj index a55938c2..f29a2fc6 100644 --- a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj +++ b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj index f46f6445..e383a96f 100644 --- a/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj +++ b/Samples/Monads.Sample.Telegram/Monads.Sample.Telegram.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Viber.Api/Viber.Api.csproj b/Viber.Api/Viber.Api.csproj index d3ba5b9f..011a8586 100644 --- a/Viber.Api/Viber.Api.csproj +++ b/Viber.Api/Viber.Api.csproj @@ -25,7 +25,7 @@ - + From 807ad2bd555ffb05baa3dfed71994b5a7163af3a Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 6 Jun 2025 19:41:43 +0300 Subject: [PATCH 037/101] - refactoring --- .../Handlers/BotUpdateHandler.cs | 82 ++++++++++--------- Botticelli.sln | 9 +- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index b6da6271..6d7655a4 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -16,7 +16,7 @@ namespace Botticelli.Framework.Telegram.Handlers; public class BotUpdateHandler(ILogger logger) : IBotUpdateHandler { private readonly MemoryCacheEntryOptions _entryOptions - = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); + = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); private readonly MemoryCache _memoryCache = new(new MemoryCacheOptions { @@ -26,8 +26,8 @@ private readonly MemoryCacheEntryOptions _entryOptions private readonly List _subHandlers = []; public async Task HandleUpdateAsync(ITelegramBotClient botClient, - Update update, - CancellationToken cancellationToken) + Update update, + CancellationToken cancellationToken) { try { @@ -44,13 +44,15 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, if (botMessage == null) { - if (ProcessCallback(update, ref botticelliMessage)) + if (ProcessCallback(update, ref botticelliMessage)) return; - + botticelliMessage = ProcessPoll(update, botticelliMessage); } else + { botticelliMessage = ProcessOrdinaryMessage(botMessage); + } foreach (var subHandler in _subHandlers) await subHandler.Process(botClient, update, cancellationToken); @@ -59,10 +61,10 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, await ProcessInProcessors(botticelliMessage, cancellationToken); MessageReceived?.Invoke(this, - new MessageReceivedBotEventArgs - { - Message = botticelliMessage - }); + new MessageReceivedBotEventArgs + { + Message = botticelliMessage + }); } logger.LogDebug($"{nameof(HandleUpdateAsync)}() finished..."); @@ -73,17 +75,34 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, } } + public Task HandleErrorAsync(ITelegramBotClient botClient, + Exception exception, + HandleErrorSource source, + CancellationToken cancellationToken) + { + logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); + + return Task.CompletedTask; + } + + public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler + { + _subHandlers.Add(subHandler); + } + + public event IBotUpdateHandler.MsgReceivedEventHandler? MessageReceived; + private static Message ProcessOrdinaryMessage(global::Telegram.Bot.Types.Message botMessage) { var botticelliMessage = new Message(botMessage.MessageId.ToString()) { - ChatIdInnerIdLinks = new System.Collections.Generic.Dictionary> - {{botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()]}}, + ChatIdInnerIdLinks = new Dictionary> + { { botMessage.Chat.Id.ToString(), [botMessage.MessageId.ToString()] } }, ChatIds = [botMessage.Chat.Id.ToString()], Subject = string.Empty, Body = botMessage.Text ?? string.Empty, LastModifiedAt = botMessage.Date, - Attachments = new System.Collections.Generic.List(5), + Attachments = new List(5), CreatedAt = botMessage.Date, From = new User { @@ -103,13 +122,13 @@ private static Message ProcessOrdinaryMessage(global::Telegram.Bot.Types.Message IsBot = botMessage.ForwardFrom?.IsBot, NickName = botMessage.ForwardFrom?.Username }, - Location = botMessage.Location != null ? - new GeoLocation - { - Latitude = (decimal) botMessage.Location.Latitude, - Longitude = (decimal) botMessage.Location.Longitude - } : - null + Location = botMessage.Location != null + ? new GeoLocation + { + Latitude = (decimal)botMessage.Location.Latitude, + Longitude = (decimal)botMessage.Location.Longitude + } + : null }; return botticelliMessage; } @@ -136,7 +155,7 @@ private static Message ProcessOrdinaryMessage(global::Telegram.Bot.Types.Message private bool ProcessCallback(Update update, ref Message? botticelliMessage) { - if (update.CallbackQuery == null) + if (update.CallbackQuery == null) return false; if (update.CallbackQuery?.Message == null) @@ -173,23 +192,6 @@ private bool ProcessCallback(Update update, ref Message? botticelliMessage) return false; } - public Task HandleErrorAsync(ITelegramBotClient botClient, - Exception exception, - HandleErrorSource source, - CancellationToken cancellationToken) - { - logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); - - return Task.CompletedTask; - } - - public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler - { - _subHandlers.Add(subHandler); - } - - public event IBotUpdateHandler.MsgReceivedEventHandler? MessageReceived; - /// /// Processes requests /// @@ -199,15 +201,15 @@ protected async Task ProcessInProcessors(Message request, CancellationToken toke { logger.LogDebug($"{nameof(ProcessInProcessors)}({request.Uid}) started..."); - if (token is {CanBeCanceled: true, IsCancellationRequested: true}) return; + if (token is { CanBeCanceled: true, IsCancellationRequested: true }) return; var processorFactory = ProcessorFactoryBuilder.Build(); var clientNonChainedTasks = processorFactory.GetProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientChainedTasks = processorFactory.GetCommandChainProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)); var clientTasks = clientNonChainedTasks.Concat(clientChainedTasks).ToArray(); diff --git a/Botticelli.sln b/Botticelli.sln index 4aecc61a..cea7dbbd 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -273,6 +273,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Dal EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Shared", "Botticelli.Broadcasting.Shared\Botticelli.Broadcasting.Shared.csproj", "{0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messengers", "Messengers", "{9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -592,8 +594,6 @@ Global {2E0D3754-2B71-445F-9944-E07B79BCD804} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {B1EEED03-3863-4673-8690-8A89912AA865} = {41AA926F-88C1-4FC4-8F04-6FBA3CA6C741} {B620052D-DA8D-42F6-ACC8-6228B1840A23} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {71008870-2212-4A74-8777-B5B268C27911} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {CDFC4EAD-60BD-419A-ADF5-451766D22F29} = {A05D5A20-E0D3-4844-8311-C98E8B67DA7F} {9F4B5D3F-661C-433B-BC9F-CAC4048EF9EA} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {2851D399-D071-421A-B5B1-AB24883F82E0} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} @@ -616,7 +616,6 @@ Global {85DE8BC6-2A06-4088-B4AA-DD520CBC2369} = {912B05D1-DC22-411E-9D6E-E06FFE96E4E6} {184C8F8D-DC80-4C52-88F9-8AE799C51266} = {85DE8BC6-2A06-4088-B4AA-DD520CBC2369} {3DFA2DF1-6720-44D7-91E2-B50572F8A2E6} = {912B05D1-DC22-411E-9D6E-E06FFE96E4E6} - {343922EC-2CF3-4C0E-B745-08F2B333B4E3} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1} = {B7B4B68F-7086-49F2-B401-C76B4EF0B320} {5BB351E1-460B-48C1-912C-E13702968BB2} = {35480478-646E-45F1-96A3-EE7AC498ACE3} {B610D900-6713-4D8F-8E9D-23AA02F846FF} = {5BB351E1-460B-48C1-912C-E13702968BB2} @@ -699,6 +698,10 @@ Global {88039BCA-7501-48C8-9F94-9F8C2C0A19A5} = {39412AE4-B325-4827-B24E-C15415DCA7E9} {9F898466-739B-4DE0-BFD7-6B5203957236} = {39412AE4-B325-4827-B24E-C15415DCA7E9} {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637} = {39412AE4-B325-4827-B24E-C15415DCA7E9} + {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} + {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} + {71008870-2212-4A74-8777-B5B268C27911} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} + {343922EC-2CF3-4C0E-B745-08F2B333B4E3} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From d50448a6e3e9c4e5a7cf14a4b844042c0bf2bdaa Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 6 Jun 2025 20:01:25 +0300 Subject: [PATCH 038/101] - wechat was excluded --- Botticelli.Shared/ValueObjects/Message.cs | 18 +++------ Botticelli.WeChat/Botticelli.WeChat.csproj | 9 ----- Botticelli.WeChat/Program.cs | 6 --- .../Properties/launchSettings.json | 37 ------------------- .../appsettings.Development.json | 8 ---- Botticelli.WeChat/appsettings.json | 9 ----- Botticelli.sln | 7 ---- .../Processors/StartCommandProcessor.cs | 1 - 8 files changed, 5 insertions(+), 90 deletions(-) delete mode 100644 Botticelli.WeChat/Botticelli.WeChat.csproj delete mode 100644 Botticelli.WeChat/Program.cs delete mode 100644 Botticelli.WeChat/Properties/launchSettings.json delete mode 100644 Botticelli.WeChat/appsettings.Development.json delete mode 100644 Botticelli.WeChat/appsettings.json diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index d17ee2fb..962fd072 100644 --- a/Botticelli.Shared/ValueObjects/Message.cs +++ b/Botticelli.Shared/ValueObjects/Message.cs @@ -4,7 +4,7 @@ /// Received/sent message /// [Serializable] -public class Message +public class Message() { /// /// Type of message @@ -27,14 +27,6 @@ public enum MessageType Extended } - public Message() - { - ChatIds = []; - Uid = Guid.NewGuid().ToString(); - CreatedAt = DateTime.Now; - ProcessingArgs = new List(1); - } - public Message(string uid) : this() { Uid = uid; @@ -48,7 +40,7 @@ public Message(string uid) : this() /// /// Message uid /// - public string? Uid { get; set; } + public string? Uid { get; set; } = Guid.NewGuid().ToString(); /// /// Chat Id <=> Inner message id links @@ -58,7 +50,7 @@ public Message(string uid) : this() /// /// Chat ids /// - public List ChatIds { get; set; } + public List ChatIds { get; set; } = []; /// /// Message subj @@ -73,7 +65,7 @@ public Message(string uid) : this() /// /// Message arguments for processing /// - public IList? ProcessingArgs { get; set; } + public IList? ProcessingArgs { get; set; } = new List(1); /// /// Message attachments @@ -118,7 +110,7 @@ public Message(string uid) : this() /// /// Message creation date /// - public DateTime CreatedAt { get; init; } + public DateTime CreatedAt { get; init; } = DateTime.Now; /// /// Message modification date diff --git a/Botticelli.WeChat/Botticelli.WeChat.csproj b/Botticelli.WeChat/Botticelli.WeChat.csproj deleted file mode 100644 index 9af7d037..00000000 --- a/Botticelli.WeChat/Botticelli.WeChat.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/Botticelli.WeChat/Program.cs b/Botticelli.WeChat/Program.cs deleted file mode 100644 index ec8e4f41..00000000 --- a/Botticelli.WeChat/Program.cs +++ /dev/null @@ -1,6 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -app.MapGet("/", () => "Hello World!"); - -app.Run(); \ No newline at end of file diff --git a/Botticelli.WeChat/Properties/launchSettings.json b/Botticelli.WeChat/Properties/launchSettings.json deleted file mode 100644 index 8e3d0ec4..00000000 --- a/Botticelli.WeChat/Properties/launchSettings.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:43410", - "sslPort": 44380 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5225", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7237;http://localhost:5225", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Botticelli.WeChat/appsettings.Development.json b/Botticelli.WeChat/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/Botticelli.WeChat/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/Botticelli.WeChat/appsettings.json b/Botticelli.WeChat/appsettings.json deleted file mode 100644 index 10f68b8c..00000000 --- a/Botticelli.WeChat/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Botticelli.sln b/Botticelli.sln index cea7dbbd..b567fc25 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -175,8 +175,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.AI.GptJ", "Botti EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ai.DeepSeek.Sample.Telegram", "Samples\Ai.DeepSeek.Sample.Telegram\Ai.DeepSeek.Sample.Telegram.csproj", "{3D52F777-3A84-4238-974F-A5969085B472}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.WeChat", "Botticelli.WeChat\Botticelli.WeChat.csproj", "{B8EE61E9-F029-49EA-81E5-D0BCBA265418}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Hangfire", "Botticelli.Scheduler.Hangfire\Botticelli.Scheduler.Hangfire.csproj", "{85C84F76-3730-4D17-923E-96B665544B57}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Scheduler.Quartz", "Botticelli.Scheduler.Quartz\Botticelli.Scheduler.Quartz.csproj", "{F958B922-0418-4AB8-B5C6-86510DB52F38}" @@ -445,10 +443,6 @@ Global {3D52F777-3A84-4238-974F-A5969085B472}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D52F777-3A84-4238-974F-A5969085B472}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D52F777-3A84-4238-974F-A5969085B472}.Release|Any CPU.Build.0 = Release|Any CPU - {B8EE61E9-F029-49EA-81E5-D0BCBA265418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B8EE61E9-F029-49EA-81E5-D0BCBA265418}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B8EE61E9-F029-49EA-81E5-D0BCBA265418}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B8EE61E9-F029-49EA-81E5-D0BCBA265418}.Release|Any CPU.Build.0 = Release|Any CPU {85C84F76-3730-4D17-923E-96B665544B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85C84F76-3730-4D17-923E-96B665544B57}.Debug|Any CPU.Build.0 = Debug|Any CPU {85C84F76-3730-4D17-923E-96B665544B57}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -638,7 +632,6 @@ Global {80F461AA-3C96-4AEA-90CA-48C1B878D97F} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} {252B974D-7725-44F5-BE69-330B85731B08} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} {1BC0B4EF-7C35-4BB7-B207-180FBC55681D} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} - {B8EE61E9-F029-49EA-81E5-D0BCBA265418} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {70EB3FE2-9EB5-4B62-9405-71D1A332B0FD} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {2DF28873-A172-42EB-8FBD-324DF8AD9323} = {2AD59714-541D-4794-9216-6EAB25F271DA} {9B731750-D28D-4DE0-AC8E-280E932BD301} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} diff --git a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs index d2d9dcea..02e6febb 100644 --- a/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs +++ b/Samples/Messaging.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -94,7 +94,6 @@ protected override async Task InnerProcess(Message message, CancellationToken to { Message = new Message { - Uid = Guid.NewGuid().ToString(), ChatIds = message.ChatIds, Body = "Bot started..." } From cd2ade0db543121b22881057d8f94832da74470e Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 6 Jun 2025 23:00:03 +0300 Subject: [PATCH 039/101] - npgsql v9 --- Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj index b7ee967b..92e32cd6 100644 --- a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj +++ b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj @@ -55,7 +55,7 @@ - + From 103daf7dc6a23813936a9690d1ca2b559ebcd48b Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 6 Jun 2025 23:37:27 +0300 Subject: [PATCH 040/101] - contact & new chat members processing --- .../NewChatMembersBotEventArgs.cs | 8 ++++ .../SharedContactBotEventArgs.cs | 8 ++++ .../Handlers/BotUpdateHandler.cs | 40 +++++++++++++++++++ .../Handlers/IBotUpdateHandler.cs | 8 +++- 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Botticelli.Framework.Common/NewChatMembersBotEventArgs.cs create mode 100644 Botticelli.Framework.Common/SharedContactBotEventArgs.cs diff --git a/Botticelli.Framework.Common/NewChatMembersBotEventArgs.cs b/Botticelli.Framework.Common/NewChatMembersBotEventArgs.cs new file mode 100644 index 00000000..48ada5bc --- /dev/null +++ b/Botticelli.Framework.Common/NewChatMembersBotEventArgs.cs @@ -0,0 +1,8 @@ +using Botticelli.Shared.ValueObjects; + +namespace Botticelli.Framework.Events; + +public class NewChatMembersBotEventArgs : BotEventArgs +{ + public required User User { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Framework.Common/SharedContactBotEventArgs.cs b/Botticelli.Framework.Common/SharedContactBotEventArgs.cs new file mode 100644 index 00000000..a3151e66 --- /dev/null +++ b/Botticelli.Framework.Common/SharedContactBotEventArgs.cs @@ -0,0 +1,8 @@ +using Botticelli.Shared.ValueObjects; + +namespace Botticelli.Framework.Events; + +public class SharedContactBotEventArgs : BotEventArgs +{ + public required Contact Contact { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index 6d7655a4..7b5704d8 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -7,6 +7,7 @@ using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; +using Contact = Botticelli.Shared.ValueObjects.Contact; using Message = Botticelli.Shared.ValueObjects.Message; using Poll = Botticelli.Shared.ValueObjects.Poll; using User = Botticelli.Shared.ValueObjects.User; @@ -52,6 +53,8 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, else { botticelliMessage = ProcessOrdinaryMessage(botMessage); + ProcessNewChatMembers(botClient, botMessage); + ProcessContact(botMessage); } foreach (var subHandler in _subHandlers) await subHandler.Process(botClient, update, cancellationToken); @@ -75,6 +78,41 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, } } + private void ProcessContact(global::Telegram.Bot.Types.Message? botMessage) + { + if (botMessage?.Contact != null) + ContactShared?.Invoke(this, new SharedContactBotEventArgs + { + Contact = new Contact + { + Phone = botMessage.Contact.PhoneNumber, + Name = botMessage.Contact.FirstName, + Surname = botMessage.Contact.LastName + } + }); + } + + private void ProcessNewChatMembers(ITelegramBotClient botClient, global::Telegram.Bot.Types.Message botMessage) + { + foreach (var newChatMember in botMessage.NewChatMembers ?? []) + { + var @event = new NewChatMembersBotEventArgs + { + User = new User + { + Id = newChatMember.Id.ToString(), + Name = newChatMember.FirstName, + Surname = newChatMember.LastName, + Info = string.Empty, + NickName = newChatMember.Username, + IsBot = newChatMember.IsBot + } + }; + + NewChatMembers?.Invoke(botClient, @event); + } + } + public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, @@ -91,6 +129,8 @@ public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler } public event IBotUpdateHandler.MsgReceivedEventHandler? MessageReceived; + public event IBotUpdateHandler.NewChatMembersHandler? NewChatMembers; + public event IBotUpdateHandler.ContactSharedHandler? ContactShared; private static Message ProcessOrdinaryMessage(global::Telegram.Bot.Types.Message botMessage) { diff --git a/Botticelli.Framework.Telegram/Handlers/IBotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/IBotUpdateHandler.cs index be1e86e5..40143fc8 100644 --- a/Botticelli.Framework.Telegram/Handlers/IBotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/IBotUpdateHandler.cs @@ -6,8 +6,12 @@ namespace Botticelli.Framework.Telegram.Handlers; public interface IBotUpdateHandler : IUpdateHandler { public delegate void MsgReceivedEventHandler(object sender, MessageReceivedBotEventArgs e); - + public delegate void NewChatMembersHandler(object sender, NewChatMembersBotEventArgs e); + public delegate void ContactSharedHandler(object sender, SharedContactBotEventArgs e); + public void AddSubHandler(T subHandler) where T : IBotUpdateSubHandler; public event MsgReceivedEventHandler MessageReceived; -} \ No newline at end of file + public event NewChatMembersHandler NewChatMembers; + public event ContactSharedHandler ContactShared; +} From f175969cf801046525eaf6d239ffd6969cece908 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 6 Jun 2025 23:51:31 +0300 Subject: [PATCH 041/101] - events rethrowing --- .../Builders/TelegramBotBuilder.cs | 2 ++ Botticelli.Framework.Telegram/TelegramBot.cs | 8 +++++++- Botticelli.Framework/BaseBot.cs | 4 ++++ Botticelli.Framework/Builders/BotBuilder.cs | 18 +++++++++++++++++- Viber.Api/Viber.Api.csproj | 6 ------ 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 58dafec8..00ce3ddd 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -188,6 +188,8 @@ private void AddEvents(TBot bot) bot.MessageSent += MessageSent; bot.MessageReceived += MessageReceived; bot.MessageRemoved += MessageRemoved; + bot.ContactShared += SharedContact; + bot.NewChatMembers += NewChatMembers; } protected TelegramBotBuilder AddBotSettings( diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index 5b66ae5a..b2eb7262 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -54,6 +54,8 @@ public TelegramBot(ITelegramBotClient client, public override event MsgSentEventHandler? MessageSent; public override event MsgReceivedEventHandler? MessageReceived; public override event MsgRemovedEventHandler? MessageRemoved; + public override event ContactSharedEventHandler? ContactShared; + public override event NewChatMembersEventHandler? NewChatMembers; /// /// Deletes a message @@ -512,9 +514,13 @@ protected override Task InnerStartBotAsync(StartBotRequest req BotStatusKeeper.IsStarted = true; - // Rethrowing an event from BotUpdateHandler + // Rethrowing events from BotUpdateHandler _handler.MessageReceived += (sender, e) => MessageReceived?.Invoke(sender, e); + _handler.ContactShared += (sender, e) + => ContactShared?.Invoke(sender, e); + _handler.NewChatMembers += (sender, e) + => NewChatMembers?.Invoke(sender, e); Client.StartReceiving(_handler, cancellationToken: token); diff --git a/Botticelli.Framework/BaseBot.cs b/Botticelli.Framework/BaseBot.cs index 5164af55..a4179f88 100644 --- a/Botticelli.Framework/BaseBot.cs +++ b/Botticelli.Framework/BaseBot.cs @@ -28,10 +28,14 @@ public abstract class BaseBot public delegate void StartedEventHandler(object sender, StartedBotEventArgs e); public delegate void StoppedEventHandler(object sender, StoppedBotEventArgs e); + public delegate void ContactSharedEventHandler(object sender, SharedContactBotEventArgs e); + public delegate void NewChatMembersEventHandler(object sender, NewChatMembersBotEventArgs e); public virtual event MsgSentEventHandler? MessageSent; public virtual event MsgReceivedEventHandler? MessageReceived; public virtual event MsgRemovedEventHandler? MessageRemoved; + public virtual event ContactSharedEventHandler? ContactShared; + public virtual event NewChatMembersEventHandler? NewChatMembers; } /// diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 0de6bfd0..c57f3312 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -30,7 +30,9 @@ public abstract class BotBuilder : BotBuilder protected BaseBot.MsgSentEventHandler? MessageSent; protected BaseBot.MsgReceivedEventHandler? MessageReceived; protected BaseBot.MsgRemovedEventHandler? MessageRemoved; - + protected BaseBot.ContactSharedEventHandler? SharedContact; + protected BaseBot.NewChatMembersEventHandler? NewChatMembers; + protected override void Assert() { } @@ -83,4 +85,18 @@ public BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEvent return this; } + + public BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler) + { + NewChatMembers += handler; + + return this; + } + + public BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler) + { + SharedContact += handler; + + return this; + } } \ No newline at end of file diff --git a/Viber.Api/Viber.Api.csproj b/Viber.Api/Viber.Api.csproj index 011a8586..42fc72b5 100644 --- a/Viber.Api/Viber.Api.csproj +++ b/Viber.Api/Viber.Api.csproj @@ -10,12 +10,6 @@ logo.jpg https://github.com/devgopher/botticelli - - - - - - True From 6e274a31082f316c340ed2e177fa5e3440e1a993 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 12 Jun 2025 13:01:34 +0300 Subject: [PATCH 042/101] BOTTICELLI-55: - server-side form draft - cleanup --- Botticelli.Broadcasting/Broadcaster.cs | 5 +- .../Services/Broadcasting/BroadcastService.cs | 17 +--- .../Broadcasting/IBroadcastService.cs | 3 - .../Models/BotBroadcastModel.cs | 8 ++ .../Pages/BotBroadcast.razor | 77 +++++++++++++++++++ 5 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 Botticelli.Server.FrontNew/Models/BotBroadcastModel.cs create mode 100644 Botticelli.Server.FrontNew/Pages/BotBroadcast.razor diff --git a/Botticelli.Broadcasting/Broadcaster.cs b/Botticelli.Broadcasting/Broadcaster.cs index 8250bff2..8bea5716 100644 --- a/Botticelli.Broadcasting/Broadcaster.cs +++ b/Botticelli.Broadcasting/Broadcaster.cs @@ -7,7 +7,6 @@ using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; using Flurl.Http; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -41,7 +40,7 @@ public Broadcaster(IServiceProvider serviceProvider, IOptionsSnapshot(ex => ex.Call.Response.ResponseMessage.StatusCode == HttpStatusCode.RequestTimeout) .WaitAndRetryForeverAsync((_, _) => _retryPause); @@ -78,7 +77,7 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task GetUpdates(CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.Value.ServerUri}" + var updatesResponse = await $"{_settings.Value.ServerUri}/client/broadcast" .WithTimeout(_longPollTimeout) .PostJsonAsync(new GetBroadCastMessagesRequest { diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index e3ddcf99..0c3815cd 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -22,8 +22,8 @@ public async Task> GetMessages(string botId) public async Task MarkReceived(string botId, string messageId) { - var messages = context.BroadcastMessages.Where(bm => bm.BotId == botId && bm.Id == messageId) - .ToList(); + var messages = await context.BroadcastMessages.Where(bm => bm.BotId == botId && bm.Id == messageId) + .ToListAsync(); foreach (var message in messages) message.Received = true; @@ -39,17 +39,4 @@ public Task> GetBroadcasts(string botId) return Task.FromResult(broadcasts); } - - public async Task MarkAsReceived(string messageId) - { - var broadcast = context.BroadcastMessages.FirstOrDefault(x => x.Id == messageId); - - if (broadcast == null) return; - - broadcast.Received = true; - - context.Update(broadcast); - - await context.SaveChangesAsync(); - } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Services/Broadcasting/IBroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/IBroadcastService.cs index 2333f724..ba89935c 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/IBroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/IBroadcastService.cs @@ -29,7 +29,4 @@ public interface IBroadcastService /// /// public Task MarkReceived(string botId, string messageId); - - public Task> GetBroadcasts(string botId); - public Task MarkAsReceived(string messageId); } \ No newline at end of file diff --git a/Botticelli.Server.FrontNew/Models/BotBroadcastModel.cs b/Botticelli.Server.FrontNew/Models/BotBroadcastModel.cs new file mode 100644 index 00000000..95ef060e --- /dev/null +++ b/Botticelli.Server.FrontNew/Models/BotBroadcastModel.cs @@ -0,0 +1,8 @@ +namespace Botticelli.Server.FrontNew.Models; + +internal class BotBroadcastModel +{ + public string BotId { get; set; } + public string BotName { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor new file mode 100644 index 00000000..34b20f75 --- /dev/null +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -0,0 +1,77 @@ +@page "/BotBroadcast" +@using System.Net.Http.Headers +@using Botticelli.Server.Data.Entities.Bot.Broadcasting +@using Botticelli.Server.FrontNew.Models +@using Botticelli.Server.FrontNew.Settings +@using Botticelli.Shared.API.Client.Requests +@using Botticelli.Shared.API.Client.Responses +@using Botticelli.Shared.Constants +@using Flurl +@using Microsoft.Extensions.Options +@inject HttpClient Http +@inject NavigationManager UriHelper; +@inject IOptionsMonitor BackSettings; +@inject CookieStorageAccessor Cookies; + + + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +@code { + readonly BotBroadcastModel _model = new(); + + private async Task OnSubmit(BotBroadcastModel model) + { + var request = new Broadcast + { + BotId = model.BotId, + Id = Guid.NewGuid().ToString(), + Body = model.Message + }; + + var sessionToken = await Cookies.GetValueAsync("SessionToken"); + + if (string.IsNullOrWhiteSpace(sessionToken)) return; + + Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); + var response = await Http.GetFromJsonAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, "/admin/SendBroadcast")); + + UriHelper.NavigateTo("/your_bots", true); + } + +} \ No newline at end of file From d28e1af8b9504f709555f2d5158ab9d27c34a7b9 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 12 Jun 2025 14:10:20 +0300 Subject: [PATCH 043/101] - migrations were remade --- .../Controllers/BotController.cs | 9 +- Botticelli.Server.Back/Program.cs | 3 +- Botticelli.Server.Back/database.db | Bin 77824 -> 0 bytes .../Bot/Broadcasting/Broadcast.cs | 2 - .../20230112102521_init.Designer.cs | 44 ---- .../Migrations/20230112102521_init.cs | 36 --- .../20230118100238_botType.Designer.cs | 47 ---- .../Migrations/20230118100238_botType.cs | 28 -- .../20230311100413_auth.Designer.cs | 131 --------- .../Migrations/20230311100413_auth.cs | 79 ------ ...40913211216_botcontextInSqlite.Designer.cs | 157 ----------- .../20240913211216_botcontextInSqlite.cs | 45 ---- ...240913211518_botAdditionalInfo.Designer.cs | 192 -------------- .../20240913211518_botAdditionalInfo.cs | 106 -------- .../20241014112906_OneDataSource.Designer.cs | 192 -------------- .../20241014112906_OneDataSource.cs | 132 ---------- ...41201155452_BroadCastingServer.Designer.cs | 248 ------------------ .../20241201155452_BroadCastingServer.cs | 65 ----- .../Migrations/20250310193726_broadcasting.cs | 163 ------------ ....cs => 20250612110926_initial.Designer.cs} | 33 +-- .../Migrations/20250612110926_initial.cs | 173 ++++++++++++ ...t.cs => ServerDataContextModelSnapshot.cs} | 31 +-- .../Pages/BotBroadcast.razor | 22 +- .../Pages/YourBots.razor | 4 + 24 files changed, 214 insertions(+), 1728 deletions(-) delete mode 100644 Botticelli.Server.Back/database.db delete mode 100644 Botticelli.Server.Data/Migrations/20230112102521_init.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20230112102521_init.cs delete mode 100644 Botticelli.Server.Data/Migrations/20230118100238_botType.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20230118100238_botType.cs delete mode 100644 Botticelli.Server.Data/Migrations/20230311100413_auth.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20230311100413_auth.cs delete mode 100644 Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.cs delete mode 100644 Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.cs delete mode 100644 Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.cs delete mode 100644 Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.cs delete mode 100644 Botticelli.Server.Data/Migrations/20250310193726_broadcasting.cs rename Botticelli.Server.Data/Migrations/{20250310193726_broadcasting.Designer.cs => 20250612110926_initial.Designer.cs} (86%) create mode 100644 Botticelli.Server.Data/Migrations/20250612110926_initial.cs rename Botticelli.Server.Data/Migrations/{BotInfoContextModelSnapshot.cs => ServerDataContextModelSnapshot.cs} (86%) diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 4fec7d3e..e9ff2038 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -115,14 +115,7 @@ public async Task Broadcast([FromBody] GetBroadCas { Type = Message.MessageType.Messaging, Subject = string.Empty, - Body = bm.Body, - Attachments = bm.Attachments?.Select(a => - new BinaryBaseAttachment(Guid.NewGuid().ToString(), - a.Filename, - (MediaType) a.MediaType, - string.Empty, - a.Content)) - .ToList() + Body = bm.Body }) .ToArray() }; diff --git a/Botticelli.Server.Back/Program.cs b/Botticelli.Server.Back/Program.cs index 3b4e5557..b5f15da2 100644 --- a/Botticelli.Server.Back/Program.cs +++ b/Botticelli.Server.Back/Program.cs @@ -60,7 +60,7 @@ Id = "Bearer" } }, - Array.Empty() + [] } }); }); @@ -108,7 +108,6 @@ builder.Services.AddControllers(); - builder.ApplyMigrations(); var app = builder.Build(); diff --git a/Botticelli.Server.Back/database.db b/Botticelli.Server.Back/database.db deleted file mode 100644 index ff7d1b6a832a785afe3b8b6327ea2bd201c41aa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77824 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 diff --git a/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs b/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs index bb987443..62f86aa3 100644 --- a/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs +++ b/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs @@ -12,8 +12,6 @@ public class Broadcast public required string BotId { get; set; } public required string Body { get; set; } public DateTime Timestamp { get; set; } - - public BroadcastAttachment[]? Attachments { get; set; } public bool Sent { get; set; } = false; public bool Received { get; set; } = false; } \ No newline at end of file diff --git a/Botticelli.Server.Data/Migrations/20230112102521_init.Designer.cs b/Botticelli.Server.Data/Migrations/20230112102521_init.Designer.cs deleted file mode 100644 index 26f0ba31..00000000 --- a/Botticelli.Server.Data/Migrations/20230112102521_init.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20230112102521_init")] - partial class init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotName") - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20230112102521_init.cs b/Botticelli.Server.Data/Migrations/20230112102521_init.cs deleted file mode 100644 index a67866c2..00000000 --- a/Botticelli.Server.Data/Migrations/20230112102521_init.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "BotInfo", - columns: table => new - { - BotId = table.Column(type: "TEXT", nullable: false), - BotName = table.Column(type: "TEXT", nullable: true), - LastKeepAlive = table.Column(type: "TEXT", nullable: true), - Status = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BotInfo", x => x.BotId); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "BotInfo"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20230118100238_botType.Designer.cs b/Botticelli.Server.Data/Migrations/20230118100238_botType.Designer.cs deleted file mode 100644 index c6c52809..00000000 --- a/Botticelli.Server.Data/Migrations/20230118100238_botType.Designer.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20230118100238_botType")] - partial class botType - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotName") - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20230118100238_botType.cs b/Botticelli.Server.Data/Migrations/20230118100238_botType.cs deleted file mode 100644 index 3e4650a9..00000000 --- a/Botticelli.Server.Data/Migrations/20230118100238_botType.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class botType : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Type", - table: "BotInfo", - type: "INTEGER", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Type", - table: "BotInfo"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20230311100413_auth.Designer.cs b/Botticelli.Server.Data/Migrations/20230311100413_auth.Designer.cs deleted file mode 100644 index 91fe8e01..00000000 --- a/Botticelli.Server.Data/Migrations/20230311100413_auth.Designer.cs +++ /dev/null @@ -1,131 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20230311100413_auth")] - partial class auth - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotName") - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.ToTable("ApplicationUserRoles"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20230311100413_auth.cs b/Botticelli.Server.Data/Migrations/20230311100413_auth.cs deleted file mode 100644 index 67a558c0..00000000 --- a/Botticelli.Server.Data/Migrations/20230311100413_auth.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class auth : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ApplicationRoles", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: true), - NormalizedName = table.Column(type: "TEXT", nullable: true), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ApplicationRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ApplicationUserRoles", - columns: table => new - { - UserId = table.Column(type: "TEXT", nullable: false), - RoleId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApplicationUserRoles", x => new { x.UserId, x.RoleId }); - }); - - migrationBuilder.CreateTable( - name: "ApplicationUsers", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - UserName = table.Column(type: "TEXT", nullable: true), - NormalizedUserName = table.Column(type: "TEXT", nullable: true), - Email = table.Column(type: "TEXT", nullable: true), - NormalizedEmail = table.Column(type: "TEXT", nullable: true), - EmailConfirmed = table.Column(type: "INTEGER", nullable: false), - PasswordHash = table.Column(type: "TEXT", nullable: true), - SecurityStamp = table.Column(type: "TEXT", nullable: true), - ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - PhoneNumber = table.Column(type: "TEXT", nullable: true), - PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), - TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), - LockoutEnd = table.Column(type: "TEXT", nullable: true), - LockoutEnabled = table.Column(type: "INTEGER", nullable: false), - AccessFailedCount = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApplicationUsers", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ApplicationRoles"); - - migrationBuilder.DropTable( - name: "ApplicationUserRoles"); - - migrationBuilder.DropTable( - name: "ApplicationUsers"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.Designer.cs b/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.Designer.cs deleted file mode 100644 index 5874282f..00000000 --- a/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.Designer.cs +++ /dev/null @@ -1,157 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20240913211216_botcontextInSqlite")] - partial class botcontextInSqlite - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotKey") - .HasColumnType("TEXT"); - - b.Property("BotName") - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationRoles"); - - b.HasData( - new - { - Id = "91f76d24-9f30-446c-b3db-42a6cff5c1a6", - ConcurrencyStamp = "09/13/2024 21:12:15", - Name = "admin", - NormalizedName = "ADMIN" - }, - new - { - Id = "4b585771-7feb-4e6a-aa05-09076415edf1", - ConcurrencyStamp = "09/13/2024 21:12:15", - Name = "bot_manager", - NormalizedName = "BOT_MANAGER" - }, - new - { - Id = "4997b014-6ca0-4736-aa14-db77741c9fbb", - ConcurrencyStamp = "09/13/2024 21:12:15", - Name = "viewer", - NormalizedName = "VIEWER" - }); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.ToTable("ApplicationUserRoles"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.cs b/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.cs deleted file mode 100644 index fa04cb7a..00000000 --- a/Botticelli.Server.Data/Migrations/20240913211216_botcontextInSqlite.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class botcontextInSqlite : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "4997b014-6ca0-4736-aa14-db77741c9fbb", "09/13/2024 21:12:15", "viewer", "VIEWER" }, - { "4b585771-7feb-4e6a-aa05-09076415edf1", "09/13/2024 21:12:15", "bot_manager", "BOT_MANAGER" }, - { "91f76d24-9f30-446c-b3db-42a6cff5c1a6", "09/13/2024 21:12:15", "admin", "ADMIN" } - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "4997b014-6ca0-4736-aa14-db77741c9fbb"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "4b585771-7feb-4e6a-aa05-09076415edf1"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "91f76d24-9f30-446c-b3db-42a6cff5c1a6"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.Designer.cs b/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.Designer.cs deleted file mode 100644 index bac6f6d1..00000000 --- a/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.Designer.cs +++ /dev/null @@ -1,192 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20240913211518_botAdditionalInfo")] - partial class botAdditionalInfo - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("ItemName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ItemValue") - .HasColumnType("TEXT"); - - b.HasKey("BotId"); - - b.ToTable("BotAdditionalInfo"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("AdditionalInfoBotId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("BotKey") - .HasColumnType("TEXT"); - - b.Property("BotName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.HasIndex("AdditionalInfoBotId"); - - b.ToTable("BotInfo"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationRoles"); - - b.HasData( - new - { - Id = "2cb59f48-3d78-4056-b43a-5042b67d8b8d", - ConcurrencyStamp = "09/13/2024 21:15:18", - Name = "admin", - NormalizedName = "ADMIN" - }, - new - { - Id = "11594aea-0910-40f8-8e37-53efc5e1a80f", - ConcurrencyStamp = "09/13/2024 21:15:18", - Name = "bot_manager", - NormalizedName = "BOT_MANAGER" - }, - new - { - Id = "e8bcf27d-e87d-462c-bbbd-3fc1a42c5701", - ConcurrencyStamp = "09/13/2024 21:15:18", - Name = "viewer", - NormalizedName = "VIEWER" - }); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.ToTable("ApplicationUserRoles"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", "AdditionalInfo") - .WithMany() - .HasForeignKey("AdditionalInfoBotId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AdditionalInfo"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.cs b/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.cs deleted file mode 100644 index 9602827b..00000000 --- a/Botticelli.Server.Data/Migrations/20240913211518_botAdditionalInfo.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class botAdditionalInfo : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "BotName", - table: "BotInfo", - type: "TEXT", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "TEXT", - oldNullable: true); - - migrationBuilder.AddColumn( - name: "AdditionalInfoBotId", - table: "BotInfo", - type: "TEXT", - nullable: false, - defaultValue: ""); - - migrationBuilder.CreateTable( - name: "BotAdditionalInfo", - columns: table => new - { - BotId = table.Column(type: "TEXT", nullable: false), - ItemName = table.Column(type: "TEXT", nullable: false), - ItemValue = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BotAdditionalInfo", x => x.BotId); - }); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "11594aea-0910-40f8-8e37-53efc5e1a80f", "09/13/2024 21:15:18", "bot_manager", "BOT_MANAGER" }, - { "2cb59f48-3d78-4056-b43a-5042b67d8b8d", "09/13/2024 21:15:18", "admin", "ADMIN" }, - { "e8bcf27d-e87d-462c-bbbd-3fc1a42c5701", "09/13/2024 21:15:18", "viewer", "VIEWER" } - }); - - migrationBuilder.CreateIndex( - name: "IX_BotInfo_AdditionalInfoBotId", - table: "BotInfo", - column: "AdditionalInfoBotId"); - - migrationBuilder.AddForeignKey( - name: "FK_BotInfo_BotAdditionalInfo_AdditionalInfoBotId", - table: "BotInfo", - column: "AdditionalInfoBotId", - principalTable: "BotAdditionalInfo", - principalColumn: "BotId", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BotInfo_BotAdditionalInfo_AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.DropTable( - name: "BotAdditionalInfo"); - - migrationBuilder.DropIndex( - name: "IX_BotInfo_AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.DropColumn( - name: "AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.AlterColumn( - name: "BotName", - table: "BotInfo", - type: "TEXT", - nullable: true, - oldClrType: typeof(string), - oldType: "TEXT"); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "4997b014-6ca0-4736-aa14-db77741c9fbb", "09/13/2024 21:12:15", "viewer", "VIEWER" }, - { "4b585771-7feb-4e6a-aa05-09076415edf1", "09/13/2024 21:12:15", "bot_manager", "BOT_MANAGER" }, - { "91f76d24-9f30-446c-b3db-42a6cff5c1a6", "09/13/2024 21:12:15", "admin", "ADMIN" } - }); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs b/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs deleted file mode 100644 index f80a0045..00000000 --- a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.Designer.cs +++ /dev/null @@ -1,192 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20241014112906_OneDataSource")] - partial class OneDataSource - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotInfoBotId") - .HasColumnType("TEXT"); - - b.Property("ItemName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ItemValue") - .HasColumnType("TEXT"); - - b.HasKey("BotId"); - - b.HasIndex("BotInfoBotId"); - - b.ToTable("BotAdditionalInfo"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotKey") - .HasColumnType("TEXT"); - - b.Property("BotName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationRoles"); - - b.HasData( - new - { - Id = "969600de-70d9-43e9-bc01-56415c57863b", - ConcurrencyStamp = "10/14/2024 11:29:05", - Name = "admin", - NormalizedName = "ADMIN" - }, - new - { - Id = "b57f3fda-a6d3-4f93-8faa-ce99a06a476d", - ConcurrencyStamp = "10/14/2024 11:29:05", - Name = "bot_manager", - NormalizedName = "BOT_MANAGER" - }, - new - { - Id = "22c98256-7794-4d42-bdc8-4725445de3c9", - ConcurrencyStamp = "10/14/2024 11:29:05", - Name = "viewer", - NormalizedName = "VIEWER" - }); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.ToTable("ApplicationUserRoles"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.BotInfo", null) - .WithMany("AdditionalInfo") - .HasForeignKey("BotInfoBotId"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Navigation("AdditionalInfo"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.cs b/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.cs deleted file mode 100644 index e7e5277d..00000000 --- a/Botticelli.Server.Data/Migrations/20241014112906_OneDataSource.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class OneDataSource : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BotInfo_BotAdditionalInfo_AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.DropIndex( - name: "IX_BotInfo_AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "11594aea-0910-40f8-8e37-53efc5e1a80f"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "2cb59f48-3d78-4056-b43a-5042b67d8b8d"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "e8bcf27d-e87d-462c-bbbd-3fc1a42c5701"); - - migrationBuilder.DropColumn( - name: "AdditionalInfoBotId", - table: "BotInfo"); - - migrationBuilder.AddColumn( - name: "BotInfoBotId", - table: "BotAdditionalInfo", - type: "TEXT", - nullable: true); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "22c98256-7794-4d42-bdc8-4725445de3c9", "10/14/2024 11:29:05", "viewer", "VIEWER" }, - { "969600de-70d9-43e9-bc01-56415c57863b", "10/14/2024 11:29:05", "admin", "ADMIN" }, - { "b57f3fda-a6d3-4f93-8faa-ce99a06a476d", "10/14/2024 11:29:05", "bot_manager", "BOT_MANAGER" } - }); - - migrationBuilder.CreateIndex( - name: "IX_BotAdditionalInfo_BotInfoBotId", - table: "BotAdditionalInfo", - column: "BotInfoBotId"); - - migrationBuilder.AddForeignKey( - name: "FK_BotAdditionalInfo_BotInfo_BotInfoBotId", - table: "BotAdditionalInfo", - column: "BotInfoBotId", - principalTable: "BotInfo", - principalColumn: "BotId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BotAdditionalInfo_BotInfo_BotInfoBotId", - table: "BotAdditionalInfo"); - - migrationBuilder.DropIndex( - name: "IX_BotAdditionalInfo_BotInfoBotId", - table: "BotAdditionalInfo"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "22c98256-7794-4d42-bdc8-4725445de3c9"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "969600de-70d9-43e9-bc01-56415c57863b"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "b57f3fda-a6d3-4f93-8faa-ce99a06a476d"); - - migrationBuilder.DropColumn( - name: "BotInfoBotId", - table: "BotAdditionalInfo"); - - migrationBuilder.AddColumn( - name: "AdditionalInfoBotId", - table: "BotInfo", - type: "TEXT", - nullable: false, - defaultValue: ""); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "11594aea-0910-40f8-8e37-53efc5e1a80f", "09/13/2024 21:15:18", "bot_manager", "BOT_MANAGER" }, - { "2cb59f48-3d78-4056-b43a-5042b67d8b8d", "09/13/2024 21:15:18", "admin", "ADMIN" }, - { "e8bcf27d-e87d-462c-bbbd-3fc1a42c5701", "09/13/2024 21:15:18", "viewer", "VIEWER" } - }); - - migrationBuilder.CreateIndex( - name: "IX_BotInfo_AdditionalInfoBotId", - table: "BotInfo", - column: "AdditionalInfoBotId"); - - migrationBuilder.AddForeignKey( - name: "FK_BotInfo_BotAdditionalInfo_AdditionalInfoBotId", - table: "BotInfo", - column: "AdditionalInfoBotId", - principalTable: "BotAdditionalInfo", - principalColumn: "BotId", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs b/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs deleted file mode 100644 index cc5598a6..00000000 --- a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.Designer.cs +++ /dev/null @@ -1,248 +0,0 @@ -// -using System; -using Botticelli.Server.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Botticelli.Server.Data.Migrations -{ - [DbContext(typeof(ServerDataContext))] - [Migration("20241201155452_BroadCastingServer")] - partial class BroadCastingServer - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotInfoBotId") - .HasColumnType("TEXT"); - - b.Property("ItemName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ItemValue") - .HasColumnType("TEXT"); - - b.HasKey("BotId"); - - b.HasIndex("BotInfoBotId"); - - b.ToTable("BotAdditionalInfo"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("BotKey") - .HasColumnType("TEXT"); - - b.Property("BotName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("LastKeepAlive") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("BotId"); - - b.ToTable("BotInfo"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => - { - b.Property("BotId") - .HasColumnType("TEXT"); - - b.Property("Body") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("BotId"); - - b.ToTable("Broadcasts"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("BroadcastBotId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasColumnType("BLOB"); - - b.Property("Filename") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("MediaType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("BroadcastBotId"); - - b.ToTable("BroadcastAttachments"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationRoles"); - - b.HasData( - new - { - Id = "1027bf58-8ea4-4042-9651-690ef7eff777", - ConcurrencyStamp = "12/01/2024 15:54:52", - Name = "admin", - NormalizedName = "ADMIN" - }, - new - { - Id = "19291d61-00bc-497c-8162-aa1ed022d1a7", - ConcurrencyStamp = "12/01/2024 15:54:52", - Name = "bot_manager", - NormalizedName = "BOT_MANAGER" - }, - new - { - Id = "0704f04d-1faa-44ab-b0fa-3ec05a168255", - ConcurrencyStamp = "12/01/2024 15:54:52", - Name = "viewer", - NormalizedName = "VIEWER" - }); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ApplicationUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.ToTable("ApplicationUserRoles"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.BotInfo", null) - .WithMany("AdditionalInfo") - .HasForeignKey("BotInfoBotId"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", null) - .WithMany("Attachments") - .HasForeignKey("BroadcastBotId"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => - { - b.Navigation("AdditionalInfo"); - }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => - { - b.Navigation("Attachments"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.cs b/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.cs deleted file mode 100644 index 1b0c801e..00000000 --- a/Botticelli.Server.Data/Migrations/20241201155452_BroadCastingServer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class BroadCastingServer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Broadcasts", - columns: table => new - { - BotId = table.Column(type: "TEXT", nullable: false), - Body = table.Column(type: "TEXT", nullable: false), - Timestamp = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Broadcasts", x => x.BotId); - }); - - migrationBuilder.CreateTable( - name: "BroadcastAttachments", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - MediaType = table.Column(type: "INTEGER", nullable: false), - Filename = table.Column(type: "TEXT", nullable: false), - Content = table.Column(type: "BLOB", nullable: false), - BroadcastBotId = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BroadcastAttachments", x => x.Id); - table.ForeignKey( - name: "FK_BroadcastAttachments_Broadcasts_BroadcastBotId", - column: x => x.BroadcastBotId, - principalTable: "Broadcasts", - principalColumn: "BotId"); - }); - - migrationBuilder.CreateIndex( - name: "IX_BroadcastAttachments_BroadcastBotId", - table: "BroadcastAttachments", - column: "BroadcastBotId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "BroadcastAttachments"); - - migrationBuilder.DropTable( - name: "Broadcasts"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.cs b/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.cs deleted file mode 100644 index fcaaef0d..00000000 --- a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Botticelli.Server.Data.Migrations -{ - /// - public partial class broadcasting : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BroadcastAttachments_Broadcasts_BroadcastBotId", - table: "BroadcastAttachments"); - - migrationBuilder.DropPrimaryKey( - name: "PK_Broadcasts", - table: "Broadcasts"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "0704f04d-1faa-44ab-b0fa-3ec05a168255"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "1027bf58-8ea4-4042-9651-690ef7eff777"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "19291d61-00bc-497c-8162-aa1ed022d1a7"); - - migrationBuilder.RenameColumn( - name: "BroadcastBotId", - table: "BroadcastAttachments", - newName: "BroadcastId"); - - migrationBuilder.RenameIndex( - name: "IX_BroadcastAttachments_BroadcastBotId", - table: "BroadcastAttachments", - newName: "IX_BroadcastAttachments_BroadcastId"); - - migrationBuilder.AddColumn( - name: "Id", - table: "Broadcasts", - type: "TEXT", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "Received", - table: "Broadcasts", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "Sent", - table: "Broadcasts", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddPrimaryKey( - name: "PK_Broadcasts", - table: "Broadcasts", - column: "Id"); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "2e960a01-b27e-4435-961c-231e9276a6a4", "03/10/2025 19:37:25", "viewer", "VIEWER" }, - { "85ca9cb0-e520-461b-8b2e-98877f519efd", "03/10/2025 19:37:25", "bot_manager", "BOT_MANAGER" }, - { "ebdb9ace-7a62-41cf-b4e3-749be50310e2", "03/10/2025 19:37:25", "admin", "ADMIN" } - }); - - migrationBuilder.AddForeignKey( - name: "FK_BroadcastAttachments_Broadcasts_BroadcastId", - table: "BroadcastAttachments", - column: "BroadcastId", - principalTable: "Broadcasts", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BroadcastAttachments_Broadcasts_BroadcastId", - table: "BroadcastAttachments"); - - migrationBuilder.DropPrimaryKey( - name: "PK_Broadcasts", - table: "Broadcasts"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "2e960a01-b27e-4435-961c-231e9276a6a4"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "85ca9cb0-e520-461b-8b2e-98877f519efd"); - - migrationBuilder.DeleteData( - table: "ApplicationRoles", - keyColumn: "Id", - keyValue: "ebdb9ace-7a62-41cf-b4e3-749be50310e2"); - - migrationBuilder.DropColumn( - name: "Id", - table: "Broadcasts"); - - migrationBuilder.DropColumn( - name: "Received", - table: "Broadcasts"); - - migrationBuilder.DropColumn( - name: "Sent", - table: "Broadcasts"); - - migrationBuilder.RenameColumn( - name: "BroadcastId", - table: "BroadcastAttachments", - newName: "BroadcastBotId"); - - migrationBuilder.RenameIndex( - name: "IX_BroadcastAttachments_BroadcastId", - table: "BroadcastAttachments", - newName: "IX_BroadcastAttachments_BroadcastBotId"); - - migrationBuilder.AddPrimaryKey( - name: "PK_Broadcasts", - table: "Broadcasts", - column: "BotId"); - - migrationBuilder.InsertData( - table: "ApplicationRoles", - columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, - values: new object[,] - { - { "0704f04d-1faa-44ab-b0fa-3ec05a168255", "12/01/2024 15:54:52", "viewer", "VIEWER" }, - { "1027bf58-8ea4-4042-9651-690ef7eff777", "12/01/2024 15:54:52", "admin", "ADMIN" }, - { "19291d61-00bc-497c-8162-aa1ed022d1a7", "12/01/2024 15:54:52", "bot_manager", "BOT_MANAGER" } - }); - - migrationBuilder.AddForeignKey( - name: "FK_BroadcastAttachments_Broadcasts_BroadcastBotId", - table: "BroadcastAttachments", - column: "BroadcastBotId", - principalTable: "Broadcasts", - principalColumn: "BotId"); - } - } -} diff --git a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs b/Botticelli.Server.Data/Migrations/20250612110926_initial.Designer.cs similarity index 86% rename from Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs rename to Botticelli.Server.Data/Migrations/20250612110926_initial.Designer.cs index d0738c2e..b4b3daf8 100644 --- a/Botticelli.Server.Data/Migrations/20250310193726_broadcasting.Designer.cs +++ b/Botticelli.Server.Data/Migrations/20250612110926_initial.Designer.cs @@ -11,8 +11,8 @@ namespace Botticelli.Server.Data.Migrations { [DbContext(typeof(ServerDataContext))] - [Migration("20250310193726_broadcasting")] - partial class broadcasting + [Migration("20250612110926_initial")] + partial class initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -101,9 +101,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("BroadcastId") - .HasColumnType("TEXT"); - b.Property("Content") .IsRequired() .HasColumnType("BLOB"); @@ -117,8 +114,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BroadcastId"); - b.ToTable("BroadcastAttachments"); }); @@ -143,22 +138,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "ebdb9ace-7a62-41cf-b4e3-749be50310e2", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "155a1281-61f2-4cea-b7b4-6f97b30d48d7", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "admin", NormalizedName = "ADMIN" }, new { - Id = "85ca9cb0-e520-461b-8b2e-98877f519efd", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "90283ad1-e01d-436b-879d-75bdab45fdfd", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "bot_manager", NormalizedName = "BOT_MANAGER" }, new { - Id = "2e960a01-b27e-4435-961c-231e9276a6a4", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "e56de249-c6df-4699-9874-96d8247e7e57", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "viewer", NormalizedName = "VIEWER" }); @@ -236,22 +231,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("BotInfoBotId"); }); - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", null) - .WithMany("Attachments") - .HasForeignKey("BroadcastId"); - }); - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => { b.Navigation("AdditionalInfo"); }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => - { - b.Navigation("Attachments"); - }); #pragma warning restore 612, 618 } } diff --git a/Botticelli.Server.Data/Migrations/20250612110926_initial.cs b/Botticelli.Server.Data/Migrations/20250612110926_initial.cs new file mode 100644 index 00000000..949f9d9d --- /dev/null +++ b/Botticelli.Server.Data/Migrations/20250612110926_initial.cs @@ -0,0 +1,173 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Botticelli.Server.Data.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApplicationRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + NormalizedName = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ApplicationUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUserRoles", x => new { x.UserId, x.RoleId }); + }); + + migrationBuilder.CreateTable( + name: "ApplicationUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", nullable: true), + NormalizedUserName = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + NormalizedEmail = table.Column(type: "TEXT", nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BotInfo", + columns: table => new + { + BotId = table.Column(type: "TEXT", nullable: false), + BotName = table.Column(type: "TEXT", nullable: false), + LastKeepAlive = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "INTEGER", nullable: true), + Type = table.Column(type: "INTEGER", nullable: true), + BotKey = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BotInfo", x => x.BotId); + }); + + migrationBuilder.CreateTable( + name: "BroadcastAttachments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + MediaType = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "BLOB", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BroadcastAttachments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Broadcasts", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + BotId = table.Column(type: "TEXT", nullable: false), + Body = table.Column(type: "TEXT", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false), + Sent = table.Column(type: "INTEGER", nullable: false), + Received = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Broadcasts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BotAdditionalInfo", + columns: table => new + { + BotId = table.Column(type: "TEXT", nullable: false), + ItemName = table.Column(type: "TEXT", nullable: false), + ItemValue = table.Column(type: "TEXT", nullable: true), + BotInfoBotId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BotAdditionalInfo", x => x.BotId); + table.ForeignKey( + name: "FK_BotAdditionalInfo_BotInfo_BotInfoBotId", + column: x => x.BotInfoBotId, + principalTable: "BotInfo", + principalColumn: "BotId"); + }); + + migrationBuilder.InsertData( + table: "ApplicationRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "155a1281-61f2-4cea-b7b4-6f97b30d48d7", "06/12/2025 11:09:25", "admin", "ADMIN" }, + { "90283ad1-e01d-436b-879d-75bdab45fdfd", "06/12/2025 11:09:25", "bot_manager", "BOT_MANAGER" }, + { "e56de249-c6df-4699-9874-96d8247e7e57", "06/12/2025 11:09:25", "viewer", "VIEWER" } + }); + + migrationBuilder.CreateIndex( + name: "IX_BotAdditionalInfo_BotInfoBotId", + table: "BotAdditionalInfo", + column: "BotInfoBotId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApplicationRoles"); + + migrationBuilder.DropTable( + name: "ApplicationUserRoles"); + + migrationBuilder.DropTable( + name: "ApplicationUsers"); + + migrationBuilder.DropTable( + name: "BotAdditionalInfo"); + + migrationBuilder.DropTable( + name: "BroadcastAttachments"); + + migrationBuilder.DropTable( + name: "Broadcasts"); + + migrationBuilder.DropTable( + name: "BotInfo"); + } + } +} diff --git a/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs b/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs similarity index 86% rename from Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs rename to Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs index cad5c226..3541c524 100644 --- a/Botticelli.Server.Data/Migrations/BotInfoContextModelSnapshot.cs +++ b/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs @@ -10,7 +10,7 @@ namespace Botticelli.Server.Data.Migrations { [DbContext(typeof(ServerDataContext))] - partial class BotInfoContextModelSnapshot : ModelSnapshot + partial class ServerDataContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { @@ -98,9 +98,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("BroadcastId") - .HasColumnType("TEXT"); - b.Property("Content") .IsRequired() .HasColumnType("BLOB"); @@ -114,8 +111,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BroadcastId"); - b.ToTable("BroadcastAttachments"); }); @@ -140,22 +135,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "ebdb9ace-7a62-41cf-b4e3-749be50310e2", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "155a1281-61f2-4cea-b7b4-6f97b30d48d7", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "admin", NormalizedName = "ADMIN" }, new { - Id = "85ca9cb0-e520-461b-8b2e-98877f519efd", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "90283ad1-e01d-436b-879d-75bdab45fdfd", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "bot_manager", NormalizedName = "BOT_MANAGER" }, new { - Id = "2e960a01-b27e-4435-961c-231e9276a6a4", - ConcurrencyStamp = "03/10/2025 19:37:25", + Id = "e56de249-c6df-4699-9874-96d8247e7e57", + ConcurrencyStamp = "06/12/2025 11:09:25", Name = "viewer", NormalizedName = "VIEWER" }); @@ -233,22 +228,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("BotInfoBotId"); }); - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => - { - b.HasOne("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", null) - .WithMany("Attachments") - .HasForeignKey("BroadcastId"); - }); - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => { b.Navigation("AdditionalInfo"); }); - - modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => - { - b.Navigation("Attachments"); - }); #pragma warning restore 612, 618 } } diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 34b20f75..1184f78d 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -1,4 +1,4 @@ -@page "/BotBroadcast" +@page "/broadcast/send/{botId}" @using System.Net.Http.Headers @using Botticelli.Server.Data.Entities.Bot.Broadcasting @using Botticelli.Server.FrontNew.Models @@ -55,6 +55,24 @@ @code { readonly BotBroadcastModel _model = new(); + [Parameter] + public string? BotId { get; set; } + + protected override bool ShouldRender() + { + return true; + } + + protected override Task OnInitializedAsync() + { + if (BotId == null) + UriHelper.NavigateTo("/your_bots", true); + else + _model.BotId = BotId; + + return Task.CompletedTask; + } + private async Task OnSubmit(BotBroadcastModel model) { var request = new Broadcast @@ -69,7 +87,7 @@ if (string.IsNullOrWhiteSpace(sessionToken)) return; Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); - var response = await Http.GetFromJsonAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, "/admin/SendBroadcast")); + await Http.GetFromJsonAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, "/admin/SendBroadcast")); UriHelper.NavigateTo("/your_bots", true); } diff --git a/Botticelli.Server.FrontNew/Pages/YourBots.razor b/Botticelli.Server.FrontNew/Pages/YourBots.razor index de8c1017..d04b1723 100644 --- a/Botticelli.Server.FrontNew/Pages/YourBots.razor +++ b/Botticelli.Server.FrontNew/Pages/YourBots.razor @@ -44,6 +44,10 @@ else + Date: Thu, 12 Jun 2025 17:24:07 +0300 Subject: [PATCH 044/101] - BotBuilder: Build() is virtual --- Botticelli.Framework/Builders/BotBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index c57f3312..4a30ad05 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -9,7 +9,7 @@ public abstract class BotBuilder { protected abstract void Assert(); - public TBot? Build() + public virtual TBot? Build() { Assert(); From 08888e906da5db424f7f4a48621034196d41f4d6 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 13 Jun 2025 16:08:22 +0300 Subject: [PATCH 045/101] - bot builder - virtual methods --- Botticelli.Framework/Builders/BotBuilder.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 4a30ad05..831d8470 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -37,63 +37,63 @@ protected override void Assert() { } - public BotBuilder AddServices(IServiceCollection services) + public virtual BotBuilder AddServices(IServiceCollection services) { Services = services; return this; } - public BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) + public virtual BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) { AnalyticsClientSettingsBuilder = clientSettingsBuilder; return this; } - protected BotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) + protected virtual BotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) { ServerSettingsBuilder = settingsBuilder; return this; } - public BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) + public virtual BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder) { BotDataAccessSettingsBuilder = botDataAccessBuilder; return this; } - public BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler) + public virtual BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler) { MessageSent += handler; return this; } - public BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler) + public virtual BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler) { MessageReceived += handler; return this; } - public BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler) + public virtual BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler) { MessageRemoved += handler; return this; } - public BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler) + public virtual BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler) { NewChatMembers += handler; return this; } - public BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler) + public virtual BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler) { SharedContact += handler; From 0df6a34566dc03dd27879fb11c3f08873d01fa68 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 21 Jun 2025 19:12:07 +0300 Subject: [PATCH 046/101] - refactoring - filterbot draft --- .../Builders/TelegramBotBuilder.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 199 +++++++++--------- 2 files changed, 101 insertions(+), 100 deletions(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 00ce3ddd..12bf1429 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -48,7 +48,7 @@ public class TelegramBotBuilder : BotBuilder BotDataSettingsBuilder = new(); private static readonly AnalyticsClientSettingsBuilder AnalyticsClientOptionsBuilder = - new(); + new(); private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - { - return AddTelegramBot(services, configuration, telegramBotBuilderFunc); - } - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + public static IServiceCollection AddTelegramBot(this IServiceCollection services) + where TBotBuilder : TelegramBotBuilder> => + services.AddTelegramBot(); + + public static IServiceCollection AddTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(TelegramBotSettings)}!"); var analyticsClientSettings = configuration - .GetSection(AnalyticsClientSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); + .GetSection(AnalyticsClientSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); var serverSettings = configuration - .GetSection(ServerSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(ServerSettings)}!"); + .GetSection(ServerSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(ServerSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); - - return services.AddTelegramBot(telegramBotSettings, - analyticsClientSettings, - serverSettings, - dataAccessSettings, - telegramBotBuilderFunc); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(DataAccessSettings)}!"); + + return services.AddTelegramBot(o => + o.Set(telegramBotSettings), + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - TelegramBotSettings botSettings, - AnalyticsClientSettings analyticsClientSettings, - ServerSettings serverSettings, - DataAccessSettings dataAccessSettings, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + public static IServiceCollection AddTelegramBot(this IServiceCollection services, + TelegramBotSettings botSettings, + AnalyticsClientSettings analyticsClientSettings, + ServerSettings serverSettings, + DataAccessSettings dataAccessSettings, + Action>>? telegramBotBuilderFunc = null) { return services.AddTelegramBot(o => o.Set(botSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } ///

@@ -89,13 +90,13 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se /// /// /// - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + public static IServiceCollection AddTelegramBot(this IServiceCollection services, + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action? telegramBotBuilderFunc = null) + where TBotBuilder : TelegramBotBuilder> { optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); @@ -104,59 +105,61 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection se var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); - var botBuilder = TelegramBotBuilder.Instance(services, - ServerSettingsBuilder, - SettingsBuilder, - DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder, - false) - .AddClient(clientBuilder); + var botBuilder = TelegramBotBuilder.Instance(services, + ServerSettingsBuilder, + SettingsBuilder, + DataAccessSettingsBuilder, + AnalyticsClientOptionsBuilder, + false) + .AddClient(clientBuilder); - telegramBotBuilderFunc?.Invoke(botBuilder); - TelegramBot? bot = botBuilder.Build(); + telegramBotBuilderFunc?.Invoke(botBuilder as TBotBuilder); + var bot = botBuilder.Build(); return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport(); } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - { - return AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); - } + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) => + AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(TelegramBotSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(DataAccessSettings)}!"); var botDataSettings = configuration - .GetSection(BotDataSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(BotDataSettings)}!"); - - return services.AddStandaloneTelegramBot(botSettingsBuilder => botSettingsBuilder.Set(telegramBotSettings), - dataAccessSettingsBuilder => dataAccessSettingsBuilder.Set(dataAccessSettings), - botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings)); + .GetSection(BotDataSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException( + $"Can't load configuration for {nameof(BotDataSettings)}!"); + + return services.AddStandaloneTelegramBot( + botSettingsBuilder => botSettingsBuilder.Set(telegramBotSettings), + dataAccessSettingsBuilder => dataAccessSettingsBuilder.Set(dataAccessSettings), + botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings)); } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action> botDataSettingsBuilderFunc, - Action>? telegramBotBuilderFunc = null) - where TBot : TelegramBot + Action> optionsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action> botDataSettingsBuilderFunc, + Action>? telegramBotBuilderFunc = null) + where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); @@ -165,26 +168,24 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); var botBuilder = TelegramStandaloneBotBuilder.Instance(services, - SettingsBuilder, - DataAccessSettingsBuilder) - .AddBotData(BotDataSettingsBuilder) - .AddClient(clientBuilder); + SettingsBuilder, + DataAccessSettingsBuilder) + .AddBotData(BotDataSettingsBuilder) + .AddClient(clientBuilder); - telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder) botBuilder); + telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); TelegramBot? bot = botBuilder.Build(); return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport(); } - public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) - { - return services.AddSingleton() - .AddSingleton, ReplyTelegramLayoutSupplier>() - .AddSingleton, InlineTelegramLayoutSupplier>() - .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() - .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); - } + public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => + services.AddSingleton() + .AddSingleton, ReplyTelegramLayoutSupplier>() + .AddSingleton, InlineTelegramLayoutSupplier>() + .AddSingleton, LayoutLoader, ReplyKeyboardMarkup>>() + .AddSingleton, LayoutLoader, InlineKeyboardMarkup>>(); } \ No newline at end of file From e5b5a83db9215839fe29cda87d629ef4c1cfa372 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 22 Jun 2025 12:00:08 +0300 Subject: [PATCH 047/101] - Bot builder refactoring - Add: decisionmakers, executors - Executor interface --- .../Extensions/ServiceCollectionExtensions.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 372af91b..60233cd9 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -26,13 +26,10 @@ public static class ServiceCollectionExtensions private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); - public static IServiceCollection AddTelegramBot(this IServiceCollection services) - where TBotBuilder : TelegramBotBuilder> => - services.AddTelegramBot(); - - public static IServiceCollection AddTelegramBot(this IServiceCollection services, + public static IServiceCollection AddTelegramBot(this IServiceCollection services, IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) + Action? telegramBotBuilderFunc = null) + where TBotBuilder : TelegramBotBuilder> { var telegramBotSettings = configuration .GetSection(TelegramBotSettings.Section) From 77333e5f73a7108b383a0164a400ddf6527d2bbf Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 29 Jun 2025 12:51:46 +0300 Subject: [PATCH 048/101] -v 0.8 --- Botticelli.AI/Botticelli.AI.csproj | 2 +- Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj | 2 +- Botticelli.Audio/Botticelli.Audio.csproj | 2 +- Botticelli.Bot.Interfaces/Botticelli.Bot.Interfaces.csproj | 2 +- Botticelli.Bus.Rabbit/Botticelli.Bus.Rabbit.csproj | 2 +- Botticelli.Bus/Botticelli.Bus.None.csproj | 2 +- Botticelli.Client.Analytics/Botticelli.Client.Analytics.csproj | 2 +- .../Botticelli.Framework.Telegram.csproj | 2 +- Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj | 2 +- Botticelli.Framework/Botticelli.Framework.csproj | 2 +- Botticelli.Interfaces/Botticelli.Interfaces.csproj | 2 +- Botticelli.Pay/Botticelli.Pay.csproj | 2 +- Botticelli.Scheduler/Botticelli.Scheduler.csproj | 2 +- Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj | 2 +- Botticelli.Server.Back/Botticelli.Server.Back.csproj | 2 +- .../Botticelli.Server.Data.Entities.csproj | 2 +- Botticelli.Server.Data/Botticelli.Server.Data.csproj | 2 +- Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj | 2 +- Botticelli.Shared/Botticelli.Shared.csproj | 2 +- Botticelli.Talks/Botticelli.Talks.csproj | 2 +- Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj | 2 +- Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj | 2 +- Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj | 2 +- .../CommandChain.Sample.Telegram.csproj | 2 +- Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj | 2 +- Samples/Messaging.Sample.Common/Messaging.Sample.Common.csproj | 2 +- .../Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj | 2 +- Samples/TelegramAiSample/Ai.ChatGpt.Sample.Telegram.csproj | 2 +- Viber.Api/Viber.Api.csproj | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Botticelli.AI/Botticelli.AI.csproj b/Botticelli.AI/Botticelli.AI.csproj index c714f90a..7d5c957d 100644 --- a/Botticelli.AI/Botticelli.AI.csproj +++ b/Botticelli.AI/Botticelli.AI.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj b/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj index 246d9e9c..468a8476 100644 --- a/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj +++ b/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj @@ -5,7 +5,7 @@ enable enable true - 0.7.0 + 0.8.0 https://botticellibots.com new_logo_compact.png https://github.com/devgopher/botticelli diff --git a/Botticelli.Audio/Botticelli.Audio.csproj b/Botticelli.Audio/Botticelli.Audio.csproj index c324d600..6bf41b03 100644 --- a/Botticelli.Audio/Botticelli.Audio.csproj +++ b/Botticelli.Audio/Botticelli.Audio.csproj @@ -5,7 +5,7 @@ enable enable true - 0.7.0 + 0.8.0 Botticelli.Audio BotticelliBots new_logo_compact.png diff --git a/Botticelli.Bot.Interfaces/Botticelli.Bot.Interfaces.csproj b/Botticelli.Bot.Interfaces/Botticelli.Bot.Interfaces.csproj index e8c1c898..b7e41fee 100644 --- a/Botticelli.Bot.Interfaces/Botticelli.Bot.Interfaces.csproj +++ b/Botticelli.Bot.Interfaces/Botticelli.Bot.Interfaces.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Bus.Rabbit/Botticelli.Bus.Rabbit.csproj b/Botticelli.Bus.Rabbit/Botticelli.Bus.Rabbit.csproj index de8ad7b1..e04851e8 100644 --- a/Botticelli.Bus.Rabbit/Botticelli.Bus.Rabbit.csproj +++ b/Botticelli.Bus.Rabbit/Botticelli.Bus.Rabbit.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Bus.Rabbit BotticelliBots new_logo_compact.png diff --git a/Botticelli.Bus/Botticelli.Bus.None.csproj b/Botticelli.Bus/Botticelli.Bus.None.csproj index 6b7117cc..aea412cb 100644 --- a/Botticelli.Bus/Botticelli.Bus.None.csproj +++ b/Botticelli.Bus/Botticelli.Bus.None.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli BotticelliBots https://botticellibots.com diff --git a/Botticelli.Client.Analytics/Botticelli.Client.Analytics.csproj b/Botticelli.Client.Analytics/Botticelli.Client.Analytics.csproj index 5c9a796c..0b64dc16 100644 --- a/Botticelli.Client.Analytics/Botticelli.Client.Analytics.csproj +++ b/Botticelli.Client.Analytics/Botticelli.Client.Analytics.csproj @@ -5,7 +5,7 @@ enable enable true - 0.7.0 + 0.8.0 https://botticellibots.com new_logo_compact.png https://github.com/devgopher/botticelli diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index ac4c452a..4254a8b0 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Framework.Telegram BotticelliBots new_logo_compact.png diff --git a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj index a98872e7..6b24b8a3 100644 --- a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj +++ b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Framework.Vk.Messages BotticelliBots new_logo_compact.png diff --git a/Botticelli.Framework/Botticelli.Framework.csproj b/Botticelli.Framework/Botticelli.Framework.csproj index 9477f10a..48e2c495 100644 --- a/Botticelli.Framework/Botticelli.Framework.csproj +++ b/Botticelli.Framework/Botticelli.Framework.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Framework BotticelliBots new_logo_compact.png diff --git a/Botticelli.Interfaces/Botticelli.Interfaces.csproj b/Botticelli.Interfaces/Botticelli.Interfaces.csproj index 9f42f3c0..e47b9d40 100644 --- a/Botticelli.Interfaces/Botticelli.Interfaces.csproj +++ b/Botticelli.Interfaces/Botticelli.Interfaces.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Pay/Botticelli.Pay.csproj b/Botticelli.Pay/Botticelli.Pay.csproj index e5d9cfc4..ead78eb7 100644 --- a/Botticelli.Pay/Botticelli.Pay.csproj +++ b/Botticelli.Pay/Botticelli.Pay.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Scheduler/Botticelli.Scheduler.csproj b/Botticelli.Scheduler/Botticelli.Scheduler.csproj index 4bc9c37f..a49f32d4 100644 --- a/Botticelli.Scheduler/Botticelli.Scheduler.csproj +++ b/Botticelli.Scheduler/Botticelli.Scheduler.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Scheduler BotticelliBots new_logo_compact.png diff --git a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj index 92e32cd6..ea1124cf 100644 --- a/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj +++ b/Botticelli.Server.Analytics/Botticelli.Server.Analytics.csproj @@ -13,7 +13,7 @@ enable enable true - 0.7.0 + 0.8.0 https://botticellibots.com new_logo_compact.png https://github.com/devgopher/botticelli diff --git a/Botticelli.Server.Back/Botticelli.Server.Back.csproj b/Botticelli.Server.Back/Botticelli.Server.Back.csproj index d3ed9793..ce935b8f 100644 --- a/Botticelli.Server.Back/Botticelli.Server.Back.csproj +++ b/Botticelli.Server.Back/Botticelli.Server.Back.csproj @@ -8,7 +8,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli BotticelliBots https://botticellibots.com diff --git a/Botticelli.Server.Data.Entities/Botticelli.Server.Data.Entities.csproj b/Botticelli.Server.Data.Entities/Botticelli.Server.Data.Entities.csproj index 506d05a1..2a5c5d15 100644 --- a/Botticelli.Server.Data.Entities/Botticelli.Server.Data.Entities.csproj +++ b/Botticelli.Server.Data.Entities/Botticelli.Server.Data.Entities.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Server.Data/Botticelli.Server.Data.csproj b/Botticelli.Server.Data/Botticelli.Server.Data.csproj index beb1d44d..43b9631d 100644 --- a/Botticelli.Server.Data/Botticelli.Server.Data.csproj +++ b/Botticelli.Server.Data/Botticelli.Server.Data.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj index 5af27db1..061e3d04 100644 --- a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj +++ b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Shared/Botticelli.Shared.csproj b/Botticelli.Shared/Botticelli.Shared.csproj index bc7053b8..af2ae641 100644 --- a/Botticelli.Shared/Botticelli.Shared.csproj +++ b/Botticelli.Shared/Botticelli.Shared.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Botticelli.Talks/Botticelli.Talks.csproj b/Botticelli.Talks/Botticelli.Talks.csproj index 78ca1e94..0a7fbe6e 100644 --- a/Botticelli.Talks/Botticelli.Talks.csproj +++ b/Botticelli.Talks/Botticelli.Talks.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli.Talks BotticelliBots new_logo_compact.png diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj b/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj index e8ec10e5..ec9cb9d8 100644 --- a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj +++ b/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj b/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj index 8e665623..3d894ab3 100644 --- a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj +++ b/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj b/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj index f3af7548..b1297698 100644 --- a/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj +++ b/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/CommandChain.Sample.Telegram/CommandChain.Sample.Telegram.csproj b/Samples/CommandChain.Sample.Telegram/CommandChain.Sample.Telegram.csproj index 8fbb15d6..07a28fa1 100644 --- a/Samples/CommandChain.Sample.Telegram/CommandChain.Sample.Telegram.csproj +++ b/Samples/CommandChain.Sample.Telegram/CommandChain.Sample.Telegram.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj b/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj index 29ef38fe..dce5cd35 100644 --- a/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj +++ b/Samples/Layouts.Sample.Telegram/Layouts.Sample.Telegram.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/Messaging.Sample.Common/Messaging.Sample.Common.csproj b/Samples/Messaging.Sample.Common/Messaging.Sample.Common.csproj index d6857265..0ee43464 100644 --- a/Samples/Messaging.Sample.Common/Messaging.Sample.Common.csproj +++ b/Samples/Messaging.Sample.Common/Messaging.Sample.Common.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj index f29a2fc6..02f93558 100644 --- a/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj +++ b/Samples/Messaging.Sample.Telegram/Messaging.Sample.Telegram.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Samples/TelegramAiSample/Ai.ChatGpt.Sample.Telegram.csproj b/Samples/TelegramAiSample/Ai.ChatGpt.Sample.Telegram.csproj index eeaf9843..bf57098d 100644 --- a/Samples/TelegramAiSample/Ai.ChatGpt.Sample.Telegram.csproj +++ b/Samples/TelegramAiSample/Ai.ChatGpt.Sample.Telegram.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli diff --git a/Viber.Api/Viber.Api.csproj b/Viber.Api/Viber.Api.csproj index 42fc72b5..6de46f56 100644 --- a/Viber.Api/Viber.Api.csproj +++ b/Viber.Api/Viber.Api.csproj @@ -3,7 +3,7 @@ netstandard2.1 enable - 0.7.0 + 0.8.0 Botticelli Igor Evdokimov https://github.com/devgopher/botticelli From 465b534d9baade9c88fd114a4309ac7c6af5c732 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Thu, 10 Jul 2025 19:19:48 +0300 Subject: [PATCH 049/101] BM-2: bot init process was changed --- .../Botticelli.Framework.Telegram.csproj | 5 ++ .../Builders/TelegramBotBuilder.cs | 20 +++++-- .../Extensions/ServiceCollectionExtensions.cs | 54 +++++++++++++------ 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index 4254a8b0..38402912 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -37,4 +37,9 @@ \ + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.17\Microsoft.AspNetCore.Http.Abstractions.dll + + \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 12bf1429..1810f016 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Botticelli.Bot.Data; using Botticelli.Bot.Data.Repositories; using Botticelli.Bot.Data.Settings; @@ -32,7 +33,9 @@ namespace Botticelli.Framework.Telegram.Builders; /// public abstract class TelegramBotBuilder(bool isStandalone) : TelegramBotBuilder>(isStandalone) - where TBot : TelegramBot; + where TBot : TelegramBot +{ +} /// /// Builder for a non-standalone Telegram bot @@ -48,6 +51,8 @@ public class TelegramBotBuilder : BotBuilder Instance(IServiceCollection services, + public static TBotBuilderNew? Instance(IServiceCollection services, ServerSettingsBuilder serverSettingsBuilder, BotSettingsBuilder settingsBuilder, DataAccessSettingsBuilder dataAccessSettingsBuilder, AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, - bool isStandalone) => - (TelegramBotBuilder)new TelegramBotBuilder(isStandalone) + bool isStandalone) + where TBotBuilderNew : BotBuilder + { + var botBuilder = Activator.CreateInstance(typeof(TBotBuilderNew)) as TBotBuilderNew; + + (botBuilder as TelegramBotBuilder>) .AddBotSettings(settingsBuilder) .AddServerSettings(serverSettingsBuilder) .AddAnalyticsSettings(analyticsClientSettingsBuilder) .AddBotDataAccessSettings(dataAccessSettingsBuilder) .AddServices(services); + + return botBuilder; + } public TelegramBotBuilder AddSubHandler() where T : class, IBotUpdateSubHandler diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 60233cd9..d1cb93a3 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -2,14 +2,17 @@ using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; using Botticelli.Controls.Parsers; +using Botticelli.Framework.Builders; using Botticelli.Framework.Options; using Botticelli.Framework.Telegram.Builders; using Botticelli.Framework.Telegram.Decorators; using Botticelli.Framework.Telegram.Layout; using Botticelli.Framework.Telegram.Options; using Botticelli.Interfaces; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Extensions; @@ -25,7 +28,6 @@ public static class ServiceCollectionExtensions private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); - public static IServiceCollection AddTelegramBot(this IServiceCollection services, IConfiguration configuration, Action? telegramBotBuilderFunc = null) @@ -55,6 +57,8 @@ public static IServiceCollection AddTelegramBot(this IServiceCollec throw new ConfigurationErrorsException( $"Can't load configuration for {nameof(DataAccessSettings)}!"); + services.AddSingleton, TBotBuilder>(); + return services.AddTelegramBot(o => o.Set(telegramBotSettings), o => o.Set(analyticsClientSettings), @@ -94,6 +98,20 @@ public static IServiceCollection AddTelegramBot(this IServiceCollec Action> dataAccessSettingsBuilderFunc, Action? telegramBotBuilderFunc = null) where TBotBuilder : TelegramBotBuilder> + { + var botBuilder = InnerBuild(services, optionsBuilderFunc, analyticsOptionsBuilderFunc, serverSettingsBuilderFunc, dataAccessSettingsBuilderFunc, telegramBotBuilderFunc); + + services.AddSingleton>(botBuilder); + + return services.AddTelegramLayoutsSupport(); + } + + static TBotBuilder InnerBuild(IServiceCollection services, Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action? telegramBotBuilderFunc) + where TBotBuilder : TelegramBotBuilder> { optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); @@ -102,19 +120,21 @@ public static IServiceCollection AddTelegramBot(this IServiceCollec var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); - var botBuilder = TelegramBotBuilder.Instance(services, - ServerSettingsBuilder, - SettingsBuilder, - DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder, - false) - .AddClient(clientBuilder); - - telegramBotBuilderFunc?.Invoke(botBuilder as TBotBuilder); - var bot = botBuilder.Build(); - - return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + TBotBuilder botBuilder = TelegramBotBuilder.Instance(services, + ServerSettingsBuilder, + SettingsBuilder, + DataAccessSettingsBuilder, + AnalyticsClientOptionsBuilder, + false); + + botBuilder.AddClient(clientBuilder) + .AddServices(services); + + telegramBotBuilderFunc?.Invoke(botBuilder); + + services.AddSingleton>(botBuilder); + + return botBuilder; } public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, @@ -171,10 +191,10 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); - TelegramBot? bot = botBuilder.Build(); - return services.AddSingleton(bot!) - .AddTelegramLayoutsSupport(); + services.AddSingleton>(botBuilder); + + return services.AddTelegramLayoutsSupport(); } public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => From 477655e6e4170372d6370773e9bb6b5f25d7ca08 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 11 Jul 2025 00:08:08 +0300 Subject: [PATCH 050/101] - bot creation fix for isStandalone parameter --- Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 1810f016..90fc5ad7 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -69,7 +69,7 @@ protected TelegramBotBuilder(bool isStandalone) bool isStandalone) where TBotBuilderNew : BotBuilder { - var botBuilder = Activator.CreateInstance(typeof(TBotBuilderNew)) as TBotBuilderNew; + var botBuilder = Activator.CreateInstance(typeof(TBotBuilderNew), [isStandalone]) as TBotBuilderNew; (botBuilder as TelegramBotBuilder>) .AddBotSettings(settingsBuilder) From 7e9a0f6732b0e69ded96c8f517fdcd08b1b38774 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 13 Jul 2025 00:31:15 +0300 Subject: [PATCH 051/101] BOTTICELLI: - bot building process was changed --- .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../Builders/TelegramBotBuilder.cs | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 150 ++++++++++-------- .../Extensions/ServiceCollectionExtensions.cs | 16 +- .../Program.cs | 8 +- Pay.Sample.Telegram/Program.cs | 10 +- .../Ai.DeepSeek.Sample.Telegram/Program.cs | 8 +- Samples/Ai.YaGpt.Sample.Telegram/Program.cs | 6 +- Samples/Auth.Sample.Telegram/Program.cs | 9 +- .../CommandChain.Sample.Telegram/Program.cs | 10 +- Samples/Layouts.Sample.Telegram/Program.cs | 9 +- .../Layouts.Sample.Telegram/appsettings.json | 2 +- Samples/Messaging.Sample.Telegram/Program.cs | 9 +- Samples/Monads.Sample.Telegram/Program.cs | 11 +- Samples/TelegramAiSample/Program.cs | 23 +-- Samples/TelegramAiSample/appsettings.json | 25 +++ 16 files changed, 198 insertions(+), 107 deletions(-) create mode 100644 Samples/TelegramAiSample/appsettings.json diff --git a/Botticelli.AI.DeepSeekGpt/Extensions/ServiceCollectionExtensions.cs b/Botticelli.AI.DeepSeekGpt/Extensions/ServiceCollectionExtensions.cs index ca197523..dc14a82f 100644 --- a/Botticelli.AI.DeepSeekGpt/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.AI.DeepSeekGpt/Extensions/ServiceCollectionExtensions.cs @@ -33,7 +33,8 @@ public static IServiceCollection AddDeepSeekProvider(this IServiceCollection ser s.Instruction = deepSeekGptSettings.Instruction; }); - services.AddSingleton(); + services.AddSingleton() + .AddHttpClient(); return services; } diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 90fc5ad7..d101d684 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -52,8 +52,12 @@ public class TelegramBotBuilder : BotBuilder DataAccessSettingsBuilder = new(); - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action? telegramBotBuilderFunc = null) - where TBotBuilder : TelegramBotBuilder> + public static TelegramBotBuilder> AddTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) + { + return AddTelegramBot>>(services, configuration, telegramBotBuilderFunc); + } + + public static TBotBuilder AddTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action? telegramBotBuilderFunc = null) + where TBotBuilder : TelegramBotBuilder> + where TBot : TelegramBot { var telegramBotSettings = configuration - .GetSection(TelegramBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(TelegramBotSettings)}!"); + .GetSection(TelegramBotSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(TelegramBotSettings)}!"); var analyticsClientSettings = configuration - .GetSection(AnalyticsClientSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); + .GetSection(AnalyticsClientSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); var serverSettings = configuration - .GetSection(ServerSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(ServerSettings)}!"); + .GetSection(ServerSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(ServerSettings)}!"); var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException( - $"Can't load configuration for {nameof(DataAccessSettings)}!"); - - services.AddSingleton, TBotBuilder>(); - - return services.AddTelegramBot(o => - o.Set(telegramBotSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + .GetSection(DataAccessSettings.Section) + .Get() ?? + throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); + + services.AddSingleton, TBotBuilder>(); + + return services.AddTelegramBot(o => + o.Set(telegramBotSettings), + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - TelegramBotSettings botSettings, - AnalyticsClientSettings analyticsClientSettings, - ServerSettings serverSettings, - DataAccessSettings dataAccessSettings, - Action>>? telegramBotBuilderFunc = null) + public static TelegramBotBuilder> AddTelegramBot(this IServiceCollection services, + TelegramBotSettings botSettings, + AnalyticsClientSettings analyticsClientSettings, + ServerSettings serverSettings, + DataAccessSettings dataAccessSettings, + Action>>? telegramBotBuilderFunc = null) { - return services.AddTelegramBot(o => o.Set(botSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + return services.AddTelegramBot>>(o => o.Set(botSettings), + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } /// @@ -91,27 +94,36 @@ public static IServiceCollection AddTelegramBot(this IServiceCollection services /// /// /// - public static IServiceCollection AddTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action? telegramBotBuilderFunc = null) - where TBotBuilder : TelegramBotBuilder> + public static TBotBuilder AddTelegramBot(this IServiceCollection services, + Action> optionsBuilderFunc, + Action> analyticsOptionsBuilderFunc, + Action> serverSettingsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action? telegramBotBuilderFunc = null) + where TBotBuilder : TelegramBotBuilder> + where TBot : TelegramBot { - var botBuilder = InnerBuild(services, optionsBuilderFunc, analyticsOptionsBuilderFunc, serverSettingsBuilderFunc, dataAccessSettingsBuilderFunc, telegramBotBuilderFunc); + var botBuilder = InnerBuild(services, + optionsBuilderFunc, + analyticsOptionsBuilderFunc, + serverSettingsBuilderFunc, + dataAccessSettingsBuilderFunc, + telegramBotBuilderFunc); - services.AddSingleton>(botBuilder); - - return services.AddTelegramLayoutsSupport(); + services.AddSingleton(botBuilder); + + services.AddTelegramLayoutsSupport(); + + return botBuilder; } - static TBotBuilder InnerBuild(IServiceCollection services, Action> optionsBuilderFunc, + static TBotBuilder InnerBuild(IServiceCollection services, Action> optionsBuilderFunc, Action> analyticsOptionsBuilderFunc, Action> serverSettingsBuilderFunc, Action> dataAccessSettingsBuilderFunc, Action? telegramBotBuilderFunc) - where TBotBuilder : TelegramBotBuilder> + where TBotBuilder : TelegramBotBuilder> + where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); @@ -120,7 +132,7 @@ static TBotBuilder InnerBuild(IServiceCollection services, Action.Instance(services, + TBotBuilder botBuilder = TelegramBotBuilder.Instance(services, ServerSettingsBuilder, SettingsBuilder, DataAccessSettingsBuilder, @@ -132,19 +144,19 @@ static TBotBuilder InnerBuild(IServiceCollection services, Action>(botBuilder); + services.AddSingleton>(botBuilder); return botBuilder; } - public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) => + public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) => AddStandaloneTelegramBot(services, configuration, telegramBotBuilderFunc); - public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - IConfiguration configuration, - Action>>? telegramBotBuilderFunc = null) + public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot(this IServiceCollection services, + IConfiguration configuration, + Action>>? telegramBotBuilderFunc = null) where TBot : TelegramBot { var telegramBotSettings = configuration @@ -171,11 +183,11 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol botDataSettingsBuilder => botDataSettingsBuilder.Set(botDataSettings)); } - public static IServiceCollection AddStandaloneTelegramBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> dataAccessSettingsBuilderFunc, - Action> botDataSettingsBuilderFunc, - Action>? telegramBotBuilderFunc = null) + public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot(this IServiceCollection services, + Action> optionsBuilderFunc, + Action> dataAccessSettingsBuilderFunc, + Action> botDataSettingsBuilderFunc, + Action>? telegramBotBuilderFunc = null) where TBot : TelegramBot { optionsBuilderFunc(SettingsBuilder); @@ -193,8 +205,10 @@ public static IServiceCollection AddStandaloneTelegramBot(this IServiceCol telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); services.AddSingleton>(botBuilder); + + services.AddTelegramLayoutsSupport(); - return services.AddTelegramLayoutsSupport(); + return botBuilder as TelegramStandaloneBotBuilder; } public static IServiceCollection AddTelegramLayoutsSupport(this IServiceCollection services) => diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index e1cfb206..e8e69169 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; using Botticelli.Framework.Options; +using Botticelli.Framework.Telegram; +using Botticelli.Framework.Telegram.Builders; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Options; using Botticelli.Pay.Extensions; @@ -24,8 +26,8 @@ public static class ServiceCollectionExtensions /// /// /// - public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, - Action> optionsBuilderFunc, + public static TelegramBotBuilder> AddTelegramPayBot(this IServiceCollection services, + Action> optionsBuilderFunc, Action> analyticsOptionsBuilderFunc, Action> serverSettingsBuilderFunc, Action> dataAccessSettingsBuilderFunc) @@ -34,7 +36,7 @@ public static IServiceCollection AddTelegramPayBot { services.AddPayments(); - return services.AddTelegramBot(optionsBuilderFunc, + return services.AddTelegramBot>(optionsBuilderFunc, analyticsOptionsBuilderFunc, serverSettingsBuilderFunc, dataAccessSettingsBuilderFunc, @@ -49,13 +51,13 @@ public static IServiceCollection AddTelegramPayBot /// /// /// - public static IServiceCollection AddTelegramPayBot(this IServiceCollection services, IConfiguration configuration) + public static TelegramBotBuilder> AddTelegramPayBot(this IServiceCollection services, IConfiguration configuration) where THandler : IPreCheckoutHandler, new() where TProcessor : IPayProcessor { services.AddPayments(); - return services.AddTelegramBot(configuration, + return services.AddTelegramBot>(configuration, o => o.AddSubHandler() .AddSubHandler()); } @@ -66,8 +68,8 @@ public static IServiceCollection AddTelegramPayBot(this IS /// /// /// - public static IServiceCollection AddStandaloneTelegramPayBot(this IServiceCollection services, - IConfiguration configuration) + public static TelegramStandaloneBotBuilder AddStandaloneTelegramPayBot(this IServiceCollection services, + IConfiguration configuration) where THandler : IPreCheckoutHandler, new() where TProcessor : IPayProcessor { diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs index 2c063044..4474cc42 100644 --- a/Messaging.Sample.Telegram.Standalone/Program.cs +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -1,6 +1,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Interfaces; using Botticelli.Schedule.Quartz.Extensions; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; @@ -9,8 +10,11 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services +var bot = builder.Services .AddStandaloneTelegramBot(builder.Configuration) + .Build(); + +builder.Services .AddTelegramLayoutsSupport() .AddLogging(cfg => cfg.AddNLog()) .AddQuartzScheduler(builder.Configuration); @@ -27,4 +31,6 @@ .AddProcessor>() .AddValidator>(); +builder.Services.AddSingleton(bot); + await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Pay.Sample.Telegram/Program.cs b/Pay.Sample.Telegram/Program.cs index 6dec2fb0..af3ca321 100644 --- a/Pay.Sample.Telegram/Program.cs +++ b/Pay.Sample.Telegram/Program.cs @@ -1,6 +1,7 @@ using Botticelli.Controls.Parsers; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; +using Botticelli.Interfaces; using Botticelli.Pay.Models; using Botticelli.Pay.Processors; using Botticelli.Pay.Telegram.Extensions; @@ -13,11 +14,14 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services +var bot = builder.Services .Configure(builder.Configuration.GetSection("PaySettings")) .AddTelegramPayBot>(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) - .AddSingleton(); + .Build(); + +builder.Services.AddLogging(cfg => cfg.AddNLog()) + .AddSingleton() + .AddSingleton(bot); builder.Services.AddBotCommand() .AddProcessor>() diff --git a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs index 5133159e..66bbba79 100644 --- a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs +++ b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs @@ -14,8 +14,11 @@ var builder = WebApplication.CreateBuilder(args); +var bot = builder.Services + .AddTelegramBot(builder.Configuration) + .Build(); + builder.Services - .AddTelegramBot(builder.Configuration) .AddLogging(cfg => cfg.AddNLog()) .AddDeepSeekProvider(builder.Configuration) .AddAiValidation() @@ -25,7 +28,8 @@ .UsePassBusClient>() .UsePassEventBusClient>() .AddBotCommand, PassValidator>() - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport() + .AddSingleton(bot); var app = builder.Build(); diff --git a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs index beeb6b4b..98d24351 100644 --- a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs +++ b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs @@ -14,8 +14,12 @@ var builder = WebApplication.CreateBuilder(args); +var bot = builder.Services + .AddTelegramBot(builder.Configuration) + .Build(); + builder.Services - .AddTelegramBot(builder.Configuration) + .AddSingleton(bot) .AddLogging(cfg => cfg.AddNLog()) .AddYaGptProvider(builder.Configuration) .AddAiValidation() diff --git a/Samples/Auth.Sample.Telegram/Program.cs b/Samples/Auth.Sample.Telegram/Program.cs index b978985d..e3da6ce6 100644 --- a/Samples/Auth.Sample.Telegram/Program.cs +++ b/Samples/Auth.Sample.Telegram/Program.cs @@ -4,16 +4,21 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Interfaces; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; var builder = WebApplication.CreateBuilder(args); +var bot = builder.Services + .AddTelegramBot(builder.Configuration) + .Build(); + builder.Services - .AddTelegramBot(builder.Configuration) .AddTelegramLayoutsSupport() .AddLogging(cfg => cfg.AddNLog()) - .AddSqliteBasicBotUserAuth(builder.Configuration); + .AddSqliteBasicBotUserAuth(builder.Configuration) + .AddSingleton(bot); builder.Services.AddBotCommand() .AddProcessor>() diff --git a/Samples/CommandChain.Sample.Telegram/Program.cs b/Samples/CommandChain.Sample.Telegram/Program.cs index e04f0403..ebab9088 100644 --- a/Samples/CommandChain.Sample.Telegram/Program.cs +++ b/Samples/CommandChain.Sample.Telegram/Program.cs @@ -3,6 +3,7 @@ using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram; using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Interfaces; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; using NLog.Extensions.Logging; @@ -12,9 +13,12 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services - .AddTelegramBot(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) +var bot = builder.Services + .AddTelegramBot(builder.Configuration) + .Build(); + +builder.Services.AddLogging(cfg => cfg.AddNLog()) + .AddSingleton(bot) .AddSingleton>() .AddSingleton>() .AddSingleton>() diff --git a/Samples/Layouts.Sample.Telegram/Program.cs b/Samples/Layouts.Sample.Telegram/Program.cs index 848fee38..36aee9d4 100644 --- a/Samples/Layouts.Sample.Telegram/Program.cs +++ b/Samples/Layouts.Sample.Telegram/Program.cs @@ -3,6 +3,7 @@ using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Layout; +using Botticelli.Interfaces; using Botticelli.Locations.Telegram.Extensions; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; @@ -11,12 +12,16 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services +var bot = builder.Services .AddTelegramBot(builder.Configuration) + .Build(); + +builder.Services .AddLogging(cfg => cfg.AddNLog()) .AddBotCommand>() .AddInlineCalendar() - .AddOsmLocations(builder.Configuration); + .AddOsmLocations(builder.Configuration) + .AddSingleton(bot); var app = builder.Build(); diff --git a/Samples/Layouts.Sample.Telegram/appsettings.json b/Samples/Layouts.Sample.Telegram/appsettings.json index 591ccfaa..db80d85d 100644 --- a/Samples/Layouts.Sample.Telegram/appsettings.json +++ b/Samples/Layouts.Sample.Telegram/appsettings.json @@ -6,7 +6,7 @@ } }, "DataAccess": { - "ConnectionString": "Filename=database.db;Password=123;ReadOnly=false" + "ConnectionString": "Filename=database.db;Password=123" }, "Server": { "ServerUri": "http://113.30.189.83:5042/v1/" diff --git a/Samples/Messaging.Sample.Telegram/Program.cs b/Samples/Messaging.Sample.Telegram/Program.cs index d4fb93f4..417ffd47 100644 --- a/Samples/Messaging.Sample.Telegram/Program.cs +++ b/Samples/Messaging.Sample.Telegram/Program.cs @@ -2,6 +2,7 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; +using Botticelli.Interfaces; using Botticelli.Schedule.Quartz.Extensions; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; @@ -11,12 +12,16 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services +var bot = builder.Services .AddTelegramBot(builder.Configuration, botBuilder => botBuilder.AddBroadcasting(builder.Configuration, optionsBuilder => optionsBuilder.UseSqlite())) + .Build(); + +builder.Services .AddTelegramLayoutsSupport() .AddLogging(cfg => cfg.AddNLog()) - .AddQuartzScheduler(builder.Configuration); + .AddQuartzScheduler(builder.Configuration) + .AddSingleton(bot); builder.Services.AddBotCommand() .AddProcessor>() diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index ee3a0d86..b8a6759e 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -5,16 +5,21 @@ using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Layout; +using Botticelli.Interfaces; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; using TelegramMonadsBasedBot.Commands; var builder = WebApplication.CreateBuilder(args); -builder.Services - .AddTelegramBot(builder.Configuration) +var bot = builder.Services + .AddTelegramBot(builder.Configuration) + .Build(); + +builder.Services .AddLogging(cfg => cfg.AddNLog()) - .AddTelegramLayoutsSupport(); + .AddTelegramLayoutsSupport() + .AddSingleton(bot); builder.Services .AddChainedRedisStorage(builder.Configuration) diff --git a/Samples/TelegramAiSample/Program.cs b/Samples/TelegramAiSample/Program.cs index fe5c6c3e..a0539c59 100644 --- a/Samples/TelegramAiSample/Program.cs +++ b/Samples/TelegramAiSample/Program.cs @@ -14,17 +14,20 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services +var bot = builder.Services .AddTelegramBot(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) - .AddChatGptProvider(builder.Configuration) - .AddAiValidation() - .AddScoped, PassValidator>() - .AddSingleton() - .UsePassBusAgent, AiHandler>() - .UsePassBusClient>() - .UsePassEventBusClient>() - .AddBotCommand, PassValidator>(); + .Build(); + +builder.Services.AddLogging(cfg => cfg.AddNLog()) + .AddChatGptProvider(builder.Configuration) + .AddAiValidation() + .AddScoped, PassValidator>() + .AddSingleton() + .UsePassBusAgent, AiHandler>() + .UsePassBusClient>() + .UsePassEventBusClient>() + .AddBotCommand, PassValidator>() + .AddSingleton(bot); var app = builder.Build(); diff --git a/Samples/TelegramAiSample/appsettings.json b/Samples/TelegramAiSample/appsettings.json new file mode 100644 index 00000000..b9677c7e --- /dev/null +++ b/Samples/TelegramAiSample/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DataAccess": { + "ConnectionString": "Filename=database.db;Password=123;ReadOnly=false" + }, + "Server": { + "ServerUri": "http://113.30.189.83:5042/v1/" + }, + "AnalyticsClient": { + "TargetUrl": "http://localhost:5251/v1/" + }, + "TelegramBot": { + "timeout": 60, + "useThrottling": true, + "useTestEnvironment": false, + "name": "TestBot" + }, + "AllowedHosts": "*" +} + From 0e3081f221cc47481aff1e3f67bfe4eb9b015b49 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 13 Jul 2025 14:26:22 +0300 Subject: [PATCH 052/101] Botbuilder init fix --- Botticelli.Broadcasting.Dal/BroadcastingContext.cs | 10 ++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 6 ------ Messaging.Sample.Telegram.Standalone/appsettings.json | 2 +- Samples/Messaging.Sample.Telegram/Program.cs | 3 +-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs index a1e1cf45..a8e2c207 100644 --- a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs +++ b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs @@ -9,6 +9,16 @@ public class BroadcastingContext : DbContext public DbSet MessageCaches { get; set; } public DbSet MessageStatuses { get; set; } + public BroadcastingContext() + { + + } + + public BroadcastingContext(DbContextOptions options) : base(options) + { + + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 3f58e2f8..cae3c972 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -110,8 +110,6 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect dataAccessSettingsBuilderFunc, telegramBotBuilderFunc); - services.AddSingleton(botBuilder); - services.AddTelegramLayoutsSupport(); return botBuilder; @@ -143,8 +141,6 @@ static TBotBuilder InnerBuild(IServiceCollection services, Ac .AddServices(services); telegramBotBuilderFunc?.Invoke(botBuilder); - - services.AddSingleton>(botBuilder); return botBuilder; } @@ -203,8 +199,6 @@ public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot( .AddClient(clientBuilder); telegramBotBuilderFunc?.Invoke((TelegramStandaloneBotBuilder)botBuilder); - - services.AddSingleton>(botBuilder); services.AddTelegramLayoutsSupport(); diff --git a/Messaging.Sample.Telegram.Standalone/appsettings.json b/Messaging.Sample.Telegram.Standalone/appsettings.json index 385e2420..8d03590a 100644 --- a/Messaging.Sample.Telegram.Standalone/appsettings.json +++ b/Messaging.Sample.Telegram.Standalone/appsettings.json @@ -16,7 +16,7 @@ }, "BotData": { "BotId": "botId", - "BotKey": "botKey" + "BotKey": "5746549361:AAFZcvuRcEk7QO4OfAjTYQQUeUpcaES3kqk" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Samples/Messaging.Sample.Telegram/Program.cs b/Samples/Messaging.Sample.Telegram/Program.cs index 417ffd47..6ec50c22 100644 --- a/Samples/Messaging.Sample.Telegram/Program.cs +++ b/Samples/Messaging.Sample.Telegram/Program.cs @@ -13,8 +13,7 @@ var builder = WebApplication.CreateBuilder(args); var bot = builder.Services - .AddTelegramBot(builder.Configuration, botBuilder => botBuilder.AddBroadcasting(builder.Configuration, - optionsBuilder => optionsBuilder.UseSqlite())) + .AddTelegramBot(builder.Configuration) .Build(); builder.Services From 8ff1c15bf58d30853f16ffa6fe5a27498c8b79cb Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 13 Jul 2025 22:07:05 +0300 Subject: [PATCH 053/101] - fix --- .../Extensions/Processors/ProcessorFactoryBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs index 5e391a10..f9dbc1b3 100644 --- a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs +++ b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs @@ -18,7 +18,8 @@ public static void AddProcessor(IServiceCollection serviceCollection public static ProcessorFactory Build() { - if (_serviceCollection == null) throw new NullReferenceException("Service collection is null! PLease, call AddProcessor() first!"); + if (_serviceCollection == null) + return new ProcessorFactory([]); var sp = _serviceCollection.BuildServiceProvider(); From ef924b59e3cc4c039a9275c1874d2193f0e6f839 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 13 Jul 2025 22:17:17 +0300 Subject: [PATCH 054/101] BM-2: - sample comments --- .../Extensions/ServiceCollectionExtensions.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index cae3c972..2bd32711 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -59,9 +59,7 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect .GetSection(DataAccessSettings.Section) .Get() ?? throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); - - services.AddSingleton, TBotBuilder>(); - + return services.AddTelegramBot(o => o.Set(telegramBotSettings), o => o.Set(analyticsClientSettings), From a2b8e902c6cb5b5b3925c3c384b04ad6180f2f44 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 13 Jul 2025 23:47:06 +0300 Subject: [PATCH 055/101] - throttling fix --- Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index d101d684..fef1d1a0 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -166,7 +166,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu #endregion - if (BotSettings?.UseThrottling is false) _builder.AddThrottler(new Throttler()); + if (BotSettings?.UseThrottling is true) _builder.AddThrottler(new Throttler()); if (!string.IsNullOrWhiteSpace(_botToken)) _builder.AddToken(_botToken); From 6829c3d030caeb22a3338b59150ceb3f17fb3e7c Mon Sep 17 00:00:00 2001 From: stone1985 Date: Mon, 14 Jul 2025 21:40:41 +0300 Subject: [PATCH 056/101] - bugfix --- .../Extensions/ServiceCollectionExtensions.cs | 3 --- .../Extensions/ServiceCollectionExtensions.cs | 1 - 2 files changed, 4 deletions(-) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 2bd32711..d1cc10c8 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -2,16 +2,13 @@ using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; using Botticelli.Controls.Parsers; -using Botticelli.Framework.Builders; using Botticelli.Framework.Options; using Botticelli.Framework.Telegram.Builders; using Botticelli.Framework.Telegram.Decorators; using Botticelli.Framework.Telegram.Layout; using Botticelli.Framework.Telegram.Options; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Extensions; diff --git a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs index e8e69169..4ba6e31f 100644 --- a/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Pay.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using Botticelli.Bot.Data.Settings; using Botticelli.Client.Analytics.Settings; using Botticelli.Framework.Options; -using Botticelli.Framework.Telegram; using Botticelli.Framework.Telegram.Builders; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Options; From 922e5590c3e1166abde14566b407a3c984670f31 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Mon, 14 Jul 2025 21:42:12 +0300 Subject: [PATCH 057/101] - Telegram.Bot v 22.6.0 --- .../Botticelli.Framework.Telegram.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj index 38402912..cfdb78dc 100644 --- a/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj +++ b/Botticelli.Framework.Telegram/Botticelli.Framework.Telegram.csproj @@ -29,7 +29,7 @@ - + From 1547b85f698dacea118d98adc2189bf5de2922b4 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Wed, 16 Jul 2025 15:07:18 +0300 Subject: [PATCH 058/101] - Executor injection fix --- .../Builders/ITelegramBotBuilder.cs | 35 +++++++++++++++++++ Botticelli.Framework/Builders/BotBuilder.cs | 21 ++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 Botticelli.Framework.Telegram/Builders/ITelegramBotBuilder.cs diff --git a/Botticelli.Framework.Telegram/Builders/ITelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/ITelegramBotBuilder.cs new file mode 100644 index 00000000..a6add4ef --- /dev/null +++ b/Botticelli.Framework.Telegram/Builders/ITelegramBotBuilder.cs @@ -0,0 +1,35 @@ +using Botticelli.Bot.Data.Settings; +using Botticelli.Client.Analytics.Settings; +using Botticelli.Framework.Builders; +using Botticelli.Framework.Options; +using Botticelli.Framework.Telegram.Decorators; +using Botticelli.Framework.Telegram.Handlers; +using Botticelli.Framework.Telegram.Options; +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Telegram.Builders; + +public interface ITelegramBotBuilder where TBot : TelegramBot where TBotBuilder : BotBuilder +{ + ITelegramBotBuilder AddSubHandler() + where T : class, IBotUpdateSubHandler; + + ITelegramBotBuilder AddToken(string botToken); + ITelegramBotBuilder AddClient(TelegramClientDecoratorBuilder builder); + TBot? Build(); + BotBuilder AddServices(IServiceCollection services); + BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder); + BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder); + BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler); + BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler); + BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler); + BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler); + BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler); + + static abstract ITelegramBotBuilder Instance(IServiceCollection services, + ServerSettingsBuilder serverSettingsBuilder, + BotSettingsBuilder settingsBuilder, + DataAccessSettingsBuilder dataAccessSettingsBuilder, + AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder, + bool isStandalone); +} \ No newline at end of file diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 831d8470..267e272d 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -19,8 +19,21 @@ public abstract class BotBuilder protected abstract TBot? InnerBuild(); } -public abstract class BotBuilder : BotBuilder - where TBotBuilder : BotBuilder +public interface IBotBuilder where TBotBuilder : BotBuilder +{ + BotBuilder AddServices(IServiceCollection services); + BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder); + BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder); + BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler); + BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler); + BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler); + BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler); + BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler); + TBot? Build(); +} + +public abstract class BotBuilder : BotBuilder, IBotBuilder + where TBotBuilder : BotBuilder { protected AnalyticsClientSettingsBuilder? AnalyticsClientSettingsBuilder; protected DataAccessSettingsBuilder? BotDataAccessSettingsBuilder; @@ -44,14 +57,14 @@ public virtual BotBuilder AddServices(IServiceCollection serv return this; } - public virtual BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) + public virtual BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder) { AnalyticsClientSettingsBuilder = clientSettingsBuilder; return this; } - protected virtual BotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) + protected virtual BotBuilder AddServerSettings(ServerSettingsBuilder settingsBuilder) { ServerSettingsBuilder = settingsBuilder; From 13101e7587947cb92ff690bf489cd44ad359864a Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 18 Jul 2025 18:48:06 +0300 Subject: [PATCH 059/101] - enhanced throttling for Telegram --- .../Builders/TelegramBotBuilder.cs | 4 ++- .../Decorators/TelegramClientDecorator.cs | 7 ----- .../TelegramClientDecoratorBuilder.cs | 16 ++++++---- .../Decorators/Throttler.cs | 2 +- .../OutcomeThrottlingDelegatingHandler.cs | 30 +++++++++++++++++++ 5 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index fef1d1a0..79e9a914 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -15,6 +15,7 @@ using Botticelli.Framework.Telegram.Decorators; using Botticelli.Framework.Telegram.Handlers; using Botticelli.Framework.Telegram.HostedService; +using Botticelli.Framework.Telegram.Http; using Botticelli.Framework.Telegram.Layout; using Botticelli.Framework.Telegram.Options; using Botticelli.Framework.Telegram.Utils; @@ -166,7 +167,8 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu #endregion - if (BotSettings?.UseThrottling is true) _builder.AddThrottler(new Throttler()); + if (BotSettings?.UseThrottling is true) + _builder.AddThrottler(new OutcomeThrottlingDelegatingHandler()); if (!string.IsNullOrWhiteSpace(_botToken)) _builder.AddToken(_botToken); diff --git a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs index c1025224..27882458 100644 --- a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs +++ b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs @@ -12,15 +12,12 @@ namespace Botticelli.Framework.Telegram.Decorators; public class TelegramClientDecorator : ITelegramBotClient { private readonly HttpClient? _httpClient; - private readonly IThrottler? _throttler; private TelegramBotClient? _innerClient; private TelegramBotClientOptions _options; internal TelegramClientDecorator(TelegramBotClientOptions options, - IThrottler? throttler, HttpClient? httpClient = null) { - _throttler = throttler; _options = options; _httpClient = httpClient; _innerClient = !string.IsNullOrWhiteSpace(options.Token) ? new TelegramBotClient(options, httpClient) : null; @@ -32,10 +29,6 @@ public async Task SendRequest(IRequest request, { try { - if (_throttler != null) - return await _throttler.Throttle(async () => await _innerClient?.SendRequest(request, cancellationToken)!, - cancellationToken); - return await _innerClient?.SendRequest(request, cancellationToken)!; } catch (ApiRequestException ex) diff --git a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs index 2f3a229d..ce87ab0a 100644 --- a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs +++ b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecoratorBuilder.cs @@ -1,4 +1,5 @@ using Botticelli.Framework.Options; +using Botticelli.Framework.Telegram.Http; using Botticelli.Framework.Telegram.Options; using Microsoft.Extensions.DependencyInjection; using Telegram.Bot; @@ -11,7 +12,7 @@ public class TelegramClientDecoratorBuilder private readonly BotSettingsBuilder _settingsBuilder; private HttpClient? _httpClient; private TelegramClientDecorator? _telegramClient; - private IThrottler? _throttler; + private OutcomeThrottlingDelegatingHandler? _throttler; private string? _token; private TelegramClientDecoratorBuilder(IServiceCollection services, @@ -27,7 +28,7 @@ public static TelegramClientDecoratorBuilder Instance(IServiceCollection service return new TelegramClientDecoratorBuilder(services, settingsBuilder); } - public TelegramClientDecoratorBuilder AddThrottler(IThrottler throttler) + public TelegramClientDecoratorBuilder AddThrottler(OutcomeThrottlingDelegatingHandler throttler) { _throttler = throttler; @@ -49,9 +50,14 @@ public TelegramClientDecoratorBuilder AddToken(string? token) if (_httpClient == null) { - var factory = _services.BuildServiceProvider().GetRequiredService(); + if (_throttler == null) + { + var factory = _services.BuildServiceProvider().GetRequiredService(); - _httpClient = factory.CreateClient(nameof(TelegramClientDecorator)); + _httpClient = factory.CreateClient(nameof(TelegramClientDecorator)); + } + else + _httpClient = new HttpClient(_throttler); } var botOptions = _settingsBuilder.Build(); @@ -61,7 +67,7 @@ public TelegramClientDecoratorBuilder AddToken(string? token) RetryThreshold = 60, RetryCount = botOptions.RetryOnFailure }; - _telegramClient = new TelegramClientDecorator(clientOptions, _throttler, _httpClient); + _telegramClient = new TelegramClientDecorator(clientOptions, _httpClient); return _telegramClient; } diff --git a/Botticelli.Framework.Telegram/Decorators/Throttler.cs b/Botticelli.Framework.Telegram/Decorators/Throttler.cs index b4528778..c0802a00 100644 --- a/Botticelli.Framework.Telegram/Decorators/Throttler.cs +++ b/Botticelli.Framework.Telegram/Decorators/Throttler.cs @@ -15,7 +15,7 @@ public ValueTask Throttle(Func> action, CancellationToken ct) { var diff = DateTime.UtcNow - _prevDt; var randComponent = - TimeSpan.FromMilliseconds(_random.Next(-MaxDeviation.Milliseconds, MaxDeviation.Milliseconds)); + TimeSpan.FromMilliseconds(_random.Next((int)-MaxDeviation.TotalMilliseconds, (int)MaxDeviation.TotalMilliseconds)); var sumDelay = Delay + randComponent; if (diff < sumDelay) Task.Delay(sumDelay - diff, ct).WaitAsync(ct); diff --git a/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs b/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs new file mode 100644 index 00000000..0b80da07 --- /dev/null +++ b/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs @@ -0,0 +1,30 @@ +namespace Botticelli.Framework.Telegram.Http; + +public class OutcomeThrottlingDelegatingHandler() : DelegatingHandler(new HttpClientHandler()) +{ + private static readonly TimeSpan Delay = TimeSpan.FromSeconds(3); + private static readonly TimeSpan MaxDeviation = TimeSpan.FromSeconds(1); + private readonly Random _random = Random.Shared; + private readonly SemaphoreSlim _throttler = new(1, 1); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var randComponent = + TimeSpan.FromMilliseconds(_random.Next((int)-MaxDeviation.TotalMilliseconds, (int)MaxDeviation.TotalMilliseconds)); + var sumDelay = Delay + randComponent; + + await _throttler.WaitAsync(cancellationToken); + await Task.Delay(sumDelay, cancellationToken); + + try + { + return await base.SendAsync(request, cancellationToken); + } + finally + { + _throttler.Release(); + } + } +} \ No newline at end of file From 15002292a49637b88d279a2b56efdd522a411065 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 20 Jul 2025 16:40:37 +0300 Subject: [PATCH 060/101] - small fixes --- .../Http/OutcomeThrottlingDelegatingHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs b/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs index 0b80da07..270641c5 100644 --- a/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs +++ b/Botticelli.Framework.Telegram/Http/OutcomeThrottlingDelegatingHandler.cs @@ -2,10 +2,10 @@ public class OutcomeThrottlingDelegatingHandler() : DelegatingHandler(new HttpClientHandler()) { - private static readonly TimeSpan Delay = TimeSpan.FromSeconds(3); - private static readonly TimeSpan MaxDeviation = TimeSpan.FromSeconds(1); + private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan MaxDeviation = TimeSpan.FromMilliseconds(100); private readonly Random _random = Random.Shared; - private readonly SemaphoreSlim _throttler = new(1, 1); + private readonly SemaphoreSlim _throttler = new(3, 3); protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { From cccf5504f96bef942019b524ed80511d5b20ea55 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 20 Jul 2025 16:49:35 +0300 Subject: [PATCH 061/101] Create jekyll-gh-pages.yml (cherry picked from commit 1177161826831b5841e9261bca1c57bb8c447052) --- .github/workflows/jekyll-gh-pages.yml | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/jekyll-gh-pages.yml diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000..f8822cea --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,51 @@ +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll with GitHub Pages dependencies preinstalled + +on: + # Runs on pushes targeting the default branch + push: + branches: ["develop"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 88680a094a8ad68266a92d1eda3e442ab4c8d3bb Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 20 Jul 2025 22:21:48 +0300 Subject: [PATCH 062/101] - VK Messenger in a separate repository now! --- Botticelli.Framework.Vk/API/Markups/Action.cs | 24 -- Botticelli.Framework.Vk/API/Markups/VkItem.cs | 12 - .../API/Markups/VkKeyboardMarkup.cs | 12 - .../API/Requests/PollRequest.cs | 30 -- .../API/Requests/VkSendMessageRequest.cs | 41 -- .../API/Responses/AudioMessage.cs | 27 -- .../API/Responses/AudioResponseData.cs | 12 - .../API/Responses/Document.cs | 33 -- .../API/Responses/DocumentResponseData.cs | 12 - .../API/Responses/ErrorResponse.cs | 33 -- .../GetMessageSessionDataResponse.cs | 9 - .../API/Responses/GetUploadAddress.cs | 9 - .../API/Responses/GetUploadAddressResponse.cs | 19 - .../API/Responses/SendPhotoSize.cs | 18 - .../API/Responses/SessionDataResponse.cs | 15 - .../API/Responses/UpdateEvent.cs | 34 -- .../API/Responses/UpdatesResponse.cs | 21 - .../API/Responses/UploadDocResult.cs | 15 - .../API/Responses/UploadPhotoResponse.cs | 33 -- .../API/Responses/UploadPhotoResult.cs | 15 - .../API/Responses/UploadPhotoSize.cs | 18 - .../API/Responses/UploadVideoResult.cs | 15 - .../API/Responses/VkErrorEventArgs.cs | 11 - .../API/Responses/VkSendAudioResponse.cs | 9 - .../API/Responses/VkSendDocumentResponse.cs | 9 - .../Responses/VkSendPhotoPartialResponse.cs | 33 -- .../API/Responses/VkSendPhotoResponse.cs | 9 - .../API/Responses/VkSendVideoResponse.cs | 9 - .../API/Responses/VkSendVideoResponseData.cs | 24 -- .../API/Responses/VkUpdatesEventArgs.cs | 11 - Botticelli.Framework.Vk/API/Utils/ApiUtils.cs | 66 --- .../Botticelli.Framework.Vk.Messages.csproj | 43 -- .../LongPollMessagesProviderBuilder.cs | 44 -- .../Builders/MessagePublisherBuilder.cs | 44 -- .../Builders/VkBotBuilder.cs | 121 ------ .../Builders/VkStorageUploaderBuilder.cs | 51 --- .../Extensions/ServiceCollectionExtensions.cs | 108 ----- .../Handlers/BotUpdateHandler.cs | 99 ----- .../Handlers/IBotUpdateHandler.cs | 13 - .../HostedService/VkBotHostedService.cs | 25 -- .../Layout/IVkLayoutSupplier.cs | 8 - .../Layout/VkLayoutSupplier.cs | 66 --- .../LongPollMessagesProvider.cs | 190 --------- Botticelli.Framework.Vk/MessagePublisher.cs | 49 --- .../Options/VkBotSettings.cs | 11 - .../Utils/VkTextTransformer.cs | 37 -- Botticelli.Framework.Vk/VkBot.cs | 325 --------------- Botticelli.Framework.Vk/VkStorageUploader.cs | 386 ------------------ Botticelli.Framework.Vk/vk_test.txt | 1 - 49 files changed, 2259 deletions(-) delete mode 100644 Botticelli.Framework.Vk/API/Markups/Action.cs delete mode 100644 Botticelli.Framework.Vk/API/Markups/VkItem.cs delete mode 100644 Botticelli.Framework.Vk/API/Markups/VkKeyboardMarkup.cs delete mode 100644 Botticelli.Framework.Vk/API/Requests/PollRequest.cs delete mode 100644 Botticelli.Framework.Vk/API/Requests/VkSendMessageRequest.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/AudioMessage.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/AudioResponseData.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/Document.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/DocumentResponseData.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/ErrorResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/GetMessageSessionDataResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/GetUploadAddress.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/GetUploadAddressResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/SendPhotoSize.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/SessionDataResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UpdateEvent.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UpdatesResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UploadDocResult.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UploadPhotoResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UploadPhotoResult.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UploadPhotoSize.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/UploadVideoResult.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkErrorEventArgs.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendAudioResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendDocumentResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendPhotoPartialResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendPhotoResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendVideoResponse.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkSendVideoResponseData.cs delete mode 100644 Botticelli.Framework.Vk/API/Responses/VkUpdatesEventArgs.cs delete mode 100644 Botticelli.Framework.Vk/API/Utils/ApiUtils.cs delete mode 100644 Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj delete mode 100644 Botticelli.Framework.Vk/Builders/LongPollMessagesProviderBuilder.cs delete mode 100644 Botticelli.Framework.Vk/Builders/MessagePublisherBuilder.cs delete mode 100644 Botticelli.Framework.Vk/Builders/VkBotBuilder.cs delete mode 100644 Botticelli.Framework.Vk/Builders/VkStorageUploaderBuilder.cs delete mode 100644 Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs delete mode 100644 Botticelli.Framework.Vk/Handlers/BotUpdateHandler.cs delete mode 100644 Botticelli.Framework.Vk/Handlers/IBotUpdateHandler.cs delete mode 100644 Botticelli.Framework.Vk/HostedService/VkBotHostedService.cs delete mode 100644 Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs delete mode 100644 Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs delete mode 100644 Botticelli.Framework.Vk/LongPollMessagesProvider.cs delete mode 100644 Botticelli.Framework.Vk/MessagePublisher.cs delete mode 100644 Botticelli.Framework.Vk/Options/VkBotSettings.cs delete mode 100644 Botticelli.Framework.Vk/Utils/VkTextTransformer.cs delete mode 100644 Botticelli.Framework.Vk/VkBot.cs delete mode 100644 Botticelli.Framework.Vk/VkStorageUploader.cs delete mode 100644 Botticelli.Framework.Vk/vk_test.txt diff --git a/Botticelli.Framework.Vk/API/Markups/Action.cs b/Botticelli.Framework.Vk/API/Markups/Action.cs deleted file mode 100644 index e8b6af6a..00000000 --- a/Botticelli.Framework.Vk/API/Markups/Action.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Markups; - -public class Action -{ - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("hash")] - public string Hash { get; set; } - - [JsonPropertyName("payload")] - public string Payload { get; set; } - - [JsonPropertyName("label")] - public string Label { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Markups/VkItem.cs b/Botticelli.Framework.Vk/API/Markups/VkItem.cs deleted file mode 100644 index 62fe3543..00000000 --- a/Botticelli.Framework.Vk/API/Markups/VkItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Markups; - -public class VkItem -{ - [JsonPropertyName("action")] - public Action Action { get; set; } - - [JsonPropertyName("color")] - public string Color { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Markups/VkKeyboardMarkup.cs b/Botticelli.Framework.Vk/API/Markups/VkKeyboardMarkup.cs deleted file mode 100644 index b753f2e3..00000000 --- a/Botticelli.Framework.Vk/API/Markups/VkKeyboardMarkup.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Markups; - -public class VkKeyboardMarkup -{ - [JsonPropertyName("one_time")] - public bool OneTime { get; set; } - - [JsonPropertyName("buttons")] - public IEnumerable> Buttons { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Requests/PollRequest.cs b/Botticelli.Framework.Vk/API/Requests/PollRequest.cs deleted file mode 100644 index b9639824..00000000 --- a/Botticelli.Framework.Vk/API/Requests/PollRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Botticelli.Framework.Vk.Messages.API.Requests; - -public class PollRequest -{ - /// - /// Session key - /// - public string Key { get; set; } - - /// - /// Number of the last event, from which we need to get - /// subsequent events - /// - public long Ts { get; set; } - - /// - /// Time of waiting in seconds - /// - public int Wait { get; set; } = 100; - - /// - /// Additional mode settings - /// - public int? Mode { get; set; } - - /// - /// Long poll API ver - /// - public int Version => 3; -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Requests/VkSendMessageRequest.cs b/Botticelli.Framework.Vk/API/Requests/VkSendMessageRequest.cs deleted file mode 100644 index 7000c06b..00000000 --- a/Botticelli.Framework.Vk/API/Requests/VkSendMessageRequest.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Requests; - -public class VkSendMessageRequest -{ - public VkSendMessageRequest() - { - RandomId = new Random(DateTime.Now.Millisecond).Next(); - } - - [JsonPropertyName("user_id")] - public string UserId { get; set; } - - [JsonPropertyName("peer_id")] - public string PeerId { get; set; } - - [JsonPropertyName("random_id")] - public int RandomId { get; } - - [JsonPropertyName("message")] - public string? Body { get; set; } - - [JsonPropertyName("reply_to")] - public string? ReplyTo { get; set; } - - [JsonPropertyName("lat")] - public decimal? Lat { get; set; } - - [JsonPropertyName("long")] - public decimal? Long { get; set; } - - [JsonPropertyName("access_token")] - public string AccessToken { get; set; } - - [JsonPropertyName("attachment")] - public string? Attachment { get; set; } - - [JsonPropertyName("v")] - public string ApiVersion => "5.131"; -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/AudioMessage.cs b/Botticelli.Framework.Vk/API/Responses/AudioMessage.cs deleted file mode 100644 index b2325431..00000000 --- a/Botticelli.Framework.Vk/API/Responses/AudioMessage.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class AudioMessage -{ - [JsonPropertyName("duration")] - public int Duration { get; set; } - - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("link_mp3")] - public string LinkMp3 { get; set; } - - [JsonPropertyName("link_ogg")] - public string LinkOgg { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("access_key")] - public string AccessKey { get; set; } - - [JsonPropertyName("waveform")] - public List Waveform { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/AudioResponseData.cs b/Botticelli.Framework.Vk/API/Responses/AudioResponseData.cs deleted file mode 100644 index af908ef6..00000000 --- a/Botticelli.Framework.Vk/API/Responses/AudioResponseData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class AudioResponseData -{ - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("audio_message")] - public AudioMessage AudioMessage { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/Document.cs b/Botticelli.Framework.Vk/API/Responses/Document.cs deleted file mode 100644 index 2c6e0cff..00000000 --- a/Botticelli.Framework.Vk/API/Responses/Document.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class Document -{ - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("size")] - public int Size { get; set; } - - [JsonPropertyName("ext")] - public string Ext { get; set; } - - [JsonPropertyName("date")] - public int Date { get; set; } - - [JsonPropertyName("type")] - public int Type { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } - - [JsonPropertyName("is_unsafe")] - public int IsUnsafe { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/DocumentResponseData.cs b/Botticelli.Framework.Vk/API/Responses/DocumentResponseData.cs deleted file mode 100644 index 1f3b9020..00000000 --- a/Botticelli.Framework.Vk/API/Responses/DocumentResponseData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class DocumentResponseData -{ - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("doc")] - public Document Document { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/ErrorResponse.cs b/Botticelli.Framework.Vk/API/Responses/ErrorResponse.cs deleted file mode 100644 index 8d919f15..00000000 --- a/Botticelli.Framework.Vk/API/Responses/ErrorResponse.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -/// -/// Update() response -/// -public class ErrorResponse -{ - /// - /// Offset - /// - [JsonPropertyName("ts")] - public int? Ts { get; set; } - - /// - /// Error code - /// - [JsonPropertyName("failed")] - public int Failed { get; set; } - - /// - /// Min API version - /// - [JsonPropertyName("min_version")] - public int MinVersion { get; set; } - - /// - /// Max API version - /// - [JsonPropertyName("max_version")] - public int MaxVersion { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/GetMessageSessionDataResponse.cs b/Botticelli.Framework.Vk/API/Responses/GetMessageSessionDataResponse.cs deleted file mode 100644 index ca85d3b9..00000000 --- a/Botticelli.Framework.Vk/API/Responses/GetMessageSessionDataResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class GetMessageSessionDataResponse -{ - [JsonPropertyName("response")] - public SessionDataResponse Response { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/GetUploadAddress.cs b/Botticelli.Framework.Vk/API/Responses/GetUploadAddress.cs deleted file mode 100644 index 70b74ed8..00000000 --- a/Botticelli.Framework.Vk/API/Responses/GetUploadAddress.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class GetUploadAddress -{ - [JsonPropertyName("response")] - public GetUploadAddressResponse Response { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/GetUploadAddressResponse.cs b/Botticelli.Framework.Vk/API/Responses/GetUploadAddressResponse.cs deleted file mode 100644 index 3420acb2..00000000 --- a/Botticelli.Framework.Vk/API/Responses/GetUploadAddressResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -// VkSendDocumentResponse myDeserializedClass = JsonSerializer.Deserialize(myJsonResponse); -public class GetUploadAddressResponse -{ - [JsonPropertyName("album_id")] - public int AlbumId { get; set; } - - [JsonPropertyName("upload_url")] - public string UploadUrl { get; set; } - - [JsonPropertyName("user_id")] - public int UserId { get; set; } - - [JsonPropertyName("group_id")] - public int GroupId { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/SendPhotoSize.cs b/Botticelli.Framework.Vk/API/Responses/SendPhotoSize.cs deleted file mode 100644 index 1bcd8e24..00000000 --- a/Botticelli.Framework.Vk/API/Responses/SendPhotoSize.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class SendPhotoSize -{ - [JsonPropertyName("height")] - public int Height { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("width")] - public int Width { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/SessionDataResponse.cs b/Botticelli.Framework.Vk/API/Responses/SessionDataResponse.cs deleted file mode 100644 index 2dcafdf1..00000000 --- a/Botticelli.Framework.Vk/API/Responses/SessionDataResponse.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class SessionDataResponse -{ - [JsonPropertyName("server")] - public string Server { get; set; } - - [JsonPropertyName("key")] - public string Key { get; set; } - - [JsonPropertyName("ts")] - public string Ts { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UpdateEvent.cs b/Botticelli.Framework.Vk/API/Responses/UpdateEvent.cs deleted file mode 100644 index 75b93ab2..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UpdateEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -/// -/// Particular events -/// -public class UpdateEvent -{ - /// - /// Event type - /// - [JsonPropertyName("type")] - public string Type { get; set; } - - /// - /// Id of an event - /// - [JsonPropertyName("event_id")] - public string EventId { get; set; } - - /// - /// Id of a group - /// - [JsonPropertyName("group_id")] - public long GroupId { get; set; } - - /// - /// Inner object - /// - [JsonPropertyName("object")] - public Dictionary Object { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UpdatesResponse.cs b/Botticelli.Framework.Vk/API/Responses/UpdatesResponse.cs deleted file mode 100644 index c9df6bf7..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UpdatesResponse.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -/// -/// Update() response -/// -public class UpdatesResponse -{ - /// - /// Offset - /// - [JsonPropertyName("ts")] - public string Ts { get; set; } - - /// - /// Updated objects - /// - [JsonPropertyName("updates")] - public List Updates { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UploadDocResult.cs b/Botticelli.Framework.Vk/API/Responses/UploadDocResult.cs deleted file mode 100644 index 5905555d..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UploadDocResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class UploadDocResult -{ - [JsonPropertyName("server")] - public int Server { get; set; } - - [JsonPropertyName("file")] - public string File { get; set; } - - [JsonPropertyName("hash")] - public string Hash { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UploadPhotoResponse.cs b/Botticelli.Framework.Vk/API/Responses/UploadPhotoResponse.cs deleted file mode 100644 index b96cbf5c..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UploadPhotoResponse.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class UploadPhotoResponse -{ - [JsonPropertyName("album_id")] - public int AlbumId { get; set; } - - [JsonPropertyName("date")] - public int Date { get; set; } - - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("access_key")] - public string AccessKey { get; set; } - - [JsonPropertyName("sizes")] - public List Sizes { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } - - [JsonPropertyName("user_id")] - public int UserId { get; set; } - - [JsonPropertyName("has_tags")] - public bool HasTags { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UploadPhotoResult.cs b/Botticelli.Framework.Vk/API/Responses/UploadPhotoResult.cs deleted file mode 100644 index ffaab75e..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UploadPhotoResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class UploadPhotoResult -{ - [JsonPropertyName("server")] - public int Server { get; set; } - - [JsonPropertyName("photo")] - public string Photo { get; set; } - - [JsonPropertyName("hash")] - public string Hash { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UploadPhotoSize.cs b/Botticelli.Framework.Vk/API/Responses/UploadPhotoSize.cs deleted file mode 100644 index dd69cd18..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UploadPhotoSize.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class UploadPhotoSize -{ - [JsonPropertyName("height")] - public int Height { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("width")] - public int Width { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/UploadVideoResult.cs b/Botticelli.Framework.Vk/API/Responses/UploadVideoResult.cs deleted file mode 100644 index b92104f1..00000000 --- a/Botticelli.Framework.Vk/API/Responses/UploadVideoResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class UploadVideoResult -{ - [JsonPropertyName("server")] - public int Server { get; set; } - - [JsonPropertyName("video")] - public string Video { get; set; } - - [JsonPropertyName("hash")] - public string Hash { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkErrorEventArgs.cs b/Botticelli.Framework.Vk/API/Responses/VkErrorEventArgs.cs deleted file mode 100644 index 19485f64..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkErrorEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkErrorEventArgs -{ - public VkErrorEventArgs(ErrorResponse response) - { - Response = response; - } - - public ErrorResponse Response { get; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendAudioResponse.cs b/Botticelli.Framework.Vk/API/Responses/VkSendAudioResponse.cs deleted file mode 100644 index da912ac5..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendAudioResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendAudioResponse -{ - [JsonPropertyName("response")] - public AudioResponseData AudioResponseData { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendDocumentResponse.cs b/Botticelli.Framework.Vk/API/Responses/VkSendDocumentResponse.cs deleted file mode 100644 index 581a110e..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendDocumentResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendDocumentResponse -{ - [JsonPropertyName("response")] - public DocumentResponseData DocumentResponseData { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendPhotoPartialResponse.cs b/Botticelli.Framework.Vk/API/Responses/VkSendPhotoPartialResponse.cs deleted file mode 100644 index 828e0a16..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendPhotoPartialResponse.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendPhotoPartialResponse -{ - [JsonPropertyName("album_id")] - public int AlbumId { get; set; } - - [JsonPropertyName("date")] - public int Date { get; set; } - - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("access_key")] - public string AccessKey { get; set; } - - [JsonPropertyName("sizes")] - public List Sizes { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } - - [JsonPropertyName("user_id")] - public int UserId { get; set; } - - [JsonPropertyName("has_tags")] - public bool HasTags { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendPhotoResponse.cs b/Botticelli.Framework.Vk/API/Responses/VkSendPhotoResponse.cs deleted file mode 100644 index 3a2a0c34..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendPhotoResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendPhotoResponse -{ - [JsonPropertyName("response")] - public List? Response { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponse.cs b/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponse.cs deleted file mode 100644 index 76f57cc5..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendVideoResponse -{ - [JsonPropertyName("response")] - public VkSendVideoResponseData Response { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponseData.cs b/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponseData.cs deleted file mode 100644 index 5a450a73..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkSendVideoResponseData.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkSendVideoResponseData -{ - [JsonPropertyName("access_key")] - public string AccessKey { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("owner_id")] - public int OwnerId { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("upload_url")] - public string UploadUrl { get; set; } - - [JsonPropertyName("video_id")] - public int VideoId { get; set; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Responses/VkUpdatesEventArgs.cs b/Botticelli.Framework.Vk/API/Responses/VkUpdatesEventArgs.cs deleted file mode 100644 index 43585ecc..00000000 --- a/Botticelli.Framework.Vk/API/Responses/VkUpdatesEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Botticelli.Framework.Vk.Messages.API.Responses; - -public class VkUpdatesEventArgs -{ - public VkUpdatesEventArgs(UpdatesResponse response) - { - Response = response; - } - - public UpdatesResponse Response { get; } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/API/Utils/ApiUtils.cs b/Botticelli.Framework.Vk/API/Utils/ApiUtils.cs deleted file mode 100644 index 341d958c..00000000 --- a/Botticelli.Framework.Vk/API/Utils/ApiUtils.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Reflection; -using System.Text.Json.Serialization; -using Botticelli.Shared.Utils; -using Flurl; - -namespace Botticelli.Framework.Vk.Messages.API.Utils; - -public static class ApiUtils -{ - public static Uri GetMethodUri(string baseAddress, - string method, - object? methodParams = null, - bool snakeCase = true) - { - if (methodParams == null) return new Uri(Url.Combine(baseAddress, "method", method)); - - if (!snakeCase) return new Uri(Url.Combine(baseAddress, "method", method).SetQueryParams(methodParams)); - - var snaked = new Dictionary(); - var props = methodParams.GetType().GetProperties(); - - - foreach (var prop in props) - { - var value = prop.GetValue(methodParams) ?? string.Empty; - - snaked[prop.Name.ToSnakeCase()] = value; - } - - return new Uri(Url.Combine(baseAddress, "method", method).SetQueryParams(snaked)); - } - - public static MultipartFormDataContent GetMethodMultipartFormContent(object? methodParams = null, - bool snakeCase = true) - { - var props = methodParams.GetType().GetProperties(); - var content = new MultipartFormDataContent(); - - foreach (var prop in props) - { - var value = prop.GetValue(methodParams) as string ?? string.Empty; - - content.Add(new StringContent(value), snakeCase ? prop.Name.ToSnakeCase() : prop.Name); - } - - return content; - } - - public static Uri GetMethodUriWithJson(string baseAddress, string method, object? methodParams = null) - { - var snaked = new Dictionary(); - var props = methodParams.GetType().GetProperties(); - - - foreach (var prop in props) - { - var value = prop.GetValue(methodParams) ?? string.Empty; - - var jpName = prop.GetCustomAttribute(); - - snaked[jpName?.Name ?? prop.Name] = value; - } - - return new Uri(Url.Combine(baseAddress, "method", method).SetQueryParams(snaked)); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj b/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj deleted file mode 100644 index 6b24b8a3..00000000 --- a/Botticelli.Framework.Vk/Botticelli.Framework.Vk.Messages.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - net8.0 - enable - enable - 0.8.0 - Botticelli.Framework.Vk.Messages - BotticelliBots - new_logo_compact.png - Botticelli VK messenger integration - BotticelliBots - https://botticellibots.com - https://github.com/devgopher/botticelli - telegram, bots, botticelli, vk, facebook, wechat, whatsapp - true - - - - - True - - new_logo_compact.png - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Builders/LongPollMessagesProviderBuilder.cs b/Botticelli.Framework.Vk/Builders/LongPollMessagesProviderBuilder.cs deleted file mode 100644 index bba1f63a..00000000 --- a/Botticelli.Framework.Vk/Builders/LongPollMessagesProviderBuilder.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Botticelli.Framework.Options; -using Botticelli.Framework.Vk.Messages.Options; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages.Builders; - -/// -/// Builds a Long poll message provider|receiver -/// -public class LongPollMessagesProviderBuilder -{ - private readonly BotSettingsBuilder _settingsBuilder; - private IHttpClientFactory _httpClientFactory; - private ILogger _logger; - - private LongPollMessagesProviderBuilder(BotSettingsBuilder settingsBuilder) - { - _settingsBuilder = settingsBuilder; - } - - public static LongPollMessagesProviderBuilder Instance(BotSettingsBuilder settingsBuilder) - { - return new LongPollMessagesProviderBuilder(settingsBuilder); - } - - public LongPollMessagesProviderBuilder AddLogger(ILogger logger) - { - _logger = logger; - - return this; - } - - public LongPollMessagesProviderBuilder AddHttpClientFactory(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - - return this; - } - - public LongPollMessagesProvider Build() - { - return new LongPollMessagesProvider(_settingsBuilder.Build(), _httpClientFactory, _logger); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Builders/MessagePublisherBuilder.cs b/Botticelli.Framework.Vk/Builders/MessagePublisherBuilder.cs deleted file mode 100644 index c552870c..00000000 --- a/Botticelli.Framework.Vk/Builders/MessagePublisherBuilder.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Botticelli.Framework.Options; -using Botticelli.Framework.Vk.Messages.Options; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages.Builders; - -/// -/// Builds a MessagePublisher -/// -public class MessagePublisherBuilder -{ - private readonly BotSettingsBuilder _settingsBuilder; - private IHttpClientFactory _httpClientFactory; - private ILogger _logger; - - private MessagePublisherBuilder(BotSettingsBuilder settingsBuilder) - { - _settingsBuilder = settingsBuilder; - } - - public static MessagePublisherBuilder Instance(BotSettingsBuilder settingsBuilder) - { - return new MessagePublisherBuilder(settingsBuilder); - } - - public MessagePublisherBuilder AddLogger(ILogger logger) - { - _logger = logger; - - return this; - } - - public MessagePublisherBuilder AddHttpClientFactory(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - - return this; - } - - public MessagePublisher Build() - { - return new MessagePublisher(_httpClientFactory, _logger); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs b/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs deleted file mode 100644 index 95bbc6f1..00000000 --- a/Botticelli.Framework.Vk/Builders/VkBotBuilder.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Botticelli.Bot.Data; -using Botticelli.Bot.Data.Repositories; -using Botticelli.Bot.Data.Settings; -using Botticelli.Bot.Utils; -using Botticelli.Bot.Utils.TextUtils; -using Botticelli.Client.Analytics; -using Botticelli.Client.Analytics.Settings; -using Botticelli.Framework.Builders; -using Botticelli.Framework.Extensions; -using Botticelli.Framework.Options; -using Botticelli.Framework.Security; -using Botticelli.Framework.Services; -using Botticelli.Framework.Vk.Messages.Handlers; -using Botticelli.Framework.Vk.Messages.HostedService; -using Botticelli.Framework.Vk.Messages.Options; -using Botticelli.Framework.Vk.Messages.Utils; -using Botticelli.Interfaces; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages.Builders; - -public class VkBotBuilder : BotBuilder -{ - private LongPollMessagesProvider? _longPollMessagesProvider; - private LongPollMessagesProviderBuilder? _longPollMessagesProviderBuilder; - private MessagePublisher? _messagePublisher; - private MessagePublisherBuilder? _messagePublisherBuilder; - private VkStorageUploader? _vkStorageUploader; - private VkStorageUploaderBuilder? _vkStorageUploaderBuilder; - - private VkBotSettings? BotSettings { get; set; } - - protected override VkBot InnerBuild() - { - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); - Services!.AddHttpClient() - .AddServerCertificates(BotSettings); - Services!.AddHttpClient>() - .AddServerCertificates(BotSettings); - Services!.AddHostedService(); - Services!.AddHostedService>>(); - - Services!.AddHostedService(); - var botId = BotDataUtils.GetBotId(); - - if (botId == null) throw new InvalidDataException($"{nameof(botId)} shouldn't be null!"); - - #region Metrics - - var metricsPublisher = new MetricsPublisher(AnalyticsClientSettingsBuilder.Build()); - var metricsProcessor = new MetricsProcessor(metricsPublisher); - Services!.AddSingleton(metricsPublisher); - Services!.AddSingleton(metricsProcessor); - - #endregion - - #region Data - - Services!.AddDbContext(o => - o.UseSqlite($"Data source={BotDataAccessSettingsBuilder.Build().ConnectionString}")); - Services!.AddScoped(); - - #endregion - - #region TextTransformer - - Services!.AddTransient(); - - #endregion - - _longPollMessagesProvider = _longPollMessagesProviderBuilder.Build(); - _messagePublisher = _messagePublisherBuilder.Build(); - _vkStorageUploader = _vkStorageUploaderBuilder.Build(); - - Services!.AddBotticelliFramework() - .AddSingleton(); - - var sp = Services!.BuildServiceProvider(); - - return new VkBot(_longPollMessagesProvider, - _messagePublisher, - _vkStorageUploader, - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService>()); - } - - protected virtual VkBotBuilder AddBotSettings(BotSettingsBuilder settingsBuilder) - where TBotSettings : BotSettings, new() - { - BotSettings = settingsBuilder.Build() as VkBotSettings ?? throw new InvalidOperationException(); - - return this; - } - - public VkBotBuilder AddClient(LongPollMessagesProviderBuilder builder) - { - _longPollMessagesProviderBuilder = builder; - - return this; - } - - public static VkBotBuilder Instance(IServiceCollection services, - ServerSettingsBuilder serverSettingsBuilder, - BotSettingsBuilder settingsBuilder, - DataAccessSettingsBuilder dataAccessSettingsBuilder, - AnalyticsClientSettingsBuilder analyticsClientSettingsBuilder) - { - return (VkBotBuilder) new VkBotBuilder() - .AddBotSettings(settingsBuilder) - .AddServerSettings(serverSettingsBuilder) - .AddServices(services) - .AddAnalyticsSettings(analyticsClientSettingsBuilder) - .AddBotDataAccessSettings(dataAccessSettingsBuilder); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Builders/VkStorageUploaderBuilder.cs b/Botticelli.Framework.Vk/Builders/VkStorageUploaderBuilder.cs deleted file mode 100644 index 204a3658..00000000 --- a/Botticelli.Framework.Vk/Builders/VkStorageUploaderBuilder.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Botticelli.Audio; -using Botticelli.Framework.Options; -using Botticelli.Framework.Vk.Messages.Options; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages.Builders; - -/// -/// Builder for VK media uploader -/// -public class VkStorageUploaderBuilder -{ - private IConvertor _audioConvertor; - private IHttpClientFactory _httpClientFactory; - private ILogger _logger; - - private VkStorageUploaderBuilder() - { - } - - public static VkStorageUploaderBuilder Instance(BotSettingsBuilder settingsBuilder) - { - return new VkStorageUploaderBuilder(); - } - - public VkStorageUploaderBuilder AddLogger(ILogger logger) - { - _logger = logger; - - return this; - } - - public VkStorageUploaderBuilder AddHttpClientFactory(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - - return this; - } - - public VkStorageUploaderBuilder AddAudioConvertor(IConvertor audioConvertor) - { - _audioConvertor = audioConvertor; - - return this; - } - - public VkStorageUploader Build() - { - return new VkStorageUploader(_httpClientFactory, _audioConvertor, _logger); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 87dbbf4b..00000000 --- a/Botticelli.Framework.Vk/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Configuration; -using Botticelli.Bot.Data.Settings; -using Botticelli.Client.Analytics.Settings; -using Botticelli.Controls.Parsers; -using Botticelli.Framework.Options; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Framework.Vk.Messages.Builders; -using Botticelli.Framework.Vk.Messages.Layout; -using Botticelli.Framework.Vk.Messages.Options; -using Botticelli.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Botticelli.Framework.Vk.Messages.Extensions; - -public static class ServiceCollectionExtensions -{ - private static readonly BotSettingsBuilder SettingsBuilder = new(); - private static readonly ServerSettingsBuilder ServerSettingsBuilder = new(); - - private static readonly AnalyticsClientSettingsBuilder AnalyticsClientOptionsBuilder = - new(); - - private static readonly DataAccessSettingsBuilder DataAccessSettingsBuilder = new(); - - public static IServiceCollection AddVkBot(this IServiceCollection services, IConfiguration configuration) - { - var vkBotSettings = configuration - .GetSection(VkBotSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(VkBotSettings)}!"); - - var analyticsClientSettings = configuration - .GetSection(AnalyticsClientSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(AnalyticsClientSettings)}!"); - - var serverSettings = configuration - .GetSection(ServerSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(ServerSettings)}!"); - - var dataAccessSettings = configuration - .GetSection(DataAccessSettings.Section) - .Get() ?? - throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); - ; - - return services.AddVkBot(vkBotSettings, - analyticsClientSettings, - serverSettings, - dataAccessSettings); - } - - public static IServiceCollection AddVkBot(this IServiceCollection services, - VkBotSettings botSettings, - AnalyticsClientSettings analyticsClientSettings, - ServerSettings serverSettings, - DataAccessSettings dataAccessSettings) - { - return services.AddVkBot(o => o.Set(botSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings)); - } - - /// - /// Adds a Vk bot - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddVkBot(this IServiceCollection services, - Action> optionsBuilderFunc, - Action> analyticsOptionsBuilderFunc, - Action> serverSettingsBuilderFunc, - Action> dataAccessSettingsBuilderFunc) - { - optionsBuilderFunc(SettingsBuilder); - serverSettingsBuilderFunc(ServerSettingsBuilder); - analyticsOptionsBuilderFunc(AnalyticsClientOptionsBuilder); - dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); - - var clientBuilder = LongPollMessagesProviderBuilder.Instance(SettingsBuilder); - - var botBuilder = VkBotBuilder.Instance(services, - ServerSettingsBuilder, - SettingsBuilder, - DataAccessSettingsBuilder, - AnalyticsClientOptionsBuilder) - .AddClient(clientBuilder); - var bot = botBuilder.Build(); - - return services.AddSingleton>(bot) - .AddSingleton(bot); - } - - public static IServiceCollection AddVkLayoutsSupport(this IServiceCollection services) - { - return services.AddSingleton() - .AddSingleton, VkLayoutSupplier>() - .AddSingleton, - LayoutLoader, VkKeyboardMarkup>>(); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Vk/Handlers/BotUpdateHandler.cs deleted file mode 100644 index a5b2e241..00000000 --- a/Botticelli.Framework.Vk/Handlers/BotUpdateHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Botticelli.Framework.Commands.Processors; -using Botticelli.Framework.Vk.Messages.API.Responses; -using Botticelli.Shared.Utils; -using Botticelli.Shared.ValueObjects; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages.Handlers; - -public class BotUpdateHandler : IBotUpdateHandler -{ - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - - public BotUpdateHandler(ILogger logger, IServiceProvider serviceProvider) - { - _logger = logger; - _serviceProvider = serviceProvider; - } - - public async Task HandleUpdateAsync(List update, CancellationToken cancellationToken) - { - _logger.LogDebug($"{nameof(HandleUpdateAsync)}() started..."); - - var botMessages = update? - .Where(x => x.Type == "message_new") - .ToList(); - - var messagesText = botMessages?.Select(bm => - new - { - eventId = bm.EventId, - message = bm.Object["message"] - }); - - foreach (var botMessage in messagesText.EmptyIfNull()) - try - { - var eventId = botMessage.eventId; - var peerId = botMessage.message["peer_id"]?.ToString(); - var text = botMessage.message["text"]?.ToString(); - var fromId = botMessage.message["from_id"]?.ToString(); - - var botticelliMessage = new Message(eventId) - { - ChatIds = - [ - peerId.EmptyIfNull(), - fromId.EmptyIfNull() - ], - Subject = string.Empty, - Body = text, - Attachments = new List(5), - From = new User - { - Id = fromId - }, - ForwardedFrom = null, - Location = null! - // LastModifiedAt = botMessage. - }; - - await Process(botticelliMessage, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, ex.Message); - } - - _logger.LogDebug($"{nameof(HandleUpdateAsync)}() finished..."); - } - - public event IBotUpdateHandler.MsgReceivedEventHandler? MessageReceived; - - /// - /// Processes requests - /// - /// - /// - protected Task Process(Message message, CancellationToken token) - { - _logger.LogDebug($"{nameof(Process)}({message.Uid}) started..."); - - if (token is {CanBeCanceled: true, IsCancellationRequested: true}) return Task.CompletedTask; - - var clientNonChainedTasks = _serviceProvider.GetServices() - .Where(p => !p.GetType().IsAssignableTo(typeof(ICommandChainProcessor))) - .Select(p => p.ProcessAsync(message, token)); - - var clientChainedTasks = _serviceProvider.GetServices() - .Select(p => p.ProcessAsync(message, token)); - - Task.WaitAll(clientNonChainedTasks.Concat(clientChainedTasks).ToArray(), token); - - _logger.LogDebug($"{nameof(Process)}({message.Uid}) finished..."); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Handlers/IBotUpdateHandler.cs b/Botticelli.Framework.Vk/Handlers/IBotUpdateHandler.cs deleted file mode 100644 index 9934ffb8..00000000 --- a/Botticelli.Framework.Vk/Handlers/IBotUpdateHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Botticelli.Framework.Events; -using Botticelli.Framework.Vk.Messages.API.Responses; - -namespace Botticelli.Framework.Vk.Messages.Handlers; - -public interface IBotUpdateHandler -{ - public delegate void MsgReceivedEventHandler(object sender, MessageReceivedBotEventArgs e); - - public Task HandleUpdateAsync(List update, CancellationToken cancellationToken); - - public event MsgReceivedEventHandler MessageReceived; -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/HostedService/VkBotHostedService.cs b/Botticelli.Framework.Vk/HostedService/VkBotHostedService.cs deleted file mode 100644 index 67429b3f..00000000 --- a/Botticelli.Framework.Vk/HostedService/VkBotHostedService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Botticelli.Interfaces; -using Botticelli.Shared.API.Admin.Requests; -using Microsoft.Extensions.Hosting; - -namespace Botticelli.Framework.Vk.Messages.HostedService; - -public class VkBotHostedService : IHostedService -{ - private readonly IBot _bot; - - public VkBotHostedService(IBot bot) - { - _bot = bot; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - await _bot.StartBotAsync(StartBotRequest.GetInstance(), CancellationToken.None); - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - await _bot.StopBotAsync(StopBotRequest.GetInstance(), CancellationToken.None); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs b/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs deleted file mode 100644 index 6b82eac1..00000000 --- a/Botticelli.Framework.Vk/Layout/IVkLayoutSupplier.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Botticelli.Controls.Parsers; -using Botticelli.Framework.Vk.Messages.API.Markups; - -namespace Botticelli.Framework.Vk.Messages.Layout; - -public interface IVkLayoutSupplier : ILayoutSupplier -{ -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs b/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs deleted file mode 100644 index a578d51c..00000000 --- a/Botticelli.Framework.Vk/Layout/VkLayoutSupplier.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Botticelli.Controls.BasicControls; -using Botticelli.Controls.Exceptions; -using Botticelli.Controls.Extensions; -using Botticelli.Controls.Layouts; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Shared.Utils; -using Action = Botticelli.Framework.Vk.Messages.API.Markups.Action; - -namespace Botticelli.Framework.Vk.Messages.Layout; - -public class VkLayoutSupplier : IVkLayoutSupplier -{ - public VkKeyboardMarkup GetMarkup(ILayout layout) - { - if (layout == null) throw new LayoutException("Layout = null!"); - - layout.Rows.NotNull(); - - var buttons = new List>(10); - - foreach (var layoutRow in layout.Rows.EmptyIfNull()) - { - var keyboardElement = new List(); - - keyboardElement.AddRange(layoutRow.Items.Where(i => i.Control != null) - .Select(item => - { - item.Control.NotNull(); - item.Control.Content.NotNull(); - item.Control.MessengerSpecificParams.NotNull(); - - var controlParams = item.Control.MessengerSpecificParams.ContainsKey("VK") ? - item.Control?.MessengerSpecificParams["VK"] : - new Dictionary(); - - controlParams.NotNull(); - - var action = new Action - { - Type = item.Control is TextButton ? "text" : "button", - Payload = $"{{\"button\": \"{layout.Rows.IndexOf(layoutRow)}\"}}", - Label = item.Control!.Content, - AppId = controlParams.ReturnValueOrDefault("AppId"), - OwnerId = controlParams.ReturnValueOrDefault("OwnerId"), - Hash = controlParams.ReturnValueOrDefault("Hash") - }; - - return new VkItem - { - Action = action, - Color = controlParams.ReturnValueOrDefault("Color") - }; - })); - - buttons.Add(keyboardElement); - } - - var markup = new VkKeyboardMarkup - { - OneTime = true, - Buttons = buttons - }; - - return markup; - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/LongPollMessagesProvider.cs b/Botticelli.Framework.Vk/LongPollMessagesProvider.cs deleted file mode 100644 index fa0dc8a7..00000000 --- a/Botticelli.Framework.Vk/LongPollMessagesProvider.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Text.Json; -using Botticelli.Framework.Exceptions; -using Botticelli.Framework.Vk.Messages.API.Responses; -using Botticelli.Framework.Vk.Messages.API.Utils; -using Botticelli.Framework.Vk.Messages.Options; -using Flurl; -using Flurl.Http; -using Microsoft.Extensions.Logging; -using Polly; - -namespace Botticelli.Framework.Vk.Messages; - -/// -/// Long poll method provider -/// https://dev.vk.com/ru/api/bots/getting-started -/// -public class LongPollMessagesProvider : IDisposable -{ - public delegate void GotError(VkErrorEventArgs args, CancellationToken token); - - public delegate void GotUpdates(VkUpdatesEventArgs args, CancellationToken token); - - private readonly int? _groupId = 0; - - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - private readonly VkBotSettings _settings; - private readonly object _syncObj = new(); - private readonly CancellationTokenSource _tokenSource; - private string _apiKey; - private HttpClient _client; - private bool _isStarted; - private string _key; - private int? _lastTs = 0; - private string _server; - - public LongPollMessagesProvider(VkBotSettings settings, - IHttpClientFactory httpClientFactory, - ILogger logger) - { - _settings = settings; - _httpClientFactory = httpClientFactory; - _logger = logger; - _tokenSource = new CancellationTokenSource(); - _groupId = settings.GroupId; - } - - private string ApiVersion => "5.199"; - - public void Dispose() - { - var stopTask = Stop(); - stopTask.Wait(5000); - _client?.Dispose(); - } - - public event GotUpdates OnUpdates; - public event GotError OnError; - - - public void SetApiKey(string key) - { - _apiKey = key; - } - - public async Task Start(CancellationToken token) - { - try - { - lock (_syncObj) - { - if (_isStarted && - !string.IsNullOrWhiteSpace(_key) && - !string.IsNullOrWhiteSpace(_server) && - !string.IsNullOrWhiteSpace(_apiKey)) - return; - } - - _client = _httpClientFactory.CreateClient(); - - // 1. Get Session - await GetSessionData(); - - // 2. Start polling - if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_server)) - { - _logger.LogError($"{nameof(_key)} or {nameof(_server)} are null or empty!"); - - return; - } - - - int[] codesForRetry = [408, 500, 502, 503, 504]; - - var updatePolicy = Policy - .Handle(ex => - { - _logger.LogError(ex, $"Long polling error! session: {_key}, server: {_server}"); - - return codesForRetry.Contains(ex.Call.Response.StatusCode); - }) - .WaitAndRetryAsync(3, n => n * TimeSpan.FromMilliseconds(_settings.PollIntervalMs)); - - - var repeatPolicy = Policy.HandleResult(r => true) - .WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(_settings.PollIntervalMs)); - var pollingTask = repeatPolicy.WrapAsync(updatePolicy) - .ExecuteAsync(async () => - { - try - { - var updatesResponse = await $"{_server}".SetQueryParams(new - { - act = "a_check", - key = _key, - wait = 90, - ts = _lastTs, - mode = 2, - v = ApiVersion - }) - .GetStringAsync(cancellationToken: _tokenSource.Token); - - var updates = JsonSerializer.Deserialize(updatesResponse); - - - if (updates?.Updates == null) - { - var error = JsonSerializer.Deserialize(updatesResponse); - - if (error == null) return null; - - _lastTs = error.Ts ?? _lastTs; - OnError?.Invoke(new VkErrorEventArgs(error), token); - - return null; - } - - _lastTs = int.Parse(updates?.Ts ?? "0"); - - if (updates?.Updates != null) OnUpdates?.Invoke(new VkUpdatesEventArgs(updates), token); - - return updates; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Long polling error: {ex.Message}"); - } - - return null; - }); - - _isStarted = true; - await pollingTask; - } - catch (Exception ex) when (ex is not BotException) - { - _logger.LogError(ex, $"Can't start a {nameof(LongPollMessagesProvider)}!"); - } - } - - private async Task GetSessionData() - { - var request = new HttpRequestMessage(HttpMethod.Get, - ApiUtils.GetMethodUri("https://api.vk.com", - "groups.getLongPollServer", - new - { - access_token = _apiKey, - group_id = _groupId, - v = ApiVersion - })); - var response = await _client.SendAsync(request, _tokenSource.Token); - var resultString = await response.Content.ReadAsStringAsync(); - - var result = JsonSerializer.Deserialize(resultString); - - _server = result?.Response?.Server; - _key = result?.Response?.Key; - _lastTs = int.Parse(result?.Response?.Ts ?? "0"); - } - - public async Task Stop() - { - _client?.CancelPendingRequests(); - _tokenSource.Cancel(false); - _key = string.Empty; - _server = string.Empty; - _isStarted = false; - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/MessagePublisher.cs b/Botticelli.Framework.Vk/MessagePublisher.cs deleted file mode 100644 index 290e740a..00000000 --- a/Botticelli.Framework.Vk/MessagePublisher.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Net; -using Botticelli.Framework.Vk.Messages.API.Requests; -using Botticelli.Framework.Vk.Messages.API.Utils; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages; - -public class MessagePublisher -{ - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - private string _apiKey; - - public MessagePublisher(IHttpClientFactory httpClientFactory, ILogger logger) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public void SetApiKey(string key) - { - _apiKey = key; - } - - - public async Task SendAsync(VkSendMessageRequest vkMessageRequest, CancellationToken token) - { - try - { - vkMessageRequest.AccessToken = _apiKey; - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, - ApiUtils.GetMethodUriWithJson("https://api.vk.com", - "messages.send", - vkMessageRequest)); - - var response = await httpClient.SendAsync(request, token); - if (response.StatusCode != HttpStatusCode.OK) _logger.LogError($"Error sending a message: {response.StatusCode} {response.ReasonPhrase}!"); - - var responseContent = await response.Content.ReadAsStringAsync(token); - - if (string.IsNullOrEmpty(responseContent)) _logger.LogError($"Error sending a message {responseContent}"); - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "Error sending a message!"); - } - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Options/VkBotSettings.cs b/Botticelli.Framework.Vk/Options/VkBotSettings.cs deleted file mode 100644 index 346e7e29..00000000 --- a/Botticelli.Framework.Vk/Options/VkBotSettings.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Botticelli.Framework.Options; - -namespace Botticelli.Framework.Vk.Messages.Options; - -/// -public class VkBotSettings : BotSettings -{ - public int PollIntervalMs { get; set; } = 500; - public int GroupId { get; set; } - public static string Section => "VkBot"; -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/Utils/VkTextTransformer.cs b/Botticelli.Framework.Vk/Utils/VkTextTransformer.cs deleted file mode 100644 index 3a279211..00000000 --- a/Botticelli.Framework.Vk/Utils/VkTextTransformer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text; -using Botticelli.Bot.Utils.TextUtils; - -namespace Botticelli.Framework.Vk.Messages.Utils; - -public class VkTextTransformer : ITextTransformer -{ - /// - /// Autoescape for special symbols - /// - /// - /// - public StringBuilder Escape(StringBuilder text) - { - return text.Replace("!", @"\!") - .Replace("*", @"\*") - .Replace("'", @"\'") - .Replace(".", @"\.") - .Replace("+", @"\+") - .Replace("~", @"\~") - .Replace("@", @"\@") - .Replace("_", @"\_") - .Replace("(", @"\(") - .Replace(")", @"\)") - .Replace("-", @"\-") - .Replace("`", @"\`") - .Replace("=", @"\=") - .Replace(">", @"\>") - .Replace("<", @"\<") - .Replace("{", @"\{") - .Replace("}", @"\}") - .Replace("[", @"\[") - .Replace("]", @"\]") - .Replace("|", @"\|") - .Replace("#", @"\#"); - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/VkBot.cs b/Botticelli.Framework.Vk/VkBot.cs deleted file mode 100644 index 23bd10d9..00000000 --- a/Botticelli.Framework.Vk/VkBot.cs +++ /dev/null @@ -1,325 +0,0 @@ -using Botticelli.Bot.Data.Repositories; -using Botticelli.Client.Analytics; -using Botticelli.Framework.Events; -using Botticelli.Framework.Exceptions; -using Botticelli.Framework.Global; -using Botticelli.Framework.Vk.Messages.API.Requests; -using Botticelli.Framework.Vk.Messages.API.Responses; -using Botticelli.Framework.Vk.Messages.Handlers; -using Botticelli.Interfaces; -using Botticelli.Shared.API; -using Botticelli.Shared.API.Admin.Requests; -using Botticelli.Shared.API.Admin.Responses; -using Botticelli.Shared.API.Client.Requests; -using Botticelli.Shared.API.Client.Responses; -using Botticelli.Shared.Constants; -using Botticelli.Shared.Utils; -using Botticelli.Shared.ValueObjects; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages; - -public class VkBot : BaseBot -{ - private readonly IBotDataAccess _data; - private readonly IBotUpdateHandler _handler; - private readonly MessagePublisher? _messagePublisher; - private readonly LongPollMessagesProvider _messagesProvider; - private readonly VkStorageUploader? _vkUploader; - private bool _eventsAttached; - - public VkBot(LongPollMessagesProvider messagesProvider, - MessagePublisher? messagePublisher, - VkStorageUploader? vkUploader, - IBotDataAccess data, - IBotUpdateHandler handler, - MetricsProcessor metrics, - ILogger logger) : base(logger, metrics) - { - _messagesProvider = messagesProvider; - _messagePublisher = messagePublisher; - _data = data; - _handler = handler; - _vkUploader = vkUploader; - BotUserId = null; // TODO get it from VK - } - - public override BotType Type => BotType.Vk; - - - protected override async Task InnerStopBotAsync(StopBotRequest request, CancellationToken token) - { - try - { - await _messagesProvider.Stop(); - - return StopBotResponse.GetInstance(request.Uid, string.Empty, AdminCommandStatus.Ok); - } - catch (Exception ex) - { - Logger.LogError(ex, ex.Message); - } - - return StopBotResponse.GetInstance(request.Uid, "Error stopping a bot", AdminCommandStatus.Fail); - } - - protected override async Task InnerStartBotAsync(StartBotRequest request, CancellationToken token) - { - try - { - Logger.LogInformation($"{nameof(StartBotAsync)}..."); - var response = StartBotResponse.GetInstance(AdminCommandStatus.Ok, ""); - - if (BotStatusKeeper.IsStarted) - { - Logger.LogInformation($"{nameof(StartBotAsync)}: already started"); - - return response; - } - - if (response.Status != AdminCommandStatus.Ok || BotStatusKeeper.IsStarted) return response; - - if (!_eventsAttached) - { - _messagesProvider.OnUpdates += (args, ct) => - { - var updates = args?.Response?.Updates; - - if (updates == null || !updates.Any()) return; - - _handler.HandleUpdateAsync(updates, ct); - }; - - _eventsAttached = true; - } - - await _messagesProvider.Start(token); - - BotStatusKeeper.IsStarted = true; - Logger.LogInformation($"{nameof(StartBotAsync)}: started"); - - return response; - } - catch (Exception ex) - { - Logger.LogError(ex, ex.Message); - } - - return StartBotResponse.GetInstance(AdminCommandStatus.Fail, "error"); - } - - public override async Task SetBotContext(BotData.Entities.Bot.BotData? context, CancellationToken token) - { - if (context == null) return; - - var currentContext = _data.GetData(); - - if (currentContext?.BotKey != context.BotKey) - { - var stopRequest = StopBotRequest.GetInstance(); - var startRequest = StartBotRequest.GetInstance(); - await StopBotAsync(stopRequest, token); - - _data.SetData(context); - - await _messagesProvider.Stop(); - SetApiKey(context); - - await _messagesProvider.Start(token); - await StartBotAsync(startRequest, token); - } - else - { - SetApiKey(context); - } - } - - private void SetApiKey(BotData.Entities.Bot.BotData? context) - { - _messagesProvider.SetApiKey(context.BotKey); - _messagePublisher.SetApiKey(context.BotKey); - _vkUploader.SetApiKey(context.BotKey); - } - - private string CreateVkAttach(VkSendPhotoResponse fk, string type) - { - return $"{type}" + - $"{fk.Response?.FirstOrDefault()?.OwnerId.ToString()}" + - $"_{fk.Response?.FirstOrDefault()?.Id.ToString()}"; - } - - - private string CreateVkAttach(VkSendVideoResponse fk, string type) - { - return $"{type}" + - $"{fk.Response?.OwnerId.ToString()}" + - $"_{fk.Response?.VideoId.ToString()}"; - } - - - private string CreateVkAttach(VkSendAudioResponse fk, string type) - { - return $"{type}" + - $"{fk.AudioResponseData.AudioMessage.OwnerId}" + - $"_{fk.AudioResponseData.AudioMessage.Id}"; - } - - private string CreateVkAttach(VkSendDocumentResponse fk, string type) - { - return $"{type}" + - $"{fk.DocumentResponseData.Document.OwnerId}" + - $"_{fk.DocumentResponseData.Document.Id}"; - } - - - protected override async Task InnerSendMessageAsync(SendMessageRequest request, - ISendOptionsBuilder? optionsBuilder, - bool isUpdate, - CancellationToken token) - { - foreach (var peerId in request.Message.ChatIds) - try - { - var requests = await CreateRequestsWithAttachments(request, - peerId, - token); - - foreach (var vkRequest in requests) await _messagePublisher.SendAsync(vkRequest, token); - } - catch (Exception? ex) - { - throw new BotException("Can't send a message!", ex); - } - - MessageSent.Invoke(this, - new MessageSentBotEventArgs - { - Message = request.Message - }); - - return new SendMessageResponse(request.Uid, string.Empty); - } - - protected override Task InnerDeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token) - { - throw new NotImplementedException(); - } - - private async Task> CreateRequestsWithAttachments(SendMessageRequest request, - string peerId, - CancellationToken token) - { - var currentContext = _data.GetData(); - var result = new List(100); - var first = true; - - currentContext.NotNull(); - currentContext.BotKey.NotNull(); - - if (request.Message.Attachments == null) - { - var vkRequest = new VkSendMessageRequest - { - AccessToken = currentContext.BotKey, - PeerId = peerId, - Body = first ? request.Message.Body : string.Empty, - Lat = request.Message.Location.Latitude, - Long = request.Message.Location.Longitude, - ReplyTo = request.Message.ReplyToMessageUid, - Attachment = null - }; - result.Add(vkRequest); - - return result; - } - - foreach (var attach in request.Message.Attachments) - try - { - var vkRequest = new VkSendMessageRequest - { - AccessToken = currentContext.BotKey, - //UserId = peerId, - Body = first ? request?.Message.Body : string.Empty, - Lat = request?.Message.Location?.Latitude, - Long = request?.Message.Location?.Longitude, - ReplyTo = request?.Message.ReplyToMessageUid, - PeerId = peerId, - Attachment = null - }; - - switch (attach) - { - case BinaryBaseAttachment ba: - { - switch (ba) - { - case {MediaType: MediaType.Image}: - case {MediaType: MediaType.Sticker}: - var sendPhotoResponse = await _vkUploader.SendPhotoAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendPhotoResponse != null) vkRequest.Attachment = CreateVkAttach(sendPhotoResponse, "photo"); - - break; - case {MediaType: MediaType.Video}: - //var sendVideoResponse = await _vkUploader.SendVideoAsync(vkRequest, - // ba.Name, - // ba.Data, - // token); - - //if (sendVideoResponse != default) vkRequest.Attachment = CreateVkAttach(sendVideoResponse, currentContext, "video"); - - break; - case {MediaType: MediaType.Voice}: - case {MediaType: MediaType.Audio}: - var sendAudioMessageResponse = await _vkUploader.SendAudioMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendAudioMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendAudioMessageResponse, "doc"); - - - break; - case {MediaType: MediaType.Document}: - var sendDocMessageResponse = await _vkUploader.SendDocsMessageAsync(vkRequest, - ba.Name, - ba.Data, - token); - if (sendDocMessageResponse != null) vkRequest.Attachment = CreateVkAttach(sendDocMessageResponse, "doc"); - - - break; - } - } - - break; - case InvoiceBaseAttachment: - // Not implemented - break; - } - - result.Add(vkRequest); - first = false; - } - catch (Exception ex) - { - Logger.LogInformation($"Error sending a message with attach: {attach.Uid}", ex); - } - - return result; - } - - public override Task DeleteMessageAsync(DeleteMessageRequest request, - CancellationToken token) - { - throw new NotImplementedException(); - } - - public virtual event MsgSentEventHandler MessageSent; - public virtual event MsgReceivedEventHandler MessageReceived; - public virtual event MsgRemovedEventHandler MessageRemoved; - public virtual event MessengerSpecificEventHandler MessengerSpecificEvent; -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/VkStorageUploader.cs b/Botticelli.Framework.Vk/VkStorageUploader.cs deleted file mode 100644 index deafc66d..00000000 --- a/Botticelli.Framework.Vk/VkStorageUploader.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System.Net.Http.Json; -using Botticelli.Audio; -using Botticelli.Framework.Exceptions; -using Botticelli.Framework.Vk.Messages.API.Requests; -using Botticelli.Framework.Vk.Messages.API.Responses; -using Botticelli.Framework.Vk.Messages.API.Utils; -using Microsoft.Extensions.Logging; - -namespace Botticelli.Framework.Vk.Messages; - -/// -/// VK storage upload component -/// -public class VkStorageUploader -{ - private readonly IConvertor _audioConvertor; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - private string _apiKey; - - public VkStorageUploader(IHttpClientFactory httpClientFactory, - IConvertor audioConvertor, - ILogger logger) - { - _httpClientFactory = httpClientFactory; - _audioConvertor = audioConvertor; - _logger = logger; - } - - private string ApiVersion => "5.199"; - - public void SetApiKey(string key) - { - _apiKey = key; - } - - /// - /// Get an upload address for a photo - /// - /// - /// - /// - private async Task GetPhotoUploadAddress(VkSendMessageRequest vkMessageRequest, - CancellationToken token) - { - try - { - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, - ApiUtils.GetMethodUri("https://api.vk.com", - "photos.getMessagesUploadServer", - new - { - access_token = _apiKey, - v = ApiVersion, - peer_id = vkMessageRequest.PeerId - })); - - var response = await httpClient.SendAsync(request, token); - - return await response.Content.ReadFromJsonAsync(token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting an upload address!"); - } - - return null; - } - - - /// - /// Get an upload address for an audio - /// - /// - /// - /// - private async Task GetAudioUploadAddress(VkSendMessageRequest vkMessageRequest, - CancellationToken token) - { - return await GetDocsUploadAddress(vkMessageRequest, "audio_message", token); - } - - private async Task GetDocsUploadAddress(VkSendMessageRequest vkMessageRequest, - string type, - CancellationToken token) - { - try - { - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, - ApiUtils.GetMethodUri("https://api.vk.com", - "docs.getMessagesUploadServer", - new - { - access_token = _apiKey, - v = ApiVersion, - peer_id = vkMessageRequest.PeerId, - type - })); - - var response = await httpClient.SendAsync(request, token); - - return await response.Content.ReadFromJsonAsync(token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting an upload address!"); - } - - return null; - } - - /// - /// Uploads a photo (binaries) - /// - /// - /// - /// - /// - /// - private async Task UploadPhoto(string uploadUrl, - string name, - byte[] binaryContent, - CancellationToken token) - { - using var httpClient = _httpClientFactory.CreateClient(); - using var memoryContentStream = new MemoryStream(binaryContent); - memoryContentStream.Seek(0, SeekOrigin.Begin); - - var content = new MultipartFormDataContent {{new StreamContent(memoryContentStream), "photo", name}}; - - var response = await httpClient.PostAsync(uploadUrl, content, token); - - return await response.Content.ReadFromJsonAsync(token); - } - - - /// - /// Uploads an audio message (binaries) - /// - /// - /// - /// - /// - /// - private async Task UploadAudioMessage(string uploadUrl, - string name, - byte[] binaryContent, - CancellationToken token) - { - // convert to ogg in order to meet VK requirements - var oggContent = _audioConvertor.Convert(binaryContent, - new AudioInfo - { - AudioFormat = AudioFormat.Ogg - }); - - return await PushDocument(uploadUrl, - name, - oggContent, - token); - } - - private async Task PushDocument(string uploadUrl, - string name, - byte[] binContent, - CancellationToken token) - { - using var httpClient = _httpClientFactory.CreateClient(); - var content = new MultipartFormDataContent - { - { - new ByteArrayContent(binContent, 0, binContent.Length), - "file", - $"{Path.GetFileNameWithoutExtension(name)}{Guid.NewGuid()}.{Path.GetExtension(name)}" - } - }; - - var response = await httpClient.PostAsync(uploadUrl, content, token); - - return await response.Content.ReadFromJsonAsync(token); - } - - /// - /// Uploads a document message (binaries) - /// - /// - /// - /// - /// - /// - private async Task UploadDocMessage(string uploadUrl, - string name, - byte[] binaryContent, - CancellationToken token) - { - return await PushDocument(uploadUrl, - name, - binaryContent, - token); - } - - /// - /// Uploads a video (binaries) - /// - /// - /// - /// - /// - /// - private async Task UploadVideo(string uploadUrl, - string name, - byte[] binaryContent, - CancellationToken token) - { - using var httpClient = _httpClientFactory.CreateClient(); - using var memoryContentStream = new MemoryStream(binaryContent); - memoryContentStream.Seek(0, SeekOrigin.Begin); - - var content = new MultipartFormDataContent {{new StreamContent(memoryContentStream), "video", name}}; - - var response = await httpClient.PostAsync(uploadUrl, content, token); - - return await response.Content.ReadFromJsonAsync(token); - } - - /// - /// The main public method for sending a photo - /// - /// - /// - /// - /// - /// - /// - public async Task SendPhotoAsync(VkSendMessageRequest vkMessageRequest, - string name, - byte[] binaryContent, - CancellationToken token) - { - try - { - var address = await GetPhotoUploadAddress(vkMessageRequest, token); - - if (address?.Response == null) throw new BotException("Sending photo error: no upload server address!"); - - var uploadedPhoto = await UploadPhoto(address.Response.UploadUrl, - name, - binaryContent, - token); - - if (uploadedPhoto?.Photo == null) throw new BotException("Sending photo error: no media uploaded!"); - - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, - ApiUtils.GetMethodUri("https://api.vk.com", - "photos.saveMessagesPhoto", - new - { - server = uploadedPhoto.Server, - hash = uploadedPhoto.Hash, - access_token = _apiKey, - v = ApiVersion - })); - request.Content = ApiUtils.GetMethodMultipartFormContent(new - { - photo = uploadedPhoto.Photo - }); - - var response = await httpClient.SendAsync(request, token); - - return await response.Content.ReadFromJsonAsync(token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error uploading media"); - } - - return null; - } - - - /// - /// The main public method for sending an audio message - /// - /// - /// - /// - /// - /// - /// - public async Task SendAudioMessageAsync(VkSendMessageRequest vkMessageRequest, - string name, - byte[] binaryContent, - CancellationToken token) - { - try - { - var address = await GetAudioUploadAddress(vkMessageRequest, token); - - if (address?.Response == null) throw new BotException("Sending audio error: no upload server address!"); - - var uploadedAudio = await UploadAudioMessage(address.Response.UploadUrl, - name, - binaryContent, - token); - - if (uploadedAudio?.File == null) throw new BotException("Sending audio error: no media uploaded!"); - - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, - ApiUtils.GetMethodUri("https://api.vk.com", - "docs.save", - new - { - title = "voice", - tags = "string.Empty", - file = uploadedAudio.File, - // audio = uploadedAudio.File, - access_token = _apiKey, - v = ApiVersion - })); - request.Content = ApiUtils.GetMethodMultipartFormContent(new - { - audio = uploadedAudio.File - }); - - var response = await httpClient.SendAsync(request, token); - - return await response.Content.ReadFromJsonAsync(token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error uploading media"); - } - - return null; - } - - - public async Task SendDocsMessageAsync(VkSendMessageRequest vkMessageRequest, - string name, - byte[] binaryContent, - CancellationToken token) - { - try - { - var address = await GetDocsUploadAddress(vkMessageRequest, "doc", token); - - if (address?.Response == null) throw new BotException("Sending doc error: no upload server address!"); - - var uploadedDoc = await UploadDocMessage(address.Response.UploadUrl, - name, - binaryContent, - token); - - if (uploadedDoc?.File == null) throw new BotException("Sending doc error: no file uploaded!"); - - using var httpClient = _httpClientFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, - ApiUtils.GetMethodUri("https://api.vk.com", - "docs.save", - new - { - file = uploadedDoc.File, - access_token = _apiKey, - v = ApiVersion - })); - request.Content = ApiUtils.GetMethodMultipartFormContent(new - { - doc = uploadedDoc.File - }); - - var response = await httpClient.SendAsync(request, token); - - return await response.Content.ReadFromJsonAsync(token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error uploading media"); - } - - return null; - } -} \ No newline at end of file diff --git a/Botticelli.Framework.Vk/vk_test.txt b/Botticelli.Framework.Vk/vk_test.txt deleted file mode 100644 index f589148a..00000000 --- a/Botticelli.Framework.Vk/vk_test.txt +++ /dev/null @@ -1 +0,0 @@ -vk1.a.Hd7QxN4U9RKGNHK2AwtpbQ-LJq0Hvh18Z5hw2mi1rbrxmHYHawp4yjqltdbS5IrVJVmqojGDKNPuvOWTVOp72CVf2fEFKEqU1h374jf7dclSTVv_aQEUGXKNRWVKcg9Crtg7e2hwRN-YZORDiSxldYTlnuvTzI7R6wnTG1xusVRVM7xG2dt-NN4p8ShCMdqWfJ4VkQ2WtfjfvC6t8xRRqA \ No newline at end of file From 0e52c12b73b9404ce601c9e0070795c44fd957de Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 20 Jul 2025 22:23:29 +0300 Subject: [PATCH 063/101] - Botticelli.Locations.VK in a separate repository also! --- .../Botticelli.Locations.Vk.csproj | 28 ----------- .../Extensions/ServiceCollectionExtensions.cs | 49 ------------------- 2 files changed, 77 deletions(-) delete mode 100644 Botticelli.Locations.Vk/Botticelli.Locations.Vk.csproj delete mode 100644 Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs diff --git a/Botticelli.Locations.Vk/Botticelli.Locations.Vk.csproj b/Botticelli.Locations.Vk/Botticelli.Locations.Vk.csproj deleted file mode 100644 index ad045dd9..00000000 --- a/Botticelli.Locations.Vk/Botticelli.Locations.Vk.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - enable - enable - Botticelli.Locations.Vk1 - - - - - ..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.Configuration.Abstractions.dll - - - ..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - - - - - - - - - - - - diff --git a/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 85291402..00000000 --- a/Botticelli.Locations.Vk/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Reflection; -using Botticelli.Controls.Parsers; -using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Framework.Vk.Messages.Layout; -using Botticelli.Locations.Commands; -using Botticelli.Locations.Commands.CommandProcessors; -using Botticelli.Locations.Integration; -using Botticelli.Locations.Options; -using Flurl; -using Mapster; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Nominatim.API.Address; -using Nominatim.API.Geocoders; -using Nominatim.API.Interfaces; -using Nominatim.API.Web; - -namespace Botticelli.Locations.Vk.Extensions; - -public static class ServiceCollectionExtensions -{ - /// - /// Adds an OSM location provider - /// - /// - public static IServiceCollection AddOsmLocations(this IServiceCollection services, - IConfiguration config, - string url = "https://nominatim.openstreetmap.org") - { - services.AddHttpClient(); - TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly()); - - return services.Configure(config) - .AddScoped, PassValidator>() - .AddScoped>() - .AddScoped>() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped, VkLayoutSupplier>() - .AddScoped(sp => - new ForwardGeocoder(sp.GetRequiredService(), - Url.Combine(url, "search"))) - .AddScoped(sp => - new ReverseGeocoder(sp.GetRequiredService(), - Url.Combine(url, "reverse"))); - } -} \ No newline at end of file From 05efd1f21c9ce34c795d69e6dfc7e25de8236095 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 20 Jul 2025 23:06:40 +0300 Subject: [PATCH 064/101] - fix --- Botticelli.sln | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Botticelli.sln b/Botticelli.sln index b567fc25..0e320ff7 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -112,8 +112,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Scheduler", "Bot BotticelliSchedulerDescription.txt = BotticelliSchedulerDescription.txt EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Framework.Vk.Messages", "Botticelli.Framework.Vk\Botticelli.Framework.Vk.Messages.csproj", "{343922EC-2CF3-4C0E-B745-08F2B333B4E3}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Vk.Tests", "Tests\Botticelli.Vk.Tests\Botticelli.Vk.Tests.csproj", "{2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VK", "VK", "{5BB351E1-460B-48C1-912C-E13702968BB2}" @@ -199,8 +197,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Location", "Bott EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Locations.Telegram", "Botticelli.Locations.Telegram\Botticelli.Locations.Telegram.csproj", "{3585C2D1-9AA8-4B83-918D-588D701E37F5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Locations.Vk", "Botticelli.Locations.Vk\Botticelli.Locations.Vk.csproj", "{89865D7E-8769-477B-8CED-C5B53C981178}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandChain.Sample.Telegram", "Samples\CommandChain.Sample.Telegram\CommandChain.Sample.Telegram.csproj", "{E33D4F38-48D2-44C7-A56B-4CE080A5B593}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.AI.Test", "Botticelli.AI.Test\Botticelli.AI.Test.csproj", "{332A0B08-B1C9-4321-AD31-B2102B95AFEA}" @@ -347,10 +343,6 @@ Global {2423A645-22D1-4736-946F-10A99353581C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2423A645-22D1-4736-946F-10A99353581C}.Release|Any CPU.ActiveCfg = Release|Any CPU {2423A645-22D1-4736-946F-10A99353581C}.Release|Any CPU.Build.0 = Release|Any CPU - {343922EC-2CF3-4C0E-B745-08F2B333B4E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {343922EC-2CF3-4C0E-B745-08F2B333B4E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {343922EC-2CF3-4C0E-B745-08F2B333B4E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {343922EC-2CF3-4C0E-B745-08F2B333B4E3}.Release|Any CPU.Build.0 = Release|Any CPU {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -483,10 +475,6 @@ Global {3585C2D1-9AA8-4B83-918D-588D701E37F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {3585C2D1-9AA8-4B83-918D-588D701E37F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {3585C2D1-9AA8-4B83-918D-588D701E37F5}.Release|Any CPU.Build.0 = Release|Any CPU - {89865D7E-8769-477B-8CED-C5B53C981178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89865D7E-8769-477B-8CED-C5B53C981178}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89865D7E-8769-477B-8CED-C5B53C981178}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89865D7E-8769-477B-8CED-C5B53C981178}.Release|Any CPU.Build.0 = Release|Any CPU {E33D4F38-48D2-44C7-A56B-4CE080A5B593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E33D4F38-48D2-44C7-A56B-4CE080A5B593}.Debug|Any CPU.Build.0 = Debug|Any CPU {E33D4F38-48D2-44C7-A56B-4CE080A5B593}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -642,7 +630,6 @@ Global {12AB0F40-141F-4C65-A630-7A4D4421EEEE} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {25B7376F-D770-4C4A-8EAB-73465BA962BA} = {12AB0F40-141F-4C65-A630-7A4D4421EEEE} {3585C2D1-9AA8-4B83-918D-588D701E37F5} = {12AB0F40-141F-4C65-A630-7A4D4421EEEE} - {89865D7E-8769-477B-8CED-C5B53C981178} = {12AB0F40-141F-4C65-A630-7A4D4421EEEE} {332A0B08-B1C9-4321-AD31-B2102B95AFEA} = {B7B4B68F-7086-49F2-B401-C76B4EF0B320} {0530F522-2556-4D69-8275-86947E9D51EF} = {8C52EB41-3815-4B12-9E40-C0D1472361CA} {1A6572FD-D274-4700-901F-6F9F7A49F57B} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} @@ -694,7 +681,6 @@ Global {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} {71008870-2212-4A74-8777-B5B268C27911} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} - {343922EC-2CF3-4C0E-B745-08F2B333B4E3} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} From 31c729af3fd77ff1d87c7744894f95f32d65f4dc Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 28 Jul 2025 14:58:04 +0300 Subject: [PATCH 065/101] - add/set for bot building --- .../Builders/TelegramBotBuilder.cs | 46 +++++++++++-------- .../Builders/TelegramStandaloneBotBuilder.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 6 ++- .../Extensions/ServiceProviderExtensions.cs | 16 +++++++ .../Handlers/BotUpdateHandler.cs | 4 +- Botticelli.Framework/Builders/BotBuilder.cs | 8 ++-- .../CommandChainFirstElementProcessor.cs | 2 - .../CommandChainProcessorBuilder.cs | 11 +++-- .../Processors/ProcessorFactoryBuilder.cs | 4 +- .../Extensions/StartupExtensions.cs | 7 +-- 10 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 79e9a914..3ee7b0e7 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -24,6 +24,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Telegram.Bot; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Builders; @@ -117,7 +118,27 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu return this; } - protected override TBot? InnerBuild() + protected override TBot? InnerBuild(IServiceProvider serviceProvider) + { + ApplyMigrations(serviceProvider); + + foreach (var sh in _subHandlers) sh.Invoke(serviceProvider); + + if (Activator.CreateInstance(typeof(TBot), + _builder.Build(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetService()) is not TBot bot) + throw new InvalidDataException($"{nameof(bot)} shouldn't be null!"); + + AddEvents(bot); + + return bot; + } + + public TelegramBotBuilder Prepare() { if (!_isStandalone) { @@ -180,25 +201,10 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu Services.AddSingleton, ReplyTelegramLayoutSupplier>() .AddBotticelliFramework() - .AddSingleton(); - - var sp = Services.BuildServiceProvider(); - ApplyMigrations(sp); - - foreach (var sh in _subHandlers) sh.Invoke(sp); - - if (Activator.CreateInstance(typeof(TBot), - client, - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetService()) is not TBot bot) - throw new InvalidDataException($"{nameof(bot)} shouldn't be null!"); - - AddEvents(bot); - - return bot; + .AddSingleton() + .AddSingleton(client); + + return this; } private void AddEvents(TBot bot) diff --git a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs index 775d1eb8..7460b2bc 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramStandaloneBotBuilder.cs @@ -48,7 +48,7 @@ public TelegramStandaloneBotBuilder AddBotData(BotDataSettingsBuilder() .AddServerCertificates(BotSettings); @@ -58,6 +58,6 @@ public TelegramStandaloneBotBuilder AddBotData(BotDataSettingsBuilder() .AddSingleton(BotData); - return base.InnerBuild(); + 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 d1cc10c8..8a79a12d 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Botticelli.Framework.Telegram.Decorators; using Botticelli.Framework.Telegram.Layout; using Botticelli.Framework.Telegram.Options; +using Botticelli.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Telegram.Bot.Types.ReplyMarkups; @@ -105,7 +106,8 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect dataAccessSettingsBuilderFunc, telegramBotBuilderFunc); - services.AddTelegramLayoutsSupport(); + services.AddTelegramLayoutsSupport() + .AddSingleton(botBuilder); return botBuilder; } @@ -137,6 +139,8 @@ static TBotBuilder InnerBuild(IServiceCollection services, Ac telegramBotBuilderFunc?.Invoke(botBuilder); + services.AddSingleton(sp => sp.GetRequiredService>>().Build(sp)!); + return botBuilder; } diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 00000000..38ea1245 --- /dev/null +++ b/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,16 @@ +using Botticelli.Framework.Telegram.Builders; +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Telegram.Extensions; + +public static class ServiceProviderExtensions +{ + public static IServiceProvider UseTelegramBot(this IServiceProvider serviceProvider) + { + var builder = serviceProvider.GetRequiredService>>(); + + builder.Build(serviceProvider); + + return serviceProvider; + } +} \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index 7b5704d8..e3cb8eee 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -14,7 +14,7 @@ namespace Botticelli.Framework.Telegram.Handlers; -public class BotUpdateHandler(ILogger logger) : IBotUpdateHandler +public class BotUpdateHandler(ILogger logger, IServiceProvider serviceProvider) : IBotUpdateHandler { private readonly MemoryCacheEntryOptions _entryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); @@ -243,7 +243,7 @@ protected async Task ProcessInProcessors(Message request, CancellationToken toke if (token is { CanBeCanceled: true, IsCancellationRequested: true }) return; - var processorFactory = ProcessorFactoryBuilder.Build(); + var processorFactory = ProcessorFactoryBuilder.Build(serviceProvider); var clientNonChainedTasks = processorFactory.GetProcessors() .Select(p => p.ProcessAsync(request, token)); diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index 267e272d..ae23ceac 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -9,14 +9,14 @@ public abstract class BotBuilder { protected abstract void Assert(); - public virtual TBot? Build() + public virtual TBot? Build(IServiceProvider serviceProvider) { Assert(); - return InnerBuild(); + return InnerBuild(serviceProvider); } - protected abstract TBot? InnerBuild(); + protected abstract TBot? InnerBuild(IServiceProvider serviceProvider); } public interface IBotBuilder where TBotBuilder : BotBuilder @@ -29,7 +29,7 @@ public interface IBotBuilder where TBotBuilder : BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler); BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler); BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler); - TBot? Build(); + TBot? Build(IServiceProvider serviceProvider); } public abstract class BotBuilder : BotBuilder, IBotBuilder diff --git a/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs index 624c5c10..f0da2b77 100644 --- a/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandChainFirstElementProcessor.cs @@ -21,8 +21,6 @@ public CommandChainFirstElementProcessor(ILogger)); - _services.AddScoped>(); + _services.AddSingleton>(); + ProcessorFactoryBuilder.AddProcessor>(_services); } public CommandChainProcessorBuilder AddNext() where TNextProcessor : class, ICommandChainProcessor { _typesChain.Add(typeof(TNextProcessor)); - _services.AddScoped(); + _services.AddSingleton(); + ProcessorFactoryBuilder.AddProcessor(_services); return this; } - public ICommandChainProcessor? Build() + public ICommandChainProcessor? Build(IServiceProvider sp) { if (_typesChain.Count == 0) return null; // initializing chain processors... - var sp = _services.BuildServiceProvider(); - _chainProcessor ??= sp.GetRequiredService(_typesChain.First()) as ICommandChainProcessor; // making a chain... diff --git a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs index f9dbc1b3..e3d4d20c 100644 --- a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs +++ b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs @@ -16,13 +16,11 @@ public static void AddProcessor(IServiceCollection serviceCollection ProcessorTypes.Add(typeof(TProcessor)); } - public static ProcessorFactory Build() + public static ProcessorFactory Build(IServiceProvider sp) { if (_serviceCollection == null) return new ProcessorFactory([]); - var sp = _serviceCollection.BuildServiceProvider(); - var processors = ProcessorTypes .Select(pt => { diff --git a/Botticelli.Framework/Extensions/StartupExtensions.cs b/Botticelli.Framework/Extensions/StartupExtensions.cs index 34848c51..b5b3893e 100644 --- a/Botticelli.Framework/Extensions/StartupExtensions.cs +++ b/Botticelli.Framework/Extensions/StartupExtensions.cs @@ -42,10 +42,11 @@ public static IServiceCollection AddBotticelliFramework(this IServiceCollection public static CommandAddServices AddBotCommand(this IServiceCollection services) where TCommand : class, ICommand { + var cmd = new CommandAddServices(services); services.AddScoped() - .AddSingleton>(_ => new CommandAddServices(services)); + .AddSingleton>(_ => cmd); - return services.BuildServiceProvider().GetRequiredService>(); + return cmd; } public static IServiceCollection AddBotCommand(this IS where TBot : IBot { var commandChainProcessorBuilder = sp.GetRequiredService>(); - commandChainProcessorBuilder.Build(); + commandChainProcessorBuilder.Build(sp); return sp; } From 847c749b1f8f7fb58216939630b13d462504fcae Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 28 Jul 2025 19:17:44 +0300 Subject: [PATCH 066/101] - bot context fix --- Botticelli.Bot.Dal/BotInfoContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Bot.Dal/BotInfoContext.cs b/Botticelli.Bot.Dal/BotInfoContext.cs index 49d3c579..fe1cc31c 100644 --- a/Botticelli.Bot.Dal/BotInfoContext.cs +++ b/Botticelli.Bot.Dal/BotInfoContext.cs @@ -11,7 +11,7 @@ public class BotInfoContext : DbContext // // } - public BotInfoContext(DbContextOptions options) : base(options) + public BotInfoContext(DbContextOptions options) : base(options) { } From 5bb8a60e6b278c6fd9c5dd16a43861d476a87721 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 3 Aug 2025 11:15:46 +0300 Subject: [PATCH 067/101] - message structure attachments fix --- Botticelli.Shared/ValueObjects/Message.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index 962fd072..1a4e684b 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; } = new(); /// /// From user From 390afe5f89ac95e0e242188d1015494f6bd5bcee Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 9 Aug 2025 14:44:34 +0300 Subject: [PATCH 068/101] - project draft --- Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs b/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs index 75b2cb2c..e75eb15a 100644 --- a/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs +++ b/Botticelli.Shared/API/Client/Requests/DeleteMessageRequest.cs @@ -1,4 +1,4 @@ -using Botticelli.Shared.API; +namespace Botticelli.Shared.API.Client.Requests; public class DeleteMessageRequest : BaseRequest { From fecbe76dbf46505187c46135f86a11bc31724aa2 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 21 Aug 2025 12:57:04 +0300 Subject: [PATCH 069/101] - oauth fixed --- Botticelli.Framework/SendOptions/SendOptionsBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Botticelli.Framework/SendOptions/SendOptionsBuilder.cs b/Botticelli.Framework/SendOptions/SendOptionsBuilder.cs index 2429c67a..25da06cc 100644 --- a/Botticelli.Framework/SendOptions/SendOptionsBuilder.cs +++ b/Botticelli.Framework/SendOptions/SendOptionsBuilder.cs @@ -55,7 +55,7 @@ public static SendOptionsBuilder CreateBuilder() return new SendOptionsBuilder(); } - public static SendOptionsBuilder CreateBuilder(T input) + public static SendOptionsBuilder? CreateBuilder(T input) { return new SendOptionsBuilder(input); } From af8fba96719fc77130b458d82edfbdce36aeda2f Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 26 Aug 2025 19:39:33 +0300 Subject: [PATCH 070/101] - update --- .../Extensions/ServiceCollectionExtensions.cs | 4 ++ .../Program.cs | 7 +--- Pay.Sample.Telegram/Program.cs | 8 ++-- .../Ai.DeepSeek.Sample.Telegram/Program.cs | 27 +++++++------ Samples/Ai.YaGpt.Sample.Telegram/Program.cs | 7 ++-- Samples/Auth.Sample.Telegram/Program.cs | 10 ++--- .../Properties/launchSettings.json | 38 +++---------------- .../CommandChain.Sample.Telegram/Program.cs | 8 ++-- Samples/Layouts.Sample.Telegram/Program.cs | 10 ++--- Samples/Messaging.Sample.Telegram/Program.cs | 30 +++++++-------- Samples/Monads.Sample.Telegram/Program.cs | 9 ++--- Samples/TelegramAiSample/Program.cs | 23 ++++++----- 12 files changed, 72 insertions(+), 109 deletions(-) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 8a79a12d..57abc627 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -99,6 +99,7 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect where TBotBuilder : TelegramBotBuilder> where TBot : TelegramBot { + services.AddHttpClient(); var botBuilder = InnerBuild(services, optionsBuilderFunc, analyticsOptionsBuilderFunc, @@ -120,6 +121,7 @@ static TBotBuilder InnerBuild(IServiceCollection services, Ac where TBotBuilder : TelegramBotBuilder> where TBot : TelegramBot { + services.AddHttpClient(); optionsBuilderFunc(SettingsBuilder); serverSettingsBuilderFunc(ServerSettingsBuilder); analyticsOptionsBuilderFunc(AnalyticsClientOptionsBuilder); @@ -154,6 +156,7 @@ public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot( Action>>? telegramBotBuilderFunc = null) where TBot : TelegramBot { + services.AddHttpClient(); var telegramBotSettings = configuration .GetSection(TelegramBotSettings.Section) .Get() ?? @@ -185,6 +188,7 @@ public static TelegramStandaloneBotBuilder AddStandaloneTelegramBot( Action>? telegramBotBuilderFunc = null) where TBot : TelegramBot { + services.AddHttpClient(); optionsBuilderFunc(SettingsBuilder); dataAccessSettingsBuilderFunc(DataAccessSettingsBuilder); botDataSettingsBuilderFunc(BotDataSettingsBuilder); diff --git a/Messaging.Sample.Telegram.Standalone/Program.cs b/Messaging.Sample.Telegram.Standalone/Program.cs index 4474cc42..66f5ec1a 100644 --- a/Messaging.Sample.Telegram.Standalone/Program.cs +++ b/Messaging.Sample.Telegram.Standalone/Program.cs @@ -1,7 +1,6 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; -using Botticelli.Interfaces; using Botticelli.Schedule.Quartz.Extensions; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; @@ -10,9 +9,9 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services +builder.Services .AddStandaloneTelegramBot(builder.Configuration) - .Build(); + .Prepare(); builder.Services .AddTelegramLayoutsSupport() @@ -31,6 +30,4 @@ .AddProcessor>() .AddValidator>(); -builder.Services.AddSingleton(bot); - await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Pay.Sample.Telegram/Program.cs b/Pay.Sample.Telegram/Program.cs index af3ca321..fdbcb85a 100644 --- a/Pay.Sample.Telegram/Program.cs +++ b/Pay.Sample.Telegram/Program.cs @@ -14,14 +14,14 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services +builder.Services .Configure(builder.Configuration.GetSection("PaySettings")) .AddTelegramPayBot>(builder.Configuration) - .Build(); + .Prepare(); + builder.Services.AddLogging(cfg => cfg.AddNLog()) - .AddSingleton() - .AddSingleton(bot); + .AddSingleton(); builder.Services.AddBotCommand() .AddProcessor>() diff --git a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs index 66bbba79..395b6218 100644 --- a/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs +++ b/Samples/Ai.DeepSeek.Sample.Telegram/Program.cs @@ -14,22 +14,21 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); +builder.Services + .AddTelegramBot(builder.Configuration) + .Prepare(); builder.Services - .AddLogging(cfg => cfg.AddNLog()) - .AddDeepSeekProvider(builder.Configuration) - .AddAiValidation() - .AddScoped, PassValidator>() - .AddSingleton() - .UsePassBusAgent, AiHandler>() - .UsePassBusClient>() - .UsePassEventBusClient>() - .AddBotCommand, PassValidator>() - .AddTelegramLayoutsSupport() - .AddSingleton(bot); + .AddLogging(cfg => cfg.AddNLog()) + .AddDeepSeekProvider(builder.Configuration) + .AddAiValidation() + .AddScoped, PassValidator>() + .AddSingleton() + .UsePassBusAgent, AiHandler>() + .UsePassBusClient>() + .UsePassEventBusClient>() + .AddBotCommand, PassValidator>() + .AddTelegramLayoutsSupport(); var app = builder.Build(); diff --git a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs index 98d24351..3b748839 100644 --- a/Samples/Ai.YaGpt.Sample.Telegram/Program.cs +++ b/Samples/Ai.YaGpt.Sample.Telegram/Program.cs @@ -14,12 +14,11 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); +builder.Services + .AddTelegramBot(builder.Configuration) + .Prepare(); builder.Services - .AddSingleton(bot) .AddLogging(cfg => cfg.AddNLog()) .AddYaGptProvider(builder.Configuration) .AddAiValidation() diff --git a/Samples/Auth.Sample.Telegram/Program.cs b/Samples/Auth.Sample.Telegram/Program.cs index e3da6ce6..7014355c 100644 --- a/Samples/Auth.Sample.Telegram/Program.cs +++ b/Samples/Auth.Sample.Telegram/Program.cs @@ -4,21 +4,19 @@ using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; -using Botticelli.Interfaces; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); +builder.Services + .AddTelegramBot(builder.Configuration) + .Prepare(); builder.Services .AddTelegramLayoutsSupport() .AddLogging(cfg => cfg.AddNLog()) - .AddSqliteBasicBotUserAuth(builder.Configuration) - .AddSingleton(bot); + .AddSqliteBasicBotUserAuth(builder.Configuration); builder.Services.AddBotCommand() .AddProcessor>() diff --git a/Samples/Auth.Sample.Telegram/Properties/launchSettings.json b/Samples/Auth.Sample.Telegram/Properties/launchSettings.json index 787507ea..81282668 100644 --- a/Samples/Auth.Sample.Telegram/Properties/launchSettings.json +++ b/Samples/Auth.Sample.Telegram/Properties/launchSettings.json @@ -1,41 +1,15 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:25409", - "sslPort": 44388 - } - }, + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "TelegramBotSample": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5128", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7171;http://localhost:5128", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "launchUrl": "", + "applicationUrl": "http://localhost:5045", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/Samples/CommandChain.Sample.Telegram/Program.cs b/Samples/CommandChain.Sample.Telegram/Program.cs index ebab9088..b6a973f3 100644 --- a/Samples/CommandChain.Sample.Telegram/Program.cs +++ b/Samples/CommandChain.Sample.Telegram/Program.cs @@ -3,7 +3,6 @@ using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram; using Botticelli.Framework.Telegram.Extensions; -using Botticelli.Interfaces; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; using NLog.Extensions.Logging; @@ -13,12 +12,11 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); +builder.Services + .AddTelegramBot(builder.Configuration) + .Prepare(); builder.Services.AddLogging(cfg => cfg.AddNLog()) - .AddSingleton(bot) .AddSingleton>() .AddSingleton>() .AddSingleton>() diff --git a/Samples/Layouts.Sample.Telegram/Program.cs b/Samples/Layouts.Sample.Telegram/Program.cs index 36aee9d4..6be577e6 100644 --- a/Samples/Layouts.Sample.Telegram/Program.cs +++ b/Samples/Layouts.Sample.Telegram/Program.cs @@ -3,7 +3,6 @@ using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; using Botticelli.Framework.Telegram.Layout; -using Botticelli.Interfaces; using Botticelli.Locations.Telegram.Extensions; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; @@ -12,17 +11,18 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services +builder.Services .AddTelegramBot(builder.Configuration) - .Build(); + .Prepare(); builder.Services .AddLogging(cfg => cfg.AddNLog()) .AddBotCommand>() .AddInlineCalendar() - .AddOsmLocations(builder.Configuration) - .AddSingleton(bot); + .AddOsmLocations(builder.Configuration); var app = builder.Build(); +app.Services.UseTelegramBot(); + await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Messaging.Sample.Telegram/Program.cs b/Samples/Messaging.Sample.Telegram/Program.cs index 6ec50c22..8fab3ca1 100644 --- a/Samples/Messaging.Sample.Telegram/Program.cs +++ b/Samples/Messaging.Sample.Telegram/Program.cs @@ -1,37 +1,33 @@ -using Botticelli.Broadcasting.Extensions; using Botticelli.Framework.Commands.Validators; using Botticelli.Framework.Extensions; using Botticelli.Framework.Telegram.Extensions; -using Botticelli.Interfaces; using Botticelli.Schedule.Quartz.Extensions; using MessagingSample.Common.Commands; using MessagingSample.Common.Commands.Processors; -using Microsoft.EntityFrameworkCore; using NLog.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); - builder.Services - .AddTelegramLayoutsSupport() - .AddLogging(cfg => cfg.AddNLog()) - .AddQuartzScheduler(builder.Configuration) - .AddSingleton(bot); + .AddTelegramBot(builder.Configuration) + .Prepare(); + +builder.Services + .AddTelegramLayoutsSupport() + .AddLogging(cfg => cfg.AddNLog()) + .AddQuartzScheduler(builder.Configuration); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); builder.Services.AddBotCommand() - .AddProcessor>() - .AddValidator>(); + .AddProcessor>() + .AddValidator>(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Samples/Monads.Sample.Telegram/Program.cs b/Samples/Monads.Sample.Telegram/Program.cs index b8a6759e..a4801dc2 100644 --- a/Samples/Monads.Sample.Telegram/Program.cs +++ b/Samples/Monads.Sample.Telegram/Program.cs @@ -14,12 +14,11 @@ var bot = builder.Services .AddTelegramBot(builder.Configuration) - .Build(); + .Prepare(); -builder.Services - .AddLogging(cfg => cfg.AddNLog()) - .AddTelegramLayoutsSupport() - .AddSingleton(bot); +builder.Services + .AddLogging(cfg => cfg.AddNLog()) + .AddTelegramLayoutsSupport(); builder.Services .AddChainedRedisStorage(builder.Configuration) diff --git a/Samples/TelegramAiSample/Program.cs b/Samples/TelegramAiSample/Program.cs index a0539c59..ca360322 100644 --- a/Samples/TelegramAiSample/Program.cs +++ b/Samples/TelegramAiSample/Program.cs @@ -14,20 +14,19 @@ var builder = WebApplication.CreateBuilder(args); -var bot = builder.Services - .AddTelegramBot(builder.Configuration) - .Build(); +builder.Services + .AddTelegramBot(builder.Configuration) + .Prepare(); builder.Services.AddLogging(cfg => cfg.AddNLog()) - .AddChatGptProvider(builder.Configuration) - .AddAiValidation() - .AddScoped, PassValidator>() - .AddSingleton() - .UsePassBusAgent, AiHandler>() - .UsePassBusClient>() - .UsePassEventBusClient>() - .AddBotCommand, PassValidator>() - .AddSingleton(bot); + .AddChatGptProvider(builder.Configuration) + .AddAiValidation() + .AddScoped, PassValidator>() + .AddSingleton() + .UsePassBusAgent, AiHandler>() + .UsePassBusClient>() + .UsePassEventBusClient>() + .AddBotCommand, PassValidator>(); var app = builder.Build(); From a5aa63b8f0c3a5e18aa9333cecaf06baaaa5200f Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 5 Sep 2025 18:48:36 +0300 Subject: [PATCH 071/101] BOTTICELLI-66: VK/Max messenger support removed! --- Botticelli.sln | 31 ----------- .../Ai.ChatGpt.Sample.Vk.csproj | 39 -------------- Samples/Ai.ChatGpt.Sample.Vk/Program.cs | 27 ---------- .../Properties/launchSettings.json | 12 ----- .../Ai.Messaging.Sample.Vk.csproj | 42 --------------- Samples/Ai.Messaging.Sample.Vk/Program.cs | 26 ---------- .../Properties/launchSettings.json | 15 ------ .../Ai.Messaging.Sample.Vk/appsettings.json | 22 -------- .../deploy/run_standalone.sh | 18 ------- .../Ai.YaGpt.Sample.Vk.csproj | 30 ----------- Samples/Ai.YaGpt.Sample.Vk/Program.cs | 27 ---------- .../Properties/launchSettings.json | 12 ----- .../Botticelli.Vk.Tests.csproj | 45 ---------------- .../EnvironmentDataProvider.cs | 14 ----- .../LongPollMessagesProviderTests.cs | 51 ------------------- .../MessagePublisherTests.cs | 35 ------------- .../TestHttpClientFactory.cs | 34 ------------- Tests/Botticelli.Vk.Tests/appsettings.json | 6 --- 18 files changed, 486 deletions(-) delete mode 100644 Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj delete mode 100644 Samples/Ai.ChatGpt.Sample.Vk/Program.cs delete mode 100644 Samples/Ai.ChatGpt.Sample.Vk/Properties/launchSettings.json delete mode 100644 Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj delete mode 100644 Samples/Ai.Messaging.Sample.Vk/Program.cs delete mode 100644 Samples/Ai.Messaging.Sample.Vk/Properties/launchSettings.json delete mode 100644 Samples/Ai.Messaging.Sample.Vk/appsettings.json delete mode 100644 Samples/Ai.Messaging.Sample.Vk/deploy/run_standalone.sh delete mode 100644 Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj delete mode 100644 Samples/Ai.YaGpt.Sample.Vk/Program.cs delete mode 100644 Samples/Ai.YaGpt.Sample.Vk/Properties/launchSettings.json delete mode 100644 Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj delete mode 100644 Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs delete mode 100644 Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs delete mode 100644 Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs delete mode 100644 Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs delete mode 100644 Tests/Botticelli.Vk.Tests/appsettings.json diff --git a/Botticelli.sln b/Botticelli.sln index 0e320ff7..029403e6 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -112,12 +112,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.Scheduler", "Bot BotticelliSchedulerDescription.txt = BotticelliSchedulerDescription.txt EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Vk.Tests", "Tests\Botticelli.Vk.Tests\Botticelli.Vk.Tests.csproj", "{2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VK", "VK", "{5BB351E1-460B-48C1-912C-E13702968BB2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ai.Messaging.Sample.Vk", "Samples\Ai.Messaging.Sample.Vk\Ai.Messaging.Sample.Vk.csproj", "{B610D900-6713-4D8F-8E9D-23AA02F846FF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Audio", "Botticelli.Audio\Botticelli.Audio.csproj", "{0B5AB7F0-080D-49F3-A564-CE22F6F808AB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Audio.Tests", "Tests\Botticelli.Audio.Tests\Botticelli.Audio.Tests.csproj", "{868ED8BB-D451-42F3-AB3C-EE76AA9F483E}" @@ -138,8 +132,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.Sample.Common", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ai.Common.Sample", "Samples\Ai.Common.Sample\Ai.Common.Sample.csproj", "{8A5ECE73-1529-4B53-8907-735ADA05C20A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ai.ChatGpt.Sample.Vk", "Samples\Ai.ChatGpt.Sample.Vk\Ai.ChatGpt.Sample.Vk.csproj", "{AAD10628-7441-419D-8FE8-F37BF8FB4681}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Framework.Events", "Botticelli.Framework.Common\Botticelli.Framework.Events.csproj", "{B0863634-2EE0-4F24-898F-6FB0154E3EB5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Bot.Utils", "Botticelli.Bot.Utils\Botticelli.Bot.Utils.csproj", "{E843665C-F541-4915-838D-346150389C36}" @@ -159,8 +151,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.Sample.Telegram", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ai.YaGpt.Sample.Telegram", "Samples\Ai.YaGpt.Sample.Telegram\Ai.YaGpt.Sample.Telegram.csproj", "{D44E775F-7474-4F49-8633-384DC87BE40B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ai.YaGpt.Sample.Vk", "Samples\Ai.YaGpt.Sample.Vk\Ai.YaGpt.Sample.Vk.csproj", "{20AE866A-89D1-4C72-ADAC-545E44AB1354}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Botticelli.AI", "Botticelli.AI", "{3B5A6BD8-2B94-42F9-B122-06D92C470B31}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.AI.ChatGpt", "Botticelli.AI.ChatGpt\Botticelli.AI.ChatGpt.csproj", "{D92C29CF-581E-425B-B96B-01D5EFDA1614}" @@ -343,14 +333,6 @@ Global {2423A645-22D1-4736-946F-10A99353581C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2423A645-22D1-4736-946F-10A99353581C}.Release|Any CPU.ActiveCfg = Release|Any CPU {2423A645-22D1-4736-946F-10A99353581C}.Release|Any CPU.Build.0 = Release|Any CPU - {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1}.Release|Any CPU.Build.0 = Release|Any CPU - {B610D900-6713-4D8F-8E9D-23AA02F846FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B610D900-6713-4D8F-8E9D-23AA02F846FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B610D900-6713-4D8F-8E9D-23AA02F846FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B610D900-6713-4D8F-8E9D-23AA02F846FF}.Release|Any CPU.Build.0 = Release|Any CPU {0B5AB7F0-080D-49F3-A564-CE22F6F808AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0B5AB7F0-080D-49F3-A564-CE22F6F808AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {0B5AB7F0-080D-49F3-A564-CE22F6F808AB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -383,10 +365,6 @@ Global {8A5ECE73-1529-4B53-8907-735ADA05C20A}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A5ECE73-1529-4B53-8907-735ADA05C20A}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A5ECE73-1529-4B53-8907-735ADA05C20A}.Release|Any CPU.Build.0 = Release|Any CPU - {AAD10628-7441-419D-8FE8-F37BF8FB4681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAD10628-7441-419D-8FE8-F37BF8FB4681}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAD10628-7441-419D-8FE8-F37BF8FB4681}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAD10628-7441-419D-8FE8-F37BF8FB4681}.Release|Any CPU.Build.0 = Release|Any CPU {B0863634-2EE0-4F24-898F-6FB0154E3EB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0863634-2EE0-4F24-898F-6FB0154E3EB5}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0863634-2EE0-4F24-898F-6FB0154E3EB5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -411,10 +389,6 @@ Global {D44E775F-7474-4F49-8633-384DC87BE40B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D44E775F-7474-4F49-8633-384DC87BE40B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D44E775F-7474-4F49-8633-384DC87BE40B}.Release|Any CPU.Build.0 = Release|Any CPU - {20AE866A-89D1-4C72-ADAC-545E44AB1354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20AE866A-89D1-4C72-ADAC-545E44AB1354}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20AE866A-89D1-4C72-ADAC-545E44AB1354}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20AE866A-89D1-4C72-ADAC-545E44AB1354}.Release|Any CPU.Build.0 = Release|Any CPU {D92C29CF-581E-425B-B96B-01D5EFDA1614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D92C29CF-581E-425B-B96B-01D5EFDA1614}.Debug|Any CPU.Build.0 = Debug|Any CPU {D92C29CF-581E-425B-B96B-01D5EFDA1614}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -598,22 +572,17 @@ Global {85DE8BC6-2A06-4088-B4AA-DD520CBC2369} = {912B05D1-DC22-411E-9D6E-E06FFE96E4E6} {184C8F8D-DC80-4C52-88F9-8AE799C51266} = {85DE8BC6-2A06-4088-B4AA-DD520CBC2369} {3DFA2DF1-6720-44D7-91E2-B50572F8A2E6} = {912B05D1-DC22-411E-9D6E-E06FFE96E4E6} - {2EE26ED9-C949-48E8-BF48-93EEC9E51AF1} = {B7B4B68F-7086-49F2-B401-C76B4EF0B320} - {5BB351E1-460B-48C1-912C-E13702968BB2} = {35480478-646E-45F1-96A3-EE7AC498ACE3} - {B610D900-6713-4D8F-8E9D-23AA02F846FF} = {5BB351E1-460B-48C1-912C-E13702968BB2} {0B5AB7F0-080D-49F3-A564-CE22F6F808AB} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {868ED8BB-D451-42F3-AB3C-EE76AA9F483E} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {AA4F8F66-D022-4E02-B03F-E7E101AF527A} = {1DBF2C91-4F7A-42EB-B5CC-C692F8167898} {B8AC3C63-EA2F-4D6A-9CF7-5935D787C4E3} = {1DBF2C91-4F7A-42EB-B5CC-C692F8167898} {DC84E739-621E-41F5-9250-0B961EE90B3E} = {1DBF2C91-4F7A-42EB-B5CC-C692F8167898} {6F38611B-7798-4514-91E3-B74A2FA228E4} = {8C52EB41-3815-4B12-9E40-C0D1472361CA} - {AAD10628-7441-419D-8FE8-F37BF8FB4681} = {5BB351E1-460B-48C1-912C-E13702968BB2} {B0863634-2EE0-4F24-898F-6FB0154E3EB5} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {E843665C-F541-4915-838D-346150389C36} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {255BE0A1-C453-466B-9BEF-11AFF0DBD2CF} = {346A18CA-EB1F-483B-8BC3-EDF250DA72E6} {17C89EE6-7845-4895-B945-05FC1134D680} = {255BE0A1-C453-466B-9BEF-11AFF0DBD2CF} {F3D3569B-39C7-427C-B123-EF268F0CB895} = {9B590747-74F6-41C8-8733-B5B77A8FCAC1} - {20AE866A-89D1-4C72-ADAC-545E44AB1354} = {5BB351E1-460B-48C1-912C-E13702968BB2} {3B5A6BD8-2B94-42F9-B122-06D92C470B31} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {F2270850-361A-4654-A893-718332560B02} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} {D92C29CF-581E-425B-B96B-01D5EFDA1614} = {3B5A6BD8-2B94-42F9-B122-06D92C470B31} diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj b/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj deleted file mode 100644 index ec9cb9d8..00000000 --- a/Samples/Ai.ChatGpt.Sample.Vk/Ai.ChatGpt.Sample.Vk.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net8.0 - enable - enable - 0.8.0 - Botticelli - Igor Evdokimov - https://github.com/devgopher/botticelli - logo.jpg - https://github.com/devgopher/botticelli - VkAiChatGptSample - - - - - - - - - - - - - - - PreserveNewest - - - - - - nlog.config - PreserveNewest - - - - \ No newline at end of file diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs b/Samples/Ai.ChatGpt.Sample.Vk/Program.cs deleted file mode 100644 index 423dd8da..00000000 --- a/Samples/Ai.ChatGpt.Sample.Vk/Program.cs +++ /dev/null @@ -1,27 +0,0 @@ -using AiSample.Common; -using AiSample.Common.Commands; -using AiSample.Common.Handlers; -using Botticelli.AI.ChatGpt.Extensions; -using Botticelli.Bus.None.Extensions; -using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Extensions; -using Botticelli.Framework.Vk.Messages; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Framework.Vk.Messages.Extensions; -using Botticelli.Interfaces; -using NLog.Extensions.Logging; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddVkBot(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) - .AddChatGptProvider(builder.Configuration) - .AddScoped, PassValidator>() - .AddSingleton() - .UsePassBusAgent, AiHandler>() - .UsePassBusClient>() - .AddBotCommand, PassValidator>(); - -var app = builder.Build(); - -await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.ChatGpt.Sample.Vk/Properties/launchSettings.json b/Samples/Ai.ChatGpt.Sample.Vk/Properties/launchSettings.json deleted file mode 100644 index cd6d2064..00000000 --- a/Samples/Ai.ChatGpt.Sample.Vk/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "VkAiChatGptSample": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:54851;http://localhost:54852" - } - } -} \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj b/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj deleted file mode 100644 index 3d894ab3..00000000 --- a/Samples/Ai.Messaging.Sample.Vk/Ai.Messaging.Sample.Vk.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net8.0 - enable - enable - 0.8.0 - Botticelli - Igor Evdokimov - https://github.com/devgopher/botticelli - logo.jpg - https://github.com/devgopher/botticelli - VkMessagingSample - - - - - - - - - - - - - - - - - - - Never - true - Never - - - nlog.config - PreserveNewest - - - - \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/Program.cs b/Samples/Ai.Messaging.Sample.Vk/Program.cs deleted file mode 100644 index 3f5723a2..00000000 --- a/Samples/Ai.Messaging.Sample.Vk/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Extensions; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Framework.Vk.Messages.Extensions; -using Botticelli.Schedule.Quartz.Extensions; -using Botticelli.Talks.Extensions; -using MessagingSample.Common.Commands; -using MessagingSample.Common.Commands.Processors; -using NLog.Extensions.Logging; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services - .AddVkBot(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) - .AddQuartzScheduler(builder.Configuration) - .AddScoped>() - .AddScoped>() - .AddOpenTtsTalks(builder.Configuration) - .AddBotCommand, PassValidator>() - .AddBotCommand, PassValidator>(); - - -var app = builder.Build(); - -await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/Properties/launchSettings.json b/Samples/Ai.Messaging.Sample.Vk/Properties/launchSettings.json deleted file mode 100644 index d66674ad..00000000 --- a/Samples/Ai.Messaging.Sample.Vk/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "TelegramBotSample": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "launchUrl": "", - "applicationUrl": "http://localhost:5073", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/appsettings.json b/Samples/Ai.Messaging.Sample.Vk/appsettings.json deleted file mode 100644 index 40e760c4..00000000 --- a/Samples/Ai.Messaging.Sample.Vk/appsettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "DataAccess": { - "ConnectionString": "Filename=database.db;Password=123;ReadOnly=false" - }, - "Server": { - "ServerUri": "http://113.30.189.83:5042/v1/" - }, - "AnalyticsClient": { - "TargetUrl": "http://113.30.189.83:5251/v1/" - }, - "VkBot": { - "PollIntervalMs": 100, - "GroupId": 225327325 - }, - "AllowedHosts": "*" -} \ No newline at end of file diff --git a/Samples/Ai.Messaging.Sample.Vk/deploy/run_standalone.sh b/Samples/Ai.Messaging.Sample.Vk/deploy/run_standalone.sh deleted file mode 100644 index e50d9012..00000000 --- a/Samples/Ai.Messaging.Sample.Vk/deploy/run_standalone.sh +++ /dev/null @@ -1,18 +0,0 @@ -sudo apt-get update -sudo apt-get install -y dotnet-sdk-7.0 dotnet-runtime-7.0 aspnetcore-runtime-7.0 - - -rm -rf botticelli/ -git clone https://github.com/devgopher/botticelli.git -pushd botticelli/ -git checkout release/0.3 -git pull - -pushd VkMessagingSample - -dotnet run VkMessagingSample.csproj & - -echo BOT ID: -cat Data/botId - -popd \ No newline at end of file diff --git a/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj b/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj deleted file mode 100644 index b1297698..00000000 --- a/Samples/Ai.YaGpt.Sample.Vk/Ai.YaGpt.Sample.Vk.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net8.0 - enable - enable - 0.8.0 - Botticelli - Igor Evdokimov - https://github.com/devgopher/botticelli - logo.jpg - https://github.com/devgopher/botticelli - VkAiChatGptSample - - - - - nlog.config - PreserveNewest - - - - - - - - - - - diff --git a/Samples/Ai.YaGpt.Sample.Vk/Program.cs b/Samples/Ai.YaGpt.Sample.Vk/Program.cs deleted file mode 100644 index 5f48415d..00000000 --- a/Samples/Ai.YaGpt.Sample.Vk/Program.cs +++ /dev/null @@ -1,27 +0,0 @@ -using AiSample.Common; -using AiSample.Common.Commands; -using AiSample.Common.Handlers; -using Botticelli.AI.YaGpt.Extensions; -using Botticelli.Bus.None.Extensions; -using Botticelli.Framework.Commands.Validators; -using Botticelli.Framework.Extensions; -using Botticelli.Framework.Vk.Messages; -using Botticelli.Framework.Vk.Messages.API.Markups; -using Botticelli.Framework.Vk.Messages.Extensions; -using Botticelli.Interfaces; -using NLog.Extensions.Logging; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddVkBot(builder.Configuration) - .AddLogging(cfg => cfg.AddNLog()) - .AddYaGptProvider(builder.Configuration) - .AddScoped, PassValidator>() - .AddSingleton() - .UsePassBusAgent, AiHandler>() - .UsePassBusClient>() - .AddBotCommand, PassValidator>(); - -var app = builder.Build(); - -await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Ai.YaGpt.Sample.Vk/Properties/launchSettings.json b/Samples/Ai.YaGpt.Sample.Vk/Properties/launchSettings.json deleted file mode 100644 index f102d779..00000000 --- a/Samples/Ai.YaGpt.Sample.Vk/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "VkAiYaGptSample": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:54851;http://localhost:54852" - } - } -} \ No newline at end of file diff --git a/Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj b/Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj deleted file mode 100644 index 809ed791..00000000 --- a/Tests/Botticelli.Vk.Tests/Botticelli.Vk.Tests.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - PreserveNewest - true - PreserveNewest - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs b/Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs deleted file mode 100644 index 6766e403..00000000 --- a/Tests/Botticelli.Vk.Tests/EnvironmentDataProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Botticelli.Vk.Tests; - -internal static class EnvironmentDataProvider -{ - public static string GetApiKey() - { - return Environment.GetEnvironmentVariable("TEST_VK_API_KEY") ?? "test_empty_key"; - } - - public static int GetTargetUserId() - { - return int.Parse(Environment.GetEnvironmentVariable("TEST_VK_TARGET_USER_ID") ?? "-1"); - } -} \ No newline at end of file diff --git a/Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs b/Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs deleted file mode 100644 index 3f786fa4..00000000 --- a/Tests/Botticelli.Vk.Tests/LongPollMessagesProviderTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Botticelli.Framework.Vk.Messages; -using Botticelli.Framework.Vk.Messages.Options; -using Botticelli.Shared.Utils; -using NUnit.Framework; -using Mocks; - -namespace Botticelli.Vk.Tests; - -[TestFixture] -public class LongPollMessagesProviderTests -{ - [SetUp] - public void Setup() - { - _provider = new LongPollMessagesProvider(new OptionsMonitorMock(new VkBotSettings - { - Name = "test", - PollIntervalMs = 500, - GroupId = 221973506 - }).CurrentValue, - new TestHttpClientFactory(), - LoggerMocks.CreateConsoleLogger()); - } - - private LongPollMessagesProvider? _provider; - - [Test] - public async Task StartTest() - { - _provider.NotNull(); - await _provider!.Stop(); - _provider.SetApiKey(EnvironmentDataProvider.GetApiKey()); - - var task = Task.Run(() => _provider.Start(CancellationToken.None)); - - Thread.Sleep(5000); - - Assert.That(task.Exception == null); - } - - [Test] - public void StopTest() - { - _provider.NotNull(); - _ = _provider!.Start(CancellationToken.None); - - Thread.Sleep(2000); - - Assert.DoesNotThrowAsync(_provider.Stop); - } -} \ No newline at end of file diff --git a/Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs b/Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs deleted file mode 100644 index 700e91db..00000000 --- a/Tests/Botticelli.Vk.Tests/MessagePublisherTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Globalization; -using Botticelli.Framework.Vk.Messages; -using Botticelli.Framework.Vk.Messages.API.Requests; -using NUnit.Framework; -using Mocks; - -namespace Botticelli.Vk.Tests; - -[TestFixture] -public class MessagePublisherTests(MessagePublisher publisher) -{ - [SetUp] - public void Setup() - { - _publisher = new MessagePublisher(new TestHttpClientFactory(), - LoggerMocks.CreateConsoleLogger()); - } - - private MessagePublisher _publisher = publisher; - - [Test] - public Task SendAsyncTest() - { - _publisher.SetApiKey(EnvironmentDataProvider.GetApiKey()); - Assert.DoesNotThrowAsync(async () => await _publisher.SendAsync(new VkSendMessageRequest - { - AccessToken = EnvironmentDataProvider.GetApiKey(), - Body = $"test msg {DateTime.Now.ToString(CultureInfo.InvariantCulture)}", - UserId = EnvironmentDataProvider.GetTargetUserId().ToString() - }, - CancellationToken.None)); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs b/Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs deleted file mode 100644 index a279bc46..00000000 --- a/Tests/Botticelli.Vk.Tests/TestHttpClientFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json; -using Botticelli.Framework.Vk.Messages.API.Responses; -using Botticelli.Framework.Vk.Messages.API.Utils; -using RichardSzalay.MockHttp; - -namespace Botticelli.Vk.Tests; - -internal class TestHttpClientFactory : IHttpClientFactory -{ - public HttpClient CreateClient(string name) - { - var mockHttp = new MockHttpMessageHandler(); - - mockHttp.When(ApiUtils.GetMethodUri("https://api.vk.com", - "messages.send") - .ToString()) - .Respond("application/json", "{'Result' : 'OK'}"); - - var mockResponse = new GetMessageSessionDataResponse - { - Response = new SessionDataResponse - { - Server = "https://test.mock", - Key = "test_key", - Ts = "12323213123" - } - }; - - mockHttp.When(ApiUtils.GetMethodUri("https://api.vk.com", "groups.getLongPollServer").ToString()) - .Respond("application/json", JsonSerializer.Serialize(mockResponse)); - - return mockHttp.ToHttpClient(); - } -} \ No newline at end of file diff --git a/Tests/Botticelli.Vk.Tests/appsettings.json b/Tests/Botticelli.Vk.Tests/appsettings.json deleted file mode 100644 index aa81a1f4..00000000 --- a/Tests/Botticelli.Vk.Tests/appsettings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "SampleSettings": { - "SecureStorageConnectionString": "Filename=database.db;Password=123;ReadOnly=false" - }, - "AllowedHosts": "*" -} \ No newline at end of file From 398baf74bd9122eb4a444c1a5494558967c14448 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 5 Sep 2025 19:02:19 +0300 Subject: [PATCH 072/101] - build errors fixed --- .../Botticelli.Analytics.Shared.csproj | 6 +----- .../Botticelli.Auth.Data.Postgres.csproj | 2 +- Botticelli.Talks/Botticelli.Talks.csproj | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj b/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj index 468a8476..e9231fbc 100644 --- a/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj +++ b/Botticelli.Analytics.Shared/Botticelli.Analytics.Shared.csproj @@ -14,11 +14,7 @@ - - True - - new_logo_compact.png - + diff --git a/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj b/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj index f093aa48..ec092d5e 100644 --- a/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj +++ b/Botticelli.Auth.Data.Postgres/Botticelli.Auth.Data.Postgres.csproj @@ -10,7 +10,7 @@ - + diff --git a/Botticelli.Talks/Botticelli.Talks.csproj b/Botticelli.Talks/Botticelli.Talks.csproj index 0a7fbe6e..81163ed8 100644 --- a/Botticelli.Talks/Botticelli.Talks.csproj +++ b/Botticelli.Talks/Botticelli.Talks.csproj @@ -32,9 +32,7 @@ - - new_logo_compact.png - + \ No newline at end of file From 2977a2196d69e7fd36af9cfe0c6ba9fa21101a7d Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 7 Sep 2025 12:25:57 +0300 Subject: [PATCH 073/101] BOTTICELLI-55: - BotController: Broadcast => GetBroadcast - Broadcaster => BroadcastReceiver - BroadcastReceiver: SendBroadcastReceived method --- ...{Broadcaster.cs => BroadcasterReceiver.cs} | 31 ++++++++-- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Controllers/AdminController.cs | 12 ++-- .../Controllers/BotController.cs | 8 +-- .../Extensions/StartupExtensions.cs | 13 ++-- Botticelli.Server.Back/Program.cs | 1 + .../Pages/BotBroadcast.razor | 60 ++++++++++--------- 7 files changed, 80 insertions(+), 47 deletions(-) rename Botticelli.Broadcasting/{Broadcaster.cs => BroadcasterReceiver.cs} (67%) diff --git a/Botticelli.Broadcasting/Broadcaster.cs b/Botticelli.Broadcasting/BroadcasterReceiver.cs similarity index 67% rename from Botticelli.Broadcasting/Broadcaster.cs rename to Botticelli.Broadcasting/BroadcasterReceiver.cs index 8bea5716..177331ec 100644 --- a/Botticelli.Broadcasting/Broadcaster.cs +++ b/Botticelli.Broadcasting/BroadcasterReceiver.cs @@ -4,6 +4,7 @@ using Botticelli.Broadcasting.Settings; using Botticelli.Framework; using Botticelli.Interfaces; +using Botticelli.Shared.API; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; using Flurl.Http; @@ -20,7 +21,7 @@ namespace Botticelli.Broadcasting; /// the lifecycle of the service within a hosted environment. /// /// The type of bot that implements IBot interface. -public class Broadcaster : IHostedService +public class BroadcastReceiver : IHostedService where TBot : BaseBot, IBot { private readonly TBot _bot; @@ -30,7 +31,7 @@ public class Broadcaster : IHostedService private readonly IServiceScope _scope; private readonly IOptionsSnapshot _settings; - public Broadcaster(IServiceProvider serviceProvider, IOptionsSnapshot settings) + public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot settings) { _settings = settings; _scope = serviceProvider.CreateScope(); @@ -51,6 +52,8 @@ await updatePolicy.ExecuteAsync(async () => if (updates?.Messages == null) return updates; + var messageIds = new List(); + foreach (var update in updates.Messages) { // if no chat were specified - broadcast on all chats, we've @@ -61,7 +64,10 @@ await updatePolicy.ExecuteAsync(async () => Message = update }; - await _bot.SendMessageAsync(request, cancellationToken); + var response = await _bot.SendMessageAsync(request, cancellationToken); + + if (response.MessageSentStatus == MessageSentStatus.Ok) + await SendBroadcastReceived(messageIds, cancellationToken); } return updates; @@ -77,7 +83,7 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task GetUpdates(CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.Value.ServerUri}/client/broadcast" + var updatesResponse = await $"{_settings.Value.ServerUri}/client/GetBroadcast" .WithTimeout(_longPollTimeout) .PostJsonAsync(new GetBroadCastMessagesRequest { @@ -91,4 +97,21 @@ public Task StopAsync(CancellationToken cancellationToken) .ReadFromJsonAsync( cancellationToken); } + + private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) + { + var updatesResponse = await $"{_settings.Value.ServerUri}/client/BroadcastReceived" + .WithTimeout(_longPollTimeout) + .PostJsonAsync(new BroadCastMessagesReceivedRequest + { + BotId = _settings.Value.BotId, + MessageIds = chatIds.ToArray() + }, + cancellationToken: cancellationToken); + + if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) return null; + + return await updatesResponse.ResponseMessage.Content + .ReadFromJsonAsync(cancellationToken); + } } \ No newline at end of file diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index a8de63d1..b02bc992 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static BotBuilder AddBroadcasting(t if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); botBuilder.Services - .AddHostedService>() + .AddHostedService>() .AddDbContext(dbOptionsBuilder); ApplyMigrations(botBuilder.Services); diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index 8234ed7e..328af13c 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -5,6 +5,7 @@ using Botticelli.Shared.API.Admin.Responses; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; +using Botticelli.Shared.ValueObjects; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -75,14 +76,15 @@ public async Task UpdateBot([FromBody] UpdateBotRequest reque /// /// /// - [HttpGet("[action]")] - public async Task SendBroadcast([FromQuery] string botId, [FromQuery] string message) + [HttpPost("[action]")] + public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) { await _broadcastService.BroadcastMessage(new Broadcast { - Id = Guid.NewGuid().ToString(), - BotId = botId, - Body = message + Id = message.Uid ?? throw new NullReferenceException("Id cannot be null!"), + BotId = botId ?? throw new NullReferenceException("BotId cannot be null!"), + Body = message.Body ?? throw new NullReferenceException("Body cannot be null!"), + Sent = true }); } diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index e9ff2038..6b5d094b 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -98,11 +98,11 @@ public async Task KeepAlive([FromBody] KeepAliveN /// [AllowAnonymous] [HttpPost("client/[action]")] - public async Task Broadcast([FromBody] GetBroadCastMessagesRequest request) + public async Task GetBroadcast([FromBody] GetBroadCastMessagesRequest request) { try { - logger.LogTrace($"{nameof(Broadcast)}({request.BotId})..."); + logger.LogTrace($"{nameof(GetBroadcast)}({request.BotId})..."); request.BotId?.NotNullOrEmpty(); var broadcastMessages = await broadcastService.GetMessages(request.BotId!); @@ -122,7 +122,7 @@ public async Task Broadcast([FromBody] GetBroadCas } catch (Exception ex) { - logger.LogError(ex, $"{nameof(Broadcast)}({request.BotId}) error: {ex.Message}"); + logger.LogError(ex, $"{nameof(GetBroadcast)}({request.BotId}) error: {ex.Message}"); return new GetBroadCastMessagesResponse { @@ -144,7 +144,7 @@ public async Task BroadcastReceived([FromBody { try { - logger.LogTrace($"{nameof(Broadcast)}({request.BotId})..."); + logger.LogTrace($"{nameof(GetBroadcast)}({request.BotId})..."); request.BotId?.NotNullOrEmpty(); foreach (var messageId in request.MessageIds) await broadcastService.MarkReceived(request.BotId!, messageId); diff --git a/Botticelli.Server.Back/Extensions/StartupExtensions.cs b/Botticelli.Server.Back/Extensions/StartupExtensions.cs index a46f2dfa..54d0a780 100644 --- a/Botticelli.Server.Back/Extensions/StartupExtensions.cs +++ b/Botticelli.Server.Back/Extensions/StartupExtensions.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Botticelli.Server.Back.Services.Broadcasting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; @@ -22,6 +23,9 @@ public static void ApplyMigrations(this WebApplicationBuilder webAppli if (pendingMigrations.Any()) db.Database.Migrate(); } + + public static IServiceCollection AddBroadcasting(this IServiceCollection services) + => services.AddScoped(); public static IServiceCollection AddIdentity(this IServiceCollection services) { @@ -60,6 +64,8 @@ public static IServiceCollection AddIdentity(this IServiceCollection services) return services; } + + public static IWebHostBuilder AddSsl(this IWebHostBuilder builder, IConfiguration config) { // in Linux put here: ~/.dotnet/corefx/cryptography/x509stores/ @@ -86,12 +92,7 @@ public static IWebHostBuilder AddSsl(this IWebHostBuilder builder, IConfiguratio { ServerCertificate = certificate, ClientCertificateMode = ClientCertificateMode.AllowCertificate, - ClientCertificateValidation = (_, _, errors) => - { - if (errors != SslPolicyErrors.None) return false; - - return true; - } + ClientCertificateValidation = (_, _, errors) => errors == SslPolicyErrors.None }; listenOptions.Protocols = HttpProtocols.Http1AndHttp2; diff --git a/Botticelli.Server.Back/Program.cs b/Botticelli.Server.Back/Program.cs index b5f15da2..76c2276c 100644 --- a/Botticelli.Server.Back/Program.cs +++ b/Botticelli.Server.Back/Program.cs @@ -93,6 +93,7 @@ .AddScoped() .AddSingleton() .AddScoped() + .AddBroadcasting() .AddDbContext(options => options.UseSqlite($"Data source={serverSettings.SecureStorageConnection}")) .AddDefaultIdentity>(options => options diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 1184f78d..6f7ace3e 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -1,11 +1,10 @@ @page "/broadcast/send/{botId}" @using System.Net.Http.Headers -@using Botticelli.Server.Data.Entities.Bot.Broadcasting +@using System.Text +@using System.Text.Json @using Botticelli.Server.FrontNew.Models @using Botticelli.Server.FrontNew.Settings -@using Botticelli.Shared.API.Client.Requests -@using Botticelli.Shared.API.Client.Responses -@using Botticelli.Shared.Constants +@using Botticelli.Shared.ValueObjects @using Flurl @using Microsoft.Extensions.Options @inject HttpClient Http @@ -13,6 +12,7 @@ @inject IOptionsMonitor BackSettings; @inject CookieStorageAccessor Cookies; +
@@ -21,31 +21,22 @@
-
- - -
-
- - -
-
- + Name="BotId"/>
- +
- +
- + + +
@@ -53,6 +44,12 @@
@code { + + private readonly Error _error = new() + { + UserMessage = string.Empty + }; + readonly BotBroadcastModel _model = new(); [Parameter] @@ -69,16 +66,15 @@ UriHelper.NavigateTo("/your_bots", true); else _model.BotId = BotId; - + return Task.CompletedTask; } - + private async Task OnSubmit(BotBroadcastModel model) { - var request = new Broadcast + var message = new Message { - BotId = model.BotId, - Id = Guid.NewGuid().ToString(), + Uid = Guid.NewGuid().ToString(), Body = model.Message }; @@ -87,8 +83,18 @@ if (string.IsNullOrWhiteSpace(sessionToken)) return; Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); - await Http.GetFromJsonAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, "/admin/SendBroadcast")); - + + var content = new StringContent(JsonSerializer.Serialize(message), + Encoding.UTF8, + "application/json"); + + var response = await Http.PostAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, $"/admin/SendBroadcast?botId={BotId}"), + content); + + #if DEBUG + if (!response.IsSuccessStatusCode) _error.UserMessage = $"Error sending a broadcast message: {response.ReasonPhrase}"; + #endif + UriHelper.NavigateTo("/your_bots", true); } From b6d69c7664b27fd9a3f062154c76e1eb531d9973 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 7 Sep 2025 17:05:31 +0300 Subject: [PATCH 074/101] BOTTICELLI-55: - broadcasting sample draft --- .../Botticelli.Broadcasting.Telegram.csproj | 14 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 46 +++++++++++++++++ ...casterReceiver.cs => BroadcastReceiver.cs} | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 23 +++++---- .../Settings/BroadcastingSettings.cs | 2 + .../Builders/TelegramBotBuilder.cs | 7 +-- Botticelli.sln | 24 +++++++++ .../Broadcasting.Sample.Common.csproj | 14 ++++++ .../Processors/StartCommandProcessor.cs | 50 +++++++++++++++++++ .../Commands/StartCommand.cs | 8 +++ Samples/Broadcasting.Sample.Common/Program.cs | 3 ++ .../Broadcasting.Sample.Telegram.csproj | 18 +++++++ .../Broadcasting.Sample.Telegram/Program.cs | 20 ++++++++ .../appsettings.json | 30 +++++++++++ .../appsettings.json | 2 +- 15 files changed, 247 insertions(+), 18 deletions(-) create mode 100644 Botticelli.Broadcasting.Telegram/Botticelli.Broadcasting.Telegram.csproj create mode 100644 Botticelli.Broadcasting.Telegram/Extensions/ServiceCollectionExtensions.cs rename Botticelli.Broadcasting/{BroadcasterReceiver.cs => BroadcastReceiver.cs} (97%) create mode 100644 Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj create mode 100644 Samples/Broadcasting.Sample.Common/Commands/Processors/StartCommandProcessor.cs create mode 100644 Samples/Broadcasting.Sample.Common/Commands/StartCommand.cs create mode 100644 Samples/Broadcasting.Sample.Common/Program.cs create mode 100644 Samples/Broadcasting.Sample.Telegram/Broadcasting.Sample.Telegram.csproj create mode 100644 Samples/Broadcasting.Sample.Telegram/Program.cs create mode 100644 Samples/Broadcasting.Sample.Telegram/appsettings.json diff --git a/Botticelli.Broadcasting.Telegram/Botticelli.Broadcasting.Telegram.csproj b/Botticelli.Broadcasting.Telegram/Botticelli.Broadcasting.Telegram.csproj new file mode 100644 index 00000000..baa5e7cc --- /dev/null +++ b/Botticelli.Broadcasting.Telegram/Botticelli.Broadcasting.Telegram.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Botticelli.Broadcasting.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting.Telegram/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..777cfe1d --- /dev/null +++ b/Botticelli.Broadcasting.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,46 @@ +using Botticelli.Broadcasting.Extensions; +using Botticelli.Framework.Builders; +using Botticelli.Framework.Telegram; +using Botticelli.Framework.Telegram.Builders; +using Microsoft.Extensions.Configuration; + +namespace Botticelli.Broadcasting.Telegram.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds broadcasting to a bot + /// + /// + /// + /// + /// + public static TelegramBotBuilder AddBroadcasting(this TelegramBotBuilder botBuilder, + IConfiguration config) + where TBot : TelegramBot + { + var builder = botBuilder as BotBuilder>; + builder!.AddBroadcasting(config); + + return botBuilder; + } + + /// + /// Adds broadcasting to a bot + /// + /// + /// + /// + /// + /// + public static TelegramBotBuilder AddTelegramBroadcasting(this TelegramBotBuilder botBuilder, + IConfiguration config) + where TBot : TelegramBot + where TBotBuilder : BotBuilder + { + var builder = botBuilder as BotBuilder>; + builder!.AddBroadcasting(config); + + return botBuilder; + } +} \ No newline at end of file diff --git a/Botticelli.Broadcasting/BroadcasterReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs similarity index 97% rename from Botticelli.Broadcasting/BroadcasterReceiver.cs rename to Botticelli.Broadcasting/BroadcastReceiver.cs index 177331ec..43c36c04 100644 --- a/Botticelli.Broadcasting/BroadcasterReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -24,7 +24,7 @@ namespace Botticelli.Broadcasting; public class BroadcastReceiver : IHostedService where TBot : BaseBot, IBot { - private readonly TBot _bot; + private TBot? _bot; private readonly BroadcastingContext _context; private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); @@ -35,7 +35,6 @@ public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot(); _context = _scope.ServiceProvider.GetRequiredService(); } @@ -48,6 +47,7 @@ public async Task StartAsync(CancellationToken cancellationToken) await updatePolicy.ExecuteAsync(async () => { + _bot ??= _scope.ServiceProvider.GetRequiredService();; var updates = await GetUpdates(cancellationToken); if (updates?.Messages == null) return updates; diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index b02bc992..20b8847a 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -19,22 +19,20 @@ public static class ServiceCollectionExtensions /// /// /// - /// /// public static BotBuilder AddBroadcasting(this BotBuilder botBuilder, - IConfiguration config, - Action? dbOptionsBuilder = null) + IConfiguration config) where TBot : BaseBot, IBot where TBotBuilder : BotBuilder { - var settings = config.Get(); + var settings = config.GetSection(BroadcastingSettings.Section).Get(); if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); botBuilder.Services .AddHostedService>() - .AddDbContext(dbOptionsBuilder); - + .AddDbContext(opt => opt.UseSqlite($"Data source={settings.ConnectionString}")); + ApplyMigrations(botBuilder.Services); return botBuilder.AddOnMessageReceived((_, args) => @@ -50,8 +48,13 @@ public static BotBuilder AddBroadcasting(t context.SaveChanges(); }); } - - private static void ApplyMigrations(IServiceCollection services) => - services.BuildServiceProvider() - .GetRequiredService().Database.Migrate(); + + private static void ApplyMigrations(IServiceCollection services) + { + var sp = services.BuildServiceProvider(); + var context = sp.GetRequiredService(); + + if (context.Database.EnsureCreated()) + context.Database.Migrate(); + } } \ No newline at end of file diff --git a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs index c753ac4a..9e19dde0 100644 --- a/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs +++ b/Botticelli.Broadcasting/Settings/BroadcastingSettings.cs @@ -2,7 +2,9 @@ public class BroadcastingSettings { + public const string Section = "Broadcasting"; public required string BotId { get; set; } public TimeSpan? HowOld { get; set; } = TimeSpan.FromSeconds(60); public required string ServerUri { get; set; } + public required string ConnectionString { get; set; } } \ No newline at end of file diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index 3ee7b0e7..a5e454b1 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -151,11 +151,8 @@ public TelegramBotBuilder Prepare() Services.AddHttpClient() .AddServerCertificates(BotSettings); Services.AddHostedService(); - - Services.AddHttpClient>() - .AddServerCertificates(BotSettings); - Services.AddHostedService>>() - .AddHostedService(); + + Services.AddHostedService(); } var botId = BotDataUtils.GetBotId(); diff --git a/Botticelli.sln b/Botticelli.sln index 029403e6..fc6567a0 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -259,6 +259,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Sha EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messengers", "Messengers", "{9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Broadcasting", "Broadcasting", "{4B590BAD-B92D-427C-B340-68A40A9C9B95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Broadcasting.Sample.Telegram", "Samples\Broadcasting.Sample.Telegram\Broadcasting.Sample.Telegram.csproj", "{2E48B176-D743-49B3-B1F2-6EC159EB80EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Broadcasting.Sample.Common", "Samples\Broadcasting.Sample.Common\Broadcasting.Sample.Common.csproj", "{F0B6FF75-499F-463D-A39E-AB39BFAC0F8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botticelli.Broadcasting.Telegram", "Botticelli.Broadcasting.Telegram\Botticelli.Broadcasting.Telegram.csproj", "{B64F1EA1-9AEA-4828-BC8B-363FDFD8210F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -541,6 +549,18 @@ Global {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637}.Release|Any CPU.Build.0 = Release|Any CPU + {2E48B176-D743-49B3-B1F2-6EC159EB80EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E48B176-D743-49B3-B1F2-6EC159EB80EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E48B176-D743-49B3-B1F2-6EC159EB80EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E48B176-D743-49B3-B1F2-6EC159EB80EF}.Release|Any CPU.Build.0 = Release|Any CPU + {F0B6FF75-499F-463D-A39E-AB39BFAC0F8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0B6FF75-499F-463D-A39E-AB39BFAC0F8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0B6FF75-499F-463D-A39E-AB39BFAC0F8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0B6FF75-499F-463D-A39E-AB39BFAC0F8D}.Release|Any CPU.Build.0 = Release|Any CPU + {B64F1EA1-9AEA-4828-BC8B-363FDFD8210F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B64F1EA1-9AEA-4828-BC8B-363FDFD8210F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B64F1EA1-9AEA-4828-BC8B-363FDFD8210F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B64F1EA1-9AEA-4828-BC8B-363FDFD8210F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -650,6 +670,10 @@ Global {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} {71008870-2212-4A74-8777-B5B268C27911} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} + {4B590BAD-B92D-427C-B340-68A40A9C9B95} = {2AD59714-541D-4794-9216-6EAB25F271DA} + {2E48B176-D743-49B3-B1F2-6EC159EB80EF} = {4B590BAD-B92D-427C-B340-68A40A9C9B95} + {F0B6FF75-499F-463D-A39E-AB39BFAC0F8D} = {4B590BAD-B92D-427C-B340-68A40A9C9B95} + {B64F1EA1-9AEA-4828-BC8B-363FDFD8210F} = {39412AE4-B325-4827-B24E-C15415DCA7E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2012E26A-91F8-40F9-9118-457668C6B7BA} diff --git a/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj b/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj new file mode 100644 index 00000000..9c511669 --- /dev/null +++ b/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/Samples/Broadcasting.Sample.Common/Commands/Processors/StartCommandProcessor.cs b/Samples/Broadcasting.Sample.Common/Commands/Processors/StartCommandProcessor.cs new file mode 100644 index 00000000..d093cc90 --- /dev/null +++ b/Samples/Broadcasting.Sample.Common/Commands/Processors/StartCommandProcessor.cs @@ -0,0 +1,50 @@ +using Botticelli.Framework.Commands.Processors; +using Botticelli.Framework.Commands.Validators; +using Botticelli.Shared.API.Client.Requests; +using Botticelli.Shared.ValueObjects; +using FluentValidation; +using Microsoft.Extensions.Logging; + +namespace Broadcasting.Sample.Common.Commands.Processors; + +public class StartCommandProcessor : CommandProcessor where TReplyMarkup : class +{ + public StartCommandProcessor(ILogger> logger, + ICommandValidator commandValidator, + IValidator messageValidator) + : base(logger, + commandValidator, + messageValidator) + { + } + + + protected override Task InnerProcessContact(Message message, CancellationToken token) + { + return Task.CompletedTask; + } + + protected override Task InnerProcessPoll(Message message, CancellationToken token) + { + return Task.CompletedTask; + } + + protected override Task InnerProcessLocation(Message message, CancellationToken token) + { + return Task.CompletedTask; + } + + protected override async Task InnerProcess(Message message, CancellationToken token) + { + var greetingMessageRequest = new SendMessageRequest + { + Message = new Message + { + ChatIds = message.ChatIds, + Body = "Bot started. Please, wait for broadcast messages from admin..." + } + }; + + await SendMessage(greetingMessageRequest, token); + } +} \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Common/Commands/StartCommand.cs b/Samples/Broadcasting.Sample.Common/Commands/StartCommand.cs new file mode 100644 index 00000000..dc6bc814 --- /dev/null +++ b/Samples/Broadcasting.Sample.Common/Commands/StartCommand.cs @@ -0,0 +1,8 @@ +using Botticelli.Framework.Commands; + +namespace Broadcasting.Sample.Common.Commands; + +public class StartCommand : ICommand +{ + public Guid Id { get; } +} \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Common/Program.cs b/Samples/Broadcasting.Sample.Common/Program.cs new file mode 100644 index 00000000..e5dff12b --- /dev/null +++ b/Samples/Broadcasting.Sample.Common/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Telegram/Broadcasting.Sample.Telegram.csproj b/Samples/Broadcasting.Sample.Telegram/Broadcasting.Sample.Telegram.csproj new file mode 100644 index 00000000..3585068c --- /dev/null +++ b/Samples/Broadcasting.Sample.Telegram/Broadcasting.Sample.Telegram.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Samples/Broadcasting.Sample.Telegram/Program.cs b/Samples/Broadcasting.Sample.Telegram/Program.cs new file mode 100644 index 00000000..679ad51b --- /dev/null +++ b/Samples/Broadcasting.Sample.Telegram/Program.cs @@ -0,0 +1,20 @@ +using Botticelli.Broadcasting.Telegram.Extensions; +using Botticelli.Framework.Commands.Validators; +using Botticelli.Framework.Extensions; +using Botticelli.Framework.Telegram.Extensions; +using Broadcasting.Sample.Common.Commands; +using Broadcasting.Sample.Common.Commands.Processors; +using Telegram.Bot.Types.ReplyMarkups; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddTelegramBot(builder.Configuration) + .AddTelegramBroadcasting(builder.Configuration) + .Prepare(); + +builder.Services.AddBotCommand() + .AddProcessor>() + .AddValidator>(); + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Telegram/appsettings.json b/Samples/Broadcasting.Sample.Telegram/appsettings.json new file mode 100644 index 00000000..1450ea2c --- /dev/null +++ b/Samples/Broadcasting.Sample.Telegram/appsettings.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DataAccess": { + "ConnectionString": "database.db;Password=123" + }, + "Server": { + "ServerUri": "http://103.252.116.18:5042/v1/" + }, + "AnalyticsClient": { + "TargetUrl": "http://localhost:5251/v1/" + }, + "TelegramBot": { + "timeout": 60, + "useThrottling": false, + "useTestEnvironment": false, + "name": "TestBot" + }, + "Broadcasting": { + "ConnectionString": "broadcasting_database.db;Password=321", + "BotId": "XuS3Ok5dP37v06vrqvfsfXT5G0LVq0nyD68Y", + "HowOld": "00:10:00", + "ServerUri": "http://103.252.116.18:5042/v1" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/Samples/Messaging.Sample.Telegram/appsettings.json b/Samples/Messaging.Sample.Telegram/appsettings.json index 057b1f17..04e75a06 100644 --- a/Samples/Messaging.Sample.Telegram/appsettings.json +++ b/Samples/Messaging.Sample.Telegram/appsettings.json @@ -20,7 +20,7 @@ "useTestEnvironment": false, "name": "TestBot" }, - "BroadcastingSettings": { + "Broadcasting": { "BroadcastingDbConnectionString": "YourConnectionStringHere", "BotId": "XuS3Ok5dP37v06vrqvfsfXT5G0LVq0nyD68Y", "HowOld": "00:10:00", From bf540a0a8a5c58c71d4f78bf222a9dc8dde2732a Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sat, 13 Sep 2025 13:44:07 +0300 Subject: [PATCH 075/101] BOTTICELLI-55: - broadcast messages receiver logic fixed --- Botticelli.Broadcasting/BroadcastReceiver.cs | 86 ++++++++++++------- .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 6 +- Botticelli.Framework/Builders/BotBuilder.cs | 9 ++ 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index 43c36c04..02a1c484 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -7,9 +7,11 @@ using Botticelli.Shared.API; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; +using Botticelli.Shared.ValueObjects; using Flurl.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; @@ -30,10 +32,14 @@ public class BroadcastReceiver : IHostedService private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); private readonly IServiceScope _scope; private readonly IOptionsSnapshot _settings; - - public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot settings) + private readonly ILogger> _logger; + + public BroadcastReceiver(IServiceProvider serviceProvider, + IOptionsSnapshot settings, + ILogger> logger) { _settings = settings; + _logger = logger; _scope = serviceProvider.CreateScope(); _context = _scope.ServiceProvider.GetRequiredService(); } @@ -41,37 +47,51 @@ public BroadcastReceiver(IServiceProvider serviceProvider, IOptionsSnapshot(ex => - ex.Call.Response.ResponseMessage.StatusCode == HttpStatusCode.RequestTimeout) - .WaitAndRetryForeverAsync((_, _) => _retryPause); - - await updatePolicy.ExecuteAsync(async () => - { - _bot ??= _scope.ServiceProvider.GetRequiredService();; - var updates = await GetUpdates(cancellationToken); - - if (updates?.Messages == null) return updates; - - var messageIds = new List(); - - foreach (var update in updates.Messages) - { - // if no chat were specified - broadcast on all chats, we've - if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); - - var request = new SendMessageRequest - { - Message = update - }; - - var response = await _bot.SendMessageAsync(request, cancellationToken); - - if (response.MessageSentStatus == MessageSentStatus.Ok) - await SendBroadcastReceived(messageIds, cancellationToken); - } - - return updates; - }); + await Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + _bot ??= _scope.ServiceProvider.GetService(); + + if (_bot == null) + { + _logger.LogError("Bot isn't initialized yet!"); + + continue; + } + + var updates = await GetUpdates(cancellationToken); + + if (updates?.Messages == null) continue; + + var messageIds = new List(); + + foreach (var update in updates.Messages) + { + // if no chat were specified - broadcast on all chats, we've + if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); + + var request = new SendMessageRequest + { + Message = update + }; + + var response = await _bot.SendMessageAsync(request, cancellationToken); + + if (response.MessageSentStatus == MessageSentStatus.Ok) await SendBroadcastReceived(messageIds, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + } + + await Task.Delay(_retryPause, cancellationToken); + } + }, + cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index 20b8847a..c708b9ec 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -30,8 +30,9 @@ public static BotBuilder AddBroadcasting(t if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); botBuilder.Services - .AddHostedService>() .AddDbContext(opt => opt.UseSqlite($"Data source={settings.ConnectionString}")); + + botBuilder.AddBuildAction(() => botBuilder.Services.AddHostedService>()); ApplyMigrations(botBuilder.Services); diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 57abc627..371352f3 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -113,7 +113,8 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect return botBuilder; } - static TBotBuilder InnerBuild(IServiceCollection services, Action> optionsBuilderFunc, + static TBotBuilder InnerBuild(IServiceCollection services, + Action> optionsBuilderFunc, Action> analyticsOptionsBuilderFunc, Action> serverSettingsBuilderFunc, Action> dataAccessSettingsBuilderFunc, @@ -141,7 +142,8 @@ static TBotBuilder InnerBuild(IServiceCollection services, Ac telegramBotBuilderFunc?.Invoke(botBuilder); - services.AddSingleton(sp => sp.GetRequiredService>>().Build(sp)!); + services.AddSingleton(sp => sp.GetRequiredService>>() + .Build(sp)!); return botBuilder; } diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index ae23ceac..fc7048ec 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -45,6 +45,8 @@ public abstract class BotBuilder : BotBuilder, IBotBuil protected BaseBot.MsgRemovedEventHandler? MessageRemoved; protected BaseBot.ContactSharedEventHandler? SharedContact; protected BaseBot.NewChatMembersEventHandler? NewChatMembers; + + protected List BuildActions = new(); protected override void Assert() { @@ -112,4 +114,11 @@ public virtual BotBuilder AddSharedContact(BaseBot.ContactSh return this; } + + public virtual BotBuilder AddBuildAction(Action buildAction) + { + BuildActions.Add(buildAction); + + return this; + } } \ No newline at end of file From 0542c481635c2b7988664522af955e9b6803e7c1 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 13 Sep 2025 18:25:03 +0300 Subject: [PATCH 076/101] BOTTICELLI-55: - removed BuildActions (no need) - BroadcastReceiver: works now, but 'bot wasn't started' - UseTelegramBot was completely removed --- Botticelli.Broadcasting/BroadcastReceiver.cs | 114 ++++++++---------- .../Extensions/ServiceCollectionExtensions.cs | 9 +- .../Builders/TelegramBotBuilder.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Extensions/ServiceProviderExtensions.cs | 16 --- Botticelli.Framework/Builders/BotBuilder.cs | 22 ---- Botticelli.Framework/Builders/IBotBuilder.cs | 18 +++ .../Controllers/AdminController.cs | 44 +++---- .../Services/Broadcasting/BroadcastService.cs | 2 +- .../Handler/AuthDelegatingHandler.cs | 22 +--- .../Broadcasting.Sample.Telegram/Program.cs | 6 +- .../appsettings.json | 6 +- Samples/Layouts.Sample.Telegram/Program.cs | 6 +- 13 files changed, 108 insertions(+), 164 deletions(-) delete mode 100644 Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs create mode 100644 Botticelli.Framework/Builders/IBotBuilder.cs diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index 02a1c484..42e02ab3 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -7,14 +7,9 @@ using Botticelli.Shared.API; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; -using Botticelli.Shared.ValueObjects; using Flurl.Http; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Polly; - namespace Botticelli.Broadcasting; ///
@@ -26,88 +21,79 @@ namespace Botticelli.Broadcasting; public class BroadcastReceiver : IHostedService where TBot : BaseBot, IBot { - private TBot? _bot; + private readonly IBot? _bot; private readonly BroadcastingContext _context; private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); - private readonly IServiceScope _scope; - private readonly IOptionsSnapshot _settings; + private readonly BroadcastingSettings _settings; private readonly ILogger> _logger; - public BroadcastReceiver(IServiceProvider serviceProvider, - IOptionsSnapshot settings, + public BroadcastReceiver(IBot bot, + BroadcastingContext context, + BroadcastingSettings settings, ILogger> logger) { + _bot = bot; _settings = settings; _logger = logger; - _scope = serviceProvider.CreateScope(); - _context = _scope.ServiceProvider.GetRequiredService(); + _context = context; } - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) { // Polls admin API for new messages to send to our chats and adds them to a MessageCache/MessageStatus - await Task.Run(async () => - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - _bot ??= _scope.ServiceProvider.GetService(); - - if (_bot == null) - { - _logger.LogError("Bot isn't initialized yet!"); - - continue; - } - - var updates = await GetUpdates(cancellationToken); - - if (updates?.Messages == null) continue; - - var messageIds = new List(); - - foreach (var update in updates.Messages) - { - // if no chat were specified - broadcast on all chats, we've - if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); - - var request = new SendMessageRequest - { - Message = update - }; - - var response = await _bot.SendMessageAsync(request, cancellationToken); - - if (response.MessageSentStatus == MessageSentStatus.Ok) await SendBroadcastReceived(messageIds, cancellationToken); - } - } - catch (Exception ex) - { - _logger.LogError(ex, ex.Message); - } - - await Task.Delay(_retryPause, cancellationToken); - } - }, - cancellationToken); + _ = Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var updates = await GetUpdates(cancellationToken); + + if (updates?.Messages == null) continue; + + var messageIds = new List(); + + foreach (var update in updates.Messages) + { + // if no chat were specified - broadcast on all chats, we've + if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); + + var request = new SendMessageRequest + { + Message = update + }; + + var response = await _bot.SendMessageAsync(request, cancellationToken); + + if (response.MessageSentStatus == MessageSentStatus.Ok) + await SendBroadcastReceived(messageIds, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + } + + await Task.Delay(_retryPause, cancellationToken); + } + }, + cancellationToken); + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { - _scope.Dispose(); - return Task.CompletedTask; } private async Task GetUpdates(CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.Value.ServerUri}/client/GetBroadcast" + var updatesResponse = await $"{_settings.ServerUri}/bot/client/GetBroadcast" .WithTimeout(_longPollTimeout) .PostJsonAsync(new GetBroadCastMessagesRequest { - BotId = _settings.Value.BotId + BotId = _settings.BotId }, cancellationToken: cancellationToken); if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) @@ -120,11 +106,11 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.Value.ServerUri}/client/BroadcastReceived" + var updatesResponse = await $"{_settings.ServerUri}/bot/client/BroadcastReceived" .WithTimeout(_longPollTimeout) .PostJsonAsync(new BroadCastMessagesReceivedRequest { - BotId = _settings.Value.BotId, + BotId = _settings.BotId, MessageIds = chatIds.ToArray() }, cancellationToken: cancellationToken); diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index c708b9ec..398c5f18 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Botticelli.Broadcasting.Extensions; @@ -28,11 +29,15 @@ public static BotBuilder AddBroadcasting(t var settings = config.GetSection(BroadcastingSettings.Section).Get(); if (settings == null) throw new ConfigurationErrorsException("Broadcasting settings are missing!"); - + botBuilder.Services .AddDbContext(opt => opt.UseSqlite($"Data source={settings.ConnectionString}")); - botBuilder.AddBuildAction(() => botBuilder.Services.AddHostedService>()); + botBuilder.Services.AddHostedService>(s => + new BroadcastReceiver(s.GetRequiredService>(), + s.GetRequiredService(), + settings, + s.GetRequiredService>>())); ApplyMigrations(botBuilder.Services); diff --git a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs index a5e454b1..6d1c3575 100644 --- a/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs +++ b/Botticelli.Framework.Telegram/Builders/TelegramBotBuilder.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Botticelli.Bot.Data; using Botticelli.Bot.Data.Repositories; using Botticelli.Bot.Data.Settings; @@ -19,12 +18,10 @@ 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; using Microsoft.Extensions.Logging; -using Telegram.Bot; using Telegram.Bot.Types.ReplyMarkups; namespace Botticelli.Framework.Telegram.Builders; @@ -134,7 +131,7 @@ public TelegramBotBuilder AddClient(TelegramClientDecoratorBu throw new InvalidDataException($"{nameof(bot)} shouldn't be null!"); AddEvents(bot); - + return bot; } diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 371352f3..528d90b9 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -130,7 +130,7 @@ static TBotBuilder InnerBuild(IServiceCollection services, var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); - TBotBuilder botBuilder = TelegramBotBuilder.Instance(services, + TBotBuilder? botBuilder = TelegramBotBuilder.Instance(services, ServerSettingsBuilder, SettingsBuilder, DataAccessSettingsBuilder, diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs deleted file mode 100644 index 38ea1245..00000000 --- a/Botticelli.Framework.Telegram/Extensions/ServiceProviderExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Botticelli.Framework.Telegram.Builders; -using Microsoft.Extensions.DependencyInjection; - -namespace Botticelli.Framework.Telegram.Extensions; - -public static class ServiceProviderExtensions -{ - public static IServiceProvider UseTelegramBot(this IServiceProvider serviceProvider) - { - var builder = serviceProvider.GetRequiredService>>(); - - builder.Build(serviceProvider); - - return serviceProvider; - } -} \ No newline at end of file diff --git a/Botticelli.Framework/Builders/BotBuilder.cs b/Botticelli.Framework/Builders/BotBuilder.cs index fc7048ec..1eef12eb 100644 --- a/Botticelli.Framework/Builders/BotBuilder.cs +++ b/Botticelli.Framework/Builders/BotBuilder.cs @@ -19,19 +19,6 @@ public abstract class BotBuilder protected abstract TBot? InnerBuild(IServiceProvider serviceProvider); } -public interface IBotBuilder where TBotBuilder : BotBuilder -{ - BotBuilder AddServices(IServiceCollection services); - BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder); - BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder); - BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler); - BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler); - BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler); - BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler); - BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler); - TBot? Build(IServiceProvider serviceProvider); -} - public abstract class BotBuilder : BotBuilder, IBotBuilder where TBotBuilder : BotBuilder { @@ -46,8 +33,6 @@ public abstract class BotBuilder : BotBuilder, IBotBuil protected BaseBot.ContactSharedEventHandler? SharedContact; protected BaseBot.NewChatMembersEventHandler? NewChatMembers; - protected List BuildActions = new(); - protected override void Assert() { } @@ -114,11 +99,4 @@ public virtual BotBuilder AddSharedContact(BaseBot.ContactSh return this; } - - public virtual BotBuilder AddBuildAction(Action buildAction) - { - BuildActions.Add(buildAction); - - return this; - } } \ No newline at end of file diff --git a/Botticelli.Framework/Builders/IBotBuilder.cs b/Botticelli.Framework/Builders/IBotBuilder.cs new file mode 100644 index 00000000..fa750f11 --- /dev/null +++ b/Botticelli.Framework/Builders/IBotBuilder.cs @@ -0,0 +1,18 @@ +using Botticelli.Bot.Data.Settings; +using Botticelli.Client.Analytics.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace Botticelli.Framework.Builders; + +public interface IBotBuilder where TBotBuilder : BotBuilder +{ + BotBuilder AddServices(IServiceCollection services); + BotBuilder AddAnalyticsSettings(AnalyticsClientSettingsBuilder clientSettingsBuilder); + BotBuilder AddBotDataAccessSettings(DataAccessSettingsBuilder botDataAccessBuilder); + BotBuilder AddOnMessageSent(BaseBot.MsgSentEventHandler handler); + BotBuilder AddOnMessageReceived(BaseBot.MsgReceivedEventHandler handler); + BotBuilder AddOnMessageRemoved(BaseBot.MsgRemovedEventHandler handler); + BotBuilder AddNewChatMembers(BaseBot.NewChatMembersEventHandler handler); + BotBuilder AddSharedContact(BaseBot.ContactSharedEventHandler handler); + TBot? Build(IServiceProvider serviceProvider); +} \ No newline at end of file diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index 328af13c..c409d6dd 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -17,34 +17,22 @@ namespace Botticelli.Server.Back.Controllers; [ApiController] [Authorize(AuthenticationSchemes = "Bearer")] [Route("/v1/admin")] -public class AdminController +public class AdminController( + IBotManagementService botManagementService, + IBotStatusDataService botStatusDataService, + ILogger logger, + IBroadcastService broadcastService) { - private readonly IBotManagementService _botManagementService; - private readonly IBotStatusDataService _botStatusDataService; - private readonly IBroadcastService _broadcastService; - private readonly ILogger _logger; - - public AdminController(IBotManagementService botManagementService, - IBotStatusDataService botStatusDataService, - ILogger logger, - IBroadcastService broadcastService) - { - _botManagementService = botManagementService; - _botStatusDataService = botStatusDataService; - _logger = logger; - _broadcastService = broadcastService; - } - [HttpPost("[action]")] public async Task AddNewBot([FromBody] RegisterBotRequest request) { - _logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) started..."); - var success = await _botManagementService.RegisterBot(request.BotId, + logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) started..."); + var success = await botManagementService.RegisterBot(request.BotId, request.BotKey, request.BotName, request.Type); - _logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) success: {success}..."); + logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) success: {success}..."); return new RegisterBotResponse { @@ -56,12 +44,12 @@ public async Task AddNewBot([FromBody] RegisterBotRequest r [HttpPut("[action]")] public async Task UpdateBot([FromBody] UpdateBotRequest request) { - _logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) started..."); - var success = await _botManagementService.UpdateBot(request.BotId, + logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) started..."); + var success = await botManagementService.UpdateBot(request.BotId, request.BotKey, request.BotName); - _logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) success: {success}..."); + logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) success: {success}..."); return new UpdateBotResponse { @@ -79,7 +67,7 @@ public async Task UpdateBot([FromBody] UpdateBotRequest reque [HttpPost("[action]")] public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) { - await _broadcastService.BroadcastMessage(new Broadcast + await broadcastService.BroadcastMessage(new Broadcast { Id = message.Uid ?? throw new NullReferenceException("Id cannot be null!"), BotId = botId ?? throw new NullReferenceException("BotId cannot be null!"), @@ -91,24 +79,24 @@ await _broadcastService.BroadcastMessage(new Broadcast [HttpGet("[action]")] public Task> GetBots() { - return Task.FromResult(_botStatusDataService.GetBots()); + return Task.FromResult(botStatusDataService.GetBots()); } [HttpGet("[action]")] public async Task ActivateBot([FromQuery] string botId) { - await _botManagementService.SetRequiredBotStatus(botId, BotStatus.Unlocked); + await botManagementService.SetRequiredBotStatus(botId, BotStatus.Unlocked); } [HttpGet("[action]")] public async Task DeactivateBot([FromQuery] string botId) { - await _botManagementService.SetRequiredBotStatus(botId, BotStatus.Locked); + await botManagementService.SetRequiredBotStatus(botId, BotStatus.Locked); } [HttpGet("[action]")] public async Task RemoveBot([FromQuery] string botId) { - await _botManagementService.RemoveBot(botId); + await botManagementService.RemoveBot(botId); } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index 0c3815cd..a44fa1e1 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -17,7 +17,7 @@ public async Task BroadcastMessage(Broadcast message) public async Task> GetMessages(string botId) { - return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId)).ToArrayAsync(); + return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).ToArrayAsync(); } public async Task MarkReceived(string botId, string messageId) diff --git a/Botticelli.Server.FrontNew/Handler/AuthDelegatingHandler.cs b/Botticelli.Server.FrontNew/Handler/AuthDelegatingHandler.cs index a8ac5fd7..fe5d8da5 100644 --- a/Botticelli.Server.FrontNew/Handler/AuthDelegatingHandler.cs +++ b/Botticelli.Server.FrontNew/Handler/AuthDelegatingHandler.cs @@ -1,31 +1,19 @@ using System.Net.Http.Headers; -using System.Security.Authentication; using Botticelli.Server.FrontNew.Clients; namespace Botticelli.Server.FrontNew.Handler; -public class AuthDelegatingHandler : DelegatingHandler +public class AuthDelegatingHandler(SessionClient sessionClient) : DelegatingHandler { - private readonly SessionClient _sessionClient; - - public AuthDelegatingHandler(SessionClient sessionClient) - { - _sessionClient = sessionClient; - } - protected override async Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) + CancellationToken cancellationToken) { - var session = _sessionClient.GetSession(); - - Console.WriteLine($"Botticelli.Auth.Sample.Telegram delegating got session: {session?.Token}"); + var session = sessionClient.GetSession(); - if (session == null) throw new AuthenticationException("Can't find session!"); + if (session == null) throw new UnauthorizedAccessException("Can't find session!"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.Token); - var response = await base.SendAsync(request, cancellationToken); - - return response; + return await base.SendAsync(request, cancellationToken); } } \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Telegram/Program.cs b/Samples/Broadcasting.Sample.Telegram/Program.cs index 679ad51b..65e8c867 100644 --- a/Samples/Broadcasting.Sample.Telegram/Program.cs +++ b/Samples/Broadcasting.Sample.Telegram/Program.cs @@ -17,4 +17,8 @@ .AddProcessor>() .AddValidator>(); -await builder.Build().RunAsync(); \ No newline at end of file +var app = builder.Build(); +app.Urls.Clear(); +app.Urls.Add("http://localhost:5044"); + +await app.RunAsync(); \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Telegram/appsettings.json b/Samples/Broadcasting.Sample.Telegram/appsettings.json index 1450ea2c..636c3c25 100644 --- a/Samples/Broadcasting.Sample.Telegram/appsettings.json +++ b/Samples/Broadcasting.Sample.Telegram/appsettings.json @@ -9,7 +9,7 @@ "ConnectionString": "database.db;Password=123" }, "Server": { - "ServerUri": "http://103.252.116.18:5042/v1/" + "ServerUri": "https://localhost:7247/v1/" }, "AnalyticsClient": { "TargetUrl": "http://localhost:5251/v1/" @@ -22,9 +22,9 @@ }, "Broadcasting": { "ConnectionString": "broadcasting_database.db;Password=321", - "BotId": "XuS3Ok5dP37v06vrqvfsfXT5G0LVq0nyD68Y", + "BotId": "91d3z7fW87uvWn9441vdxusnhAe4sIaDTyIJVm9Q", "HowOld": "00:10:00", - "ServerUri": "http://103.252.116.18:5042/v1" + "ServerUri": "https://localhost:7247/v1" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Samples/Layouts.Sample.Telegram/Program.cs b/Samples/Layouts.Sample.Telegram/Program.cs index 6be577e6..2704316a 100644 --- a/Samples/Layouts.Sample.Telegram/Program.cs +++ b/Samples/Layouts.Sample.Telegram/Program.cs @@ -21,8 +21,4 @@ .AddInlineCalendar() .AddOsmLocations(builder.Configuration); -var app = builder.Build(); - -app.Services.UseTelegramBot(); - -await app.RunAsync(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file From 04c8b223fb90a1f469d0cb0957d0dd7cf23fff35 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 14 Sep 2025 11:35:00 +0300 Subject: [PATCH 077/101] BOTTICELLI-55: - bot update handler - botticelli server: additional information for a bot is optional now --- Botticelli.Bot.Dal/BotInfoContext.cs | 5 ---- Botticelli.Broadcasting/BroadcastReceiver.cs | 4 +-- .../Extensions/ServiceCollectionExtensions.cs | 22 ++++++++++----- .../Extensions/ServiceCollectionExtensions.cs | 5 +++- .../Handlers/BotUpdateHandler.cs | 2 +- .../Services/BotStatusService.cs | 28 ++++++++++++++++--- .../Controllers/BotController.cs | 3 +- .../GetRequiredStatusFromServerResponse.cs | 2 +- 8 files changed, 48 insertions(+), 23 deletions(-) diff --git a/Botticelli.Bot.Dal/BotInfoContext.cs b/Botticelli.Bot.Dal/BotInfoContext.cs index fe1cc31c..9970a5d2 100644 --- a/Botticelli.Bot.Dal/BotInfoContext.cs +++ b/Botticelli.Bot.Dal/BotInfoContext.cs @@ -6,11 +6,6 @@ namespace Botticelli.Bot.Data; public class BotInfoContext : DbContext { - // public BotInfoContext() : base((new DbContextOptionsBuilder().UseSqlite("Data Source=database.db")).Options) - // { - // - // } - public BotInfoContext(DbContextOptions options) : base(options) { } diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index 42e02ab3..bfafc7e1 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -104,7 +104,7 @@ public Task StopAsync(CancellationToken cancellationToken) cancellationToken); } - private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) + private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) { var updatesResponse = await $"{_settings.ServerUri}/bot/client/BroadcastReceived" .WithTimeout(_longPollTimeout) @@ -118,6 +118,6 @@ public Task StopAsync(CancellationToken cancellationToken) if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) return null; return await updatesResponse.ResponseMessage.Content - .ReadFromJsonAsync(cancellationToken); + .ReadFromJsonAsync(cancellationToken); } } \ No newline at end of file diff --git a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs index 398c5f18..2d5d44f9 100644 --- a/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Broadcasting/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using System.Configuration; using Botticelli.Broadcasting.Dal; +using Botticelli.Broadcasting.Dal.Models; using Botticelli.Broadcasting.Settings; using Botticelli.Framework; using Botticelli.Framework.Builders; @@ -33,20 +34,27 @@ public static BotBuilder AddBroadcasting(t botBuilder.Services .AddDbContext(opt => opt.UseSqlite($"Data source={settings.ConnectionString}")); - botBuilder.Services.AddHostedService>(s => - new BroadcastReceiver(s.GetRequiredService>(), - s.GetRequiredService(), + botBuilder.Services.AddHostedService>(sp => + new BroadcastReceiver(sp.GetRequiredService(), + sp.GetRequiredService(), settings, - s.GetRequiredService>>())); + sp.GetRequiredService>>())); ApplyMigrations(botBuilder.Services); return botBuilder.AddOnMessageReceived((_, args) => { var context = botBuilder.Services.BuildServiceProvider().GetRequiredService(); - - var disabledChats = context.Chats.Where(c => !c.IsActive && args.Message.ChatIds.Contains(c.ChatId)).AsQueryable(); - var nonExistingChats = context.Chats.Where(c => !args.Message.ChatIds.Contains(c.ChatId)).ToArray(); + + var disabledChats = context.Chats.Where(c => !c.IsActive && args.Message.ChatIds.Contains(c.ChatId)) + .AsQueryable(); + var nonExistingChats = args.Message.ChatIds.Where(c => context.Chats.All(cc => cc.ChatId != c)) + .Select(c => new Chat + { + ChatId = c, + IsActive = true + }) + .ToList(); context.Chats.AddRange(nonExistingChats); disabledChats.ExecuteUpdate(c => c.SetProperty(chat => chat.IsActive, true)); diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index 528d90b9..c53ab088 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -130,13 +130,16 @@ static TBotBuilder InnerBuild(IServiceCollection services, var clientBuilder = TelegramClientDecoratorBuilder.Instance(services, SettingsBuilder); - TBotBuilder? botBuilder = TelegramBotBuilder.Instance(services, + var botBuilder = TelegramBotBuilder.Instance(services, ServerSettingsBuilder, SettingsBuilder, DataAccessSettingsBuilder, AnalyticsClientOptionsBuilder, false); + if (botBuilder == null) + throw new ApplicationException("bot builder is null!"); + botBuilder.AddClient(clientBuilder) .AddServices(services); diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index e3cb8eee..dbe5a38a 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -32,7 +32,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, { try { - // caching updates in order to avoid message "cloning" + // caching updates to avoid message "cloning" if (_memoryCache.TryGetValue(update.Id, out _)) return; _memoryCache.Set(update.Id, update, _entryOptions); diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index f26ddf70..25a6218d 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -32,7 +32,7 @@ public override Task StartAsync(CancellationToken cancellationToken) } /// - /// Get required status for a bot from server + /// Get the required status for a bot from server /// /// /// @@ -64,11 +64,31 @@ private void GetRequiredStatus(CancellationToken cancellationToken) var taskResult = task.Result; - if (taskResult == null) throw new BotException("No result from server!"); + if (taskResult == null) + { + logger.LogError("No result from server!"); + + return Task.FromResult(new GetRequiredStatusFromServerResponse + { + Status = BotStatus.Unknown, + BotId = BotId ?? string.Empty, + BotContext = null + }); + } var botContext = taskResult.BotContext; - if (botContext == null) throw new BotException("No bot context from server!"); + if (botContext == null) + { + logger.LogError("No bot context from server!"); + + return Task.FromResult(new GetRequiredStatusFromServerResponse + { + Status = BotStatus.Unknown, + BotId = BotId ?? string.Empty, + BotContext = null + }); + } var botData = new BotData.Entities.Bot.BotData { @@ -77,7 +97,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) BotKey = botContext.BotKey, AdditionalInfo = botContext.Items?.Select(it => new BotAdditionalInfo { - BotId = taskResult!.BotId, + BotId = taskResult.BotId, ItemName = it.Key, ItemValue = it.Value }) diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 6b5d094b..00d78647 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -40,13 +40,12 @@ public async Task GetRequiredBotStatus([Fro var botInfo = await botStatusDataService.GetBotInfo(request.BotId!); botInfo.NotNull(); botInfo?.BotKey?.NotNullOrEmpty(); - botInfo!.AdditionalInfo!.NotNullOrEmpty(); var context = new BotContext { BotId = botInfo!.BotId, BotKey = botInfo.BotKey!, - Items = botInfo.AdditionalInfo!.ToDictionary(k => k.ItemName, k => k.ItemValue)! + Items = botInfo.AdditionalInfo?.ToDictionary(k => k.ItemName, k => k.ItemValue)! }; return new GetRequiredStatusFromServerResponse diff --git a/Botticelli.Shared/API/Client/Responses/GetRequiredStatusFromServerResponse.cs b/Botticelli.Shared/API/Client/Responses/GetRequiredStatusFromServerResponse.cs index e812e6d3..7b025c0a 100644 --- a/Botticelli.Shared/API/Client/Responses/GetRequiredStatusFromServerResponse.cs +++ b/Botticelli.Shared/API/Client/Responses/GetRequiredStatusFromServerResponse.cs @@ -8,5 +8,5 @@ public class GetRequiredStatusFromServerResponse : ServerBaseResponse public required string BotId { get; set; } public BotStatus? Status { get; set; } - public required BotContext BotContext { get; set; } + public required BotContext? BotContext { get; set; } } \ No newline at end of file From ffc27cf933973b7246765fd0dfcdf6a7173b994e Mon Sep 17 00:00:00 2001 From: stone1985 Date: Tue, 16 Sep 2025 18:42:57 +0300 Subject: [PATCH 078/101] BOTTICELLI-67: Remove Viber --- .../Botticelli.Server.FrontNew.csproj | 3 - Botticelli.Server.FrontNew/wwwroot/Viber.png | Bin 6774 -> 0 bytes Botticelli.sln | 7 - Viber.Api/Entities/Location.cs | 13 -- Viber.Api/Entities/MessageSender.cs | 25 --- Viber.Api/Entities/Sender.cs | 13 -- Viber.Api/Entities/User.cs | 25 --- Viber.Api/Entities/ViberMessage.cs | 22 --- Viber.Api/Exceptions/ViberClientException.cs | 11 -- Viber.Api/IViberService.cs | 25 --- Viber.Api/Requests/ApiSendMessageRequest.cs | 26 ---- Viber.Api/Requests/BaseRequest.cs | 10 -- Viber.Api/Requests/RemoveWebHookRequest.cs | 10 -- Viber.Api/Requests/SetWebHookRequest.cs | 20 --- Viber.Api/Responses/ApiSendMessageResponse.cs | 22 --- Viber.Api/Responses/GetWebHookEvent.cs | 29 ---- Viber.Api/Responses/SetWebHookResponse.cs | 17 -- Viber.Api/Settings/ViberApiSettings.cs | 9 -- Viber.Api/Viber.Api.csproj | 28 ---- Viber.Api/ViberService.cs | 146 ------------------ 20 files changed, 461 deletions(-) delete mode 100644 Botticelli.Server.FrontNew/wwwroot/Viber.png delete mode 100644 Viber.Api/Entities/Location.cs delete mode 100644 Viber.Api/Entities/MessageSender.cs delete mode 100644 Viber.Api/Entities/Sender.cs delete mode 100644 Viber.Api/Entities/User.cs delete mode 100644 Viber.Api/Entities/ViberMessage.cs delete mode 100644 Viber.Api/Exceptions/ViberClientException.cs delete mode 100644 Viber.Api/IViberService.cs delete mode 100644 Viber.Api/Requests/ApiSendMessageRequest.cs delete mode 100644 Viber.Api/Requests/BaseRequest.cs delete mode 100644 Viber.Api/Requests/RemoveWebHookRequest.cs delete mode 100644 Viber.Api/Requests/SetWebHookRequest.cs delete mode 100644 Viber.Api/Responses/ApiSendMessageResponse.cs delete mode 100644 Viber.Api/Responses/GetWebHookEvent.cs delete mode 100644 Viber.Api/Responses/SetWebHookResponse.cs delete mode 100644 Viber.Api/Settings/ViberApiSettings.cs delete mode 100644 Viber.Api/Viber.Api.csproj delete mode 100644 Viber.Api/ViberService.cs diff --git a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj index 061e3d04..d97cb4cb 100644 --- a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj +++ b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj @@ -62,9 +62,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/Botticelli.Server.FrontNew/wwwroot/Viber.png b/Botticelli.Server.FrontNew/wwwroot/Viber.png deleted file mode 100644 index ca4f9f5372c692ad5f3814c1b8cff5ae000e7762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6774 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 YABV#6+Eus+u>b%707*qoM6N<$f(G*{9RL6T diff --git a/Botticelli.sln b/Botticelli.sln index fc6567a0..71740ae0 100644 --- a/Botticelli.sln +++ b/Botticelli.sln @@ -23,8 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{346A18CA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Framework.Telegram", "Botticelli.Framework.Telegram\Botticelli.Framework.Telegram.csproj", "{71008870-2212-4A74-8777-B5B268C27911}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Viber.Api", "Viber.Api\Viber.Api.csproj", "{5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Server.Data", "Botticelli.Server.Data\Botticelli.Server.Data.csproj", "{CDFC4EAD-60BD-419A-ADF5-451766D22F29}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Botticelli.Talks", "Botticelli.Talks\Botticelli.Talks.csproj", "{9F4B5D3F-661C-433B-BC9F-CAC4048EF9EA}" @@ -293,10 +291,6 @@ Global {71008870-2212-4A74-8777-B5B268C27911}.Debug|Any CPU.Build.0 = Debug|Any CPU {71008870-2212-4A74-8777-B5B268C27911}.Release|Any CPU.ActiveCfg = Release|Any CPU {71008870-2212-4A74-8777-B5B268C27911}.Release|Any CPU.Build.0 = Release|Any CPU - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE}.Release|Any CPU.Build.0 = Release|Any CPU {CDFC4EAD-60BD-419A-ADF5-451766D22F29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDFC4EAD-60BD-419A-ADF5-451766D22F29}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDFC4EAD-60BD-419A-ADF5-451766D22F29}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -668,7 +662,6 @@ Global {9F898466-739B-4DE0-BFD7-6B5203957236} = {39412AE4-B325-4827-B24E-C15415DCA7E9} {0F1EE8FF-4CE4-49D5-BF49-3837EB3BD637} = {39412AE4-B325-4827-B24E-C15415DCA7E9} {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} = {8DA4E10C-3EF4-42CD-BB51-A6562A7A0633} - {5EF8192E-6AEC-4FEF-A104-FEA0E7EA2DCE} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} {71008870-2212-4A74-8777-B5B268C27911} = {9D2CCAB5-67AB-4A1A-B329-77F7973DB2A2} {4B590BAD-B92D-427C-B340-68A40A9C9B95} = {2AD59714-541D-4794-9216-6EAB25F271DA} {2E48B176-D743-49B3-B1F2-6EC159EB80EF} = {4B590BAD-B92D-427C-B340-68A40A9C9B95} diff --git a/Viber.Api/Entities/Location.cs b/Viber.Api/Entities/Location.cs deleted file mode 100644 index bdaef7bd..00000000 --- a/Viber.Api/Entities/Location.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Entities -{ - public class Location - { - [JsonPropertyName("lat")] - public double Lat { get; set; } - - [JsonPropertyName("lon")] - public double Lon { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Entities/MessageSender.cs b/Viber.Api/Entities/MessageSender.cs deleted file mode 100644 index 61c93ed2..00000000 --- a/Viber.Api/Entities/MessageSender.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Entities -{ - public class MessageSender - { - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("avatar")] - public string? Avatar { get; set; } - - [JsonPropertyName("country")] - public string? Country { get; set; } - - [JsonPropertyName("language")] - public string? Language { get; set; } - - [JsonPropertyName("api_version")] - public int ApiVersion { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Entities/Sender.cs b/Viber.Api/Entities/Sender.cs deleted file mode 100644 index 288e15f1..00000000 --- a/Viber.Api/Entities/Sender.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Entities -{ - public class Sender - { - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("avatar")] - public string? Avatar { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Entities/User.cs b/Viber.Api/Entities/User.cs deleted file mode 100644 index f286be4b..00000000 --- a/Viber.Api/Entities/User.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Entities -{ - public class User - { - [JsonPropertyName("id")] - public string? Id { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("avatar")] - public string? Avatar { get; set; } - - [JsonPropertyName("country")] - public string? Country { get; set; } - - [JsonPropertyName("language")] - public string? Language { get; set; } - - [JsonPropertyName("api_version")] - public int ApiVersion { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Entities/ViberMessage.cs b/Viber.Api/Entities/ViberMessage.cs deleted file mode 100644 index 35032ae7..00000000 --- a/Viber.Api/Entities/ViberMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Entities -{ - public class ViberMessage - { - [JsonPropertyName("type")] - public string? Type { get; set; } - - [JsonPropertyName("text")] - public string? Text { get; set; } - - [JsonPropertyName("media")] - public string? Media { get; set; } - - [JsonPropertyName("location")] - public Location? Location { get; set; } - - [JsonPropertyName("tracking_data")] - public string? TrackingData { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Exceptions/ViberClientException.cs b/Viber.Api/Exceptions/ViberClientException.cs deleted file mode 100644 index 82f05fdd..00000000 --- a/Viber.Api/Exceptions/ViberClientException.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Viber.Api.Exceptions -{ - public class ViberClientException : Exception - { - public ViberClientException(string message, Exception? inner = null) : base(message, inner) - { - } - } -} \ No newline at end of file diff --git a/Viber.Api/IViberService.cs b/Viber.Api/IViberService.cs deleted file mode 100644 index ab2b7e62..00000000 --- a/Viber.Api/IViberService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Viber.Api.Requests; -using Viber.Api.Responses; - -namespace Viber.Api -{ - public interface IViberService : IDisposable - { - delegate void GotMessageHandler(GetWebHookEvent @event); - - event GotMessageHandler GotMessage; - - void Start(); - - void Stop(); - - Task SetWebHook(SetWebHookRequest request, - CancellationToken cancellationToken = default); - - Task SendMessage(ApiSendMessageRequest request, - CancellationToken cancellationToken = default); - } -} \ No newline at end of file diff --git a/Viber.Api/Requests/ApiSendMessageRequest.cs b/Viber.Api/Requests/ApiSendMessageRequest.cs deleted file mode 100644 index 59a6b500..00000000 --- a/Viber.Api/Requests/ApiSendMessageRequest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Text.Json.Serialization; -using Viber.Api.Entities; - -namespace Viber.Api.Requests -{ - public class ApiSendMessageRequest : BaseRequest - { - [JsonPropertyName("receiver")] - public string? Receiver { get; set; } - - [JsonPropertyName("min_api_version")] - public int MinApiVersion { get; set; } - - [JsonPropertyName("sender")] - public Sender? Sender { get; set; } - - [JsonPropertyName("tracking_data")] - public string? TrackingData { get; set; } - - [JsonPropertyName("type")] - public string? Type { get; set; } - - [JsonPropertyName("text")] - public string? Text { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Requests/BaseRequest.cs b/Viber.Api/Requests/BaseRequest.cs deleted file mode 100644 index 3f96fbac..00000000 --- a/Viber.Api/Requests/BaseRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Requests -{ - public abstract class BaseRequest - { - [JsonPropertyName("auth_token")] - public string? AuthToken { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Requests/RemoveWebHookRequest.cs b/Viber.Api/Requests/RemoveWebHookRequest.cs deleted file mode 100644 index 0d16b0b8..00000000 --- a/Viber.Api/Requests/RemoveWebHookRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Requests -{ - public class RemoveWebHookRequest : BaseRequest - { - [JsonPropertyName("url")] - public string? Url { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Requests/SetWebHookRequest.cs b/Viber.Api/Requests/SetWebHookRequest.cs deleted file mode 100644 index 37433c93..00000000 --- a/Viber.Api/Requests/SetWebHookRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Viber.Api.Requests -{ - public class SetWebHookRequest : BaseRequest - { - [JsonPropertyName("url")] - public string? Url { get; set; } - - [JsonPropertyName("event_types")] - public List? EventTypes { get; set; } - - [JsonPropertyName("send_name")] - public bool SendName { get; set; } - - [JsonPropertyName("send_photo")] - public bool SendPhoto { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Responses/ApiSendMessageResponse.cs b/Viber.Api/Responses/ApiSendMessageResponse.cs deleted file mode 100644 index 89b35101..00000000 --- a/Viber.Api/Responses/ApiSendMessageResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Viber.Api.Responses -{ - public class ApiSendMessageResponse - { - [JsonPropertyName("status")] - public int Status { get; set; } - - [JsonPropertyName("status_message")] - public string? StatusMessage { get; set; } - - [JsonPropertyName("message_token")] - public long MessageToken { get; set; } - - [JsonPropertyName("chat_hostname")] - public string? ChatHostname { get; set; } - - [JsonPropertyName("billing_status")] - public int BillingStatus { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Responses/GetWebHookEvent.cs b/Viber.Api/Responses/GetWebHookEvent.cs deleted file mode 100644 index fe57c3f9..00000000 --- a/Viber.Api/Responses/GetWebHookEvent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json.Serialization; -using Viber.Api.Entities; - -namespace Viber.Api.Responses -{ - public class GetWebHookEvent - { - [JsonPropertyName("event")] - public string? Event { get; set; } - - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } - - [JsonPropertyName("message_token")] - public long MessageToken { get; set; } - - [JsonPropertyName("user_id")] - public string? UserId { get; set; } - - [JsonPropertyName("desc")] - public string? Desc { get; set; } - - [JsonPropertyName("sender")] - public MessageSender? Sender { get; set; } - - [JsonPropertyName("message")] - public ViberMessage? Message { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Responses/SetWebHookResponse.cs b/Viber.Api/Responses/SetWebHookResponse.cs deleted file mode 100644 index dfd20f09..00000000 --- a/Viber.Api/Responses/SetWebHookResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Viber.Api.Responses -{ - public class SetWebHookResponse - { - [JsonPropertyName("status")] - public int Status { get; set; } - - [JsonPropertyName("status_message")] - public string? StatusMessage { get; set; } - - [JsonPropertyName("event_types")] - public List? EventTypes { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Settings/ViberApiSettings.cs b/Viber.Api/Settings/ViberApiSettings.cs deleted file mode 100644 index 1b139c52..00000000 --- a/Viber.Api/Settings/ViberApiSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Viber.Api.Settings -{ - public class ViberApiSettings - { - public string? RemoteUrl { get; set; } - public string? HookUrl { get; set; } - public string? ViberToken { get; set; } - } -} \ No newline at end of file diff --git a/Viber.Api/Viber.Api.csproj b/Viber.Api/Viber.Api.csproj deleted file mode 100644 index 6de46f56..00000000 --- a/Viber.Api/Viber.Api.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - netstandard2.1 - enable - 0.8.0 - Botticelli - Igor Evdokimov - https://github.com/devgopher/botticelli - logo.jpg - https://github.com/devgopher/botticelli - - - - True - \ - - - - - - - - - - - - \ No newline at end of file diff --git a/Viber.Api/ViberService.cs b/Viber.Api/ViberService.cs deleted file mode 100644 index d29140c8..00000000 --- a/Viber.Api/ViberService.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Viber.Api.Exceptions; -using Viber.Api.Requests; -using Viber.Api.Responses; -using Viber.Api.Settings; - -namespace Viber.Api -{ - public class ViberService : IViberService - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly HttpListener _httpListener; - private readonly ViberApiSettings _settings; - private readonly ManualResetEventSlim _stopReceiving = new ManualResetEventSlim(false); - - public ViberService(IHttpClientFactory httpClientFactory, - ViberApiSettings settings) - { - _httpListener = new HttpListener(); - _httpClientFactory = httpClientFactory; - _settings = settings; - _httpListener.Prefixes.Add("http://127.0.0.1:5000/"); - - Start(); - } - - public event IViberService.GotMessageHandler? GotMessage; - - public void Start() - { - _httpListener.Start(); - Task.Run(() => ReceiveMessages()); - - _ = SetWebHook(new SetWebHookRequest - { - Url = _settings.HookUrl, - AuthToken = _settings.ViberToken, - EventTypes = new List - { - "delivered", - "seen", - "failed", - "subscribed", - "unsubscribed", - "conversation_started" - } - }); - } - - public void Stop() - { - _stopReceiving.Set(); - _httpListener.Stop(); - } - - public async Task SetWebHook(SetWebHookRequest request, - CancellationToken cancellationToken = default) - { - request.AuthToken = _settings.ViberToken; - - return await InnerSend(request, - "set_webhook", - cancellationToken); - } - - public async Task SendMessage(ApiSendMessageRequest request, - CancellationToken cancellationToken = default) - { - request.AuthToken = _settings.ViberToken; - - return await InnerSend(request, - "send_message", - cancellationToken); - } - - public void Dispose() - { - Stop(); - _httpListener.Close(); - } - - protected async Task ReceiveMessages(CancellationToken cancellationToken = default) - { - while (!_stopReceiving.IsSet) - try - { - if (cancellationToken is {CanBeCanceled: true, IsCancellationRequested: true}) - { - _stopReceiving.Set(); - - return; - } - - if (!_httpListener.IsListening) continue; - - var context = await _httpListener.GetContextAsync(); - - if (context.Response.StatusCode == (int) HttpStatusCode.OK) - { - using var sr = new StreamReader(context.Response.OutputStream); - var content = await sr.ReadToEndAsync(); - var deserialized = JsonSerializer.Deserialize(content); - - if (deserialized == null) continue; - - GotMessage?.Invoke(deserialized); - } - else - { - throw new ViberClientException($"Error listening: {context.Response.StatusCode}, " + - $"{context.Response.StatusDescription}"); - } - } - catch (Exception? ex) - { - throw new ViberClientException(ex.Message, ex); - } - } - - private async Task InnerSend(TReq request, - string funcName, - CancellationToken cancellationToken) - { - using var httpClient = _httpClientFactory.CreateClient(); - httpClient.BaseAddress = new Uri( /*_settings.RemoteUrl*/ _settings.HookUrl); - - var content = JsonContent.Create(request); - - var httpResponse = await httpClient.PostAsync(funcName, content, cancellationToken); - - if (!httpResponse.IsSuccessStatusCode) throw new ViberClientException($"Error sending request {nameof(SetWebHook)}: {httpResponse.StatusCode}!"); - - if (httpResponse.Content == null) throw new ViberClientException(""); - - return await httpResponse.Content.ReadFromJsonAsync(cancellationToken); - } - } -} \ No newline at end of file From 9b6e49390078e1c6423f496d165e5144bb23543f Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Wed, 17 Sep 2025 13:57:41 +0300 Subject: [PATCH 079/101] BOTTICELLI-55: - broadcast works now --- Botticelli.Broadcasting/BroadcastReceiver.cs | 2 ++ Botticelli.Framework.Telegram/TelegramBot.cs | 9 +++------ Botticelli.Server.Back/Controllers/BotController.cs | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index bfafc7e1..a455d5d0 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -64,6 +64,8 @@ public Task StartAsync(CancellationToken cancellationToken) Message = update }; + messageIds = [update.Uid]; + var response = await _bot.SendMessageAsync(request, cancellationToken); if (response.MessageSentStatus == MessageSentStatus.Ok) diff --git a/Botticelli.Framework.Telegram/TelegramBot.cs b/Botticelli.Framework.Telegram/TelegramBot.cs index b2eb7262..38ff9c5b 100644 --- a/Botticelli.Framework.Telegram/TelegramBot.cs +++ b/Botticelli.Framework.Telegram/TelegramBot.cs @@ -216,9 +216,8 @@ await AdditionalProcessing(request, replyMarkup, response, message); - message.NotNull(); - - AddChatIdInnerIdLink(response, link.chatId, message); + if (message != null) + AddChatIdInnerIdLink(response, link.chatId, message); } response.MessageSentStatus = MessageSentStatus.Ok; @@ -286,7 +285,7 @@ await Client.EditMessageText(link.chatId, } } - private async Task ProcessAttachments(SendMessageRequest request, + private async Task ProcessAttachments(SendMessageRequest request, CancellationToken token, (string chatId, string innerId) link, ReplyMarkup? replyMarkup, @@ -297,8 +296,6 @@ private async Task ProcessAttachments(SendMessageRequest request, if (request.Message.Attachments == null) return message; - request.Message.Attachments.NotNullOrEmpty(); - foreach (var attachment in request.Message .Attachments .Where(a => a is BinaryBaseAttachment) diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 00d78647..76e50ac0 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -112,6 +112,7 @@ public async Task GetBroadcast([FromBody] GetBroad IsSuccess = true, Messages = broadcastMessages.Select(bm => new Message { + Uid = bm.Id, Type = Message.MessageType.Messaging, Subject = string.Empty, Body = bm.Body From 68b18ffd42975106c7461051e9f3a3f11994a15c Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Thu, 18 Sep 2025 20:50:06 +0300 Subject: [PATCH 080/101] BOTTICELLI-55: - admin: broadcast message text ara parameters fixed --- .../Pages/BotBroadcast.razor | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 6f7ace3e..38510d3d 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -20,8 +20,11 @@
- +
@@ -30,8 +33,12 @@
- +
From 1c67ae12f7247941d44842a6fe7798a9632a3210 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 21 Sep 2025 13:18:21 +0300 Subject: [PATCH 081/101] BOTTICELLI-55: - file upload for broadcasting --- .../BroadcastingContext.cs | 2 - .../Migrations/20250603202442_initial.cs | 33 --- .../BroadcastingContextModelSnapshot.cs | 37 --- .../Controllers/AdminController.cs | 18 ++ .../Controllers/BotController.cs | 9 +- .../Services/Broadcasting/BroadcastService.cs | 2 +- .../Bot/Broadcasting/Broadcast.cs | 1 + .../Bot/Broadcasting/BroadcastAttachment.cs | 2 + .../Bot/Broadcasting/MediaTypes.cs | 15 - ...0250921100822_BroadcastIdField.Designer.cs | 261 ++++++++++++++++++ .../20250921100822_BroadcastIdField.cs | 102 +++++++ .../ServerDataContextModelSnapshot.cs | 32 ++- .../Pages/BotBroadcast.razor | 57 ++-- .../Constants/MimeTypeConverter.cs | 21 ++ Botticelli.Shared/Utils/StreamUtils.cs | 30 +- .../Broadcasting.Sample.Common.csproj | 8 +- Samples/Broadcasting.Sample.Common/Program.cs | 3 - 17 files changed, 514 insertions(+), 119 deletions(-) delete mode 100644 Botticelli.Server.Data.Entities/Bot/Broadcasting/MediaTypes.cs create mode 100644 Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.Designer.cs create mode 100644 Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.cs create mode 100644 Botticelli.Shared/Constants/MimeTypeConverter.cs delete mode 100644 Samples/Broadcasting.Sample.Common/Program.cs diff --git a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs index a8e2c207..3271a44d 100644 --- a/Botticelli.Broadcasting.Dal/BroadcastingContext.cs +++ b/Botticelli.Broadcasting.Dal/BroadcastingContext.cs @@ -6,8 +6,6 @@ namespace Botticelli.Broadcasting.Dal; public class BroadcastingContext : DbContext { public DbSet Chats { get; set; } - public DbSet MessageCaches { get; set; } - public DbSet MessageStatuses { get; set; } public BroadcastingContext() { diff --git a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs index a2f29479..5df3d4ad 100644 --- a/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs +++ b/Botticelli.Broadcasting.Dal/Migrations/20250603202442_initial.cs @@ -22,33 +22,6 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_Chats", x => x.ChatId); }); - - migrationBuilder.CreateTable( - name: "MessageCaches", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - SerializedMessageObject = table.Column(type: "TEXT", maxLength: 100000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MessageCaches", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "MessageStatuses", - columns: table => new - { - MessageId = table.Column(type: "TEXT", nullable: false), - ChatId = table.Column(type: "TEXT", nullable: false), - IsSent = table.Column(type: "INTEGER", nullable: false), - CreatedDate = table.Column(type: "TEXT", nullable: false), - SentDate = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MessageStatuses", x => new { x.ChatId, x.MessageId }); - }); } /// @@ -56,12 +29,6 @@ protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "Chats"); - - migrationBuilder.DropTable( - name: "MessageCaches"); - - migrationBuilder.DropTable( - name: "MessageStatuses"); } } } diff --git a/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs index af4a9644..d4225d22 100644 --- a/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs +++ b/Botticelli.Broadcasting.Dal/Migrations/BroadcastingContextModelSnapshot.cs @@ -29,43 +29,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Chats"); }); - - modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageCache", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("SerializedMessageObject") - .IsRequired() - .HasMaxLength(100000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MessageCaches"); - }); - - modelBuilder.Entity("Botticelli.Broadcasting.Dal.Models.MessageStatus", b => - { - b.Property("ChatId") - .HasColumnType("TEXT"); - - b.Property("MessageId") - .HasColumnType("TEXT"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("IsSent") - .HasColumnType("INTEGER"); - - b.Property("SentDate") - .HasColumnType("TEXT"); - - b.HasKey("ChatId", "MessageId"); - - b.ToTable("MessageStatuses"); - }); #pragma warning restore 612, 618 } } diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index c409d6dd..0d2201c3 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -5,6 +5,7 @@ using Botticelli.Shared.API.Admin.Responses; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; +using Botticelli.Shared.Constants; using Botticelli.Shared.ValueObjects; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -72,6 +73,23 @@ await broadcastService.BroadcastMessage(new Broadcast Id = message.Uid ?? throw new NullReferenceException("Id cannot be null!"), BotId = botId ?? throw new NullReferenceException("BotId cannot be null!"), Body = message.Body ?? throw new NullReferenceException("Body cannot be null!"), + Attachments = message.Attachments + .Where(a => a is BinaryBaseAttachment) + .Select(a => + { + if (a is BinaryBaseAttachment baseAttachment) + return new BroadcastAttachment + { + Id = Guid.NewGuid(), + BroadcastId = message.Uid, + MediaType = baseAttachment.MediaType, + Filename = baseAttachment.Name, + Content = baseAttachment.Data + }; + + return null!; + }) + .ToList(), Sent = true }); } diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 76e50ac0..f30b985e 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -115,7 +115,14 @@ public async Task GetBroadcast([FromBody] GetBroad Uid = bm.Id, Type = Message.MessageType.Messaging, Subject = string.Empty, - Body = bm.Body + Body = bm.Body, + Attachments = bm.Attachments?.Select(BaseAttachment (att) => new BinaryBaseAttachment(att.Id.ToString(), + att.Filename, + att.MediaType, + string.Empty, + att.Content)) + .ToList() ?? + [] }) .ToArray() }; diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index a44fa1e1..e91735d8 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -17,7 +17,7 @@ public async Task BroadcastMessage(Broadcast message) public async Task> GetMessages(string botId) { - return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).ToArrayAsync(); + return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).Include(m => m.Attachments).ToArrayAsync(); } public async Task MarkReceived(string botId, string messageId) diff --git a/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs b/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs index 62f86aa3..81e05b76 100644 --- a/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs +++ b/Botticelli.Server.Data.Entities/Bot/Broadcasting/Broadcast.cs @@ -11,6 +11,7 @@ public class Broadcast public required string BotId { get; set; } public required string Body { get; set; } + public List? Attachments { get; set; } public DateTime Timestamp { get; set; } public bool Sent { get; set; } = false; public bool Received { get; set; } = false; diff --git a/Botticelli.Server.Data.Entities/Bot/Broadcasting/BroadcastAttachment.cs b/Botticelli.Server.Data.Entities/Bot/Broadcasting/BroadcastAttachment.cs index 61a141e5..2401a935 100644 --- a/Botticelli.Server.Data.Entities/Bot/Broadcasting/BroadcastAttachment.cs +++ b/Botticelli.Server.Data.Entities/Bot/Broadcasting/BroadcastAttachment.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Botticelli.Shared.Constants; namespace Botticelli.Server.Data.Entities.Bot.Broadcasting; @@ -9,6 +10,7 @@ public class BroadcastAttachment [Key] public Guid Id { get; set; } + public required string BroadcastId { get; set; } public MediaType MediaType { get; set; } public required string Filename { get; set; } public required byte[] Content { get; set; } diff --git a/Botticelli.Server.Data.Entities/Bot/Broadcasting/MediaTypes.cs b/Botticelli.Server.Data.Entities/Bot/Broadcasting/MediaTypes.cs deleted file mode 100644 index a726cb1c..00000000 --- a/Botticelli.Server.Data.Entities/Bot/Broadcasting/MediaTypes.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Botticelli.Server.Data.Entities.Bot.Broadcasting; - -public enum MediaType -{ - Audio, - Video, - Text, - Voice, - Image, - Sticker, - Contact, - Poll, - Document, - Unknown -} \ No newline at end of file diff --git a/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.Designer.cs b/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.Designer.cs new file mode 100644 index 00000000..f18e4a88 --- /dev/null +++ b/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.Designer.cs @@ -0,0 +1,261 @@ +// +using System; +using Botticelli.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Botticelli.Server.Data.Migrations +{ + [DbContext(typeof(ServerDataContext))] + [Migration("20250921100822_BroadcastIdField")] + partial class BroadcastIdField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.16"); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => + { + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("BotInfoBotId") + .HasColumnType("TEXT"); + + b.Property("ItemName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemValue") + .HasColumnType("TEXT"); + + b.HasKey("BotId"); + + b.HasIndex("BotInfoBotId"); + + b.ToTable("BotAdditionalInfo"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => + { + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("BotKey") + .HasColumnType("TEXT"); + + b.Property("BotName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastKeepAlive") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("BotId"); + + b.ToTable("BotInfo"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BotId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Received") + .HasColumnType("INTEGER"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Broadcasts"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BroadcastId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Filename") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BroadcastId"); + + b.ToTable("BroadcastAttachments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ApplicationRoles"); + + b.HasData( + new + { + Id = "abb60b3b-0b22-487f-a88f-9cac88d9e925", + ConcurrencyStamp = "09/21/2025 10:08:21", + Name = "admin", + NormalizedName = "ADMIN" + }, + new + { + Id = "9422072d-2306-434d-86a8-300d1a55f2ff", + ConcurrencyStamp = "09/21/2025 10:08:21", + Name = "bot_manager", + NormalizedName = "BOT_MANAGER" + }, + new + { + Id = "693cd866-21b0-4003-84a6-cdf57c85a6a7", + ConcurrencyStamp = "09/21/2025 10:08:21", + Name = "viewer", + NormalizedName = "VIEWER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ApplicationUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.ToTable("ApplicationUserRoles"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotAdditionalInfo", b => + { + b.HasOne("Botticelli.Server.Data.Entities.Bot.BotInfo", null) + .WithMany("AdditionalInfo") + .HasForeignKey("BotInfoBotId"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => + { + b.HasOne("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", null) + .WithMany("Attachments") + .HasForeignKey("BroadcastId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => + { + b.Navigation("AdditionalInfo"); + }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => + { + b.Navigation("Attachments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.cs b/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.cs new file mode 100644 index 00000000..9a6aab82 --- /dev/null +++ b/Botticelli.Server.Data/Migrations/20250921100822_BroadcastIdField.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Botticelli.Server.Data.Migrations +{ + /// + public partial class BroadcastIdField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "155a1281-61f2-4cea-b7b4-6f97b30d48d7"); + + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "90283ad1-e01d-436b-879d-75bdab45fdfd"); + + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "e56de249-c6df-4699-9874-96d8247e7e57"); + + migrationBuilder.AddColumn( + name: "BroadcastId", + table: "BroadcastAttachments", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.InsertData( + table: "ApplicationRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "693cd866-21b0-4003-84a6-cdf57c85a6a7", "09/21/2025 10:08:21", "viewer", "VIEWER" }, + { "9422072d-2306-434d-86a8-300d1a55f2ff", "09/21/2025 10:08:21", "bot_manager", "BOT_MANAGER" }, + { "abb60b3b-0b22-487f-a88f-9cac88d9e925", "09/21/2025 10:08:21", "admin", "ADMIN" } + }); + + migrationBuilder.CreateIndex( + name: "IX_BroadcastAttachments_BroadcastId", + table: "BroadcastAttachments", + column: "BroadcastId"); + + migrationBuilder.AddForeignKey( + name: "FK_BroadcastAttachments_Broadcasts_BroadcastId", + table: "BroadcastAttachments", + column: "BroadcastId", + principalTable: "Broadcasts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BroadcastAttachments_Broadcasts_BroadcastId", + table: "BroadcastAttachments"); + + migrationBuilder.DropIndex( + name: "IX_BroadcastAttachments_BroadcastId", + table: "BroadcastAttachments"); + + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "693cd866-21b0-4003-84a6-cdf57c85a6a7"); + + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "9422072d-2306-434d-86a8-300d1a55f2ff"); + + migrationBuilder.DeleteData( + table: "ApplicationRoles", + keyColumn: "Id", + keyValue: "abb60b3b-0b22-487f-a88f-9cac88d9e925"); + + migrationBuilder.DropColumn( + name: "BroadcastId", + table: "BroadcastAttachments"); + + migrationBuilder.InsertData( + table: "ApplicationRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "155a1281-61f2-4cea-b7b4-6f97b30d48d7", "06/12/2025 11:09:25", "admin", "ADMIN" }, + { "90283ad1-e01d-436b-879d-75bdab45fdfd", "06/12/2025 11:09:25", "bot_manager", "BOT_MANAGER" }, + { "e56de249-c6df-4699-9874-96d8247e7e57", "06/12/2025 11:09:25", "viewer", "VIEWER" } + }); + } + } +} diff --git a/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs b/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs index 3541c524..e7231a24 100644 --- a/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs +++ b/Botticelli.Server.Data/Migrations/ServerDataContextModelSnapshot.cs @@ -98,6 +98,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); + b.Property("BroadcastId") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Content") .IsRequired() .HasColumnType("BLOB"); @@ -111,6 +115,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BroadcastId"); + b.ToTable("BroadcastAttachments"); }); @@ -135,22 +141,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "155a1281-61f2-4cea-b7b4-6f97b30d48d7", - ConcurrencyStamp = "06/12/2025 11:09:25", + Id = "abb60b3b-0b22-487f-a88f-9cac88d9e925", + ConcurrencyStamp = "09/21/2025 10:08:21", Name = "admin", NormalizedName = "ADMIN" }, new { - Id = "90283ad1-e01d-436b-879d-75bdab45fdfd", - ConcurrencyStamp = "06/12/2025 11:09:25", + Id = "9422072d-2306-434d-86a8-300d1a55f2ff", + ConcurrencyStamp = "09/21/2025 10:08:21", Name = "bot_manager", NormalizedName = "BOT_MANAGER" }, new { - Id = "e56de249-c6df-4699-9874-96d8247e7e57", - ConcurrencyStamp = "06/12/2025 11:09:25", + Id = "693cd866-21b0-4003-84a6-cdf57c85a6a7", + ConcurrencyStamp = "09/21/2025 10:08:21", Name = "viewer", NormalizedName = "VIEWER" }); @@ -228,10 +234,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("BotInfoBotId"); }); + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.BroadcastAttachment", b => + { + b.HasOne("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", null) + .WithMany("Attachments") + .HasForeignKey("BroadcastId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.BotInfo", b => { b.Navigation("AdditionalInfo"); }); + + modelBuilder.Entity("Botticelli.Server.Data.Entities.Bot.Broadcasting.Broadcast", b => + { + b.Navigation("Attachments"); + }); #pragma warning restore 612, 618 } } diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 38510d3d..5e88592b 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -4,6 +4,8 @@ @using System.Text.Json @using Botticelli.Server.FrontNew.Models @using Botticelli.Server.FrontNew.Settings +@using Botticelli.Shared.Constants +@using Botticelli.Shared.Utils @using Botticelli.Shared.ValueObjects @using Flurl @using Microsoft.Extensions.Options @@ -20,11 +22,8 @@
- +
@@ -33,13 +32,19 @@
-
+
+ +
@@ -58,6 +63,10 @@ }; readonly BotBroadcastModel _model = new(); + readonly Message _message = new() + { + Uid = Guid.NewGuid().ToString(), + }; [Parameter] public string? BotId { get; set; } @@ -79,19 +88,13 @@ private async Task OnSubmit(BotBroadcastModel model) { - var message = new Message - { - Uid = Guid.NewGuid().ToString(), - Body = model.Message - }; - var sessionToken = await Cookies.GetValueAsync("SessionToken"); - + _message.Body = model.Message; if (string.IsNullOrWhiteSpace(sessionToken)) return; Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); - var content = new StringContent(JsonSerializer.Serialize(message), + var content = new StringContent(JsonSerializer.Serialize(_message), Encoding.UTF8, "application/json"); @@ -104,5 +107,27 @@ UriHelper.NavigateTo("/your_bots", true); } + + private async Task OnFileChange(UploadChangeEventArgs? fileChange) + { + if (fileChange == null) return; + foreach (var file in fileChange.Files) + { + var mediaType = MimeTypeConverter.ConvertMimeTypeToMediaType(file.ContentType); + + if (mediaType == MediaType.Unknown) + continue; + + var stream = file.OpenReadStream(); + var bytes = await stream.FromStreamToBytesAsync(); + var content = new BinaryBaseAttachment(Guid.NewGuid().ToString(), + file.Name, + mediaType, + string.Empty, + bytes); + + _message.Attachments.Add(content); + } + } } \ No newline at end of file diff --git a/Botticelli.Shared/Constants/MimeTypeConverter.cs b/Botticelli.Shared/Constants/MimeTypeConverter.cs new file mode 100644 index 00000000..fdf0c25a --- /dev/null +++ b/Botticelli.Shared/Constants/MimeTypeConverter.cs @@ -0,0 +1,21 @@ +namespace Botticelli.Shared.Constants; + +public static class MimeTypeConverter +{ + public static MediaType ConvertMimeTypeToMediaType(string mimeType) + { + if (string.IsNullOrEmpty(mimeType)) return MediaType.Unknown; + + switch (mimeType.ToLowerInvariant()) + { + case var _ when mimeType.StartsWith("audio/"): return MediaType.Audio; + case var _ when mimeType.StartsWith("video/"): return MediaType.Video; + case var _ when mimeType.StartsWith("text/"): return MediaType.Text; + case var _ when mimeType.StartsWith("image/"): return MediaType.Image; + case var _ when mimeType.StartsWith("application/"): + case var _ when mimeType.StartsWith("multipart/"): + return MediaType.Document; + default: return MediaType.Unknown; + } + } +} \ No newline at end of file diff --git a/Botticelli.Shared/Utils/StreamUtils.cs b/Botticelli.Shared/Utils/StreamUtils.cs index 704eab74..1e290d16 100644 --- a/Botticelli.Shared/Utils/StreamUtils.cs +++ b/Botticelli.Shared/Utils/StreamUtils.cs @@ -10,10 +10,38 @@ public static Stream ToStream(this byte[] input) return stream; } - public static string FromStream(this Stream input) + public static string FromStreamToString(this Stream input) { + CheckForNull(input); + using var sr = new StreamReader(input); return sr.ReadToEnd(); } + + public static byte[] FromStreamToBytes(this Stream input) + { + CheckForNull(input); + + using var memoryStream = new MemoryStream(); + input.CopyTo(memoryStream); + + return memoryStream.ToArray(); + } + + public static async Task FromStreamToBytesAsync(this Stream input) + { + CheckForNull(input); + + using var memoryStream = new MemoryStream(); + await input.CopyToAsync(memoryStream); + + return memoryStream.ToArray(); + } + + private static void CheckForNull(Stream input) + { + if (input == null) + throw new ArgumentNullException(nameof(input), "Stream cannot be null."); + } } \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj b/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj index 9c511669..5cd98459 100644 --- a/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj +++ b/Samples/Broadcasting.Sample.Common/Broadcasting.Sample.Common.csproj @@ -1,10 +1,10 @@  - Exe - net8.0 - enable - enable + net8.0 + enable + enable + 0.8.0 diff --git a/Samples/Broadcasting.Sample.Common/Program.cs b/Samples/Broadcasting.Sample.Common/Program.cs deleted file mode 100644 index e5dff12b..00000000 --- a/Samples/Broadcasting.Sample.Common/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file From 384ef1b2971c7396c88e7943a699c432a87bf2e7 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Sun, 21 Sep 2025 19:47:20 +0300 Subject: [PATCH 082/101] BOTTICELLI-55: - admin pane: file removing from upload --- .../Services/Broadcasting/BroadcastService.cs | 1 - .../Pages/BotBroadcast.razor | 55 ++++++++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index e91735d8..ce34fe62 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -32,7 +32,6 @@ public async Task MarkReceived(string botId, string messageId) await context.SaveChangesAsync(); } - public Task> GetBroadcasts(string botId) { var broadcasts = context.BroadcastMessages.Where(x => x.BotId == botId && !x.Sent && !x.Received).ToList(); diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 5e88592b..6b9145bd 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -39,8 +39,20 @@ Cols="40" Rows="100"/> + + +
+
+ +
- @@ -66,8 +78,11 @@ readonly Message _message = new() { Uid = Guid.NewGuid().ToString(), + Attachments = [] }; + const int MaxAllowedFileSize = 10485760; + [Parameter] public string? BotId { get; set; } @@ -114,20 +129,30 @@ foreach (var file in fileChange.Files) { - var mediaType = MimeTypeConverter.ConvertMimeTypeToMediaType(file.ContentType); - - if (mediaType == MediaType.Unknown) - continue; - - var stream = file.OpenReadStream(); - var bytes = await stream.FromStreamToBytesAsync(); - var content = new BinaryBaseAttachment(Guid.NewGuid().ToString(), - file.Name, - mediaType, - string.Empty, - bytes); - - _message.Attachments.Add(content); + try + { + if (file?.ContentType == null!) continue; + + var mediaType = MimeTypeConverter.ConvertMimeTypeToMediaType(file.ContentType); + + if (mediaType == MediaType.Unknown) continue; + + var stream = file.OpenReadStream(MaxAllowedFileSize); + var bytes = await stream.FromStreamToBytesAsync(); + var content = new BinaryBaseAttachment(Guid.NewGuid().ToString(), + file.Name, + mediaType, + string.Empty, + bytes); + + _message.Attachments.Add(content); + } + catch (Exception ex) + { + // nothing to do: + } } + + _message.Attachments.Clear(); } } \ No newline at end of file From 1bdedd92f5b30501b6f4da994e52cf79d83864d1 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Wed, 24 Sep 2025 17:50:10 +0300 Subject: [PATCH 083/101] - admin interface update: logo, menu --- .../Botticelli.Server.FrontNew.csproj | 3 +++ Botticelli.Server.FrontNew/Pages/Login.razor | 5 ++++ Botticelli.Server.FrontNew/Pages/Logoff.razor | 5 ++++ .../Pages/Register.razor | 6 +++++ .../Pages/YourBots.razor | 12 +--------- .../Shared/MainLayout.razor | 2 +- .../Shared/NavMenu.razor | 24 ++++++++++++------- Botticelli.Server.FrontNew/wwwroot/index.html | 4 +++- 8 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj index d97cb4cb..bf3d9d20 100644 --- a/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj +++ b/Botticelli.Server.FrontNew/Botticelli.Server.FrontNew.csproj @@ -68,6 +68,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Botticelli.Server.FrontNew/Pages/Login.razor b/Botticelli.Server.FrontNew/Pages/Login.razor index 4613563d..87d8b3f5 100644 --- a/Botticelli.Server.FrontNew/Pages/Login.razor +++ b/Botticelli.Server.FrontNew/Pages/Login.razor @@ -32,6 +32,11 @@ Login + + + + + diff --git a/Botticelli.Server.FrontNew/Pages/Logoff.razor b/Botticelli.Server.FrontNew/Pages/Logoff.razor index cbb1596d..f3f579e7 100644 --- a/Botticelli.Server.FrontNew/Pages/Logoff.razor +++ b/Botticelli.Server.FrontNew/Pages/Logoff.razor @@ -10,6 +10,11 @@ Logoff + + + + + diff --git a/Botticelli.Server.FrontNew/Pages/Register.razor b/Botticelli.Server.FrontNew/Pages/Register.razor index aa32bbbd..42cfbb4d 100644 --- a/Botticelli.Server.FrontNew/Pages/Register.razor +++ b/Botticelli.Server.FrontNew/Pages/Register.razor @@ -22,6 +22,12 @@ Register User + + + + + + diff --git a/Botticelli.Server.FrontNew/Pages/YourBots.razor b/Botticelli.Server.FrontNew/Pages/YourBots.razor index d04b1723..29b7fb12 100644 --- a/Botticelli.Server.FrontNew/Pages/YourBots.razor +++ b/Botticelli.Server.FrontNew/Pages/YourBots.razor @@ -11,12 +11,7 @@ @inject CookieStorageAccessor Cookies; @inject IJSRuntime JsRuntime; -@code { - private string _error = string.Empty; -} - Your bots -

Bot list

@if (_bots == null) @@ -64,8 +59,6 @@ else }
- @**@ -
diff --git a/Botticelli.Server.FrontNew/Shared/NavMenu.razor b/Botticelli.Server.FrontNew/Shared/NavMenu.razor index e0b1dc25..76ffe6f6 100644 --- a/Botticelli.Server.FrontNew/Shared/NavMenu.razor +++ b/Botticelli.Server.FrontNew/Shared/NavMenu.razor @@ -1,6 +1,7 @@ @using Botticelli.Server.FrontNew.Clients @inject CookieStorageAccessor Cookies; @inject SessionClient SessionClient; +@inject NavigationManager UriHelper; @code { private bool _collapseNavMenu = true; @@ -16,6 +17,11 @@ _collapseNavMenu = !_collapseNavMenu; } + public void Refresh() + { + UriHelper.Refresh(); + } + /// /// Method invoked when the component is ready to start, having received its /// initial parameters from its parent in the render tree. @@ -58,7 +64,8 @@ { @@ -67,17 +74,18 @@ { + } - - } diff --git a/Botticelli.Server.FrontNew/wwwroot/index.html b/Botticelli.Server.FrontNew/wwwroot/index.html index 9f112ba7..e7cec476 100644 --- a/Botticelli.Server.FrontNew/wwwroot/index.html +++ b/Botticelli.Server.FrontNew/wwwroot/index.html @@ -13,7 +13,9 @@ -
Wait, please...
+
+ Wait, please... +
An unhandled error has occurred. From 5606c76f76d5f1b9d0fa9df3d94b2097886877fb Mon Sep 17 00:00:00 2001 From: stone1985 Date: Wed, 24 Sep 2025 18:09:12 +0300 Subject: [PATCH 084/101] - logo for admin pane - long poll on admin side for broadcasting --- Botticelli.Broadcasting/BroadcastReceiver.cs | 13 ++++++++----- .../Controllers/BotController.cs | 16 +++++++++++++--- .../wwwroot/new_logo_m.png | Bin 0 -> 250677 bytes 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 Botticelli.Server.FrontNew/wwwroot/new_logo_m.png diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index a455d5d0..a17f1f7d 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -21,13 +21,14 @@ namespace Botticelli.Broadcasting; public class BroadcastReceiver : IHostedService where TBot : BaseBot, IBot { - private readonly IBot? _bot; + private readonly IBot _bot; private readonly BroadcastingContext _context; private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); private readonly BroadcastingSettings _settings; private readonly ILogger> _logger; - + public CancellationTokenSource CancellationTokenSource { get; private set; } + public BroadcastReceiver(IBot bot, BroadcastingContext context, BroadcastingSettings settings, @@ -41,6 +42,7 @@ public BroadcastReceiver(IBot bot, public Task StartAsync(CancellationToken cancellationToken) { + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); // Polls admin API for new messages to send to our chats and adds them to a MessageCache/MessageStatus _ = Task.Run(async () => { @@ -52,8 +54,6 @@ public Task StartAsync(CancellationToken cancellationToken) if (updates?.Messages == null) continue; - var messageIds = new List(); - foreach (var update in updates.Messages) { // if no chat were specified - broadcast on all chats, we've @@ -64,7 +64,7 @@ public Task StartAsync(CancellationToken cancellationToken) Message = update }; - messageIds = [update.Uid]; + List messageIds = [update.Uid]; var response = await _bot.SendMessageAsync(request, cancellationToken); @@ -81,11 +81,14 @@ public Task StartAsync(CancellationToken cancellationToken) } }, cancellationToken); + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { + CancellationTokenSource.Cancel(false); + return Task.CompletedTask; } diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index f30b985e..1cd07a94 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -23,6 +23,9 @@ public class BotController( IBroadcastService broadcastService, ILogger logger) { + private const int LongPollTimeoutSeconds = 30; + private const int DefaultPollIntervalMilliseconds = 10; + #region Client pane /// @@ -101,11 +104,18 @@ public async Task GetBroadcast([FromBody] GetBroad { try { - logger.LogTrace($"{nameof(GetBroadcast)}({request.BotId})..."); request.BotId?.NotNullOrEmpty(); - var broadcastMessages = await broadcastService.GetMessages(request.BotId!); - + var broadcastMessages = new List(); + var started = DateTime.UtcNow; + + while (!broadcastMessages.Any() && DateTime.UtcNow.Subtract(started).TotalSeconds < LongPollTimeoutSeconds) + { + broadcastMessages = (await broadcastService.GetMessages(request.BotId!)).ToList(); + + await Task.Delay(DefaultPollIntervalMilliseconds); + } + return new GetBroadCastMessagesResponse { BotId = request.BotId!, diff --git a/Botticelli.Server.FrontNew/wwwroot/new_logo_m.png b/Botticelli.Server.FrontNew/wwwroot/new_logo_m.png new file mode 100644 index 0000000000000000000000000000000000000000..81bd061b4aa99b3babb178bf984b3db89f2dbccd GIT binary patch literal 250677 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 z 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 z ze(8B;@o%~@iQarl_u-TKHGL}yq;^%jIz?K9jgx+Jk0a1m!+|59ElKkHDBWI7vz4+g zdA2+8^HSU_w{a+T0#^($SD^`S+x%xkSKQ?h!Ga@8a%`_lAKIi5hoHVkSw%OsOpI?2H!2MYAKIe@sD>J!JY zUn#pQbq#F~Ji2_Z-F5B($nNIg2>q+E_&*}m`EY%ol-hEqmV-&D5=3p6CfBK)>di%o zMDrk$M*4T^;;4x_5L*+d0M!t~Sa_C15*vuTnZRLm4{^w!+*~$*UF}gXpc>Hk`c2Ju z{8=8uo_^2eKo3CnT;4V~OA_I{o!^oq@{&O0Po-B!sqS@qV}j~>XHeQvK^-{g;c)|D zzr^|eG!KIG;me2fhAG!O?_4dz&D~15d7QS9%=Z>&Eh?vc>TuWB}*YM{*2dgu6&vi_4Ku4eas0#bPtD&FjjXe!M`#>=J{t#5`aC1A~E8=dsiXK`A z4|q}8%L>1%iSR~YB>qd<-HF3k@*?gfzfS2Xkb{Nm9U$E7DnnLY=BWeH4@v!m zz~j?-yn6b*lLI{f**p2%2<5QD4r3_!GaAlXtHS?>(jNl>4L%K!H$tcm6sSla?vB|j zQm#_bS^)?Zy{znnnLi$GZkOx3x#zU6rhZSxb9eLb)y$WXhTScVo!kKsFQfh)ynIq- zDW8~4Wzsy&hhXQx;$Y!)m05n7Pk}FXQnkQt+@(s_XGWT~9^KgHK<94Z*oM{P)*=VA z;qWk3$Ztw|c%9U7ND&SIu6M~ISMy=5Qt$pzd8zRJwunMyHEq>hkxfE+|<_;Vm}&~UJD6}sFHdX7PHFcE*2$9-J9 zK9la}8Oto_@fu+c^Z;ap8MP_MOs5c%^$I-AEv!0VJ?50mMPU)f^~t zyV4#r*F9?p3*dNEyf2dagid|vyv|xgy=TwmKo3CnT;7g~vp{5=D&~jP(6jOINCyfU z{t_S@EL?Tqa20y=s8n`d*N-GpjRqm}(9S+1*(bH5i8pz*r=Bt%dorv}Y1o~3bc(a_ z1whoQ#M5`x-~%E)Z;^&wa|y&j1G_#AX~k1?>w^)^KKU0j<~x2{W&6)H47{yV8hdVg zCkJ`}vUl=%#Gz<~)={dY-&AEh0)U|5cR+B!aI*u3gN2*j50yRFWA+BX)iPpvn}V(zoKVC+xb#t6g5~iMZ?wJj?tTZTj2MJ6}|P0f78LMFueZ1sqBM zfu;nF-0XpNY83K@!0A!j+2=HSLf)p;(Dmjm)%(~Ea zOGwXsujfDyK=yi;?^=M`Gs0exkE?S10${w+Hn8=KT5dca9{y9?h$t13dtF z^}oR`#X!SaA*HIV0007mNkl0y1Mzz>Cxve z{N3yzlI&iQ{WrvWwI;}~)I7(^R&nXs?b|uf1CV|D`*e++q>^a@ia*jjOT<5^3OR`i z&kKN1c~LR*V0OK$=FsK``fg9o6WY?;%74i>k4x@`eJ67smh;HzTZNt5wepkOxgI|y z-`uU0dmhe6^v&LLvU^dW@oVPL z`+3dgK=;<+HJh{pFXCFYHD8tUt&&X6rZVfDqg?o40+PP6!g~&NvxgUttLX8=iyaL_ zZpO~d(DLMd-3*OCxI5rs=e(U)PM7;5Y=-bW4z1|Qu$?!m^Mc0d+%As?`Z6vamh*G} zZeOY;Uxst?2Y2V?dUto;sN_@mxVzhXSZ?ns?(XKa-PAHX@|p)eu6MIkCg?b$%JM6P z`%kjlApK_q8Y`slJ^M)x^Z;Z(`H-!Wp&tN5UX7LUl=<^| zSKtv^++B4$=iz>_=W#k$b9d|#5S=X2`EtA7dAncK^6>^X57YHIEp{30IdZ#QuYVIu zhsQ_pI7{!_+`sIhca!#T3`ac?_ny~w4*Y+<*3S1BtbNA-0000 Date: Fri, 26 Sep 2025 16:42:26 +0300 Subject: [PATCH 085/101] BOTTICELLI-55: - file upload is being disabled now after file uploading (cause of an error while poressing an event from RadzenUpload) - attachments receiving bugfix for Bot --- .../Services/Broadcasting/BroadcastService.cs | 1 + .../Pages/BotBroadcast.razor | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index ce34fe62..83ed2b8c 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -17,6 +17,7 @@ public async Task BroadcastMessage(Broadcast message) public async Task> GetMessages(string botId) { + var bm = context.BroadcastMessages.Where(m => !m.Received).ToArray(); return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).Include(m => m.Attachments).ToArrayAsync(); } diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 6b9145bd..2ed103ac 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -46,16 +46,18 @@
- + Change="@OnFileChange" + Disabled="!_uploadEnabled" + Multiple="true" + Style="margin: 10px;" />
@@ -81,6 +83,8 @@ Attachments = [] }; + private bool _uploadEnabled = true; + const int MaxAllowedFileSize = 10485760; [Parameter] @@ -124,13 +128,17 @@ } private async Task OnFileChange(UploadChangeEventArgs? fileChange) - { + { if (fileChange == null) return; - + // _message.Attachments.Clear(); + // + // Console.WriteLine($"FileChange: Files {fileChange.Files.Count()}"); + foreach (var file in fileChange.Files) { try { + Console.WriteLine($"FileChange: file content type {file.ContentType}"); if (file?.ContentType == null!) continue; var mediaType = MimeTypeConverter.ConvertMimeTypeToMediaType(file.ContentType); @@ -146,13 +154,14 @@ bytes); _message.Attachments.Add(content); + + _uploadEnabled = false; } catch (Exception ex) { + Console.WriteLine(ex.Message + ": " + ex.StackTrace); // nothing to do: } } - - _message.Attachments.Clear(); } } \ No newline at end of file From e336924a0cf52157b6ca3ea0929edfa82ff3db36 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 26 Sep 2025 16:51:25 +0300 Subject: [PATCH 086/101] - cosmetic --- Botticelli.Server.FrontNew/Pages/BotBroadcast.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 2ed103ac..8e987338 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -138,7 +138,6 @@ { try { - Console.WriteLine($"FileChange: file content type {file.ContentType}"); if (file?.ContentType == null!) continue; var mediaType = MimeTypeConverter.ConvertMimeTypeToMediaType(file.ContentType); @@ -159,7 +158,6 @@ } catch (Exception ex) { - Console.WriteLine(ex.Message + ": " + ex.StackTrace); // nothing to do: } } From 15354a14706e620d5166097c3ef419cf07d51ee4 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 26 Sep 2025 16:57:50 +0300 Subject: [PATCH 087/101] - reduce bot status updates --- Botticelli.Framework/Services/BotStatusService.cs | 2 +- Botticelli.Server.FrontNew/Pages/UpdateBot.razor | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 25a6218d..2fe9a94a 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -21,7 +21,7 @@ public class BotStatusService( logger, serverSettings) { - private const short GetStatusPeriod = 5000; + private const short GetStatusPeriod = 30000; private Task? _getRequiredStatusEventTask; public override Task StartAsync(CancellationToken cancellationToken) diff --git a/Botticelli.Server.FrontNew/Pages/UpdateBot.razor b/Botticelli.Server.FrontNew/Pages/UpdateBot.razor index 436d8c46..3e8f906c 100644 --- a/Botticelli.Server.FrontNew/Pages/UpdateBot.razor +++ b/Botticelli.Server.FrontNew/Pages/UpdateBot.razor @@ -71,14 +71,14 @@ var sessionToken = await Cookies.GetValueAsync("SessionToken"); var bot = await GetBot(sessionToken); - var botStatus = await GetBotStatus(bot, sessionToken); + var botStatus = await GetBotStatus(sessionToken); _model.BotId = BotId; _model.BotName = bot.BotName; _model.BotKey = botStatus.BotContext.BotKey; } - private async Task GetBotStatus(BotInfo? bot, string sessionToken) + private async Task GetBotStatus(string sessionToken) { using var http = HttpClientFactory.Create(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); From 33eec289070c075a20a9cc3926a373f6dcd3fc6d Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Fri, 26 Sep 2025 17:05:10 +0300 Subject: [PATCH 088/101] - reduced number of requests to RDMS (broadcasts) --- Botticelli.Server.Back/Controllers/BotController.cs | 2 +- .../Services/Broadcasting/BroadcastService.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 1cd07a94..8340cf34 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -24,7 +24,7 @@ public class BotController( ILogger logger) { private const int LongPollTimeoutSeconds = 30; - private const int DefaultPollIntervalMilliseconds = 10; + private const int DefaultPollIntervalMilliseconds = 300; #region Client pane diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index 83ed2b8c..ce34fe62 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -17,7 +17,6 @@ public async Task BroadcastMessage(Broadcast message) public async Task> GetMessages(string botId) { - var bm = context.BroadcastMessages.Where(m => !m.Received).ToArray(); return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).Include(m => m.Attachments).ToArrayAsync(); } From c4b3e48b57031f84237665c46482fd27920198e0 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 27 Sep 2025 12:10:04 +0300 Subject: [PATCH 089/101] - retry policy for BroadcastReceived request --- Botticelli.Broadcasting/BroadcastReceiver.cs | 34 ++++++++++++-------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index a17f1f7d..8b755ac5 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -10,6 +10,8 @@ using Flurl.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Polly; + namespace Botticelli.Broadcasting; /// @@ -24,7 +26,9 @@ public class BroadcastReceiver : IHostedService private readonly IBot _bot; private readonly BroadcastingContext _context; private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); + private readonly TimeSpan _broadcastReceivedTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); + private const int RetryCount = 3; private readonly BroadcastingSettings _settings; private readonly ILogger> _logger; public CancellationTokenSource CancellationTokenSource { get; private set; } @@ -111,18 +115,22 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.ServerUri}/bot/client/BroadcastReceived" - .WithTimeout(_longPollTimeout) - .PostJsonAsync(new BroadCastMessagesReceivedRequest - { - BotId = _settings.BotId, - MessageIds = chatIds.ToArray() - }, - cancellationToken: cancellationToken); - - if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) return null; - - return await updatesResponse.ResponseMessage.Content - .ReadFromJsonAsync(cancellationToken); + var response = Policy + .Handle() // Handle network-related exceptions + .OrResult(r => !r.ResponseMessage.IsSuccessStatusCode) // Handle non-success status codes + .WaitAndRetryAsync(RetryCount, i => _retryPause.Multiply(10 * i)) + .ExecuteAsync(async () => await $"{_settings.ServerUri}/bot/client/BroadcastReceived" + .WithTimeout(_broadcastReceivedTimeout) + .PostJsonAsync(new BroadCastMessagesReceivedRequest + { + BotId = _settings.BotId, + MessageIds = chatIds.ToArray() + }, + cancellationToken: cancellationToken)); + + return await response.Result + .ResponseMessage + .Content + .ReadFromJsonAsync(cancellationToken); } } \ No newline at end of file From 036044b77ddf3767dd787f85bed965d5b48d823e Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 27 Sep 2025 12:10:04 +0300 Subject: [PATCH 090/101] - retry policy for BroadcastReceived request --- Botticelli.Broadcasting/BroadcastReceiver.cs | 34 ++++++++++++------- .../appsettings.json | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index a17f1f7d..8b755ac5 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -10,6 +10,8 @@ using Flurl.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Polly; + namespace Botticelli.Broadcasting; /// @@ -24,7 +26,9 @@ public class BroadcastReceiver : IHostedService private readonly IBot _bot; private readonly BroadcastingContext _context; private readonly TimeSpan _longPollTimeout = TimeSpan.FromSeconds(30); + private readonly TimeSpan _broadcastReceivedTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _retryPause = TimeSpan.FromMilliseconds(150); + private const int RetryCount = 3; private readonly BroadcastingSettings _settings; private readonly ILogger> _logger; public CancellationTokenSource CancellationTokenSource { get; private set; } @@ -111,18 +115,22 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task SendBroadcastReceived(List chatIds, CancellationToken cancellationToken) { - var updatesResponse = await $"{_settings.ServerUri}/bot/client/BroadcastReceived" - .WithTimeout(_longPollTimeout) - .PostJsonAsync(new BroadCastMessagesReceivedRequest - { - BotId = _settings.BotId, - MessageIds = chatIds.ToArray() - }, - cancellationToken: cancellationToken); - - if (!updatesResponse.ResponseMessage.IsSuccessStatusCode) return null; - - return await updatesResponse.ResponseMessage.Content - .ReadFromJsonAsync(cancellationToken); + var response = Policy + .Handle() // Handle network-related exceptions + .OrResult(r => !r.ResponseMessage.IsSuccessStatusCode) // Handle non-success status codes + .WaitAndRetryAsync(RetryCount, i => _retryPause.Multiply(10 * i)) + .ExecuteAsync(async () => await $"{_settings.ServerUri}/bot/client/BroadcastReceived" + .WithTimeout(_broadcastReceivedTimeout) + .PostJsonAsync(new BroadCastMessagesReceivedRequest + { + BotId = _settings.BotId, + MessageIds = chatIds.ToArray() + }, + cancellationToken: cancellationToken)); + + return await response.Result + .ResponseMessage + .Content + .ReadFromJsonAsync(cancellationToken); } } \ No newline at end of file diff --git a/Samples/Broadcasting.Sample.Telegram/appsettings.json b/Samples/Broadcasting.Sample.Telegram/appsettings.json index 636c3c25..01bef1ab 100644 --- a/Samples/Broadcasting.Sample.Telegram/appsettings.json +++ b/Samples/Broadcasting.Sample.Telegram/appsettings.json @@ -22,7 +22,7 @@ }, "Broadcasting": { "ConnectionString": "broadcasting_database.db;Password=321", - "BotId": "91d3z7fW87uvWn9441vdxusnhAe4sIaDTyIJVm9Q", + "BotId": "chatbot1", "HowOld": "00:10:00", "ServerUri": "https://localhost:7247/v1" }, From 9ca079ba67c160918e4610278a6d02bd31a38694 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 27 Sep 2025 12:21:02 +0300 Subject: [PATCH 091/101] - error processing for broadcast --- Botticelli.Broadcasting/BroadcastReceiver.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Botticelli.Broadcasting/BroadcastReceiver.cs b/Botticelli.Broadcasting/BroadcastReceiver.cs index 8b755ac5..0325752e 100644 --- a/Botticelli.Broadcasting/BroadcastReceiver.cs +++ b/Botticelli.Broadcasting/BroadcastReceiver.cs @@ -61,7 +61,8 @@ public Task StartAsync(CancellationToken cancellationToken) foreach (var update in updates.Messages) { // if no chat were specified - broadcast on all chats, we've - if (update.ChatIds.Count == 0) update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); + if (update.ChatIds.Count == 0) + update.ChatIds = _context.Chats.Select(x => x.ChatId).ToList(); var request = new SendMessageRequest { @@ -70,10 +71,14 @@ public Task StartAsync(CancellationToken cancellationToken) List messageIds = [update.Uid]; - var response = await _bot.SendMessageAsync(request, cancellationToken); + var sendMessageResponse = await _bot.SendMessageAsync(request, cancellationToken); - if (response.MessageSentStatus == MessageSentStatus.Ok) - await SendBroadcastReceived(messageIds, cancellationToken); + if (sendMessageResponse.MessageSentStatus != MessageSentStatus.Ok) continue; + + var broadcastResult = await SendBroadcastReceived(messageIds, cancellationToken); + + if (broadcastResult is { IsSuccess: false }) + _logger.LogError("Error sending a BroadcastReceived message!"); } } catch (Exception ex) From 0605b07ceb948ea01c985df0aaab482c442b8ed7 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sat, 27 Sep 2025 13:31:43 +0300 Subject: [PATCH 092/101] - receiving messages from channels was fixed --- .../Decorators/TelegramClientDecorator.cs | 43 ++++++++++++------- .../Handlers/BotUpdateHandler.cs | 6 +-- .../Commands/Processors/CommandProcessor.cs | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs index 27882458..3c005c9a 100644 --- a/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs +++ b/Botticelli.Framework.Telegram/Decorators/TelegramClientDecorator.cs @@ -1,4 +1,5 @@ -using Telegram.Bot; +using Botticelli.Framework.Exceptions; +using Telegram.Bot; using Telegram.Bot.Args; using Telegram.Bot.Exceptions; using Telegram.Bot.Requests.Abstractions; @@ -15,6 +16,12 @@ public class TelegramClientDecorator : ITelegramBotClient private TelegramBotClient? _innerClient; private TelegramBotClientOptions _options; + #region Limits + private const int MessagesInSecond = 30; + private DateTime? _prev; + private long? _sentMessageCount = 0; + #endregion Limits + internal TelegramClientDecorator(TelegramBotClientOptions options, HttpClient? httpClient = null) { @@ -29,7 +36,21 @@ public async Task SendRequest(IRequest request, { try { - return await _innerClient?.SendRequest(request, cancellationToken)!; + _prev ??= DateTime.UtcNow; + _sentMessageCount ??= 0; + var deltaT = (DateTime.UtcNow - _prev).Value; + + if (_sentMessageCount < MessagesInSecond && deltaT < TimeSpan.FromSeconds(1.0)) + return await _innerClient?.SendRequest(request, cancellationToken)!; + else + { + await Task.Delay(deltaT, cancellationToken); + + _sentMessageCount = 0; + _prev = DateTime.UtcNow; + + return await _innerClient?.SendRequest(request, cancellationToken); + } } catch (ApiRequestException ex) { @@ -37,20 +58,10 @@ public async Task SendRequest(IRequest request, throw; } - } - - [Obsolete("Use SendRequest")] - public Task MakeRequest(IRequest request, - CancellationToken cancellationToken = new()) - { - return SendRequest(request, cancellationToken); - } - - [Obsolete("Use SendRequest")] - public async Task MakeRequestAsync(IRequest request, - CancellationToken cancellationToken = new()) - { - return await SendRequest(request, cancellationToken); + finally + { + ++_sentMessageCount; + } } public Task TestApi(CancellationToken cancellationToken = new()) diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index dbe5a38a..d85a8462 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -39,7 +39,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, logger.LogDebug($"{nameof(HandleUpdateAsync)}() started..."); - var botMessage = update.Message; + var botMessage = update.Message ?? update.ChannelPost; Message? botticelliMessage = null; @@ -246,10 +246,10 @@ protected async Task ProcessInProcessors(Message request, CancellationToken toke var processorFactory = ProcessorFactoryBuilder.Build(serviceProvider); var clientNonChainedTasks = processorFactory.GetProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)).ToList(); var clientChainedTasks = processorFactory.GetCommandChainProcessors() - .Select(p => p.ProcessAsync(request, token)); + .Select(p => p.ProcessAsync(request, token)).ToList(); var clientTasks = clientNonChainedTasks.Concat(clientChainedTasks).ToArray(); diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index 7a8d4a0a..b7b1dfbf 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -67,7 +67,7 @@ public virtual async Task ProcessAsync(Message message, CancellationToken token) return; } - if (message.From!.Id!.Equals(_bot?.BotUserId, StringComparison.InvariantCulture)) return; + if (message.From?.Id != null && message.From!.Id!.Equals(_bot?.BotUserId, StringComparison.InvariantCulture)) return; Classify(ref message); From 14e689cb9380be19aa8b8e6a0cb23fe1a8f5d292 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Sun, 28 Sep 2025 12:56:26 +0300 Subject: [PATCH 093/101] - m4a / ogg fix - limits extended --- Botticelli.Server.FrontNew/Pages/BotBroadcast.razor | 13 ++++--------- Botticelli.Server.FrontNew/Utils/Constants.cs | 6 ++++++ Botticelli.Shared/Constants/MimeTypeConverter.cs | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 Botticelli.Server.FrontNew/Utils/Constants.cs diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index 8e987338..d5cb5393 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -4,6 +4,7 @@ @using System.Text.Json @using Botticelli.Server.FrontNew.Models @using Botticelli.Server.FrontNew.Settings +@using Botticelli.Server.FrontNew.Utils @using Botticelli.Shared.Constants @using Botticelli.Shared.Utils @using Botticelli.Shared.ValueObjects @@ -48,7 +49,7 @@
Date: Tue, 30 Sep 2025 11:11:16 +0300 Subject: [PATCH 094/101] - structured logging - Array.Empty => [] --- .../Provider/ChatGptProvider.cs | 8 +++--- .../Provider/DeepSeekGptProvider.cs | 9 +++--- Botticelli.AI.GptJ/Provider/GptJProvider.cs | 2 +- Botticelli.AI.YaGpt/Provider/YaGptProvider.cs | 9 +++--- Botticelli.AI/AIProvider/ChatGptProvider.cs | 4 +-- .../UniversalLowQualityConvertor.cs | 6 ++-- Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs | 10 +++---- .../Handlers/MetricsHandler.cs | 2 +- .../Botticelli.Controls.Layouts.csproj | 2 -- .../Handlers/BotUpdateHandler.cs | 6 ++-- .../Commands/Processors/CommandProcessor.cs | 2 +- .../Processors/ProcessorFactoryBuilder.cs | 2 +- .../ForwardGeocoderMock.cs | 6 ++-- .../FindLocationsCommandProcessor.cs | 2 +- Botticelli.Server.Analytics/Program.cs | 2 +- .../Controllers/AdminController.cs | 8 +++--- .../Controllers/BotController.cs | 10 +++---- .../Services/Auth/AdminAuthService.cs | 22 +++++++-------- .../Services/Auth/PasswordSender.cs | 8 +++--- .../Services/Auth/UserService.cs | 28 +++++++++---------- .../Services/BotManagementService.cs | 6 ++-- .../Services/Broadcasting/BroadcastService.cs | 11 ++++---- .../Pages/YourBots.razor | 2 +- Botticelli.Shared/Utils/Assertions.cs | 2 +- Botticelli.Shared/ValueObjects/Message.cs | 2 +- Botticelli.Talks/BaseTtsSpeaker.cs | 12 ++++---- Botticelli.Talks/OpenTts/OpenTtsSpeaker.cs | 4 +-- .../Ai.Common.Sample/Handlers/AiHandler.cs | 2 +- 28 files changed, 94 insertions(+), 95 deletions(-) diff --git a/Botticelli.AI.ChatGpt/Provider/ChatGptProvider.cs b/Botticelli.AI.ChatGpt/Provider/ChatGptProvider.cs index f8b72f5e..9a963eb1 100644 --- a/Botticelli.AI.ChatGpt/Provider/ChatGptProvider.cs +++ b/Botticelli.AI.ChatGpt/Provider/ChatGptProvider.cs @@ -54,7 +54,7 @@ protected override async Task ProcessGptResponse(AiMessage message, text.AppendJoin(' ', part?.Choices? .Select(c => (c.Message ?? c.Delta)?.Content) ?? - Array.Empty()); + []); var responseMessage = new SendMessageResponse(message.Uid) { @@ -99,14 +99,14 @@ protected override async Task GetGptResponse(AiMessage mess var content = JsonContent.Create(new ChatGptInputMessage { Model = Settings.Value.Model, - Messages = new List - { + Messages = + [ new() { Role = "user", Content = message.Body } - }, + ], Temperature = Settings.Value.Temperature, Stream = Settings.Value.StreamGeneration }); diff --git a/Botticelli.AI.DeepSeekGpt/Provider/DeepSeekGptProvider.cs b/Botticelli.AI.DeepSeekGpt/Provider/DeepSeekGptProvider.cs index 273701b9..91e2e027 100644 --- a/Botticelli.AI.DeepSeekGpt/Provider/DeepSeekGptProvider.cs +++ b/Botticelli.AI.DeepSeekGpt/Provider/DeepSeekGptProvider.cs @@ -67,19 +67,20 @@ protected override async Task GetGptResponse(AiMessage mess { Model = Settings.Value.Model, MaxTokens = Settings.Value.MaxTokens, - Messages = new List - { + Messages = + [ new() { Role = SystemRole, Content = Settings.Value.Instruction }, + new() { Role = UserRole, Content = message.Body } - } + ] }; deepSeekGptMessage.Messages.AddRange(message.AdditionalMessages?.Select(m => new DeepSeekInnerInputMessage @@ -91,7 +92,7 @@ protected override async Task GetGptResponse(AiMessage mess var content = JsonContent.Create(deepSeekGptMessage); - Logger.LogDebug($"{nameof(SendAsync)}({message.ChatIds}) content: {content.Value}"); + Logger.LogDebug("{SendAsyncName}({MessageChatIds}) content: {ContentValue}", nameof(SendAsync), message.ChatIds, content.Value); return await client.PostAsync(Completion, content, diff --git a/Botticelli.AI.GptJ/Provider/GptJProvider.cs b/Botticelli.AI.GptJ/Provider/GptJProvider.cs index 49e671e6..05bc074d 100644 --- a/Botticelli.AI.GptJ/Provider/GptJProvider.cs +++ b/Botticelli.AI.GptJ/Provider/GptJProvider.cs @@ -71,7 +71,7 @@ protected override async Task GetGptResponse(AiMessage mess Temperature = Settings.Value.Temperature }); - Logger.LogDebug($"{nameof(SendAsync)}({message.ChatIds}) content: {content.Value}"); + Logger.LogDebug("{SendAsyncName}({MessageChatIds}) content: {ContentValue}", nameof(SendAsync), message.ChatIds, content.Value); return await client.PostAsync(Url.Combine($"{Settings.Value.Url}", "generate"), content, diff --git a/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs b/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs index 8b3e9820..ed84c8f0 100644 --- a/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs +++ b/Botticelli.AI.YaGpt/Provider/YaGptProvider.cs @@ -89,19 +89,20 @@ protected override async Task GetGptResponse(AiMessage mess var yaGptMessage = new YaGptInputMessage { ModelUri = Settings.Value.Model, - Messages = new List - { + Messages = + [ new() { Role = SystemRole, Text = Settings.Value.Instruction }, + new() { Role = UserRole, Text = message.Body } - }, + ], CompletionOptions = new CompletionOptions { MaxTokens = Settings.Value.MaxTokens, @@ -119,7 +120,7 @@ protected override async Task GetGptResponse(AiMessage mess var content = JsonContent.Create(yaGptMessage); - Logger.LogDebug($"{nameof(SendAsync)}({message.ChatIds}) content: {content.Value}"); + Logger.LogDebug("{SendAsyncName}({MessageChatIds}) content: {ContentValue}", nameof(SendAsync), message.ChatIds, content.Value); return await client.PostAsync(Url.Combine($"{Settings.Value.Url}", Completion), content, diff --git a/Botticelli.AI/AIProvider/ChatGptProvider.cs b/Botticelli.AI/AIProvider/ChatGptProvider.cs index c7f2f137..34171eaf 100644 --- a/Botticelli.AI/AIProvider/ChatGptProvider.cs +++ b/Botticelli.AI/AIProvider/ChatGptProvider.cs @@ -37,7 +37,7 @@ public virtual async Task SendAsync(AiMessage message, CancellationToken token) try { - Logger.LogDebug($"{nameof(SendAsync)}({message.ChatIds}) started"); + Logger.LogDebug("{SendAsyncName}({MessageChatIds}) started", nameof(SendAsync), message.ChatIds); using var client = GetClient(); @@ -53,7 +53,7 @@ public virtual async Task SendAsync(AiMessage message, CancellationToken token) await SendErrorGptResponse(message, reason, token); } - Logger.LogDebug($"{nameof(SendAsync)}({message.ChatIds}) finished"); + Logger.LogDebug("{SendAsyncName}({MessageChatIds}) finished", nameof(SendAsync), message.ChatIds); } catch (Exception ex) { diff --git a/Botticelli.Audio/UniversalLowQualityConvertor.cs b/Botticelli.Audio/UniversalLowQualityConvertor.cs index cfe8a8f4..70b9554f 100644 --- a/Botticelli.Audio/UniversalLowQualityConvertor.cs +++ b/Botticelli.Audio/UniversalLowQualityConvertor.cs @@ -35,7 +35,7 @@ public byte[] Convert(Stream input, AudioInfo tgtParams) } catch (Exception ex) { - _logger.LogError($"{nameof(Convert)} => ({tgtParams.AudioFormat}, {tgtParams.Bitrate}) error", ex); + _logger.LogError("{ConvertName} => ({TgtParamsAudioFormat}, {TgtParamsBitrate}) error: {Ex}!", nameof(Convert), tgtParams.AudioFormat, tgtParams.Bitrate, ex); throw new AudioConvertorException($"Audio conversion error: {ex.Message}", ex); } @@ -94,9 +94,9 @@ private byte[] ProcessByStreamEncoder(Stream input, AudioInfo tgtParams) } catch (IOException ex) { - _logger.LogError($"{nameof(Convert)} => ({tgtParams.AudioFormat}, {tgtParams.Bitrate}) error", ex); + _logger.LogError("{ConvertName} => ({TgtParamsAudioFormat}, {TgtParamsBitrate}) error", nameof(Convert), tgtParams.AudioFormat, tgtParams.Bitrate, ex); - return Array.Empty(); + return []; } } diff --git a/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs b/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs index fd286ce5..c8d7be38 100644 --- a/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs +++ b/Botticelli.Bus.Rabbit/Agent/RabbitAgent.cs @@ -54,18 +54,18 @@ public async Task SendResponseAsync(SendMessageResponse response, { try { - _logger.LogDebug($"{nameof(SendResponseAsync)}({response.Uid}) start..."); + _logger.LogDebug("{SendResponseAsyncName}({ResponseUid}) start...", nameof(SendResponseAsync), response.Uid); var policy = Policy.Handle() .WaitAndRetryAsync(5, n => TimeSpan.FromSeconds(3 * Math.Exp(n))); await policy.ExecuteAsync(() => InnerSend(response)); - _logger.LogDebug($"{nameof(SendResponseAsync)}({response.Uid}) finished"); + _logger.LogDebug("{SendResponseAsyncName}({ResponseUid}) finished", nameof(SendResponseAsync), response.Uid); } catch (Exception ex) { - _logger.LogError(ex, $"Error sending a response: {ex.Message}"); + _logger.LogError(ex, "Error sending a response: {ExMessage}", ex.Message); } } @@ -88,7 +88,7 @@ public Task StopAsync(CancellationToken cancellationToken) ///
public Task Subscribe(CancellationToken token) { - _logger.LogDebug($"{nameof(Subscribe)}({typeof(THandler).Name}) start..."); + _logger.LogDebug("{SubscribeName}({Name}) start...", nameof(Subscribe), typeof(THandler).Name); var handler = _sp.GetRequiredService(); ProcessSubscription(token, handler); @@ -105,7 +105,7 @@ private void ProcessSubscription(CancellationToken token, THandler handler) var queue = GetRequestQueueName(); var declareResult = _settings.QueueSettings is {TryCreate: true} ? channel.QueueDeclare(queue, _settings.QueueSettings.Durable, false) : channel.QueueDeclarePassive(queue); - _logger.LogDebug($"{nameof(Subscribe)}({typeof(THandler).Name}) queue declare: {declareResult.QueueName}"); + _logger.LogDebug("{SubscribeName}({Name}) queue declare: {DeclareResultQueueName}", nameof(Subscribe), typeof(THandler).Name, declareResult.QueueName); _consumer = new EventingBasicConsumer(channel); diff --git a/Botticelli.Client.Analytics/Handlers/MetricsHandler.cs b/Botticelli.Client.Analytics/Handlers/MetricsHandler.cs index 4da094fc..1651fbcd 100644 --- a/Botticelli.Client.Analytics/Handlers/MetricsHandler.cs +++ b/Botticelli.Client.Analytics/Handlers/MetricsHandler.cs @@ -23,7 +23,7 @@ public virtual async Task Handle(IMetricRequest request, CancellationToken cance { try { - _logger.LogTrace($"Metric {request.GetType().Name} handling..."); + _logger.LogTrace("Metric {Name} handling...", request.GetType().Name); var metric = Convert(request, _context.BotId); diff --git a/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj b/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj index a40d735c..d6e3ae31 100644 --- a/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj +++ b/Botticelli.Controls.Layouts/Botticelli.Controls.Layouts.csproj @@ -7,13 +7,11 @@ - - diff --git a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs index d85a8462..806ee3ab 100644 --- a/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs +++ b/Botticelli.Framework.Telegram/Handlers/BotUpdateHandler.cs @@ -118,7 +118,7 @@ public Task HandleErrorAsync(ITelegramBotClient botClient, HandleErrorSource source, CancellationToken cancellationToken) { - logger.LogError($"{nameof(HandleErrorAsync)}() error: {exception.Message}", exception); + logger.LogError("{HandleErrorAsyncName}() error: {ExceptionMessage} exception: {Exception}", nameof(HandleErrorAsync), exception.Message, exception); return Task.CompletedTask; } @@ -239,7 +239,7 @@ private bool ProcessCallback(Update update, ref Message? botticelliMessage) /// protected async Task ProcessInProcessors(Message request, CancellationToken token) { - logger.LogDebug($"{nameof(ProcessInProcessors)}({request.Uid}) started..."); + logger.LogDebug("{ProcessInProcessorsName}({RequestUid}) started...", nameof(ProcessInProcessors), request.Uid); if (token is { CanBeCanceled: true, IsCancellationRequested: true }) return; @@ -255,6 +255,6 @@ protected async Task ProcessInProcessors(Message request, CancellationToken toke await Parallel.ForEachAsync(clientTasks, token, async (t, ct) => await t.WaitAsync(ct)); - logger.LogDebug($"{nameof(ProcessInProcessors)}({request.Uid}) finished..."); + logger.LogDebug("{ProcessInProcessorsName}({RequestUid}) finished...", nameof(ProcessInProcessors), request.Uid); } } \ No newline at end of file diff --git a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs index b7b1dfbf..e3e44e2f 100644 --- a/Botticelli.Framework/Commands/Processors/CommandProcessor.cs +++ b/Botticelli.Framework/Commands/Processors/CommandProcessor.cs @@ -130,7 +130,7 @@ await ValidateAndProcess(message, catch (Exception ex) { _metricsProcessor?.Process(MetricNames.BotError, BotDataUtils.GetBotId()); - Logger.LogError(ex, $"Error in {GetType().Name}: {ex.Message}"); + Logger.LogError(ex, "Error in {Name}: {ExMessage}", GetType().Name, ex.Message); await InnerProcessError(message, ex, token); } diff --git a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs index e3d4d20c..884e2838 100644 --- a/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs +++ b/Botticelli.Framework/Extensions/Processors/ProcessorFactoryBuilder.cs @@ -7,7 +7,7 @@ namespace Botticelli.Framework.Extensions.Processors; public static class ProcessorFactoryBuilder { private static IServiceCollection? _serviceCollection; - private static readonly List ProcessorTypes = new(); + private static readonly List ProcessorTypes = []; public static void AddProcessor(IServiceCollection serviceCollection) where TProcessor : class, ICommandProcessor diff --git a/Botticelli.Locations.Tests/ForwardGeocoderMock.cs b/Botticelli.Locations.Tests/ForwardGeocoderMock.cs index c95102a1..4db49924 100644 --- a/Botticelli.Locations.Tests/ForwardGeocoderMock.cs +++ b/Botticelli.Locations.Tests/ForwardGeocoderMock.cs @@ -8,8 +8,8 @@ public class ForwardGeocoderMock : IForwardGeocoder { public async Task Geocode(ForwardGeocodeRequest req) { - return new GeocodeResponse[] - { + return + [ new() { OSMID = 110, @@ -32,6 +32,6 @@ public async Task Geocode(ForwardGeocodeRequest req) }, GeoText = "Test Result" } - }; + ]; } } \ No newline at end of file diff --git a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs index 40616fe9..751725df 100644 --- a/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs +++ b/Botticelli.Locations/Commands/CommandProcessors/FindLocationsCommandProcessor.cs @@ -29,7 +29,7 @@ public class FindLocationsCommandProcessor( { protected override async Task InnerProcess(Message message, CancellationToken token) { - var query = string.Join(" ", message.Body?.Split(" ").Skip(1) ?? Array.Empty()); + var query = string.Join(" ", message.Body?.Split(" ").Skip(1) ?? []); var results = await locationProvider.Search(query, 10); diff --git a/Botticelli.Server.Analytics/Program.cs b/Botticelli.Server.Analytics/Program.cs index 21245598..34eb635f 100644 --- a/Botticelli.Server.Analytics/Program.cs +++ b/Botticelli.Server.Analytics/Program.cs @@ -30,7 +30,7 @@ Id = "Bearer" } }, - Array.Empty() + [] } }); }); diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index 0d2201c3..adf0cce0 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -27,13 +27,13 @@ public class AdminController( [HttpPost("[action]")] public async Task AddNewBot([FromBody] RegisterBotRequest request) { - logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) started..."); + logger.LogInformation("{AddNewBotName}({RequestBotId}) started...", nameof(AddNewBot), request.BotId); var success = await botManagementService.RegisterBot(request.BotId, request.BotKey, request.BotName, request.Type); - logger.LogInformation($"{nameof(AddNewBot)}({request.BotId}) success: {success}..."); + logger.LogInformation("{AddNewBotName}({RequestBotId}) success: {Success}...", nameof(AddNewBot), request.BotId, success); return new RegisterBotResponse { @@ -45,12 +45,12 @@ public async Task AddNewBot([FromBody] RegisterBotRequest r [HttpPut("[action]")] public async Task UpdateBot([FromBody] UpdateBotRequest request) { - logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) started..."); + logger.LogInformation("{UpdateBotName}({RequestBotId}) started...", nameof(UpdateBot), request.BotId); var success = await botManagementService.UpdateBot(request.BotId, request.BotKey, request.BotName); - logger.LogInformation($"{nameof(UpdateBot)}({request.BotId}) success: {success}..."); + logger.LogInformation("{UpdateBotName}({RequestBotId}) success: {Success}...", nameof(UpdateBot), request.BotId, success); return new UpdateBotResponse { diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index 8340cf34..afe57659 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -71,7 +71,7 @@ public async Task KeepAlive([FromBody] KeepAliveN { try { - logger.LogTrace($"{nameof(KeepAlive)}({request.BotId})..."); + logger.LogTrace("{KeepAliveName}({RequestBotId})...", nameof(KeepAlive), request.BotId); request.BotId?.NotNullOrEmpty(); await botManagementService.SetKeepAlive(request.BotId!); @@ -83,7 +83,7 @@ public async Task KeepAlive([FromBody] KeepAliveN } catch (Exception ex) { - logger.LogError(ex, $"{nameof(KeepAlive)}({request.BotId}) error: {ex.Message}"); + logger.LogError(ex, "{KeepAliveName}({RequestBotId}) error: {ExMessage}", nameof(KeepAlive), request.BotId, ex.Message); return new KeepAliveNotificationResponse { @@ -139,7 +139,7 @@ public async Task GetBroadcast([FromBody] GetBroad } catch (Exception ex) { - logger.LogError(ex, $"{nameof(GetBroadcast)}({request.BotId}) error: {ex.Message}"); + logger.LogError(ex, "{GetBroadcastName}({RequestBotId}) error: {ExMessage}", nameof(GetBroadcast), request.BotId, ex.Message); return new GetBroadCastMessagesResponse { @@ -161,7 +161,7 @@ public async Task BroadcastReceived([FromBody { try { - logger.LogTrace($"{nameof(GetBroadcast)}({request.BotId})..."); + logger.LogTrace("{GetBroadcastName}({RequestBotId})...", nameof(GetBroadcast), request.BotId); request.BotId?.NotNullOrEmpty(); foreach (var messageId in request.MessageIds) await broadcastService.MarkReceived(request.BotId!, messageId); @@ -173,7 +173,7 @@ public async Task BroadcastReceived([FromBody } catch (Exception ex) { - logger.LogError(ex, $"{nameof(BroadcastReceived)}({request.BotId}) error: {ex.Message}"); + logger.LogError(ex, "{BroadcastReceivedName}({RequestBotId}) error: {ExMessage}", nameof(BroadcastReceived), request.BotId, ex.Message); return new BroadCastMessagesReceivedResponse { diff --git a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs index 7d0afeba..b084294a 100644 --- a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs +++ b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs @@ -53,7 +53,7 @@ public async Task RegisterAsync(UserAddRequest userRegister) { try { - _logger.LogInformation($"{nameof(RegisterAsync)}({userRegister.UserName}) started..."); + _logger.LogInformation("{RegisterAsyncName}({UserRegisterUserName}) started...", nameof(RegisterAsync), userRegister.UserName); ValidateRequest(userRegister); @@ -83,11 +83,11 @@ public async Task RegisterAsync(UserAddRequest userRegister) await _context.SaveChangesAsync(); - _logger.LogInformation($"{nameof(RegisterAsync)}({userRegister.UserName}) finished..."); + _logger.LogInformation("{RegisterAsyncName}({UserRegisterUserName}) finished...", nameof(RegisterAsync), userRegister.UserName); } catch (Exception ex) { - _logger.LogError($"{nameof(RegisterAsync)}({userRegister.UserName}) error: {ex.Message}", ex); + _logger.LogError("{RegisterAsyncName}({UserRegisterUserName}) error: {ExMessage}", nameof(RegisterAsync), userRegister.UserName, ex.Message, ex); } } @@ -97,7 +97,7 @@ public async Task RegeneratePassword(UserAddRequest userRegister) { try { - _logger.LogInformation($"{nameof(RegeneratePassword)}({userRegister.UserName}) started..."); + _logger.LogInformation("{RegeneratePasswordName}({UserRegisterUserName}) started...", nameof(RegeneratePassword), userRegister.UserName); ValidateRequest(userRegister); @@ -107,11 +107,11 @@ public async Task RegeneratePassword(UserAddRequest userRegister) await _context.SaveChangesAsync(); - _logger.LogInformation($"{nameof(RegeneratePassword)}({userRegister.UserName}) finished..."); + _logger.LogInformation("{RegeneratePasswordName}({UserRegisterUserName}) finished...", nameof(RegeneratePassword), userRegister.UserName); } catch (Exception ex) { - _logger.LogError($"{nameof(RegeneratePassword)}({userRegister.UserName}) error: {ex.Message}", ex); + _logger.LogError("{RegeneratePasswordName}({UserRegisterUserName}) error: {ExMessage}", nameof(RegeneratePassword), userRegister.UserName, ex.Message, ex); } } @@ -120,13 +120,13 @@ public async Task RegeneratePassword(UserAddRequest userRegister) { try { - _logger.LogInformation($"{nameof(GenerateToken)}({userLogin.Email}) started..."); + _logger.LogInformation("{GenerateTokenName}({UserLoginEmail}) started...", nameof(GenerateToken), userLogin.Email); ValidateRequest(userLogin); if (!CheckAccess(userLogin, false).result) { - _logger.LogInformation($"{nameof(GenerateToken)}({userLogin.Email}) access denied..."); + _logger.LogInformation("{GenerateTokenName}({UserLoginEmail}) access denied...", nameof(GenerateToken), userLogin.Email); return new GetTokenResponse { @@ -184,7 +184,7 @@ public async Task RegeneratePassword(UserAddRequest userRegister) } catch (Exception ex) { - _logger.LogError(ex, $"{nameof(GenerateToken)}({userLogin.Email}) error {ex.Message}!"); + _logger.LogError(ex, "{GenerateTokenName}({UserLoginEmail}) error {ExMessage}!", nameof(GenerateToken), userLogin.Email, ex.Message); } return null; @@ -210,13 +210,13 @@ public bool CheckToken(string token) out var validatedToken); - _logger.LogInformation($"{nameof(CheckToken)}() validate token: {validatedToken != null}"); + _logger.LogInformation("{CheckTokenName}() validate token: {B}", nameof(CheckToken), validatedToken != null); return validatedToken != null; } catch (Exception ex) { - _logger.LogError($"{nameof(CheckToken)}() error: {ex.Message}"); + _logger.LogError("{CheckTokenName}() error: {ExMessage}", nameof(CheckToken), ex.Message); return false; } diff --git a/Botticelli.Server.Back/Services/Auth/PasswordSender.cs b/Botticelli.Server.Back/Services/Auth/PasswordSender.cs index 0411eac8..241d0536 100644 --- a/Botticelli.Server.Back/Services/Auth/PasswordSender.cs +++ b/Botticelli.Server.Back/Services/Auth/PasswordSender.cs @@ -20,9 +20,9 @@ public PasswordSender(ISender fluentEmail, ServerSettings serverSettings, ILogge public async Task SendPassword(string email, string password, CancellationToken ct) { - _logger.LogInformation($"Sending a password message to : {email}"); + _logger.LogInformation("Sending a password message to : {Email}", email); - if (EnvironmentExtensions.IsDevelopment()) _logger.LogInformation($"!!! Email : {email} password: {password} !!! ONLY FOR DEVELOPMENT PURPOSES !!!"); + if (EnvironmentExtensions.IsDevelopment()) _logger.LogInformation("!!! Email : {Email} password: {Password} !!! ONLY FOR DEVELOPMENT PURPOSES !!!", email, password); var message = Email.From(_serverSettings.ServerEmail, "BotticelliBots Admin Service") .To(email) @@ -35,11 +35,11 @@ public async Task SendPassword(string email, string password, CancellationToken if (!sendResult.Successful) { - _logger.LogError($"Sending a password message to : {email} error", sendResult.ErrorMessages); + _logger.LogError("Sending a password message to : {Email} error error mssage : {ErrMessage}", email, sendResult.ErrorMessages); throw new InvalidOperationException($"Sending mail errors: {string.Join(',', sendResult.ErrorMessages)}"); } - _logger.LogInformation($"Sending a password message to : {email} - OK"); + _logger.LogInformation("Sending a password message to : {Email} - OK", email); } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Services/Auth/UserService.cs b/Botticelli.Server.Back/Services/Auth/UserService.cs index 637263dc..c2a197db 100644 --- a/Botticelli.Server.Back/Services/Auth/UserService.cs +++ b/Botticelli.Server.Back/Services/Auth/UserService.cs @@ -52,7 +52,7 @@ public async Task CheckAndAddAsync(UserAddRequest request, CancellationTok } catch (Exception ex) { - _logger.LogError($"{nameof(CheckAndAddAsync)}({request.UserName}) error: {ex.Message}", ex); + _logger.LogError("{CheckAndAddAsyncName}({RequestUserName}) error: {ExMessage} {Ex}", nameof(CheckAndAddAsync), request.UserName, ex.Message, ex); throw; } @@ -62,7 +62,7 @@ public async Task AddAsync(UserAddRequest request, bool needConfirmation, Cancel { try { - _logger.LogInformation($"{nameof(AddAsync)}({request.UserName}) started..."); + _logger.LogInformation("{AddAsyncName}({RequestUserName}) started...", nameof(AddAsync), request.UserName); request.NotNull(); request.UserName.NotNull(); request.Email.NotNull(); @@ -110,15 +110,15 @@ public async Task AddAsync(UserAddRequest request, bool needConfirmation, Cancel if (needConfirmation) { - _logger.LogInformation($"{nameof(AddAsync)}({request.UserName}) sending a confirmation email to {request.Email}..."); + _logger.LogInformation("{AddAsyncName}({RequestUserName}) sending a confirmation email to {RequestEmail}...", nameof(AddAsync), request.UserName, request.Email); await _confirmationService.SendConfirmationCode(user, token); } - _logger.LogInformation($"{nameof(AddAsync)}({request.UserName}) finished..."); + _logger.LogInformation("{AddAsyncName}({RequestUserName}) finished...", nameof(AddAsync), request.UserName); } catch (Exception ex) { - _logger.LogError($"{nameof(AddAsync)}({request.UserName}) error: {ex.Message}", ex); + _logger.LogError("{AddAsyncName}({RequestUserName}) error: {ExMessage}", nameof(AddAsync), request.UserName, ex.Message, ex); throw; } @@ -128,7 +128,7 @@ public async Task UpdateAsync(UserUpdateRequest request, CancellationToken token { try { - _logger.LogInformation($"{nameof(UpdateAsync)}({request.UserName}) started..."); + _logger.LogInformation("{UpdateAsyncName}({RequestUserName}) started...", nameof(UpdateAsync), request.UserName); request.NotNull(); request.UserName.NotNull(); @@ -154,11 +154,11 @@ public async Task UpdateAsync(UserUpdateRequest request, CancellationToken token await _context.SaveChangesAsync(token); - _logger.LogInformation($"{nameof(UpdateAsync)}({request.UserName}) finished..."); + _logger.LogInformation("{UpdateAsyncName}({RequestUserName}) finished...", nameof(UpdateAsync), request.UserName); } catch (Exception ex) { - _logger.LogError($"{nameof(UpdateAsync)}({request.UserName}) error: {ex.Message}", ex); + _logger.LogError("{UpdateAsyncName}({RequestUserName}) error: {ExMessage}", nameof(UpdateAsync), request.UserName, ex.Message, ex); throw; } @@ -168,7 +168,7 @@ public async Task DeleteAsync(UserDeleteRequest request, CancellationToken token { try { - _logger.LogInformation($"{nameof(DeleteAsync)}({request.UserName}) started..."); + _logger.LogInformation("{DeleteAsyncName}({RequestUserName}) started...", nameof(DeleteAsync), request.UserName); request.NotNull(); request.UserName.NotNull(); @@ -184,11 +184,11 @@ public async Task DeleteAsync(UserDeleteRequest request, CancellationToken token await _context.SaveChangesAsync(token); - _logger.LogInformation($"{nameof(DeleteAsync)}({request.UserName}) finished..."); + _logger.LogInformation("{DeleteAsyncName}({RequestUserName}) finished...", nameof(DeleteAsync), request.UserName); } catch (Exception ex) { - _logger.LogError($"{nameof(DeleteAsync)}({request.UserName}) error: {ex.Message}", ex); + _logger.LogError("{DeleteAsyncName}({RequestUserName}) error: {ExMessage}", nameof(DeleteAsync), request.UserName, ex.Message, ex); throw; } @@ -198,7 +198,7 @@ public async Task GetAsync(UserGetRequest request, Cancellation { try { - _logger.LogInformation($"{nameof(GetAsync)}({request.UserName}) started..."); + _logger.LogInformation("{GetAsyncName}({RequestUserName}) started...", nameof(GetAsync), request.UserName); if (_context.ApplicationUsers.AsQueryable() .All(u => u.NormalizedUserName != GetNormalized(request.UserName!))) @@ -209,7 +209,7 @@ public async Task GetAsync(UserGetRequest request, Cancellation user.NotNull(); user!.Email.NotNull(); - _logger.LogInformation($"{nameof(GetAsync)}({request.UserName}) finished..."); + _logger.LogInformation("{GetAsyncName}({RequestUserName}) finished...", nameof(GetAsync), request.UserName); return new UserGetResponse { @@ -219,7 +219,7 @@ public async Task GetAsync(UserGetRequest request, Cancellation } catch (Exception ex) { - _logger.LogError($"{nameof(GetAsync)}({request.UserName}) error: {ex.Message}", ex); + _logger.LogError("{GetAsyncName}({RequestUserName}) error: {ExMessage}", nameof(GetAsync), request.UserName, ex.Message, ex); throw; } diff --git a/Botticelli.Server.Back/Services/BotManagementService.cs b/Botticelli.Server.Back/Services/BotManagementService.cs index 27d4935c..a756b56a 100644 --- a/Botticelli.Server.Back/Services/BotManagementService.cs +++ b/Botticelli.Server.Back/Services/BotManagementService.cs @@ -37,7 +37,7 @@ public Task RegisterBot(string botId, { try { - _logger.LogInformation($"{nameof(RegisterBot)}({botId}, {botKey}, {botName}, {botType}) started..."); + _logger.LogInformation("{RegisterBotName}({BotId}, {BotKey}, {BotName}, {BotType}) started...", nameof(RegisterBot), botId, botKey, botName, botType); if (GetBotInfo(botId) == null) AddNewBotInfo(botId, @@ -144,7 +144,7 @@ public async Task UpdateBot(string botId, { try { - _logger.LogInformation($"{nameof(UpdateBot)}({botId}, {botKey}, {botName}) started..."); + _logger.LogInformation("{UpdateBotName}({BotId}, {BotKey}, {BotName}) started...", nameof(UpdateBot), botId, botKey, botName); var prevStatus = await GetRequiredBotStatus(botId); if (prevStatus is not BotStatus.Unlocked) await SetRequiredBotStatus(botId, BotStatus.Unlocked); @@ -153,7 +153,7 @@ public async Task UpdateBot(string botId, if (botInfo == null) { - _logger.LogInformation($"{nameof(UpdateBot)}() : bot with id '{botId}' wasn't found!"); + _logger.LogInformation("{UpdateBotName}() : bot with id '{BotId}' wasn't found!", nameof(UpdateBot), botId); return false; } diff --git a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs index ce34fe62..22972a11 100644 --- a/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs +++ b/Botticelli.Server.Back/Services/Broadcasting/BroadcastService.cs @@ -15,15 +15,16 @@ public async Task BroadcastMessage(Broadcast message) await context.SaveChangesAsync(); } - public async Task> GetMessages(string botId) - { - return await context.BroadcastMessages.Where(m => m.BotId.Equals(botId) && !m.Received).Include(m => m.Attachments).ToArrayAsync(); - } + public async Task> GetMessages(string botId) => + await context.BroadcastMessages + .Where(m => m.BotId.Equals(botId) && !m.Received) + .Include(m => m.Attachments) + .ToArrayAsync(); public async Task MarkReceived(string botId, string messageId) { var messages = await context.BroadcastMessages.Where(bm => bm.BotId == botId && bm.Id == messageId) - .ToListAsync(); + .ToListAsync(); foreach (var message in messages) message.Received = true; diff --git a/Botticelli.Server.FrontNew/Pages/YourBots.razor b/Botticelli.Server.FrontNew/Pages/YourBots.razor index 29b7fb12..3abaaddf 100644 --- a/Botticelli.Server.FrontNew/Pages/YourBots.razor +++ b/Botticelli.Server.FrontNew/Pages/YourBots.razor @@ -92,7 +92,7 @@ else private async Task RemoveBot(string botId) { - if (!await JsRuntime.InvokeAsync("confirm", new object[] {"Are you sure?"})) return; + if (!await JsRuntime.InvokeAsync("confirm", ["Are you sure?"])) return; await _http.GetAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, $"/admin/RemoveBot?botId={botId}")); UriHelper.NavigateTo("/your_bots", true); diff --git a/Botticelli.Shared/Utils/Assertions.cs b/Botticelli.Shared/Utils/Assertions.cs index 8dbc8832..a902af52 100644 --- a/Botticelli.Shared/Utils/Assertions.cs +++ b/Botticelli.Shared/Utils/Assertions.cs @@ -16,7 +16,7 @@ public static void NotNullOrEmpty(this IEnumerable input) public static IEnumerable EmptyIfNull(this IEnumerable? input) { - return (input is null ? input : Array.Empty())!; + return (input is null ? input : [])!; } public static string EmptyIfNull(this string? input) diff --git a/Botticelli.Shared/ValueObjects/Message.cs b/Botticelli.Shared/ValueObjects/Message.cs index 1a4e684b..e6cc7941 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; } = new(); + public List Attachments { get; set; } = []; /// /// From user diff --git a/Botticelli.Talks/BaseTtsSpeaker.cs b/Botticelli.Talks/BaseTtsSpeaker.cs index 76707b67..0c0ab26f 100644 --- a/Botticelli.Talks/BaseTtsSpeaker.cs +++ b/Botticelli.Talks/BaseTtsSpeaker.cs @@ -63,18 +63,16 @@ protected async Task Compress(byte[] input, CancellationToken token) using var bufferStream = new MemoryStream(input); await using var wavReader = new WaveFileReader(bufferStream); - await using (var mp3Writer = new LameMP3FileWriter(resultStream, wavReader.WaveFormat, preset)) - { - await wavReader.CopyToAsync(mp3Writer, token); + await using var mp3Writer = new LameMP3FileWriter(resultStream, wavReader.WaveFormat, preset); + await wavReader.CopyToAsync(mp3Writer, token); - return resultStream.ToArray(); - } + return resultStream.ToArray(); } catch (Exception ex) { - Logger.LogError($"Error while compressing: {ex.Message}", ex); + Logger.LogError("Error while compressing: {ExMessage} {ex}", ex.Message, ex); } - return Array.Empty(); + return []; } } \ No newline at end of file diff --git a/Botticelli.Talks/OpenTts/OpenTtsSpeaker.cs b/Botticelli.Talks/OpenTts/OpenTtsSpeaker.cs index bc089ca5..d420a585 100644 --- a/Botticelli.Talks/OpenTts/OpenTtsSpeaker.cs +++ b/Botticelli.Talks/OpenTts/OpenTtsSpeaker.cs @@ -42,9 +42,9 @@ public override async Task Speak(string markedText, if (!result.IsSuccessStatusCode) { - Logger.LogError($"Can't get response from voice: {result.StatusCode}: {result.ReasonPhrase}!"); + Logger.LogError("Can't get response from voice: {ResultStatusCode}: {ResultReasonPhrase}!", result.StatusCode, result.ReasonPhrase); - return Array.Empty(); + return []; } var byteResult = await result.Content.ReadAsByteArrayAsync(token); diff --git a/Samples/Ai.Common.Sample/Handlers/AiHandler.cs b/Samples/Ai.Common.Sample/Handlers/AiHandler.cs index 135ae779..f51366f5 100644 --- a/Samples/Ai.Common.Sample/Handlers/AiHandler.cs +++ b/Samples/Ai.Common.Sample/Handlers/AiHandler.cs @@ -35,7 +35,7 @@ await _provider.SendAsync(new AiMessage(input.Uid) } catch (Exception ex) { - _logger.LogError($"Error while handling a message from AI backend: {ex.Message}", ex); + _logger.LogError("Error while handling a message from AI backend: {ExMessage}", ex.Message, ex); } } } \ No newline at end of file From 1cf9153409f682e305a80bfbb7f6a390dedb46e1 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Tue, 30 Sep 2025 12:52:11 +0300 Subject: [PATCH 095/101] - binary message upload support for admin/broadcasting --- .../Controllers/AdminController.cs | 61 +++++++++++++------ .../Pages/BotBroadcast.razor | 59 +++++++++++++++--- .../Utils/ProgressStreamContent.cs | 30 +++++++++ 3 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index adf0cce0..e1b885a0 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -1,11 +1,12 @@ -using Botticelli.Server.Back.Services; +using System.Text; +using System.Text.Json; +using Botticelli.Server.Back.Services; using Botticelli.Server.Back.Services.Broadcasting; using Botticelli.Server.Data.Entities.Bot; using Botticelli.Server.Data.Entities.Bot.Broadcasting; using Botticelli.Shared.API.Admin.Responses; using Botticelli.Shared.API.Client.Requests; using Botticelli.Shared.API.Client.Responses; -using Botticelli.Shared.Constants; using Botticelli.Shared.ValueObjects; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -22,7 +23,7 @@ public class AdminController( IBotManagementService botManagementService, IBotStatusDataService botStatusDataService, ILogger logger, - IBroadcastService broadcastService) + IBroadcastService broadcastService) : ControllerBase { [HttpPost("[action]")] public async Task AddNewBot([FromBody] RegisterBotRequest request) @@ -66,7 +67,26 @@ public async Task UpdateBot([FromBody] UpdateBotRequest reque /// /// [HttpPost("[action]")] - public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) + public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) => await DoBroadcast(botId, message); + + /// + /// Sends a broadcast message in binary stream + /// + /// + /// + /// + [HttpPost("[action]")] + public async Task SendBroadcastBinary([FromQuery] string botId) + { + using var memoryStream = new MemoryStream(); + await Request.Body.CopyToAsync(memoryStream); + + var message = JsonSerializer.Deserialize(Encoding.UTF8.GetString(memoryStream.ToArray())); + + await DoBroadcast(botId, message); + } + + private async Task DoBroadcast(string botId, Message? message) { await broadcastService.BroadcastMessage(new Broadcast { @@ -74,26 +94,27 @@ await broadcastService.BroadcastMessage(new Broadcast BotId = botId ?? throw new NullReferenceException("BotId cannot be null!"), Body = message.Body ?? throw new NullReferenceException("Body cannot be null!"), Attachments = message.Attachments - .Where(a => a is BinaryBaseAttachment) - .Select(a => - { - if (a is BinaryBaseAttachment baseAttachment) - return new BroadcastAttachment - { - Id = Guid.NewGuid(), - BroadcastId = message.Uid, - MediaType = baseAttachment.MediaType, - Filename = baseAttachment.Name, - Content = baseAttachment.Data - }; - - return null!; - }) - .ToList(), + .Where(a => a is BinaryBaseAttachment) + .Select(a => + { + if (a is BinaryBaseAttachment baseAttachment) + return new BroadcastAttachment + { + Id = Guid.NewGuid(), + BroadcastId = message.Uid, + MediaType = baseAttachment.MediaType, + Filename = baseAttachment.Name, + Content = baseAttachment.Data + }; + + return null!; + }) + .ToList(), Sent = true }); } + [HttpGet("[action]")] public Task> GetBots() { diff --git a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor index d5cb5393..5883f38b 100644 --- a/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor +++ b/Botticelli.Server.FrontNew/Pages/BotBroadcast.razor @@ -58,12 +58,15 @@ Change="@OnFileChange" Disabled="!_uploadEnabled" Multiple="true" + Progress="@OnProgress" Style="margin: 10px;" />
+ +
@@ -85,6 +88,7 @@ }; private bool _uploadEnabled = true; + private int _uploadProgress = 0; [Parameter] public string? BotId { get; set; } @@ -112,20 +116,47 @@ Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionToken); - var content = new StringContent(JsonSerializer.Serialize(_message), - Encoding.UTF8, - "application/json"); - - var response = await Http.PostAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, $"/admin/SendBroadcast?botId={BotId}"), - content); + if (!_message.Attachments.Any()) + { + var content = new StringContent(JsonSerializer.Serialize(_message), + Encoding.UTF8, + "application/json"); - #if DEBUG - if (!response.IsSuccessStatusCode) _error.UserMessage = $"Error sending a broadcast message: {response.ReasonPhrase}"; - #endif + var response = await Http.PostAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, $"/admin/SendBroadcast?botId={BotId}"), + content); +#if DEBUG + if (!response.IsSuccessStatusCode) _error.UserMessage = $"Error sending a broadcast message: {response.ReasonPhrase}"; + Console.WriteLine(_error.UserMessage); +#endif + } + else + { + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, _message); + + var progressStreamContent = new ProgressStreamContent(ms, + (totalRead, + totalBytes) => + { + _uploadProgress = (int)((double)totalRead / totalBytes); + Console.WriteLine($"UploadProgress: {_uploadProgress}"); + }); + + progressStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + + var binaryResponse = await Http.PostAsync(Url.Combine(BackSettings.CurrentValue.BackUrl, $"/admin/SendBroadcastBinary?botId={BotId}"), + progressStreamContent); + +#if DEBUG + if (!binaryResponse.IsSuccessStatusCode) _error.UserMessage = $"Error sending a broadcast message (binary): {binaryResponse.ReasonPhrase}"; + Console.WriteLine(_error.UserMessage); +#endif + } + UriHelper.NavigateTo("/your_bots", true); } - + private async Task OnFileChange(UploadChangeEventArgs? fileChange) { if (fileChange == null) return; @@ -157,4 +188,12 @@ } } } + + private Task OnProgress(UploadProgressArgs arg) + { + _uploadProgress = arg.Progress; + + return Task.CompletedTask; + } + } \ No newline at end of file diff --git a/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs b/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs new file mode 100644 index 00000000..d287ffa6 --- /dev/null +++ b/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs @@ -0,0 +1,30 @@ +using System.Net; + +namespace Botticelli.Server.FrontNew.Utils; + +public class ProgressStreamContent : StreamContent +{ + private readonly Action _progress; + + public ProgressStreamContent(Stream content, Action progress) : base(content) + { + _progress = progress; + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + var totalBytes = Headers.ContentLength ?? -1; + var buffer = new byte[8192]; + long totalRead = 0; + int bytesRead; + + var contentStream = await ReadAsStreamAsync(); + contentStream.Seek(0, SeekOrigin.Begin); + while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await stream.WriteAsync(buffer, 0, bytesRead); + totalRead += bytesRead; + _progress?.Invoke(totalRead, totalBytes); + } + } +} \ No newline at end of file From 4ed7680c6f399f8777071ccb947de51c7200a777 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Wed, 1 Oct 2025 18:46:46 +0300 Subject: [PATCH 096/101] - appsettings fix --- Samples/Broadcasting.Sample.Telegram/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/Broadcasting.Sample.Telegram/appsettings.json b/Samples/Broadcasting.Sample.Telegram/appsettings.json index 636c3c25..01bef1ab 100644 --- a/Samples/Broadcasting.Sample.Telegram/appsettings.json +++ b/Samples/Broadcasting.Sample.Telegram/appsettings.json @@ -22,7 +22,7 @@ }, "Broadcasting": { "ConnectionString": "broadcasting_database.db;Password=321", - "BotId": "91d3z7fW87uvWn9441vdxusnhAe4sIaDTyIJVm9Q", + "BotId": "chatbot1", "HowOld": "00:10:00", "ServerUri": "https://localhost:7247/v1" }, From a8e9f868386abc260010c82e7f3c4e110261df39 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 3 Oct 2025 12:49:08 +0300 Subject: [PATCH 097/101] - new bot status: error --- .../Extensions/ServiceCollectionExtensions.cs | 12 ++++++------ Botticelli.Framework/Services/BotStatusService.cs | 9 +++++---- Botticelli.Shared/API/Admin/Responses/BotStatus.cs | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs index c53ab088..84b71ae2 100644 --- a/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs +++ b/Botticelli.Framework.Telegram/Extensions/ServiceCollectionExtensions.cs @@ -58,12 +58,12 @@ public static TBotBuilder AddTelegramBot(this IServiceCollect .Get() ?? throw new ConfigurationErrorsException($"Can't load configuration for {nameof(DataAccessSettings)}!"); - return services.AddTelegramBot(o => - o.Set(telegramBotSettings), - o => o.Set(analyticsClientSettings), - o => o.Set(serverSettings), - o => o.Set(dataAccessSettings), - telegramBotBuilderFunc); + return services.AddTelegramBot( + o => o.Set(telegramBotSettings), + o => o.Set(analyticsClientSettings), + o => o.Set(serverSettings), + o => o.Set(dataAccessSettings), + telegramBotBuilderFunc); } public static TelegramBotBuilder> AddTelegramBot(this IServiceCollection services, diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 2fe9a94a..44cf63e5 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -21,7 +21,7 @@ public class BotStatusService( logger, serverSettings) { - private const short GetStatusPeriod = 30000; + private const short GetStatusPeriod = 30; private Task? _getRequiredStatusEventTask; public override Task StartAsync(CancellationToken cancellationToken) @@ -48,7 +48,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) }; _getRequiredStatusEventTask = Policy.HandleResult(_ => true) - .WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(GetStatusPeriod)) + .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(GetStatusPeriod)) .ExecuteAndCaptureAsync(ct => Process(request, ct)!, cancellationToken); } @@ -70,7 +70,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) return Task.FromResult(new GetRequiredStatusFromServerResponse { - Status = BotStatus.Unknown, + Status = BotStatus.Error, BotId = BotId ?? string.Empty, BotContext = null }); @@ -84,7 +84,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) return Task.FromResult(new GetRequiredStatusFromServerResponse { - Status = BotStatus.Unknown, + Status = BotStatus.Error, BotId = BotId ?? string.Empty, BotContext = null }); @@ -122,6 +122,7 @@ private void GetRequiredStatus(CancellationToken cancellationToken) break; case BotStatus.Locked: case BotStatus.Unknown: + case BotStatus.Error: case null: Bot.StopBotAsync(StopBotRequest.GetInstance(), cancellationToken); diff --git a/Botticelli.Shared/API/Admin/Responses/BotStatus.cs b/Botticelli.Shared/API/Admin/Responses/BotStatus.cs index 4a67dcc3..aa84e70c 100644 --- a/Botticelli.Shared/API/Admin/Responses/BotStatus.cs +++ b/Botticelli.Shared/API/Admin/Responses/BotStatus.cs @@ -4,5 +4,6 @@ public enum BotStatus { Unlocked, Locked, - Unknown + Unknown, + Error } \ No newline at end of file From 7a262f3617416986cd40c08e1f2422efdabb191d Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 3 Oct 2025 14:51:10 +0300 Subject: [PATCH 098/101] - dynamic delay when BotStatus = Error --- .../Services/BotStatusService.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Botticelli.Framework/Services/BotStatusService.cs b/Botticelli.Framework/Services/BotStatusService.cs index 44cf63e5..1b703c20 100644 --- a/Botticelli.Framework/Services/BotStatusService.cs +++ b/Botticelli.Framework/Services/BotStatusService.cs @@ -21,7 +21,8 @@ public class BotStatusService( logger, serverSettings) { - private const short GetStatusPeriod = 30; + private const short GetStatusPeriod = 10; + private const short MaxGetStatusPeriod = 120; private Task? _getRequiredStatusEventTask; public override Task StartAsync(CancellationToken cancellationToken) @@ -48,11 +49,21 @@ private void GetRequiredStatus(CancellationToken cancellationToken) }; _getRequiredStatusEventTask = Policy.HandleResult(_ => true) - .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(GetStatusPeriod)) - .ExecuteAndCaptureAsync(ct => Process(request, ct)!, - cancellationToken); + .WaitAndRetryForeverAsync((_, ctx) => !ctx.TryGetValue("delay", out var delay) ? TimeSpan.FromSeconds(GetStatusPeriod) : (TimeSpan)delay, (result, i, timeSpan, context) => + { + var gotRetries = context.TryGetValue("gotRetries", out var gr) ? (int)gr + 1 : 0; + var delay = GetDelay(gotRetries); + + context["gotRetries"] = result.Result.Status == BotStatus.Error ? gotRetries : 0; + context["delay"] = result.Result.Status == BotStatus.Error ? + TimeSpan.FromSeconds(delay > MaxGetStatusPeriod ? MaxGetStatusPeriod : delay) : + TimeSpan.FromSeconds(GetStatusPeriod); + }) + .ExecuteAndCaptureAsync(ct => Process(request, ct)!, cancellationToken); } + private static double GetDelay(int i) => GetStatusPeriod + GetStatusPeriod * Math.Log(i + 1, Math.E); + private Task Process(GetRequiredStatusFromServerRequest request, CancellationToken cancellationToken) { From 35881f03018ffe20d788548d47bfe6865b4f7a35 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 3 Oct 2025 15:30:28 +0300 Subject: [PATCH 099/101] - refactoring --- .../Controllers/AdminController.cs | 96 ++++++++++++------ .../Controllers/AuthController.cs | 16 ++- .../Controllers/BotController.cs | 2 +- .../Controllers/UserController.cs | 98 ++++++++++++++----- 4 files changed, 145 insertions(+), 67 deletions(-) diff --git a/Botticelli.Server.Back/Controllers/AdminController.cs b/Botticelli.Server.Back/Controllers/AdminController.cs index e1b885a0..ed1b03a5 100644 --- a/Botticelli.Server.Back/Controllers/AdminController.cs +++ b/Botticelli.Server.Back/Controllers/AdminController.cs @@ -20,21 +20,29 @@ namespace Botticelli.Server.Back.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] [Route("/v1/admin")] public class AdminController( - IBotManagementService botManagementService, - IBotStatusDataService botStatusDataService, - ILogger logger, - IBroadcastService broadcastService) : ControllerBase + IBotManagementService botManagementService, + IBotStatusDataService botStatusDataService, + ILogger logger, + IBroadcastService broadcastService) : ControllerBase { + /// + /// Adds a new bot + /// + /// + /// [HttpPost("[action]")] public async Task AddNewBot([FromBody] RegisterBotRequest request) { logger.LogInformation("{AddNewBotName}({RequestBotId}) started...", nameof(AddNewBot), request.BotId); var success = await botManagementService.RegisterBot(request.BotId, - request.BotKey, - request.BotName, - request.Type); + request.BotKey, + request.BotName, + request.Type); - logger.LogInformation("{AddNewBotName}({RequestBotId}) success: {Success}...", nameof(AddNewBot), request.BotId, success); + logger.LogInformation("{AddNewBotName}({RequestBotId}) success: {Success}...", + nameof(AddNewBot), + request.BotId, + success); return new RegisterBotResponse { @@ -43,15 +51,23 @@ public async Task AddNewBot([FromBody] RegisterBotRequest r }; } + /// + /// Updates a bot + /// + /// + /// [HttpPut("[action]")] public async Task UpdateBot([FromBody] UpdateBotRequest request) { logger.LogInformation("{UpdateBotName}({RequestBotId}) started...", nameof(UpdateBot), request.BotId); var success = await botManagementService.UpdateBot(request.BotId, - request.BotKey, - request.BotName); + request.BotKey, + request.BotName); - logger.LogInformation("{UpdateBotName}({RequestBotId}) success: {Success}...", nameof(UpdateBot), request.BotId, success); + logger.LogInformation("{UpdateBotName}({RequestBotId}) success: {Success}...", + nameof(UpdateBot), + request.BotId, + success); return new UpdateBotResponse { @@ -67,7 +83,10 @@ public async Task UpdateBot([FromBody] UpdateBotRequest reque /// /// [HttpPost("[action]")] - public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) => await DoBroadcast(botId, message); + public async Task SendBroadcast([FromQuery] string botId, [FromBody] Message message) + { + await DoBroadcast(botId, message); + } /// /// Sends a broadcast message in binary stream @@ -82,7 +101,7 @@ public async Task SendBroadcastBinary([FromQuery] string botId) await Request.Body.CopyToAsync(memoryStream); var message = JsonSerializer.Deserialize(Encoding.UTF8.GetString(memoryStream.ToArray())); - + await DoBroadcast(botId, message); } @@ -90,49 +109,64 @@ private async Task DoBroadcast(string botId, Message? message) { await broadcastService.BroadcastMessage(new Broadcast { - Id = message.Uid ?? throw new NullReferenceException("Id cannot be null!"), + Id = message.Uid ?? throw new NullReferenceException("Id cannot be null!"), BotId = botId ?? throw new NullReferenceException("BotId cannot be null!"), Body = message.Body ?? throw new NullReferenceException("Body cannot be null!"), Attachments = message.Attachments - .Where(a => a is BinaryBaseAttachment) - .Select(a => - { - if (a is BinaryBaseAttachment baseAttachment) - return new BroadcastAttachment - { - Id = Guid.NewGuid(), - BroadcastId = message.Uid, - MediaType = baseAttachment.MediaType, - Filename = baseAttachment.Name, - Content = baseAttachment.Data - }; - - return null!; - }) - .ToList(), + .Where(a => a is BinaryBaseAttachment) + .Select(a => + { + if (a is BinaryBaseAttachment baseAttachment) + return new BroadcastAttachment + { + Id = Guid.NewGuid(), + BroadcastId = message.Uid, + MediaType = baseAttachment.MediaType, + Filename = baseAttachment.Name, + Content = baseAttachment.Data + }; + + return null!; + }) + .ToList(), Sent = true }); } - + /// + /// Get bots list + /// + /// [HttpGet("[action]")] public Task> GetBots() { return Task.FromResult(botStatusDataService.GetBots()); } + /// + /// Activates a bot (BotStatus.Unlocked) + /// + /// [HttpGet("[action]")] public async Task ActivateBot([FromQuery] string botId) { await botManagementService.SetRequiredBotStatus(botId, BotStatus.Unlocked); } + /// + /// Deactivates a bot (BotStatus.Locked) + /// + /// [HttpGet("[action]")] public async Task DeactivateBot([FromQuery] string botId) { await botManagementService.SetRequiredBotStatus(botId, BotStatus.Locked); } + /// + /// Removes a bot + /// + /// [HttpGet("[action]")] public async Task RemoveBot([FromQuery] string botId) { diff --git a/Botticelli.Server.Back/Controllers/AuthController.cs b/Botticelli.Server.Back/Controllers/AuthController.cs index 0fd4fe67..6c755f53 100644 --- a/Botticelli.Server.Back/Controllers/AuthController.cs +++ b/Botticelli.Server.Back/Controllers/AuthController.cs @@ -11,19 +11,17 @@ namespace Botticelli.Server.Back.Controllers; [ApiController] [AllowAnonymous] [Route("/v1/auth")] -public class AuthController +public class AuthController(IAdminAuthService adminAuthService) { - private readonly IAdminAuthService _adminAuthService; - - public AuthController(IAdminAuthService adminAuthService) - { - _adminAuthService = adminAuthService; - } - + /// + /// Gets auth token for a user + /// + /// + /// [AllowAnonymous] [HttpPost("[action]")] public IActionResult GetToken(UserLoginRequest request) { - return new OkObjectResult(_adminAuthService.GenerateToken(request)); + return new OkObjectResult(adminAuthService.GenerateToken(request)); } } \ No newline at end of file diff --git a/Botticelli.Server.Back/Controllers/BotController.cs b/Botticelli.Server.Back/Controllers/BotController.cs index afe57659..5028a8c3 100644 --- a/Botticelli.Server.Back/Controllers/BotController.cs +++ b/Botticelli.Server.Back/Controllers/BotController.cs @@ -12,7 +12,7 @@ namespace Botticelli.Server.Back.Controllers; /// -/// Bot status controller +/// Bot status/data controller /// [ApiController] [AllowAnonymous] diff --git a/Botticelli.Server.Back/Controllers/UserController.cs b/Botticelli.Server.Back/Controllers/UserController.cs index f99978a1..1a87f357 100644 --- a/Botticelli.Server.Back/Controllers/UserController.cs +++ b/Botticelli.Server.Back/Controllers/UserController.cs @@ -9,38 +9,37 @@ namespace Botticelli.Server.Back.Controllers; /// -/// Admin controller getting/adding/removing bots +/// Controller for user authorization on admin part /// [ApiController] [Authorize(AuthenticationSchemes = "Bearer")] [Route("/v1/user")] -public class UserController : Controller +public class UserController(IUserService userService, IMapper mapper, IPasswordSender passwordSender) : Controller { - private readonly IMapper _mapper; - private readonly IPassword _password = new Password(true, true, true, false, 12); - private readonly IPasswordSender _passwordSender; - private readonly IUserService _userService; - - public UserController(IUserService userService, IMapper mapper, IPasswordSender passwordSender) - { - _userService = userService; - _mapper = mapper; - _passwordSender = passwordSender; - } - + /// + /// Does system contain any users? + /// + /// + /// [HttpGet("[action]")] [AllowAnonymous] public async Task HasUsersAsync(CancellationToken token) { - return Ok(await _userService.HasUsers(token)); + return Ok(await userService.HasUsers(token)); } + /// + /// Adds a default user + /// + /// + /// + /// [HttpPost("[action]")] [AllowAnonymous] public async Task AddDefaultUserAsync(DefaultUserAddRequest request, CancellationToken token) @@ -52,10 +51,10 @@ public async Task AddDefaultUserAsync(DefaultUserAddRequest reque request.Email.NotNull(); var password = _password.Next(); - var mapped = _mapper.Map(request); + var mapped = mapper.Map(request); mapped.Password = password; - if (await _userService.CheckAndAddAsync(mapped, token)) await _passwordSender.SendPassword(request.Email!, password, token); + if (await userService.CheckAndAddAsync(mapped, token)) await passwordSender.SendPassword(request.Email!, password, token); } catch (Exception ex) { @@ -65,6 +64,12 @@ public async Task AddDefaultUserAsync(DefaultUserAddRequest reque return Ok(); } + /// + /// Regenerates passsword + /// + /// + /// + /// [HttpPost("[action]")] [AllowAnonymous] public async Task RegeneratePasswordAsync(RegeneratePasswordRequest passwordRequest, @@ -76,11 +81,11 @@ public async Task RegeneratePasswordAsync(RegeneratePasswordReque passwordRequest.UserName.NotNull(); passwordRequest.Email.NotNull(); - var mapped = _mapper.Map(passwordRequest); + var mapped = mapper.Map(passwordRequest); mapped.Password = _password.Next(); - await _userService.UpdateAsync(mapped, token); - await _passwordSender.SendPassword(passwordRequest.Email!, mapped.Password, token); + await userService.UpdateAsync(mapped, token); + await passwordSender.SendPassword(passwordRequest.Email!, mapped.Password, token); } catch (Exception ex) { @@ -90,13 +95,19 @@ public async Task RegeneratePasswordAsync(RegeneratePasswordReque return Ok(); } + /// + /// Adds a new user + /// + /// + /// + /// [HttpPost] [AllowAnonymous] public async Task AddUserAsync(UserAddRequest request, CancellationToken token) { try { - await _userService.AddAsync(request, true, token); + await userService.AddAsync(request, true, token); } catch (Exception ex) { @@ -107,6 +118,11 @@ public async Task AddUserAsync(UserAddRequest request, Cancellati } + /// + /// Gets an authorized current user + /// + /// + /// [HttpGet("[action]")] public async Task> GetCurrentUserAsync(CancellationToken token) { @@ -118,19 +134,25 @@ public async Task> GetCurrentUserAsync(Cancellatio return await GetUserAsync(request, token); } - + private string GetCurrentUserName() { return HttpContext.User.Claims.FirstOrDefault(c => c.Type == "applicationUserName")?.Value ?? throw new NullReferenceException(); } + /// + /// Gets an information about a user + /// + /// + /// + /// [HttpGet] public async Task> GetUserAsync(UserGetRequest request, CancellationToken token) { try { - return new ActionResult(await _userService.GetAsync(request, token)); + return new ActionResult(await userService.GetAsync(request, token)); } catch (Exception ex) { @@ -138,6 +160,12 @@ public async Task> GetUserAsync(UserGetRequest req } } + /// + /// Updates a current user + /// + /// + /// + /// [HttpPut("[action]")] public async Task UpdateCurrentUserAsync(UserUpdateRequest request, CancellationToken token) { @@ -147,12 +175,18 @@ public async Task UpdateCurrentUserAsync(UserUpdateRequest reques return await UpdateUserAsync(request, token); } + /// + /// Updates a given user + /// + /// + /// + /// [HttpPut] public async Task UpdateUserAsync(UserUpdateRequest request, CancellationToken token) { try { - await _userService.UpdateAsync(request, token); + await userService.UpdateAsync(request, token); return Ok(); } @@ -162,6 +196,12 @@ public async Task UpdateUserAsync(UserUpdateRequest request, Canc } } + /// + /// Deletes a user + /// + /// + /// + /// [HttpDelete] public async Task DeleteUserAsync(UserDeleteRequest request, CancellationToken token) { @@ -171,7 +211,7 @@ public async Task DeleteUserAsync(UserDeleteRequest request, Canc if (request.UserName == user) return BadRequest("You can't delete yourself!"); - await _userService.DeleteAsync(request, token); + await userService.DeleteAsync(request, token); return Ok(); } @@ -181,6 +221,12 @@ public async Task DeleteUserAsync(UserDeleteRequest request, Canc } } + /// + /// Email confirmation method + /// + /// + /// + /// [HttpGet("[action]")] [AllowAnonymous] public async Task ConfirmEmailAsync([FromQuery] ConfirmEmailRequest request, CancellationToken token) @@ -191,7 +237,7 @@ public async Task ConfirmEmailAsync([FromQuery] ConfirmEmailReque request.Email.NotNull(); request.Token.NotNull(); - await _userService.ConfirmCodeAsync(request.Email!, request.Token!, token); + await userService.ConfirmCodeAsync(request.Email!, request.Token!, token); return Ok(); } From a8a9c8c72ef1947797fbfca8b3235b142fc13db9 Mon Sep 17 00:00:00 2001 From: stone1985 Date: Fri, 3 Oct 2025 15:36:38 +0300 Subject: [PATCH 100/101] - admin password length is in settings now --- Botticelli.Server.Back/Controllers/UserController.cs | 7 +++++-- Botticelli.Server.Back/Services/Auth/AdminAuthService.cs | 5 +---- Botticelli.Server.Back/Settings/ServerSettings.cs | 2 ++ Botticelli.Server.Back/appsettings.json | 4 +++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Botticelli.Server.Back/Controllers/UserController.cs b/Botticelli.Server.Back/Controllers/UserController.cs index 1a87f357..5e374f42 100644 --- a/Botticelli.Server.Back/Controllers/UserController.cs +++ b/Botticelli.Server.Back/Controllers/UserController.cs @@ -1,9 +1,11 @@ using Botticelli.Server.Back.Services.Auth; +using Botticelli.Server.Back.Settings; using Botticelli.Server.Data.Entities.Auth; using Botticelli.Shared.Utils; using MapsterMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using PasswordGenerator; namespace Botticelli.Server.Back.Controllers; @@ -14,13 +16,14 @@ namespace Botticelli.Server.Back.Controllers; [ApiController] [Authorize(AuthenticationSchemes = "Bearer")] [Route("/v1/user")] -public class UserController(IUserService userService, IMapper mapper, IPasswordSender passwordSender) : Controller +public class UserController(IUserService userService, IMapper mapper, IPasswordSender passwordSender, IOptionsMonitor settings) : Controller { private readonly IPassword _password = new Password(true, true, true, false, - 12); + Random.Shared.Next(settings.CurrentValue.PasswordMinLength, + settings.CurrentValue.PasswordMaxLength)); /// /// Does system contain any users? diff --git a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs index b084294a..daa03d93 100644 --- a/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs +++ b/Botticelli.Server.Back/Services/Auth/AdminAuthService.cs @@ -1,7 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -using Botticelli.Server.Back.Settings; using Botticelli.Server.Data; using Botticelli.Server.Data.Entities.Auth; using Botticelli.Server.Data.Exceptions; @@ -9,7 +8,6 @@ using Botticelli.Shared.Utils; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace Botticelli.Server.Back.Services.Auth; @@ -27,8 +25,7 @@ public class AdminAuthService : IAdminAuthService public AdminAuthService(IConfiguration config, IHttpContextAccessor httpContextAccessor, ServerDataContext context, - ILogger logger, - IOptionsMonitor settings) + ILogger logger) { _config = config; _httpContextAccessor = httpContextAccessor; diff --git a/Botticelli.Server.Back/Settings/ServerSettings.cs b/Botticelli.Server.Back/Settings/ServerSettings.cs index f4d83c28..d4aeefe7 100644 --- a/Botticelli.Server.Back/Settings/ServerSettings.cs +++ b/Botticelli.Server.Back/Settings/ServerSettings.cs @@ -18,4 +18,6 @@ public class ServerSettings public string? AnalyticsUrl { get; set; } public required string SecureStorageConnection { get; set; } public bool UseSsl { get; set; } + public int PasswordMinLength { get; set; } = 8; + public int PasswordMaxLength { get; set; } = 12; } \ No newline at end of file diff --git a/Botticelli.Server.Back/appsettings.json b/Botticelli.Server.Back/appsettings.json index 3ef7fdd0..644d7ac7 100644 --- a/Botticelli.Server.Back/appsettings.json +++ b/Botticelli.Server.Back/appsettings.json @@ -46,6 +46,8 @@ }, "serverEmail": "", "serverUrl": "", - "analyticsUrl": "" + "analyticsUrl": "", + "PasswordMinLength": 8, + "PasswordMaxLength": 12 } } \ No newline at end of file From 2561427cc35d154e7dd99d5fd527bbb475eeee95 Mon Sep 17 00:00:00 2001 From: Igor Evdokimov Date: Mon, 6 Oct 2025 21:59:41 +0300 Subject: [PATCH 101/101] - cosmetic --- .../Utils/ProgressStreamContent.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs b/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs index d287ffa6..c2db5d9a 100644 --- a/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs +++ b/Botticelli.Server.FrontNew/Utils/ProgressStreamContent.cs @@ -2,29 +2,24 @@ namespace Botticelli.Server.FrontNew.Utils; -public class ProgressStreamContent : StreamContent +public class ProgressStreamContent(Stream content, Action progress) : StreamContent(content) { - private readonly Action _progress; - - public ProgressStreamContent(Stream content, Action progress) : base(content) - { - _progress = progress; - } - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + private const int BufferSize = 8192; + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) { var totalBytes = Headers.ContentLength ?? -1; - var buffer = new byte[8192]; + var buffer = new byte[BufferSize]; long totalRead = 0; int bytesRead; var contentStream = await ReadAsStreamAsync(); contentStream.Seek(0, SeekOrigin.Begin); - while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0) { - await stream.WriteAsync(buffer, 0, bytesRead); + await stream.WriteAsync(buffer.AsMemory(0, bytesRead)); totalRead += bytesRead; - _progress?.Invoke(totalRead, totalBytes); + progress?.Invoke(totalRead, totalBytes); } } } \ No newline at end of file