using DailyDigestWorker.Configuration; // Наша конфигурация using DailyDigestWorker.Services; // Наши сервисы using Microsoft.Extensions.Options; // Для IOptions using System.Globalization; // Для TimeSpan.ParseExact и других настроек культуры using Telegram.Bot.Types.Enums; // Для ParseMode namespace DailyDigestWorker { public class Worker : BackgroundService { private readonly ILogger _logger; // Храним список времен запуска как TimeSpan private readonly List _runTimes; private readonly IServiceProvider _serviceProvider; // Внедряем зависимости через конструктор public Worker(ILogger logger, IOptions schedulingSettings, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; _runTimes = new List(); // Проверяем наличие Value и списка перед использованием var timeStrings = schedulingSettings.Value?.RunAtTimesLocal; // Теперь может быть null if (timeStrings != null) // Обрабатываем только если список задан в конфиге { foreach (var timeStr in timeStrings) { if (TimeSpan.TryParseExact(timeStr?.Trim(), @"hh\:mm\:ss", CultureInfo.InvariantCulture, out var timeSpan)) { _runTimes.Add(timeSpan); } else { _logger.LogWarning("Не удалось распознать время '{TimeString}' в настройках Scheduling:RunAtTimesLocal. Используйте формат HH:mm:ss.", timeStr); } } _runTimes = _runTimes.Distinct().OrderBy(t => t).ToList(); } // Логика обработки пустого или отсутствующего списка остается прежней if (!_runTimes.Any()) { _logger.LogError("В конфигурации не указано ни одного корректного времени запуска! Worker не сможет автоматически запускаться."); // Важно: убедитесь, что здесь НЕТ строки, добавляющей 8 утра по умолчанию, // если вы хотите, чтобы бот НЕ работал без явной конфигурации времени. } _logger.LogInformation("Worker инициализирован. Дайджест будет запускаться ежедневно в: {RunTimes}", _runTimes.Any() ? string.Join(", ", _runTimes.Select(t => t.ToString(@"hh\:mm\:ss"))) : "НЕТ ВРЕМЕН ЗАПУСКА"); } // Основной метод, который выполняется в фоне protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker запущен в: {time}", DateTimeOffset.Now); // Проверяем наличие времен запуска перед стартом цикла if (!_runTimes.Any()) { _logger.LogError("Нет сконфигурированных времен запуска. Worker завершает работу."); return; // Останавливаем выполнение, если нет времен } while (!stoppingToken.IsCancellationRequested) { // Рассчитываем задержку до ближайшего времени запуска TimeSpan delay = CalculateDelayUntilNearestRun(_runTimes); _logger.LogInformation("Следующий запуск дайджеста запланирован через: {Delay} (в {TargetTime})", delay, DateTime.Now.Add(delay).ToString("yyyy-MM-dd HH:mm:ss")); try { // Ожидаем до следующего времени запуска, прерывая ожидание, если придет сигнал остановки // Если задержка 0 или меньше (опоздали), Task.Delay корректно обработает это и не будет ждать. await Task.Delay(delay, stoppingToken); } catch (OperationCanceledException) { _logger.LogInformation("Запрос на остановку получен во время ожидания."); break; // Выходим из цикла while } // Проверяем еще раз перед выполнением задачи, на случай если остановка пришла точно в момент срабатывания if (stoppingToken.IsCancellationRequested) break; // --- Выполнение основной задачи --- _logger.LogInformation("Запуск процесса создания дайджеста в: {time}", DateTimeOffset.Now); using (var scope = _serviceProvider.CreateScope()) { // --- Получаем ВСЕ необходимые сервисы в начале scope --- var currencyService = scope.ServiceProvider.GetRequiredService(); var cryptoService = scope.ServiceProvider.GetRequiredService(); var weatherService = scope.ServiceProvider.GetRequiredService(); var digestBuilder = scope.ServiceProvider.GetRequiredService(); var telegramSender = scope.ServiceProvider.GetRequiredService(); var newsReaderService = scope.ServiceProvider.GetRequiredService(); var summarizationService = scope.ServiceProvider.GetRequiredService(); // Константы для чтения новостей (можно вынести в конфиг) const int NewsMaxAgeHours = 24; const int NewsFetchLimit = 50; // Сколько последних сообщений проверить string? newsSummary = null; // Инициализируем переменную для саммари try { // --- 1. Сбор данных --- _logger.LogInformation("Начало сбора данных для дайджеста..."); var currencyTask = currencyService.GetCurrencyRatesAsync(stoppingToken); var cryptoTask = cryptoService.GetCryptoRatesAsync(stoppingToken); var weatherTask = weatherService.GetWeatherAsync(stoppingToken); var newsTask = newsReaderService.GetRecentNewsAsync(NewsMaxAgeHours, NewsFetchLimit, stoppingToken); await Task.WhenAll(currencyTask, cryptoTask, weatherTask, newsTask); var currencyData = await currencyTask; var cryptoData = await cryptoTask; var weatherData = await weatherTask; var rawNewsTexts = await newsTask; _logger.LogInformation("Сбор всех базовых данных завершен."); // --- 1.5 Генерация саммари новостей (если есть новости) --- if (rawNewsTexts != null && rawNewsTexts.Any()) { _logger.LogInformation("Получено {NewsCount} текстов новостей. Запуск суммаризации...", rawNewsTexts.Count); try { string combinedNewsText = string.Join("\n\n---\n\n", rawNewsTexts); newsSummary = await summarizationService.SummarizeTextAsync(combinedNewsText, stoppingToken); if (newsSummary != null) { _logger.LogInformation("Саммари новостей успешно получено."); _logger.LogDebug("Текст саммари:\n{NewsSummary}", newsSummary); } else { _logger.LogWarning("Сервис суммаризации вернул null."); } } catch (OperationCanceledException) { _logger.LogInformation("Операция суммаризации была отменена."); } catch (Exception ex) { _logger.LogError(ex, "Ошибка во время вызова сервиса суммаризации."); } } else { _logger.LogInformation("Нет текстов новостей для суммаризации."); } // --- 2. Формирование текста дайджеста --- _logger.LogInformation("Формирование текста дайджеста..."); string digestText = digestBuilder.BuildDigestText(currencyData, cryptoData, weatherData, newsSummary); _logger.LogDebug("Сформированный текст получен от DigestBuilder."); // --- 3. Отправка дайджеста --- if (!string.IsNullOrWhiteSpace(digestText)) { _logger.LogInformation("Отправка дайджеста..."); bool sent = await telegramSender.SendMessageAsync(digestText, ParseMode.Markdown, stoppingToken); if (!sent) { _logger.LogWarning("Не удалось инициировать отправку дайджеста."); } } else { _logger.LogWarning("Текст дайджеста пуст, отправка не производится."); } } catch (OperationCanceledException) { _logger.LogInformation("Запрос на остановку получен во время выполнения основной задачи."); break; // Выходим из while } catch (Exception ex) { // Ловим ошибки на уровне всего цикла задачи _logger.LogError(ex, "Произошла КРИТИЧЕСКАЯ ошибка во время выполнения задачи создания дайджеста."); try { await telegramSender.SendErrorMessageAsync($"Критическая ошибка в Worker: {ex.Message}", CancellationToken.None); } catch (Exception sendEx) { _logger.LogError(sendEx, "Не удалось отправить сообщение о КРИТИЧЕСКОЙ ошибке в Telegram."); } // Не добавляем здесь Delay, чтобы не задерживать следующий цикл после критической ошибки. // Worker перейдет к расчету следующего запуска. } } // Конец scope _logger.LogInformation("Процесс создания дайджеста завершен. Расчет следующего запуска."); // Небольшая пауза (~1 сек) после выполнения, чтобы гарантированно перейти на следующую секунду // и избежать повторного срабатывания в ту же минуту, если расчет времени даст очень маленькую задержку. try { await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); } catch (OperationCanceledException) { _logger.LogInformation("Запрос на остановку получен во время короткой паузы после выполнения."); break; } } // Конец while _logger.LogInformation("Worker останавливается."); } // Метод для расчета задержки до ближайшего времени запуска из списка private TimeSpan CalculateDelayUntilNearestRun(List runTimes) { var now = DateTime.Now; var today = now.Date; TimeSpan currentTimeOfDay = now.TimeOfDay; // Ищем ближайшее время запуска СЕГОДНЯ, которое еще не прошло (или равно текущему времени с точностью до секунды) // Добавляем небольшую дельту (например, 1 секунду) к currentTimeOfDay, чтобы не пропустить запуск, // который должен был произойти точно в текущую секунду. TimeSpan? nextRunTimeToday = runTimes .Where(t => t >= currentTimeOfDay) .Cast() .FirstOrDefault(); // Список уже отсортирован, берем первое подходящее DateTime nextRunDateTime; if (nextRunTimeToday.HasValue) { // Если нашли время сегодня, планируем на сегодня nextRunDateTime = today + nextRunTimeToday.Value; _logger.LogDebug("Ближайшее время запуска сегодня: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime); } else { // Если сегодня подходящего времени нет, берем самое раннее время из списка и планируем на ЗАВТРА // runTimes гарантированно не пустой и отсортирован (проверено в конструкторе) var earliestTimeTomorrow = runTimes.First(); nextRunDateTime = today.AddDays(1) + earliestTimeTomorrow; _logger.LogDebug("Все времена запуска сегодня прошли. Ближайшее время завтра: {RunTime:yyyy-MM-dd HH:mm:ss}", nextRunDateTime); } var delay = nextRunDateTime - now; // Если задержка отрицательная или очень маленькая (меньше ~0), возвращаем Zero. return delay < TimeSpan.Zero ? TimeSpan.Zero : delay; } public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Worker StopAsync вызван."); // Можно добавить код для корректного завершения (например, Logout для WTelegramClient) return base.StopAsync(cancellationToken); } } }