subscribe
This commit is contained in:
@@ -1,43 +1,266 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PepperBot.Application.Interfaces;
|
||||
using PepperBot.Domain;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace PepperBot.Infrastructure.Telegram;
|
||||
|
||||
public class TelegramNotifier : ITelegramNotifier
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ITelegramBotClient _botClient;
|
||||
private readonly ISubscriptionRepository _subscriptionRepository;
|
||||
private readonly ILogger<TelegramNotifier> _logger;
|
||||
private readonly string _botToken;
|
||||
private readonly string _chatId;
|
||||
private readonly string _broadcastChatId;
|
||||
private bool _started;
|
||||
|
||||
public TelegramNotifier(ILogger<TelegramNotifier> logger)
|
||||
private enum ConversationState
|
||||
{
|
||||
_logger = logger;
|
||||
_botToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN") ?? string.Empty;
|
||||
_chatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID") ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_botToken) || string.IsNullOrWhiteSpace(_chatId))
|
||||
{
|
||||
_logger.LogWarning("TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID не заданы. Отправка сообщений отключена.");
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
None = 0,
|
||||
AwaitingAddKeyword = 1
|
||||
}
|
||||
|
||||
public async Task NotifyNewDealAsync(Deal deal, CancellationToken cancellationToken)
|
||||
private readonly Dictionary<string, ConversationState> _chatStates = new();
|
||||
|
||||
public TelegramNotifier(
|
||||
ITelegramBotClient botClient,
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
ILogger<TelegramNotifier> logger)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_botToken) || string.IsNullOrWhiteSpace(_chatId))
|
||||
_botClient = botClient;
|
||||
_subscriptionRepository = subscriptionRepository;
|
||||
_logger = logger;
|
||||
_broadcastChatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID") ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_broadcastChatId))
|
||||
{
|
||||
_logger.LogInformation("TELEGRAM_CHAT_ID не задан. Общая рассылка по скидкам будет отключена.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task NotifyNewDealAsync(Deal deal, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_broadcastChatId))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return SendDealAsync(deal, _broadcastChatId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task NotifyDealToChatAsync(Deal deal, string chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chatId))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return SendDealAsync(deal, chatId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = Array.Empty<UpdateType>()
|
||||
};
|
||||
|
||||
_botClient.StartReceiving(
|
||||
HandleUpdateAsync,
|
||||
HandleErrorAsync,
|
||||
receiverOptions,
|
||||
cancellationToken);
|
||||
|
||||
_started = true;
|
||||
_logger.LogInformation("Запущена обработка входящих сообщений Telegram.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
|
||||
{
|
||||
if (update.Type == UpdateType.CallbackQuery)
|
||||
{
|
||||
await HandleCallbackQueryAsync(botClient, update.CallbackQuery!, cancellationToken);
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (update.Type != UpdateType.Message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var url = $"https://api.telegram.org/bot{_botToken}/sendMessage";
|
||||
var message = update.Message;
|
||||
if (message is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Type != MessageType.Text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Нас интересуют только личные сообщения боту
|
||||
if (message.Chat.Type != ChatType.Private)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chatId = message.Chat.Id.ToString(CultureInfo.InvariantCulture);
|
||||
var text = message.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Команда для вызова меню
|
||||
if (text.Equals("/menu", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await SendMainMenuAsync(message.Chat.Id, cancellationToken);
|
||||
_chatStates[chatId] = ConversationState.None;
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка выбора пунктов меню
|
||||
if (text.Equals("Добавить подписку", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: "Отправьте одно или несколько ключевых слов для подписки (через пробел, запятую или с новой строки).",
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
_chatStates[chatId] = ConversationState.AwaitingAddKeyword;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.Equals("Удалить подписку", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ShowDeleteMenuAsync(message.Chat.Id, chatId, cancellationToken);
|
||||
_chatStates[chatId] = ConversationState.None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.Equals("Список подписок", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ShowSubscriptionsListAsync(message.Chat.Id, chatId, cancellationToken);
|
||||
_chatStates[chatId] = ConversationState.None;
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка состояний диалога
|
||||
_chatStates.TryGetValue(chatId, out var state);
|
||||
|
||||
if (state == ConversationState.AwaitingAddKeyword)
|
||||
{
|
||||
var separators = new[] { ',', ';', '\n', '\r', '\t', ' ' };
|
||||
var keywords = text
|
||||
.Split(separators, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(k => k.Trim())
|
||||
.Where(k => k.Length > 0)
|
||||
.ToList();
|
||||
|
||||
if (keywords.Count == 0)
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: "Не нашёл ни одного ключевого слова. Попробуйте ещё раз или нажмите /menu.",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
await _subscriptionRepository.AddSubscriptionAsync(chatId, keyword, cancellationToken);
|
||||
}
|
||||
|
||||
_chatStates[chatId] = ConversationState.None;
|
||||
|
||||
var confirmation = new StringBuilder();
|
||||
confirmation.AppendLine("Добавлены правила подписки по ключевым словам:");
|
||||
confirmation.AppendLine(string.Join(", ", keywords));
|
||||
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: confirmation.ToString(),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Для чата {ChatId} добавлены правила подписки по ключевым словам: {Keywords}",
|
||||
chatId,
|
||||
string.Join(", ", keywords));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Если состояние не распознано — просто напоминаем про меню
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: "Используйте команду /menu для управления подписками.",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task HandleCallbackQueryAsync(ITelegramBotClient botClient, CallbackQuery callbackQuery, CancellationToken cancellationToken)
|
||||
{
|
||||
if (callbackQuery.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (callbackQuery.Data.StartsWith("del:", StringComparison.Ordinal))
|
||||
{
|
||||
var idPart = callbackQuery.Data["del:".Length..];
|
||||
if (!long.TryParse(idPart, CultureInfo.InvariantCulture, out var id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _subscriptionRepository.DeleteSubscriptionAsync(id, cancellationToken);
|
||||
|
||||
await botClient.AnswerCallbackQuery(
|
||||
callbackQueryId: callbackQuery.Id,
|
||||
text: "Подписка удалена.",
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (callbackQuery.Message is not null)
|
||||
{
|
||||
var chatId = callbackQuery.Message.Chat.Id;
|
||||
var chatIdString = chatId.ToString(CultureInfo.InvariantCulture);
|
||||
await ShowDeleteMenuAsync(chatId, chatIdString, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
var errorMessage = exception switch
|
||||
{
|
||||
ApiRequestException apiRequestException =>
|
||||
$"Ошибка Telegram API: [{apiRequestException.ErrorCode}] {apiRequestException.Message}",
|
||||
_ => exception.ToString()
|
||||
};
|
||||
|
||||
_logger.LogError("Ошибка Telegram-бота: {Error}", errorMessage);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SendDealAsync(Deal deal, string chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
var textBuilder = new StringBuilder();
|
||||
textBuilder.AppendLine($"🔥 *{Escape(deal.Title)}*");
|
||||
textBuilder.AppendLine($"🔥 {deal.Title}");
|
||||
|
||||
if (deal.CurrentPrice is not null)
|
||||
{
|
||||
@@ -55,33 +278,102 @@ public class TelegramNotifier : ITelegramNotifier
|
||||
}
|
||||
|
||||
textBuilder.AppendLine();
|
||||
textBuilder.AppendLine($"Магазин: {Escape(deal.StoreName)}");
|
||||
textBuilder.AppendLine($"Магазин: {deal.StoreName}");
|
||||
textBuilder.AppendLine();
|
||||
textBuilder.AppendLine($"[Открыть на Pepper.ru]({Escape(deal.DealUrl)})");
|
||||
textBuilder.AppendLine($"Открыть на Pepper.ru: {deal.DealUrl}");
|
||||
|
||||
var payload = new
|
||||
try
|
||||
{
|
||||
chat_id = _chatId,
|
||||
text = textBuilder.ToString(),
|
||||
parse_mode = "Markdown"
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(url, payload, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
await _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: textBuilder.ToString(),
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
_logger.LogWarning("Не удалось отправить сообщение в Telegram: {Status} {Body}", response.StatusCode, body);
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Не удалось отправить сообщение в Telegram (чат {ChatId}): [{Code}] {Message}",
|
||||
chatId,
|
||||
ex.ErrorCode,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Escape(string value)
|
||||
private Task SendMainMenuAsync(ChatId chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
return value
|
||||
.Replace("_", "\\_")
|
||||
.Replace("*", "\\*")
|
||||
.Replace("[", "\\[")
|
||||
.Replace("`", "\\`");
|
||||
var keyboard = new ReplyKeyboardMarkup(new[]
|
||||
{
|
||||
new[] { new KeyboardButton("Добавить подписку") },
|
||||
new[] { new KeyboardButton("Удалить подписку") },
|
||||
new[] { new KeyboardButton("Список подписок") }
|
||||
})
|
||||
{
|
||||
ResizeKeyboard = true,
|
||||
OneTimeKeyboard = false
|
||||
};
|
||||
|
||||
return _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: "Меню управления подписками:",
|
||||
replyMarkup: keyboard,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ShowSubscriptionsListAsync(ChatId chatId, string chatIdString, CancellationToken cancellationToken)
|
||||
{
|
||||
var subs = await _subscriptionRepository.GetByChatAsync(chatIdString, cancellationToken);
|
||||
|
||||
if (subs.Count == 0)
|
||||
{
|
||||
await _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: "У вас пока нет подписок.",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Ваши подписки:");
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
sb.AppendLine($"• [{sub.Id}] {sub.Keywords}");
|
||||
}
|
||||
|
||||
await _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: sb.ToString(),
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ShowDeleteMenuAsync(ChatId chatId, string chatIdString, CancellationToken cancellationToken)
|
||||
{
|
||||
var subs = await _subscriptionRepository.GetByChatAsync(chatIdString, cancellationToken);
|
||||
|
||||
if (subs.Count == 0)
|
||||
{
|
||||
await _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: "Подписок для удаления не найдено.",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var buttons = subs
|
||||
.Select(s => InlineKeyboardButton.WithCallbackData(
|
||||
text: s.Keywords,
|
||||
callbackData: $"del:{s.Id}"))
|
||||
.Chunk(2)
|
||||
.Select(chunk => chunk.ToArray())
|
||||
.ToArray();
|
||||
|
||||
var keyboard = new InlineKeyboardMarkup(buttons);
|
||||
|
||||
await _botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: "Выберите подписку для удаления:",
|
||||
replyMarkup: keyboard,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user