Refactor project files for improved readability and structure
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s

- Cleaned up whitespace in Directory.Build.props and Directory.Packages.props for consistency.
- Reformatted project file references in PnvPanel.Api.csproj for better clarity.
- Enhanced code readability in various endpoint files by adjusting line breaks and indentation.
- Standardized method signatures and improved formatting in ResultExtensions and multiple endpoint classes for better maintainability.
This commit is contained in:
Leonid Pershin
2026-07-14 07:24:13 +03:00
parent 9d5424bb9c
commit df137ca5a7
285 changed files with 6911 additions and 2063 deletions
@@ -23,24 +23,41 @@ namespace PnvPanel.Api.Telegram;
/// свежие scoped-сервисы (ISender, ICurrentUserSetter, ...). Бот — read-only по конфигам в MVP.
/// </summary>
public sealed class PnvBotUpdateHandler(
IServiceScopeFactory scopeFactory, IOptions<TelegramOptions> options, ILogger<PnvBotUpdateHandler> logger)
: IUpdateHandler
IServiceScopeFactory scopeFactory,
IOptions<TelegramOptions> options,
ILogger<PnvBotUpdateHandler> logger
) : IUpdateHandler
{
// Показывается везде, где боту нужен привязанный аккаунт, а его нет — явно проговариваем оба шага,
// иначе новые пользователи не понимают, что сначала нужен обычный аккаунт на сайте.
private const string NotLinkedMessage =
"Сначала зарегистрируйтесь и войдите на сайте, затем привяжите Telegram: Настройки → «Привязать Telegram».";
public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
public async Task HandleUpdateAsync(
ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken
)
{
await using var scope = scopeFactory.CreateAsyncScope();
try
{
if (update.Message is { Text: { } text } message)
await HandleMessageAsync(botClient, scope.ServiceProvider, message, text, cancellationToken);
await HandleMessageAsync(
botClient,
scope.ServiceProvider,
message,
text,
cancellationToken
);
else if (update.CallbackQuery is { } callback)
await HandleCallbackAsync(botClient, scope.ServiceProvider, callback, cancellationToken);
await HandleCallbackAsync(
botClient,
scope.ServiceProvider,
callback,
cancellationToken
);
}
catch (Exception ex)
{
@@ -48,14 +65,24 @@ public sealed class PnvBotUpdateHandler(
}
}
public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
public Task HandleErrorAsync(
ITelegramBotClient botClient,
Exception exception,
HandleErrorSource source,
CancellationToken cancellationToken
)
{
logger.LogError(exception, "Telegram bot error (source {Source})", source);
return Task.CompletedTask;
}
private async Task HandleMessageAsync(
ITelegramBotClient botClient, IServiceProvider services, Message message, string text, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
Message message,
string text,
CancellationToken cancellationToken
)
{
var chatId = message.Chat.Id;
var fromId = message.From?.Id;
@@ -67,11 +94,32 @@ public sealed class PnvBotUpdateHandler(
var payload = text.Length > 7 ? text[7..].Trim() : string.Empty;
if (payload.StartsWith("link_", StringComparison.Ordinal))
await HandleLinkAsync(botClient, services, chatId, fromId.Value, message.From?.Username, payload[5..], cancellationToken);
await HandleLinkAsync(
botClient,
services,
chatId,
fromId.Value,
message.From?.Username,
payload[5..],
cancellationToken
);
else if (payload.StartsWith("login_", StringComparison.Ordinal))
await HandleLoginPromptAsync(botClient, services, chatId, fromId.Value, payload[6..], cancellationToken);
await HandleLoginPromptAsync(
botClient,
services,
chatId,
fromId.Value,
payload[6..],
cancellationToken
);
else
await SendWelcomeAsync(botClient, services, chatId, fromId.Value, cancellationToken);
await SendWelcomeAsync(
botClient,
services,
chatId,
fromId.Value,
cancellationToken
);
return;
}
@@ -79,25 +127,57 @@ public sealed class PnvBotUpdateHandler(
switch (text)
{
case "/configs":
await HandleConfigsAsync(botClient, services, chatId, fromId.Value, cancellationToken);
await HandleConfigsAsync(
botClient,
services,
chatId,
fromId.Value,
cancellationToken
);
break;
case "/unlink":
await HandleUnlinkAsync(botClient, services, chatId, fromId.Value, cancellationToken);
await HandleUnlinkAsync(
botClient,
services,
chatId,
fromId.Value,
cancellationToken
);
break;
case "/requests":
await HandleRequestsAsync(botClient, services, chatId, fromId.Value, cancellationToken);
await HandleRequestsAsync(
botClient,
services,
chatId,
fromId.Value,
cancellationToken
);
break;
case "/help":
await SendWelcomeAsync(botClient, services, chatId, fromId.Value, cancellationToken);
await SendWelcomeAsync(
botClient,
services,
chatId,
fromId.Value,
cancellationToken
);
break;
default:
await botClient.SendMessage(chatId, "Не понимаю эту команду. /help — список команд.", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
"Не понимаю эту команду. /help — список команд.",
cancellationToken: cancellationToken
);
break;
}
}
private async Task HandleCallbackAsync(
ITelegramBotClient botClient, IServiceProvider services, CallbackQuery callback, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
CallbackQuery callback,
CancellationToken cancellationToken
)
{
var data = callback.Data;
var chatId = callback.Message?.Chat.Id;
@@ -107,7 +187,15 @@ public sealed class PnvBotUpdateHandler(
if (data == "reg:new")
{
await HandleRegisterCallbackAsync(botClient, services, chatId.Value, fromId, callback.From.Username, callback.Id, cancellationToken);
await HandleRegisterCallbackAsync(
botClient,
services,
chatId.Value,
fromId,
callback.From.Username,
callback.Id,
cancellationToken
);
return;
}
@@ -115,15 +203,28 @@ public sealed class PnvBotUpdateHandler(
{
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
"Telegram не привязан.",
cancellationToken: cancellationToken
);
return;
}
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken);
var (configsText, configsKeyboard) = await BuildConfigsMenuAsync(services, cancellationToken);
var (configsText, configsKeyboard) = await BuildConfigsMenuAsync(
services,
cancellationToken
);
if (callback.Message is not null)
await botClient.EditMessageText(chatId.Value, callback.Message.Id, configsText, replyMarkup: configsKeyboard, cancellationToken: cancellationToken);
await botClient.EditMessageText(
chatId.Value,
callback.Message.Id,
configsText,
replyMarkup: configsKeyboard,
cancellationToken: cancellationToken
);
return;
}
@@ -135,9 +236,18 @@ public sealed class PnvBotUpdateHandler(
return;
var identityService = services.GetRequiredService<IIdentityService>();
var linkedUserId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken);
var linkedUserId = await identityService.FindUserIdByTelegramUserIdAsync(
fromId,
cancellationToken
);
var (menuText, menuKeyboard) = BuildMainMenu(isLinked: linkedUserId is not null);
await botClient.EditMessageText(chatId.Value, callback.Message.Id, menuText, replyMarkup: menuKeyboard, cancellationToken: cancellationToken);
await botClient.EditMessageText(
chatId.Value,
callback.Message.Id,
menuText,
replyMarkup: menuKeyboard,
cancellationToken: cancellationToken
);
return;
}
@@ -146,12 +256,19 @@ public sealed class PnvBotUpdateHandler(
{
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
"Telegram не привязан.",
cancellationToken: cancellationToken
);
return;
}
var unlinkSender = services.GetRequiredService<ISender>();
var unlinkResult = await unlinkSender.Send(new UnlinkTelegramCommand(), cancellationToken);
var unlinkResult = await unlinkSender.Send(
new UnlinkTelegramCommand(),
cancellationToken
);
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken);
if (callback.Message is null)
@@ -160,15 +277,23 @@ public sealed class PnvBotUpdateHandler(
if (!unlinkResult.IsSuccess)
{
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, $"❌ Ошибка: {unlinkResult.Error.Message}",
replyMarkup: BackToMenuKeyboard(), cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
$"❌ Ошибка: {unlinkResult.Error.Message}",
replyMarkup: BackToMenuKeyboard(),
cancellationToken: cancellationToken
);
return;
}
var (unlinkedText, unlinkedKeyboard) = BuildMainMenu(isLinked: false);
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, "✅ Telegram отвязан от аккаунта.\n\n" + unlinkedText,
replyMarkup: unlinkedKeyboard, cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
"✅ Telegram отвязан от аккаунта.\n\n" + unlinkedText,
replyMarkup: unlinkedKeyboard,
cancellationToken: cancellationToken
);
return;
}
@@ -183,11 +308,22 @@ public sealed class PnvBotUpdateHandler(
{
case "login":
{
var result = parts[1] == "approve"
? await sender.Send(new ApproveTelegramLoginCommand(requestId, fromId), cancellationToken)
: await sender.Send(new RejectTelegramLoginCommand(requestId, fromId), cancellationToken);
var result =
parts[1] == "approve"
? await sender.Send(
new ApproveTelegramLoginCommand(requestId, fromId),
cancellationToken
)
: await sender.Send(
new RejectTelegramLoginCommand(requestId, fromId),
cancellationToken
);
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
result.IsSuccess ? "Готово" : result.Error.Message,
cancellationToken: cancellationToken
);
if (callback.Message is not null)
{
var statusText = result.IsSuccess
@@ -195,7 +331,12 @@ public sealed class PnvBotUpdateHandler(
: $"⚠️ {result.Error.Message}";
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
text,
parseMode: ParseMode.Html,
cancellationToken: cancellationToken
);
}
break;
@@ -204,26 +345,50 @@ public sealed class PnvBotUpdateHandler(
{
if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.AnswerCallbackQuery(callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
"Недостаточно прав.",
cancellationToken: cancellationToken
);
return;
}
var result = parts[1] == "approve"
? await sender.Send(new ApproveActivationCommand(requestId), cancellationToken)
: await sender.Send(new RejectActivationCommand(requestId, Reason: null), cancellationToken);
var result =
parts[1] == "approve"
? await sender.Send(
new ApproveActivationCommand(requestId),
cancellationToken
)
: await sender.Send(
new RejectActivationCommand(requestId, Reason: null),
cancellationToken
);
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
result.IsSuccess ? "Готово" : result.Error.Message,
cancellationToken: cancellationToken
);
if (callback.Message is not null)
{
// Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки
// «Активировать/Отклонить» остаются висеть под уже обработанным запросом (в т.ч.
// если его обработали в другом месте — на сайте или из другого чата).
var statusText = result.IsSuccess
? (parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён.")
? (
parts[1] == "approve"
? "✅ Пользователь активирован."
: "❌ Запрос отклонён."
)
: $"⚠️ {result.Error.Message}";
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
text,
parseMode: ParseMode.Html,
cancellationToken: cancellationToken
);
}
break;
@@ -232,23 +397,47 @@ public sealed class PnvBotUpdateHandler(
{
if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.AnswerCallbackQuery(callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
"Недостаточно прав.",
cancellationToken: cancellationToken
);
return;
}
var result = parts[1] == "approve"
? await sender.Send(new ApproveRoleRequestCommand(requestId), cancellationToken)
: await sender.Send(new RejectRoleRequestCommand(requestId, Reason: null), cancellationToken);
var result =
parts[1] == "approve"
? await sender.Send(
new ApproveRoleRequestCommand(requestId),
cancellationToken
)
: await sender.Send(
new RejectRoleRequestCommand(requestId, Reason: null),
cancellationToken
);
await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
result.IsSuccess ? "Готово" : result.Error.Message,
cancellationToken: cancellationToken
);
if (callback.Message is not null)
{
var statusText = result.IsSuccess
? (parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена.")
? (
parts[1] == "approve"
? "✅ Заявка одобрена, роль выдана."
: "❌ Заявка отклонена."
)
: $"⚠️ {result.Error.Message}";
var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}";
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
text,
parseMode: ParseMode.Html,
cancellationToken: cancellationToken
);
}
break;
@@ -257,16 +446,30 @@ public sealed class PnvBotUpdateHandler(
{
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken);
await botClient.AnswerCallbackQuery(
callback.Id,
"Telegram не привязан.",
cancellationToken: cancellationToken
);
return;
}
var linkResult = await sender.Send(new GetConfigLinkQuery(requestId), cancellationToken);
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken);
var linkResult = await sender.Send(
new GetConfigLinkQuery(requestId),
cancellationToken
);
await botClient.AnswerCallbackQuery(
callback.Id,
cancellationToken: cancellationToken
);
if (!linkResult.IsSuccess)
{
await botClient.SendMessage(chatId.Value, $"Не удалось получить ссылку: {linkResult.Error.Message}", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId.Value,
$"Не удалось получить ссылку: {linkResult.Error.Message}",
cancellationToken: cancellationToken
);
return;
}
@@ -276,15 +479,22 @@ public sealed class PnvBotUpdateHandler(
// Редактируем то же сообщение (не плодим отдельное с сырым URL) — ссылка моноширинным
// блоком, по нему в Telegram можно тапнуть и скопировать целиком одним движением.
// Убираем именно эту кнопку из клавиатуры — остальные конфиги и «В меню» остаются на месте.
var text = $"{Escape(callback.Message.Text ?? "")}\n\n<code>{Escape(linkResult.Value.ConnectionString)}</code>";
var text =
$"{Escape(callback.Message.Text ?? "")}\n\n<code>{Escape(linkResult.Value.ConnectionString)}</code>";
var remainingRows = (callback.Message.ReplyMarkup?.InlineKeyboard ?? [])
.Where(row => row.All(b => b.CallbackData != data))
.ToArray();
await botClient.EditMessageText(
chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html,
replyMarkup: remainingRows.Length > 0 ? new InlineKeyboardMarkup(remainingRows) : null,
cancellationToken: cancellationToken);
chatId.Value,
callback.Message.Id,
text,
parseMode: ParseMode.Html,
replyMarkup: remainingRows.Length > 0
? new InlineKeyboardMarkup(remainingRows)
: null,
cancellationToken: cancellationToken
);
break;
}
@@ -292,11 +502,20 @@ public sealed class PnvBotUpdateHandler(
}
private async Task HandleLinkAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string token,
CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
string? username,
string token,
CancellationToken cancellationToken
)
{
var sender = services.GetRequiredService<ISender>();
var result = await sender.Send(new LinkTelegramCommand(token, fromId, username), cancellationToken);
var result = await sender.Send(
new LinkTelegramCommand(token, fromId, username),
cancellationToken
);
var text = result.IsSuccess
? "✅ Telegram успешно привязан к вашему аккаунту."
@@ -306,56 +525,103 @@ public sealed class PnvBotUpdateHandler(
}
private async Task HandleLoginPromptAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string requestIdRaw, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
string requestIdRaw,
CancellationToken cancellationToken
)
{
if (!Guid.TryParse(requestIdRaw, out var requestId))
{
await botClient.SendMessage(chatId, "Некорректная ссылка входа.", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
"Некорректная ссылка входа.",
cancellationToken: cancellationToken
);
return;
}
var identityService = services.GetRequiredService<IIdentityService>();
var userId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken);
var userId = await identityService.FindUserIdByTelegramUserIdAsync(
fromId,
cancellationToken
);
if (userId is null)
{
await botClient.SendMessage(
chatId, NotLinkedMessage,
replyMarkup: new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }),
cancellationToken: cancellationToken);
chatId,
NotLinkedMessage,
replyMarkup: new InlineKeyboardMarkup(
new[]
{
InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"),
}
),
cancellationToken: cancellationToken
);
return;
}
var keyboard = new InlineKeyboardMarkup(new[]
{
InlineKeyboardButton.WithCallbackData("✅ Подтвердить вход", $"login:approve:{requestId}"),
InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"),
});
var keyboard = new InlineKeyboardMarkup(
new[]
{
InlineKeyboardButton.WithCallbackData(
"✅ Подтвердить вход",
$"login:approve:{requestId}"
),
InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"),
}
);
await botClient.SendMessage(
chatId, "Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?",
replyMarkup: keyboard, cancellationToken: cancellationToken);
chatId,
"Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?",
replyMarkup: keyboard,
cancellationToken: cancellationToken
);
}
private async Task HandleConfigsAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
CancellationToken cancellationToken
)
{
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.SendMessage(
chatId, NotLinkedMessage,
replyMarkup: new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }),
cancellationToken: cancellationToken);
chatId,
NotLinkedMessage,
replyMarkup: new InlineKeyboardMarkup(
new[]
{
InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"),
}
),
cancellationToken: cancellationToken
);
return;
}
var (text, keyboard) = await BuildConfigsMenuAsync(services, cancellationToken);
await botClient.SendMessage(chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
text,
replyMarkup: keyboard,
cancellationToken: cancellationToken
);
}
/// <summary>Список конфигов одним сообщением: строка на конфиг + кнопка «🔗 {Label}» на каждый
/// не отозванный, плюс «🔙 В меню» внизу.</summary>
private static async Task<(string Text, InlineKeyboardMarkup Keyboard)> BuildConfigsMenuAsync(
IServiceProvider services, CancellationToken cancellationToken)
IServiceProvider services,
CancellationToken cancellationToken
)
{
var sender = services.GetRequiredService<ISender>();
var result = await sender.Send(new GetMyConfigsQuery(), cancellationToken);
@@ -363,29 +629,53 @@ public sealed class PnvBotUpdateHandler(
if (!result.IsSuccess || result.Value.Configs.Count == 0)
return ("У вас пока нет конфигов.", BackToMenuKeyboard());
var text = "Ваши конфиги:\n" + string.Join('\n', result.Value.Configs.Select(c =>
$"• {c.Label ?? c.Location} ({c.Protocol}) — {c.Status}"));
var text =
"Ваши конфиги:\n"
+ string.Join(
'\n',
result.Value.Configs.Select(c =>
$"• {c.Label ?? c.Location} ({c.Protocol}) — {c.Status}"
)
);
// Отозванному конфигу нечего показывать — кнопку не даём.
var rows = result.Value.Configs
.Where(c => c.Status != ConfigStatus.Revoked)
.Select(c => new[] { InlineKeyboardButton.WithCallbackData($"🔗 {c.Label ?? c.Location}", $"cfg:link:{c.Id}") })
var rows = result
.Value.Configs.Where(c => c.Status != ConfigStatus.Revoked)
.Select(c =>
new[]
{
InlineKeyboardButton.WithCallbackData(
$"🔗 {c.Label ?? c.Location}",
$"cfg:link:{c.Id}"
),
}
)
.Append(BackToMenuRow())
.ToArray();
return (text, new InlineKeyboardMarkup(rows));
}
private static InlineKeyboardButton[] BackToMenuRow() => new[] { InlineKeyboardButton.WithCallbackData("🔙 В меню", "menu:back") };
private static InlineKeyboardButton[] BackToMenuRow() =>
new[] { InlineKeyboardButton.WithCallbackData("🔙 В меню", "menu:back") };
private static InlineKeyboardMarkup BackToMenuKeyboard() => new(new[] { BackToMenuRow() });
private async Task HandleUnlinkAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
CancellationToken cancellationToken
)
{
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.SendMessage(chatId, "Telegram не привязан.", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
"Telegram не привязан.",
cancellationToken: cancellationToken
);
return;
}
@@ -393,105 +683,191 @@ public sealed class PnvBotUpdateHandler(
var result = await sender.Send(new UnlinkTelegramCommand(), cancellationToken);
await botClient.SendMessage(
chatId, result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}",
cancellationToken: cancellationToken);
chatId,
result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}",
cancellationToken: cancellationToken
);
}
private async Task HandleRequestsAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
CancellationToken cancellationToken
)
{
if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken))
{
await botClient.SendMessage(chatId, "Недостаточно прав.", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
"Недостаточно прав.",
cancellationToken: cancellationToken
);
return;
}
var sender = services.GetRequiredService<ISender>();
var result = await sender.Send(new ListActivationRequestsQuery(ActivationStatus.Pending, 1, 10), cancellationToken);
var result = await sender.Send(
new ListActivationRequestsQuery(ActivationStatus.Pending, 1, 10),
cancellationToken
);
if (!result.IsSuccess || result.Value.Items.Count == 0)
{
await botClient.SendMessage(chatId, "Нет ожидающих запросов на активацию.", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
"Нет ожидающих запросов на активацию.",
cancellationToken: cancellationToken
);
return;
}
foreach (var item in result.Value.Items)
{
var text = $"Запрос от <b>{item.UserName}</b>" + (string.IsNullOrWhiteSpace(item.Comment) ? "" : $"\n{item.Comment}");
var keyboard = new InlineKeyboardMarkup(new[]
{
InlineKeyboardButton.WithCallbackData("✅ Активировать", $"act:approve:{item.Id}"),
InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{item.Id}"),
});
await botClient.SendMessage(chatId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken);
var text =
$"Запрос от <b>{item.UserName}</b>"
+ (string.IsNullOrWhiteSpace(item.Comment) ? "" : $"\n{item.Comment}");
var keyboard = new InlineKeyboardMarkup(
new[]
{
InlineKeyboardButton.WithCallbackData(
"✅ Активировать",
$"act:approve:{item.Id}"
),
InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{item.Id}"),
}
);
await botClient.SendMessage(
chatId,
text,
parseMode: ParseMode.Html,
replyMarkup: keyboard,
cancellationToken: cancellationToken
);
}
}
private async Task SendWelcomeAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
CancellationToken cancellationToken
)
{
var identityService = services.GetRequiredService<IIdentityService>();
var userId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken);
var userId = await identityService.FindUserIdByTelegramUserIdAsync(
fromId,
cancellationToken
);
var (text, keyboard) = BuildMainMenu(isLinked: userId is not null);
await botClient.SendMessage(chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
text,
replyMarkup: keyboard,
cancellationToken: cancellationToken
);
}
/// <summary>Привязанному аккаунту — кнопки-действия вместо текстовых команд; непривязанному —
/// только регистрация (остальное ему всё равно недоступно). Кнопка на сайт — если задан PublicSiteUrl.</summary>
private (string Text, InlineKeyboardMarkup Keyboard) BuildMainMenu(bool isLinked)
{
const string text = "Привет! Это бот PnvPanel.\n\n"
const string text =
"Привет! Это бот PnvPanel.\n\n"
+ "Вход без пароля запускается кнопкой «Войти через Telegram» на сайте — бот пришлёт запрос на подтверждение.";
var rows = new List<InlineKeyboardButton[]>();
if (isLinked)
{
rows.Add(new[] { InlineKeyboardButton.WithCallbackData("📋 Мои конфиги", "menu:configs") });
rows.Add(new[] { InlineKeyboardButton.WithCallbackData("🔓 Отвязать Telegram", "menu:unlink") });
rows.Add(
new[] { InlineKeyboardButton.WithCallbackData("📋 Мои конфиги", "menu:configs") }
);
rows.Add(
new[]
{
InlineKeyboardButton.WithCallbackData("🔓 Отвязать Telegram", "menu:unlink"),
}
);
}
else
{
rows.Add(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") });
rows.Add(
new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }
);
}
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
rows.Add(new[] { InlineKeyboardButton.WithUrl("🌐 Сайт панели", options.Value.PublicSiteUrl) });
rows.Add(
new[]
{
InlineKeyboardButton.WithUrl("🌐 Сайт панели", options.Value.PublicSiteUrl),
}
);
return (text, new InlineKeyboardMarkup(rows));
}
private static async Task HandleRegisterCallbackAsync(
ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string callbackId,
CancellationToken cancellationToken)
ITelegramBotClient botClient,
IServiceProvider services,
long chatId,
long fromId,
string? username,
string callbackId,
CancellationToken cancellationToken
)
{
var sender = services.GetRequiredService<ISender>();
var result = await sender.Send(new RegisterViaTelegramCommand(fromId, username), cancellationToken);
var result = await sender.Send(
new RegisterViaTelegramCommand(fromId, username),
cancellationToken
);
await botClient.AnswerCallbackQuery(callbackId, cancellationToken: cancellationToken);
if (!result.IsSuccess)
{
await botClient.SendMessage(chatId, $"Не удалось зарегистрироваться: {result.Error.Message}", cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
$"Не удалось зарегистрироваться: {result.Error.Message}",
cancellationToken: cancellationToken
);
return;
}
var text = "✅ Аккаунт создан.\n\n"
var text =
"✅ Аккаунт создан.\n\n"
+ $"Логин: <code>{result.Value.UserName}</code>\n"
+ $"Пароль: <code>{result.Value.Password}</code>\n\n"
+ "Сохраните пароль — он присылается только один раз. Логин можно сменить в Настройках на сайте.\n\n"
+ "Дальше нужно дождаться активации администратором — после неё будут доступны конфиги. "
+ "Входить можно как по паролю, так и кнопкой «Войти через Telegram».";
await botClient.SendMessage(chatId, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken);
await botClient.SendMessage(
chatId,
text,
parseMode: ParseMode.Html,
cancellationToken: cancellationToken
);
}
private static string Escape(string text) => text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
private static string Escape(string text) =>
text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
private static async Task<bool> TrySetCurrentUserAsync(IServiceProvider services, long telegramUserId, CancellationToken cancellationToken)
private static async Task<bool> TrySetCurrentUserAsync(
IServiceProvider services,
long telegramUserId,
CancellationToken cancellationToken
)
{
var identityService = services.GetRequiredService<IIdentityService>();
var userId = await identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, cancellationToken);
var userId = await identityService.FindUserIdByTelegramUserIdAsync(
telegramUserId,
cancellationToken
);
if (userId is null)
return false;
@@ -503,7 +879,11 @@ public sealed class PnvBotUpdateHandler(
return true;
}
private async Task<bool> TrySetAdminCurrentUserAsync(IServiceProvider services, long telegramUserId, CancellationToken cancellationToken)
private async Task<bool> TrySetAdminCurrentUserAsync(
IServiceProvider services,
long telegramUserId,
CancellationToken cancellationToken
)
{
if (!options.Value.ParseAdminTelegramUserIds().Contains(telegramUserId))
return false;