diff --git a/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs b/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs new file mode 100644 index 0000000..2aced34 --- /dev/null +++ b/src/RandomAPI/APIServices/ServiceInterfaces/ICronScheduledTask.cs @@ -0,0 +1,101 @@ + +/// +/// Contract for a scheduled background task that includes its execution logic and its specific schedule. +/// +public interface ICronScheduledTask +{ + /// + /// A unique name for logging and identification. + /// + string Name { get; } + + /// + /// The schedule definition for the task. + /// Format: [DayOfWeek|Daily]@[HH:MM] + /// Examples: "Daily@08:30", "Monday@08:30", "Sunday@00:00" + /// + string Schedule { get; } + + /// + /// Executes the scheduled job logic. + /// + Task ExecuteAsync(); +} + + +/// +/// A daily scheduled task for routine maintenance (e.g., database cleanup, log rotation). +/// Runs every day at 04:00 AM UTC. +/// +public class DailyMaintenanceTask : ICronScheduledTask +{ + private readonly ILogger _logger; + + public DailyMaintenanceTask(ILogger logger) + { + _logger = logger; + } + + // --- ICronScheduledTask Implementation --- + public string Name => "Daily Maintenance & Cleanup"; + public string Schedule => "Daily@04:00"; // Run every day at 4:00 AM UTC + // ---------------------------------------- + + public async Task ExecuteAsync() + { + _logger.LogInformation("Executing Daily Maintenance Task: Running routine cleanup."); + + // --- Actual Scheduled Logic --- + await Task.Delay(100); // Simulate asynchronous database cleanup or file deletion + // Note: For a real cleanup task, you would inject and use a repository here. + // ------------------------------ + + _logger.LogInformation("Daily Maintenance Task completed successfully."); + } +} + +/// +/// Scheduled task for generating a report every Monday morning. +/// +public class BiWeeklyReportTaskMonday : ICronScheduledTask +{ + private readonly ILogger _logger; + + public BiWeeklyReportTaskMonday(ILogger logger) + { + _logger = logger; + } + + public string Name => "Bi-Weekly Report (Monday)"; + public string Schedule => "Monday@08:30"; // Run every Monday at 8:30 AM UTC + + public async Task ExecuteAsync() + { + _logger.LogInformation("Executing Monday Report Task: Compiling weekly progress report."); + await Task.Delay(100); // Simulate report generation + _logger.LogInformation("Monday Report Task completed successfully."); + } +} + +/// +/// Scheduled task for generating a report every Friday morning. +/// +public class BiWeeklyReportTaskFriday : ICronScheduledTask +{ + private readonly ILogger _logger; + + public BiWeeklyReportTaskFriday(ILogger logger) + { + _logger = logger; + } + + public string Name => "Bi-Weekly Report (Friday)"; + public string Schedule => "Friday@08:30"; // Run every Friday at 8:30 AM UTC + + public async Task ExecuteAsync() + { + _logger.LogInformation("Executing Friday Report Task: Compiling end-of-week summary report."); + await Task.Delay(100); // Simulate report generation + _logger.LogInformation("Friday Report Task completed successfully."); + } +} \ No newline at end of file diff --git a/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs b/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs new file mode 100644 index 0000000..1e80c70 --- /dev/null +++ b/src/RandomAPI/APIServices/Services/CronTaskRunnerService.cs @@ -0,0 +1,116 @@ + +/// +/// The centralized background service that monitors the clock and executes +/// all registered ICronScheduledTask instances based on their Schedule property. +/// +public class CronTaskRunnerService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + // The runner checks the schedule every 60 seconds (or less if needed). + private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(60); + + public CronTaskRunnerService( + ILogger logger, + IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + /// + /// Logic to check if a task's schedule matches the current minute. + /// Uses a custom format: [DayOfWeek|Daily]@[HH:MM] + /// + private static bool IsScheduleDue(string schedule, DateTime now) + { + // Example: "Sunday@00:00" + if (string.IsNullOrWhiteSpace(schedule) || !schedule.Contains('@')) return false; + + var parts = schedule.Split('@'); + var dayPart = parts[0].Trim(); + var timePart = parts[1].Trim(); + + // 1. Check Time (HH:MM) + // Check if the current hour and minute match the scheduled time + if (!TimeSpan.TryParseExact(timePart, "hh\\:mm", null, out TimeSpan scheduledTime)) + { + return false; + } + + // Only fire if the current UTC hour and minute match the scheduled time + if (now.Hour != scheduledTime.Hours || now.Minute != scheduledTime.Minutes) + { + return false; + } + + // 2. Check Day (Daily or Specific DayOfWeek) + if (dayPart.Equals("Daily", StringComparison.OrdinalIgnoreCase)) + { + return true; // Scheduled to run every day at this time + } + + // Check if the current DayOfWeek matches the scheduled day + if (Enum.TryParse(dayPart, true, out DayOfWeek scheduledDay)) + { + return now.DayOfWeek == scheduledDay; + } + + return false; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Cron Task Runner Service started. Checking schedule every minute."); + + while (!stoppingToken.IsCancellationRequested) + { + // Get the current time in UTC, rounded down to the nearest minute. + var now = DateTime.UtcNow.AddSeconds(-DateTime.UtcNow.Second); + _logger.LogDebug($"Checking schedules for time: {now:yyyy-MM-dd HH:mm} UTC"); + + try + { + // Must create a scope for each execution cycle + using (var scope = _serviceProvider.CreateScope()) + { + // Resolve ALL services registered under the ICronScheduledTask contract. + IEnumerable scheduledTasks = + scope.ServiceProvider.GetServices(); + + var tasksToRun = scheduledTasks + .Where(task => IsScheduleDue(task.Schedule, now)) + .ToList(); + + if (tasksToRun.Any()) + { + _logger.LogInformation($"Found {tasksToRun.Count} tasks due now. Executing concurrently."); + + var executionTasks = tasksToRun + .Select(task => task.ExecuteAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + _logger.LogError(t.Exception, $"Task '{task.Name}' failed to execute."); + } + }, TaskContinuationOptions.ExecuteSynchronously)) // Ensure logging is safe + .ToList(); + + await Task.WhenAll(executionTasks); + _logger.LogInformation("Concurrent execution completed for this cycle."); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An unhandled error occurred during the Cron execution cycle."); + } + + // Wait for the next interval check. + await Task.Delay(CheckInterval, stoppingToken); + } + + _logger.LogInformation("Cron Task Runner Service stopping."); + } +} \ No newline at end of file diff --git a/src/RandomAPI/APIServices/Services/WebhookActionService.cs b/src/RandomAPI/APIServices/Services/WebhookActionService.cs index b96268b..a394797 100644 --- a/src/RandomAPI/APIServices/Services/WebhookActionService.cs +++ b/src/RandomAPI/APIServices/Services/WebhookActionService.cs @@ -37,14 +37,13 @@ public async Task HandleRegisterActionAsync([FromBody] string url public async Task HandleUnregisterActionAsync([FromBody] string url) { - string safeUrlForLog = url; if (string.IsNullOrWhiteSpace(url)) { return new BadRequestObjectResult("URL cannot be empty."); } - safeUrlForLog = SanitizeURL(ref url); + string safeUrlForLog = SanitizeURL(ref url); var removed = await base.RemoveListenerAsync(url); if (!removed) { diff --git a/src/RandomAPI/DTOs/CalendarEventDto.cs b/src/RandomAPI/DTOs/CalendarEventDto.cs new file mode 100644 index 0000000..2f1ddb5 --- /dev/null +++ b/src/RandomAPI/DTOs/CalendarEventDto.cs @@ -0,0 +1,210 @@ +using RandomAPI.Repository; +using RandomAPI.Models; +using System.Text.Json; + +// PRODUCTION READY: +// To make this file compile with the real logic uncommented, you must install: +//Google.Apis.Calendar.v3(NuGet) +using Google.Apis.Calendar.v3; +using Google.Apis.Calendar.v3.Data; + +namespace RandomAPI.DTOs +{ + /// + /// Data Transfer Object representing a single scheduled calendar event. + /// + public class CalendarEventDTO + { + public required string Summary { get; set; } + public required DateTime StartTime { get; set; } + public required DateTime EndTime { get; set; } + public string? Location { get; set; } + public string? Description { get; set; } + } + /// + /// Contract for interacting with the Google Calendar API. + /// (Uses a mock implementation until full API integration is configured.) + /// + public interface IGoogleCalendarService + { + /// + /// Fetches all events scheduled for a specific day. + /// + /// The day for which to fetch events (e.g., Monday or Friday). + /// A list of scheduled events. + Task> GetEventsForDayAsync(DateTime date); + } + + /// + /// MOCK implementation of the Google Calendar Service. + /// NOTE: In a production environment, this is where you would integrate + /// the Google.Apis.Calendar.v3 package and use OAuth credentials. + /// + public class GoogleCalendarService : IGoogleCalendarService + { + private readonly ILogger _logger; + private readonly IEventRepository _eventRepository; + + public GoogleCalendarService(ILogger logger, IEventRepository eventRepository) + { + _logger = logger; + _eventRepository = eventRepository; + } + + public async Task> GetEventsForDayAsync(DateTime date) + { + _logger.LogWarning("Using MOCK Google Calendar Service. No real API call is being made."); + + // --- Placeholder Data Generation --- + + var mockEvents = new List(); + + if (date.DayOfWeek == DayOfWeek.Monday) + { + mockEvents.Add(new CalendarEventDTO + { + Summary = "Weekly Planning Meeting", + StartTime = date.Date.AddHours(9).AddMinutes(30), + EndTime = date.Date.AddHours(10).AddMinutes(30), + Location = "Zoom Link", + Description = "Review last week's tickets and plan sprints." + }); + mockEvents.Add(new CalendarEventDTO + { + Summary = "Client Check-in Call", + StartTime = date.Date.AddHours(14).AddMinutes(0), + EndTime = date.Date.AddHours(14).AddMinutes(45), + Location = "Google Meet", + }); + } + else if (date.DayOfWeek == DayOfWeek.Friday) + { + mockEvents.Add(new CalendarEventDTO + { + Summary = "Project Retrospective", + StartTime = date.Date.AddHours(11).AddMinutes(0), + EndTime = date.Date.AddHours(12).AddMinutes(0), + Location = "Office Lounge", + Description = "Discuss what went well and areas for improvement." + }); + } + + _logger.LogInformation($"Mock events generated for {date.ToShortDateString()}: {mockEvents.Count} events."); + + try + { + var jsonData = JsonSerializer.Serialize(mockEvents); + + var logEntry = new Models.Event( + service: "GoogleCalendarService", + type: "EventFetch", + jsonData: jsonData + ); + + _logger.LogDebug($"AUDIT LOG: Calendar events pulled. {mockEvents.Count} events logged."); + await _eventRepository.AddEventAsync(logEntry); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to log calendar fetch activity to the Event Repository."); + } + + return mockEvents; + } + } + + + /// + /// Contract for a service dedicated to external interactions with the Google Calendar API. + /// This layer is responsible for network communication and parsing raw data. + /// + public interface IGoogleCalendarExternalService + { + /// + /// Fetches calendar events directly from the Google API for a specific day. + /// + /// The day for which to fetch events. + /// A list of scheduled events (DTOs). + Task> GetRawEventsForDayAsync(DateTime date); + } + + + + /// + /// PRODUCTION implementation of the external Google Calendar client. + /// This service is solely responsible for fetching data from the real API + /// using an injected, authenticated client. + /// + public class GoogleCalendarExternalService : IGoogleCalendarExternalService + { + private readonly ILogger _logger; + + // PRODUCTION: This field holds the actual Google API client. + private readonly CalendarService _calendarApi; + + public GoogleCalendarExternalService( + ILogger logger, CalendarService calendarApi ) // PRODUCTION: Inject authenticated client + { + _logger = logger; + _calendarApi = calendarApi; + } + + public async Task> GetRawEventsForDayAsync(DateTime date) + { + _logger.LogInformation($"Attempting to fetch events from Google API for: {date.ToShortDateString()}."); + var fetchedEvents = new List(); + + try + { + // 1. Define the service request parameters + var request = _calendarApi.Events.List("primary"); // "primary" is the user's main calendar ID + + // TimeMin and TimeMax must be defined in RFC3339 format, often handled by DateTimeOffset conversion + request.TimeMin = date.Date; + request.TimeMax = date.Date.AddDays(1); // Fetch for the whole day + request.SingleEvents = true; // Required to expand recurring events into individual occurrences + request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime; + request.ShowDeleted = false; // Only get active events + + // 2. Execute the request + // Events is the Google API's data structure + Events apiEvents = await request.ExecuteAsync(); + + // 3. Map Google API Events (Event) to your DTO (CalendarEventDTO) + if (apiEvents.Items != null) + { + fetchedEvents = apiEvents.Items + .Where(item => item.Status != "cancelled") + .Select(item => new CalendarEventDTO + { + Summary = item.Summary, + Location = item.Location, + Description = item.Description, + // Note: We check for the preferred DateTimeOffset values first + StartTime = item.Start.DateTimeOffset.HasValue + ? item.Start.DateTimeOffset.Value.DateTime + : item.Start.Date is string startDateString + ? DateOnly.Parse(startDateString).ToDateTime(TimeOnly.MinValue) + : DateTime.MinValue, + EndTime = item.End.DateTimeOffset.HasValue + ? item.End.DateTimeOffset.Value.DateTime + : item.End.Date is string endDateString + ? DateOnly.Parse(endDateString).ToDateTime(TimeOnly.MinValue) + : DateTime.MinValue + }).ToList(); + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to execute Google Calendar API request."); + // Important: Throw the exception so the upstream GoogleCalendarService can handle + // the failure and prevent bad logging data. + throw; + } + + _logger.LogDebug($"External fetch completed: {fetchedEvents.Count} events returned."); + return fetchedEvents; + } + } +} diff --git a/src/RandomAPI/Program.cs b/src/RandomAPI/Program.cs index c7e3100..32753f4 100644 --- a/src/RandomAPI/Program.cs +++ b/src/RandomAPI/Program.cs @@ -34,6 +34,16 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// CRON schjeduled services +builder.Services.AddHostedService(); +// Register all tasks as Scoped +// Schedule 1: Once a day at 4:00 AM (Schedule: "Daily@04:00") +builder.Services.AddScoped(); +// Schedule 2: Monday and Friday mornings @ 8:30 AM (Schedule: "Monday@08:30" and "Friday@08:30") +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + #endregion #region Initialization diff --git a/src/RandomAPI/RandomAPI.csproj b/src/RandomAPI/RandomAPI.csproj index f587fa9..1aac139 100644 --- a/src/RandomAPI/RandomAPI.csproj +++ b/src/RandomAPI/RandomAPI.csproj @@ -10,6 +10,8 @@ + +