Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/TgLlmBot/CommandDispatcher/DefaultTelegramCommandDispatcher.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot.Types;
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;
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/TgLlmBot/Commands/GetLimit/GetLimitCommand.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
122 changes: 122 additions & 0 deletions src/TgLlmBot/Commands/GetLimit/GetLimitCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -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<GetLimitCommand>
{
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<bool> 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;
}
}
14 changes: 14 additions & 0 deletions src/TgLlmBot/Commands/GetPersonalLimit/GetPersonalLimitCommand.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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<GetPersonalLimitCommand>
{
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;
}

}
}
4 changes: 4 additions & 0 deletions src/TgLlmBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -193,6 +195,8 @@ private static HostApplicationBuilder CreateHostApplicationBuilder(
builder.Services.AddSingleton<ShowPersonalSystemPromptCommandHandler>();
builder.Services.AddSingleton<ShowChatSystemPromptCommandHandler>();
builder.Services.AddSingleton<SetLimitCommandHandler>();
builder.Services.AddSingleton<GetLimitCommandHandler>();
builder.Services.AddSingleton<GetPersonalLimitCommandHandler>();
// Channel to communicate with LLM
var llmRequestChannel = Channel.CreateBounded<ChatWithLlmCommand>(new BoundedChannelOptions(20)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
Expand All @@ -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;

Expand Down Expand Up @@ -85,4 +87,32 @@ await dbContext.Database.ExecuteSqlRawAsync(
new NpgsqlParameter($"{nameof(DbUserLimit.Limit)}", limit));
}
}

public async Task<DailyChatUsageStats> GetDailyLimitsAsync(long chatId, long userId, CancellationToken cancellationToken)
{
await using (var asyncScope = _serviceScopeFactory.CreateAsyncScope())
{
var dbContext = asyncScope.ServiceProvider.GetRequiredService<BotDbContext>();
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);
}
}
}
Loading