Refactor project files for improved readability and structure
- 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:
@@ -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("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
private static string Escape(string text) =>
|
||||
text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user