- 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.
894 lines
32 KiB
C#
894 lines
32 KiB
C#
using Microsoft.Extensions.Options;
|
||
using PnvPanel.Application.Admin.Activation;
|
||
using PnvPanel.Application.Admin.Support;
|
||
using PnvPanel.Application.Common.Interfaces;
|
||
using PnvPanel.Application.Common.Messaging;
|
||
using PnvPanel.Application.Configs.GetConfigLink;
|
||
using PnvPanel.Application.Configs.GetMyConfigs;
|
||
using PnvPanel.Application.Telegram;
|
||
using PnvPanel.Application.Telegram.Bot;
|
||
using PnvPanel.Domain.Activation;
|
||
using PnvPanel.Domain.Configs;
|
||
using PnvPanel.Infrastructure.Telegram;
|
||
using Telegram.Bot;
|
||
using Telegram.Bot.Polling;
|
||
using Telegram.Bot.Types;
|
||
using Telegram.Bot.Types.Enums;
|
||
using Telegram.Bot.Types.ReplyMarkups;
|
||
|
||
namespace PnvPanel.Api.Telegram;
|
||
|
||
/// <summary>
|
||
/// Обрабатывает апдейты бота. Каждый апдейт — свой DI-scope (как HTTP-запрос), чтобы получить
|
||
/// свежие scoped-сервисы (ISender, ICurrentUserSetter, ...). Бот — read-only по конфигам в MVP.
|
||
/// </summary>
|
||
public sealed class PnvBotUpdateHandler(
|
||
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
|
||
)
|
||
{
|
||
await using var scope = scopeFactory.CreateAsyncScope();
|
||
|
||
try
|
||
{
|
||
if (update.Message is { Text: { } text } message)
|
||
await HandleMessageAsync(
|
||
botClient,
|
||
scope.ServiceProvider,
|
||
message,
|
||
text,
|
||
cancellationToken
|
||
);
|
||
else if (update.CallbackQuery is { } callback)
|
||
await HandleCallbackAsync(
|
||
botClient,
|
||
scope.ServiceProvider,
|
||
callback,
|
||
cancellationToken
|
||
);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "Failed to process Telegram update {UpdateId}", update.Id);
|
||
}
|
||
}
|
||
|
||
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
|
||
)
|
||
{
|
||
var chatId = message.Chat.Id;
|
||
var fromId = message.From?.Id;
|
||
if (fromId is null)
|
||
return;
|
||
|
||
if (text.StartsWith("/start", StringComparison.Ordinal))
|
||
{
|
||
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
|
||
);
|
||
else if (payload.StartsWith("login_", StringComparison.Ordinal))
|
||
await HandleLoginPromptAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
payload[6..],
|
||
cancellationToken
|
||
);
|
||
else
|
||
await SendWelcomeAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
cancellationToken
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
switch (text)
|
||
{
|
||
case "/configs":
|
||
await HandleConfigsAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
cancellationToken
|
||
);
|
||
break;
|
||
case "/unlink":
|
||
await HandleUnlinkAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
cancellationToken
|
||
);
|
||
break;
|
||
case "/requests":
|
||
await HandleRequestsAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
cancellationToken
|
||
);
|
||
break;
|
||
case "/help":
|
||
await SendWelcomeAsync(
|
||
botClient,
|
||
services,
|
||
chatId,
|
||
fromId.Value,
|
||
cancellationToken
|
||
);
|
||
break;
|
||
default:
|
||
await botClient.SendMessage(
|
||
chatId,
|
||
"Не понимаю эту команду. /help — список команд.",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private async Task HandleCallbackAsync(
|
||
ITelegramBotClient botClient,
|
||
IServiceProvider services,
|
||
CallbackQuery callback,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
var data = callback.Data;
|
||
var chatId = callback.Message?.Chat.Id;
|
||
var fromId = callback.From.Id;
|
||
if (data is null || chatId is null)
|
||
return;
|
||
|
||
if (data == "reg:new")
|
||
{
|
||
await HandleRegisterCallbackAsync(
|
||
botClient,
|
||
services,
|
||
chatId.Value,
|
||
fromId,
|
||
callback.From.Username,
|
||
callback.Id,
|
||
cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (data == "menu:configs")
|
||
{
|
||
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
|
||
{
|
||
await botClient.AnswerCallbackQuery(
|
||
callback.Id,
|
||
"Telegram не привязан.",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: 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
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
if (data == "menu:back")
|
||
{
|
||
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken);
|
||
if (callback.Message is null)
|
||
return;
|
||
|
||
var identityService = services.GetRequiredService<IIdentityService>();
|
||
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
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
if (data == "menu:unlink")
|
||
{
|
||
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
|
||
{
|
||
await botClient.AnswerCallbackQuery(
|
||
callback.Id,
|
||
"Telegram не привязан.",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
var unlinkSender = services.GetRequiredService<ISender>();
|
||
var unlinkResult = await unlinkSender.Send(
|
||
new UnlinkTelegramCommand(),
|
||
cancellationToken
|
||
);
|
||
await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken);
|
||
|
||
if (callback.Message is null)
|
||
return;
|
||
|
||
if (!unlinkResult.IsSuccess)
|
||
{
|
||
await botClient.EditMessageText(
|
||
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
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
var parts = data.Split(':');
|
||
if (parts.Length != 3 || !Guid.TryParse(parts[2], out var requestId))
|
||
return;
|
||
|
||
var sender = services.GetRequiredService<ISender>();
|
||
|
||
switch (parts[0])
|
||
{
|
||
case "login":
|
||
{
|
||
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
|
||
);
|
||
if (callback.Message is not null)
|
||
{
|
||
var statusText = result.IsSuccess
|
||
? (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
|
||
);
|
||
}
|
||
|
||
break;
|
||
}
|
||
case "act":
|
||
{
|
||
if (!await TrySetAdminCurrentUserAsync(services, fromId, 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
|
||
);
|
||
|
||
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"
|
||
? "✅ Пользователь активирован."
|
||
: "❌ Запрос отклонён."
|
||
)
|
||
: $"⚠️ {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
|
||
);
|
||
}
|
||
|
||
break;
|
||
}
|
||
case "rrq":
|
||
{
|
||
if (!await TrySetAdminCurrentUserAsync(services, fromId, 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
|
||
);
|
||
|
||
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"
|
||
? "✅ Заявка одобрена, роль выдана."
|
||
: "❌ Заявка отклонена."
|
||
)
|
||
: $"⚠️ {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
|
||
);
|
||
}
|
||
|
||
break;
|
||
}
|
||
case "cfg":
|
||
{
|
||
if (!await TrySetCurrentUserAsync(services, fromId, 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
|
||
);
|
||
|
||
if (!linkResult.IsSuccess)
|
||
{
|
||
await botClient.SendMessage(
|
||
chatId.Value,
|
||
$"Не удалось получить ссылку: {linkResult.Error.Message}",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (callback.Message is null)
|
||
break;
|
||
|
||
// Редактируем то же сообщение (не плодим отдельное с сырым URL) — ссылка моноширинным
|
||
// блоком, по нему в Telegram можно тапнуть и скопировать целиком одним движением.
|
||
// Убираем именно эту кнопку из клавиатуры — остальные конфиги и «В меню» остаются на месте.
|
||
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
|
||
);
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task HandleLinkAsync(
|
||
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 text = result.IsSuccess
|
||
? "✅ Telegram успешно привязан к вашему аккаунту."
|
||
: $"❌ Не удалось привязать: {result.Error.Message}";
|
||
|
||
await botClient.SendMessage(chatId, text, cancellationToken: cancellationToken);
|
||
}
|
||
|
||
private async Task HandleLoginPromptAsync(
|
||
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
|
||
);
|
||
return;
|
||
}
|
||
|
||
var identityService = services.GetRequiredService<IIdentityService>();
|
||
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
|
||
);
|
||
return;
|
||
}
|
||
|
||
var keyboard = new InlineKeyboardMarkup(
|
||
new[]
|
||
{
|
||
InlineKeyboardButton.WithCallbackData(
|
||
"✅ Подтвердить вход",
|
||
$"login:approve:{requestId}"
|
||
),
|
||
InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"),
|
||
}
|
||
);
|
||
|
||
await botClient.SendMessage(
|
||
chatId,
|
||
"Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?",
|
||
replyMarkup: keyboard,
|
||
cancellationToken: cancellationToken
|
||
);
|
||
}
|
||
|
||
private async Task HandleConfigsAsync(
|
||
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
|
||
);
|
||
return;
|
||
}
|
||
|
||
var (text, keyboard) = await BuildConfigsMenuAsync(services, 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
|
||
)
|
||
{
|
||
var sender = services.GetRequiredService<ISender>();
|
||
var result = await sender.Send(new GetMyConfigsQuery(), cancellationToken);
|
||
|
||
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 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 InlineKeyboardMarkup BackToMenuKeyboard() => new(new[] { BackToMenuRow() });
|
||
|
||
private async Task HandleUnlinkAsync(
|
||
ITelegramBotClient botClient,
|
||
IServiceProvider services,
|
||
long chatId,
|
||
long fromId,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken))
|
||
{
|
||
await botClient.SendMessage(
|
||
chatId,
|
||
"Telegram не привязан.",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
var sender = services.GetRequiredService<ISender>();
|
||
var result = await sender.Send(new UnlinkTelegramCommand(), cancellationToken);
|
||
|
||
await botClient.SendMessage(
|
||
chatId,
|
||
result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}",
|
||
cancellationToken: cancellationToken
|
||
);
|
||
}
|
||
|
||
private async Task HandleRequestsAsync(
|
||
ITelegramBotClient botClient,
|
||
IServiceProvider services,
|
||
long chatId,
|
||
long fromId,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
if (!await TrySetAdminCurrentUserAsync(services, fromId, 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
|
||
);
|
||
|
||
if (!result.IsSuccess || result.Value.Items.Count == 0)
|
||
{
|
||
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
|
||
);
|
||
}
|
||
}
|
||
|
||
private async Task SendWelcomeAsync(
|
||
ITelegramBotClient botClient,
|
||
IServiceProvider services,
|
||
long chatId,
|
||
long fromId,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
var identityService = services.GetRequiredService<IIdentityService>();
|
||
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
|
||
);
|
||
}
|
||
|
||
/// <summary>Привязанному аккаунту — кнопки-действия вместо текстовых команд; непривязанному —
|
||
/// только регистрация (остальное ему всё равно недоступно). Кнопка на сайт — если задан PublicSiteUrl.</summary>
|
||
private (string Text, InlineKeyboardMarkup Keyboard) BuildMainMenu(bool isLinked)
|
||
{
|
||
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"),
|
||
}
|
||
);
|
||
}
|
||
else
|
||
{
|
||
rows.Add(
|
||
new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }
|
||
);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(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
|
||
)
|
||
{
|
||
var sender = services.GetRequiredService<ISender>();
|
||
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
|
||
);
|
||
return;
|
||
}
|
||
|
||
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
|
||
);
|
||
}
|
||
|
||
private static string Escape(string text) =>
|
||
text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||
|
||
private static async Task<bool> TrySetCurrentUserAsync(
|
||
IServiceProvider services,
|
||
long telegramUserId,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
var identityService = services.GetRequiredService<IIdentityService>();
|
||
var userId = await identityService.FindUserIdByTelegramUserIdAsync(
|
||
telegramUserId,
|
||
cancellationToken
|
||
);
|
||
if (userId is null)
|
||
return false;
|
||
|
||
var profile = await identityService.GetProfileAsync(userId.Value, cancellationToken);
|
||
if (profile is null)
|
||
return false;
|
||
|
||
services.GetRequiredService<ICurrentUserSetter>().SetUser(profile.Id, profile.UserName);
|
||
return true;
|
||
}
|
||
|
||
private async Task<bool> TrySetAdminCurrentUserAsync(
|
||
IServiceProvider services,
|
||
long telegramUserId,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
if (!options.Value.ParseAdminTelegramUserIds().Contains(telegramUserId))
|
||
return false;
|
||
|
||
return await TrySetCurrentUserAsync(services, telegramUserId, cancellationToken);
|
||
}
|
||
}
|