diff --git a/src/TgLlmBot/CommandDispatcher/DefaultTelegramCommandDispatcher.cs b/src/TgLlmBot/CommandDispatcher/DefaultTelegramCommandDispatcher.cs index 8de952a..2649379 100644 --- a/src/TgLlmBot/CommandDispatcher/DefaultTelegramCommandDispatcher.cs +++ b/src/TgLlmBot/CommandDispatcher/DefaultTelegramCommandDispatcher.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -6,6 +6,8 @@ using Telegram.Bot.Types.Enums; using TgLlmBot.Commands.ChatWithLlm; using TgLlmBot.Commands.DisplayHelp; +using TgLlmBot.Commands.GetLimit; +using TgLlmBot.Commands.GetPersonal; using TgLlmBot.Commands.Model; using TgLlmBot.Commands.Ping; using TgLlmBot.Commands.Rating; @@ -49,6 +51,8 @@ public class DefaultTelegramCommandDispatcher : ITelegramCommandDispatcher private readonly ShowChatSystemPromptCommandHandler _showChatSystemPrompt; private readonly ShowPersonalSystemPromptCommandHandler _showPersonalSystemPrompt; private readonly UsageCommandHandler _usage; + private readonly GetLimitCommandHandler _getLimit; + private readonly GetPersonalLimitCommandHandler _getPersonalLimit; public DefaultTelegramCommandDispatcher( DefaultTelegramCommandDispatcherOptions options, @@ -67,7 +71,9 @@ public DefaultTelegramCommandDispatcher( ResetPersonalSystemPromptCommandHandler resetPersonalSystemPrompt, ShowChatSystemPromptCommandHandler showChatSystemPrompt, ShowPersonalSystemPromptCommandHandler showPersonalSystemPrompt, - SetLimitCommandHandler setLimit) + SetLimitCommandHandler setLimit, + GetLimitCommandHandler getLimit, + GetPersonalLimitCommandHandler getPersonalLimit) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(self); @@ -86,6 +92,8 @@ public DefaultTelegramCommandDispatcher( ArgumentNullException.ThrowIfNull(showChatSystemPrompt); ArgumentNullException.ThrowIfNull(showPersonalSystemPrompt); ArgumentNullException.ThrowIfNull(setLimit); + ArgumentNullException.ThrowIfNull(getLimit); + ArgumentNullException.ThrowIfNull(getPersonalLimit); _options = options; _self = self; _messageStorage = messageStorage; @@ -103,6 +111,8 @@ public DefaultTelegramCommandDispatcher( _showChatSystemPrompt = showChatSystemPrompt; _showPersonalSystemPrompt = showPersonalSystemPrompt; _setLimit = setLimit; + _getLimit = getLimit; + _getPersonalLimit = getPersonalLimit; } public async Task HandleMessageAsync(Message? message, UpdateType type, CancellationToken cancellationToken) @@ -185,6 +195,18 @@ public async Task HandleMessageAsync(Message? message, UpdateType type, Cancella await _showChatSystemPrompt.HandleAsync(command, cancellationToken); return; } + case "!get_limit": + { + var command = new GetLimitCommand(message, type, self); + await _getLimit.HandleAsync(command, cancellationToken); + return; + } + case "!get_personal_limit": + { + var command = new GetPersonalLimitCommand(message, type, self); + await _getPersonalLimit.HandleAsync(command, cancellationToken); + return; + } } if (rawPrompt.StartsWith("!chat_role", StringComparison.Ordinal)) diff --git a/src/TgLlmBot/Commands/DisplayHelp/DisplayHelpCommandHandler.cs b/src/TgLlmBot/Commands/DisplayHelp/DisplayHelpCommandHandler.cs index 78be9d4..8ba5c38 100644 --- a/src/TgLlmBot/Commands/DisplayHelp/DisplayHelpCommandHandler.cs +++ b/src/TgLlmBot/Commands/DisplayHelp/DisplayHelpCommandHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -65,6 +65,8 @@ private static string BuildHelpTemplate(ITelegramMarkdownConverter markdownConve builder.AppendLine("* `!personal_role_show` - показывает текущий системный для общения конкретно с тобой"); builder.AppendLine( "* `!set_limit` - устанавливает пользователю лимит на общение с LLM (для этого нужно отправить эту команду реплаем на сообщение того, кому нужно установить лимит и указать количество сообщений, которое будет ему доступно в день; например: `!set_limit 5`)"); + builder.AppendLine("* `!get_limit` - показывает дневной лимит сообщений пользователя (необходим реплай)"); + builder.AppendLine("* `!get_personal_limit` - показывает твой дневной лимит сообщений"); var rawMarkdown = builder.ToString(); var optimizedMarkdown = markdownConverter.ConvertToSolidTelegramMarkdown(rawMarkdown); return optimizedMarkdown; diff --git a/src/TgLlmBot/Commands/GetLimit/GetLimitCommand.cs b/src/TgLlmBot/Commands/GetLimit/GetLimitCommand.cs new file mode 100644 index 0000000..1bc6100 --- /dev/null +++ b/src/TgLlmBot/Commands/GetLimit/GetLimitCommand.cs @@ -0,0 +1,14 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using TgLlmBot.CommandDispatcher.Abstractions; + +namespace TgLlmBot.Commands.GetLimit; + +public class GetLimitCommand : AbstractCommand +{ + public GetLimitCommand(Message message, UpdateType type, User self) : base(message, type) + { + Self = self; + } + public User Self { get; } +} diff --git a/src/TgLlmBot/Commands/GetLimit/GetLimitCommandHandler.cs b/src/TgLlmBot/Commands/GetLimit/GetLimitCommandHandler.cs new file mode 100644 index 0000000..fb3943c --- /dev/null +++ b/src/TgLlmBot/Commands/GetLimit/GetLimitCommandHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using TgLlmBot.CommandDispatcher.Abstractions; +using TgLlmBot.Services.DataAccess.Limits; +using TgLlmBot.Services.Resources; +using TgLlmBot.Services.Telegram.Markdown; + +namespace TgLlmBot.Commands.GetLimit; + +public class GetLimitCommandHandler : AbstractCommandHandler +{ + private readonly TelegramBotClient _bot; + private readonly ILlmLimitsService _limitsService; + private readonly ITelegramMarkdownConverter _markdownConverter; + + public GetLimitCommandHandler( + TelegramBotClient bot, + ILlmLimitsService limitsService, + ITelegramMarkdownConverter markdownConverter) + { + ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(limitsService); + ArgumentNullException.ThrowIfNull(markdownConverter); + _bot = bot; + _limitsService = limitsService; + _markdownConverter = markdownConverter; + } + + public override async Task HandleAsync(GetLimitCommand command, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(command); + cancellationToken.ThrowIfCancellationRequested(); + var isAdmin = await IsAdminMessageAsync(command, cancellationToken); + if (isAdmin) + { + if (command.Message.ReplyToMessage?.From is not null) + { + var chatUsage = await _limitsService.GetDailyLimitsAsync( + command.Message.Chat.Id, + command.Message.ReplyToMessage.From.Id, + cancellationToken); + + var builder = new StringBuilder(); + builder.Append("У пользователя "); + if (!string.IsNullOrEmpty(command.Message.ReplyToMessage?.From.Username)) + { + builder.Append('@').Append(command.Message.ReplyToMessage?.From.Username).AppendLine(" "); + } + + if (chatUsage.IsUnlimited) + { + builder.AppendLine("нет ограничений на количество сообщений"); + } + else + { + builder.Append("установлен лимит на сообщения - ").AppendLine(chatUsage.Limit.Value.ToString(CultureInfo.InvariantCulture)); + builder.Append("Использовано: ").AppendLine(chatUsage.Used.ToString(CultureInfo.InvariantCulture)); + builder.Append("Осталось: ").AppendLine(chatUsage.Remaining.Value.ToString(CultureInfo.InvariantCulture)); + } + + var replyText = builder.ToString(); + await ReplyWithMarkdownAsync(command, replyText, cancellationToken); + } + else + { + await ReplyWithMarkdownAsync(command, "⚠️ Просмотр лимита сообщений доступен только через реплай на сообщение того человека, лимит которого необходимо узнать", cancellationToken); + } + } + else + { + await HandleNonAdminAsync(command, cancellationToken); + } + } + + private async Task ReplyWithMarkdownAsync(GetLimitCommand command, string responseText, CancellationToken cancellationToken) + { + var telegramMarkdown = _markdownConverter.ConvertToSolidTelegramMarkdown(responseText); + var response = await _bot.SendMessage( + command.Message.Chat, + telegramMarkdown, + ParseMode.MarkdownV2, + new() + { + MessageId = command.Message.MessageId + }, + cancellationToken: cancellationToken); + } + + private async Task HandleNonAdminAsync(GetLimitCommand command, CancellationToken cancellationToken) + { + var telegramMarkdown = _markdownConverter.ConvertToSolidTelegramMarkdown("❌ Только администраторы могут смотреть лимиты других участников"); + var response = await _bot.SendPhoto( + command.Message.Chat, + new InputFileStream(new MemoryStream(EmbeddedResources.NoJpg), "no.jpg"), + telegramMarkdown, + ParseMode.MarkdownV2, + new() + { + MessageId = command.Message.MessageId + }, + cancellationToken: cancellationToken); + } + + private async Task IsAdminMessageAsync(GetLimitCommand command, CancellationToken cancellationToken) + { + if (command.Message.Chat.Type is ChatType.Group or ChatType.Supergroup && command.Message.From is not null) + { + var admins = await _bot.GetChatAdministrators(command.Message.Chat, cancellationToken); + return admins.Any(x => x.User.Id == command.Message.From.Id); + } + + return true; + } +} diff --git a/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommand.cs b/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommand.cs new file mode 100644 index 0000000..f997026 --- /dev/null +++ b/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommand.cs @@ -0,0 +1,14 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using TgLlmBot.CommandDispatcher.Abstractions; + +namespace TgLlmBot.Commands.GetPersonal; + +public class GetPersonalLimitCommand : AbstractCommand +{ + public GetPersonalLimitCommand(Message message, UpdateType type, User self) : base(message, type) + { + Self = self; + } + public User Self { get; } +} diff --git a/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommandHandler.cs b/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommandHandler.cs new file mode 100644 index 0000000..f30f7d1 --- /dev/null +++ b/src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommandHandler.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Types.Enums; +using TgLlmBot.CommandDispatcher.Abstractions; +using TgLlmBot.Services.DataAccess.Limits; +using TgLlmBot.Services.DataAccess.Limits.Models; +using TgLlmBot.Services.DataAccess.TelegramMessages; +using TgLlmBot.Services.Telegram.Markdown; + +namespace TgLlmBot.Commands.GetPersonal; + +public class GetPersonalLimitCommandHandler : AbstractCommandHandler +{ + private readonly TelegramBotClient _bot; + private readonly ILlmLimitsService _limitsService; + private readonly ITelegramMarkdownConverter _markdownConverter; + + public GetPersonalLimitCommandHandler(TelegramBotClient bot, ILlmLimitsService limitsService, ITelegramMarkdownConverter markdownConverter) + { + ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(limitsService); + ArgumentNullException.ThrowIfNull(markdownConverter); + _bot = bot; + _limitsService = limitsService; + _markdownConverter = markdownConverter; + } + + public override async Task HandleAsync(GetPersonalLimitCommand command, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(command); + cancellationToken.ThrowIfCancellationRequested(); + if (command.Message.From is null) + { + return; + } + + var chatUsage = await _limitsService.GetDailyLimitsAsync( + command.Message.Chat.Id, + command.Message.From.Id, + cancellationToken); + + var response = BuildResponseTemplate(_markdownConverter, chatUsage); + await _bot.SendMessage( + command.Message.Chat, + response, + ParseMode.MarkdownV2, + new() + { + MessageId = command.Message.MessageId + }, + cancellationToken: cancellationToken); + } + + private static string BuildResponseTemplate( + ITelegramMarkdownConverter markdownConverter, + DailyChatUsageStats chatUsage) + { + if (chatUsage.IsUnlimited) + { + return "✅ Лимит сообщений отсутствует"; + } + else + { + var builder = new StringBuilder(); + builder.Append("Дневной лимит сообщений: ").AppendLine(chatUsage.Limit.Value.ToString(CultureInfo.InvariantCulture)); + builder.Append("Использовано: ").AppendLine(chatUsage.Used.ToString(CultureInfo.InvariantCulture)); + builder.Append("Осталось: ").AppendLine(chatUsage.Remaining.Value.ToString(CultureInfo.InvariantCulture)); + + var rawMarkdown = builder.ToString(); + var optimizedMarkdown = markdownConverter.ConvertToSolidTelegramMarkdown(rawMarkdown); + return optimizedMarkdown; + } + + } +} diff --git a/src/TgLlmBot/Program.cs b/src/TgLlmBot/Program.cs index 619d90f..d49d506 100644 --- a/src/TgLlmBot/Program.cs +++ b/src/TgLlmBot/Program.cs @@ -23,6 +23,8 @@ using TgLlmBot.Commands.ChatWithLlm.BackgroundServices.LlmRequests; using TgLlmBot.Commands.ChatWithLlm.Services; using TgLlmBot.Commands.DisplayHelp; +using TgLlmBot.Commands.GetLimit; +using TgLlmBot.Commands.GetPersonal; using TgLlmBot.Commands.Model; using TgLlmBot.Commands.Ping; using TgLlmBot.Commands.Rating; @@ -193,6 +195,8 @@ private static HostApplicationBuilder CreateHostApplicationBuilder( builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Channel to communicate with LLM var llmRequestChannel = Channel.CreateBounded(new BoundedChannelOptions(20) { diff --git a/src/TgLlmBot/Services/DataAccess/Limits/DefaultLlmLimitsService.cs b/src/TgLlmBot/Services/DataAccess/Limits/DefaultLlmLimitsService.cs index aa216b0..2386b35 100644 --- a/src/TgLlmBot/Services/DataAccess/Limits/DefaultLlmLimitsService.cs +++ b/src/TgLlmBot/Services/DataAccess/Limits/DefaultLlmLimitsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -8,6 +8,8 @@ using Npgsql; using TgLlmBot.DataAccess; using TgLlmBot.DataAccess.Models; +using TgLlmBot.Models; +using TgLlmBot.Services.DataAccess.Limits.Models; namespace TgLlmBot.Services.DataAccess.Limits; @@ -85,4 +87,32 @@ await dbContext.Database.ExecuteSqlRawAsync( new NpgsqlParameter($"{nameof(DbUserLimit.Limit)}", limit)); } } + + public async Task GetDailyLimitsAsync(long chatId, long userId, CancellationToken cancellationToken) + { + await using (var asyncScope = _serviceScopeFactory.CreateAsyncScope()) + { + var dbContext = asyncScope.ServiceProvider.GetRequiredService(); + var date = _timeProvider.GetUtcNow().Date.ToUniversalTime(); + var dbLimits = await dbContext.Limits.AsNoTracking() + .Where(x => x.UserId == userId && x.ChatId == chatId) + .FirstOrDefaultAsync(cancellationToken); + if (dbLimits is not null) + { + var dbDailyUsage = await dbContext.Usage.AsNoTracking() + .Where(x => x.UserId == userId && x.Date == date && x.ChatId == chatId) + .FirstOrDefaultAsync(cancellationToken); + + int used = 0; + if (dbDailyUsage is not null) + { + used = dbDailyUsage.Used; + } + var remaining = dbLimits.Limit - used; + return new DailyChatUsageStats(used, dbLimits.Limit, remaining); + } + + return new DailyChatUsageStats(0, null, null); + } + } } diff --git a/src/TgLlmBot/Services/DataAccess/Limits/ILlmLimitsService.cs b/src/TgLlmBot/Services/DataAccess/Limits/ILlmLimitsService.cs index d86272b..ab8572b 100644 --- a/src/TgLlmBot/Services/DataAccess/Limits/ILlmLimitsService.cs +++ b/src/TgLlmBot/Services/DataAccess/Limits/ILlmLimitsService.cs @@ -1,5 +1,7 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; +using TgLlmBot.Models; +using TgLlmBot.Services.DataAccess.Limits.Models; namespace TgLlmBot.Services.DataAccess.Limits; @@ -10,4 +12,6 @@ public interface ILlmLimitsService Task IsLLmInteractionAllowedAsync(long chatId, long userId, CancellationToken cancellationToken); Task SetDailyLimitsAsync(long chatId, long userId, int limit, CancellationToken cancellationToken); + + Task GetDailyLimitsAsync(long chatId, long userId, CancellationToken cancellationToken); } diff --git a/src/TgLlmBot/Services/DataAccess/Limits/Models/DailyChatUsageStats.cs b/src/TgLlmBot/Services/DataAccess/Limits/Models/DailyChatUsageStats.cs new file mode 100644 index 0000000..9d88246 --- /dev/null +++ b/src/TgLlmBot/Services/DataAccess/Limits/Models/DailyChatUsageStats.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; + +namespace TgLlmBot.Services.DataAccess.Limits.Models; + +public class DailyChatUsageStats +{ + public DailyChatUsageStats(int used, int? limit, int? remaining) + { + Used = used; + Limit = limit; + Remaining = remaining; + } + + public int Used { get; } + public int? Remaining { get; } + public int? Limit { get; } + + [MemberNotNullWhen(false, nameof(Limit), nameof(Remaining))] + public bool IsUnlimited => !Limit.HasValue; +}