Files
Wino-Mail/Wino.Services/CalendarService.cs
T

320 lines
12 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
2024-12-28 23:17:16 +01:00
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Ical.Net.CalendarComponents;
2024-12-28 23:17:16 +01:00
using Ical.Net.DataTypes;
using Itenso.TimePeriod;
2024-12-28 23:17:16 +01:00
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar;
using Wino.Services.Extensions;
2025-02-16 11:54:23 +01:00
namespace Wino.Services;
public class CalendarService : BaseDatabaseService, ICalendarService
{
2025-02-16 11:54:23 +01:00
public CalendarService(IDatabaseService databaseService) : base(databaseService)
{
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
=> Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).OrderByDescending(a => a.IsPrimary).ToListAsync();
2025-02-16 11:54:23 +01:00
public async Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
{
2025-11-14 14:28:10 +01:00
await Connection.InsertAsync(accountCalendar, typeof(AccountCalendar));
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new CalendarListAdded(accountCalendar));
}
2025-02-16 11:54:23 +01:00
public async Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
{
2025-11-14 14:28:10 +01:00
await Connection.UpdateAsync(accountCalendar, typeof(AccountCalendar));
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(accountCalendar));
}
2025-02-16 11:54:23 +01:00
public async Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
{
2025-11-15 13:29:02 +01:00
await Connection.ExecuteAsync(
"DELETE FROM CalendarItem WHERE CalendarId = ? AND AccountId = ?",
accountCalendar.Id, accountCalendar.AccountId);
await Connection.DeleteAsync<AccountCalendar>(accountCalendar.Id);
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar));
}
2025-02-16 11:54:23 +01:00
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
{
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
2025-02-16 11:54:23 +01:00
if (calendarItem == null) return;
2025-02-16 11:54:23 +01:00
List<CalendarItem> eventsToRemove = new() { calendarItem };
2025-02-16 11:54:23 +01:00
// In case of parent event, delete all child events as well.
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
{
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
eventsToRemove.AddRange(recurringEvents);
}
2025-02-16 11:54:23 +01:00
foreach (var @event in eventsToRemove)
{
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
{
await Connection.RunInTransactionAsync((conn) =>
{
2025-11-14 14:28:10 +01:00
conn.Insert(calendarItem, typeof(CalendarItem));
2025-02-16 11:54:23 +01:00
if (attendees != null)
{
2025-11-14 14:28:10 +01:00
conn.InsertAll(attendees, typeof(CalendarEventAttendee));
2025-02-16 11:54:23 +01:00
}
});
2025-01-01 17:28:29 +01:00
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new CalendarItemAdded(calendarItem));
}
2025-01-01 17:28:29 +01:00
/// <summary>
/// Retrieves calendar events for a given calendar within the specified time period.
/// This includes regular events and expanded recurring event occurrences based on RFC 5545 patterns.
/// </summary>
/// <param name="calendar">The calendar to retrieve events from.</param>
/// <param name="period">The time period to query events for.</param>
/// <returns>List of calendar items including regular events and recurring event occurrences.</returns>
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, ITimePeriod period)
2025-02-16 11:54:23 +01:00
{
// TODO: Implement caching strategy for better performance with large event sets.
// Consider using a cache keyed by calendar ID and time period.
2025-02-16 11:54:23 +01:00
var accountEvents = await Connection.Table<CalendarItem>()
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden)
.ToListAsync();
2024-12-28 23:17:16 +01:00
2025-02-16 11:54:23 +01:00
var result = new List<CalendarItem>();
foreach (var calendarItem in accountEvents)
2025-02-16 11:54:23 +01:00
{
calendarItem.AssignedCalendar = calendar;
2024-12-28 23:17:16 +01:00
// Skip exception instances - they will be handled by their parent recurring event
if (calendarItem.RecurringCalendarItemId.HasValue)
2025-02-16 11:35:43 +01:00
{
continue;
}
2025-02-16 11:35:43 +01:00
if (string.IsNullOrEmpty(calendarItem.Recurrence))
2025-02-16 11:54:23 +01:00
{
// Regular non-recurring event - simply check if it overlaps with the requested period.
if (calendarItem.Period.OverlapsWith(period))
2025-02-16 11:35:43 +01:00
{
result.Add(calendarItem);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
else
{
// Recurring event - expand occurrences within the period.
// Wino stores recurring events as a series master with RFC 5545 recurrence rules.
// Exception instances (modified or cancelled) are stored separately and linked via RecurringCalendarItemId.
var expandedOccurrences = await ExpandRecurringEventAsync(calendarItem, period);
result.AddRange(expandedOccurrences);
}
}
2025-02-16 11:54:23 +01:00
return result;
}
2025-02-16 11:54:23 +01:00
/// <summary>
/// Expands a recurring event into its occurrences within the specified period.
/// Handles exception instances (modified or cancelled occurrences) by excluding them from the expansion.
/// </summary>
/// <param name="recurringEvent">The recurring event series master.</param>
/// <param name="period">The time period to expand occurrences within.</param>
/// <returns>List of calendar items representing individual occurrences in the period.</returns>
private async Task<List<CalendarItem>> ExpandRecurringEventAsync(CalendarItem recurringEvent, ITimePeriod period)
{
var result = new List<CalendarItem>();
// Parse the RFC 5545 recurrence pattern.
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(recurringEvent.StartDate),
End = new CalDateTime(recurringEvent.EndDate),
};
var recurrenceLines = Regex.Split(recurringEvent.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
2024-12-28 23:17:16 +01:00
// Calculate all occurrences in the requested period using iCal.NET.
var occurrences = calendarEvent.GetOccurrences(period.Start, period.End);
// Retrieve exception instances (modified or cancelled occurrences).
// These are stored as separate CalendarItem records with RecurringCalendarItemId set.
var exceptionInstances = await Connection.Table<CalendarItem>()
.Where(a => a.RecurringCalendarItemId == recurringEvent.Id)
.ToListAsync()
.ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
foreach (var occurrence in occurrences)
{
// Check if this occurrence has been modified/cancelled (exception instance exists).
// Compare by checking if an exception instance overlaps with this occurrence's time window.
var occurrenceStart = occurrence.Period.StartTime.Value;
var occurrenceEnd = occurrence.Period.EndTime?.Value ?? occurrenceStart.Add(occurrence.Period.Duration);
var exceptionInstance = exceptionInstances.FirstOrDefault(a =>
a.StartDate <= occurrenceEnd && a.EndDate >= occurrenceStart);
if (exceptionInstance == null)
{
// No exception - create a virtual occurrence from the series master.
var occurrenceItem = recurringEvent.CreateRecurrence(
occurrenceStart,
occurrence.Period.Duration.TotalSeconds);
result.Add(occurrenceItem);
}
else if (!exceptionInstance.IsHidden && exceptionInstance.Period.OverlapsWith(period))
{
// Exception exists and is not hidden - include the modified version.
exceptionInstance.AssignedCalendar = recurringEvent.AssignedCalendar;
result.Add(exceptionInstance);
2024-12-28 23:17:16 +01:00
}
// If exception is hidden, skip this occurrence entirely.
2024-12-28 23:17:16 +01:00
}
2025-02-16 11:54:23 +01:00
return result;
}
2025-02-16 11:54:23 +01:00
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
2025-02-16 11:54:23 +01:00
public Task<CalendarItem> GetCalendarItemAsync(Guid id)
{
2025-11-15 13:29:02 +01:00
return Connection.FindWithQueryAsync<CalendarItem>(
"SELECT * FROM CalendarItem WHERE Id = ?",
id);
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public async Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId)
{
2025-11-15 13:29:02 +01:00
var calendarItem = await Connection.FindWithQueryAsync<CalendarItem>(
"SELECT * FROM CalendarItem WHERE CalendarId = ? AND RemoteEventId = ?",
accountCalendarId, remoteEventId);
2025-02-16 11:54:23 +01:00
// Load assigned calendar.
if (calendarItem != null)
{
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
}
2025-01-07 13:42:10 +01:00
2025-02-16 11:54:23 +01:00
return calendarItem;
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
{
2025-11-15 13:29:02 +01:00
return Connection.ExecuteAsync(
"UPDATE AccountCalendar SET SynchronizationDeltaToken = ? WHERE Id = ?",
deltaToken, calendarId);
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
2025-02-16 11:54:23 +01:00
public async Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees)
{
await Connection.RunInTransactionAsync((connection) =>
{
2025-02-16 11:54:23 +01:00
// Clear all attendees.
2025-11-15 13:29:02 +01:00
connection.Execute(
"DELETE FROM CalendarEventAttendee WHERE CalendarItemId = ?",
calendarItemId);
2025-02-16 11:54:23 +01:00
// Insert new attendees.
2025-11-14 14:28:10 +01:00
connection.InsertAll(allAttendees, typeof(CalendarEventAttendee));
2025-02-16 11:54:23 +01:00
});
2025-02-16 11:54:23 +01:00
return await Connection.Table<CalendarEventAttendee>().Where(a => a.CalendarItemId == calendarItemId).ToListAsync();
}
2025-02-16 11:54:23 +01:00
public async Task<CalendarItem> GetCalendarItemTargetAsync(CalendarItemTarget targetDetails)
{
var eventId = targetDetails.Item.Id;
2025-02-16 11:54:23 +01:00
// Get the event by Id first.
var item = await GetCalendarItemAsync(eventId).ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
bool isRecurringChild = targetDetails.Item.IsRecurringChild;
bool isRecurringParent = targetDetails.Item.IsRecurringParent;
2025-02-16 11:54:23 +01:00
if (targetDetails.TargetType == CalendarEventTargetType.Single)
{
if (isRecurringChild)
{
2025-02-16 11:54:23 +01:00
if (item == null)
{
2025-02-16 11:54:23 +01:00
// This is an occurrence of a recurring event.
// They don't exist in db.
2025-02-16 11:54:23 +01:00
return targetDetails.Item;
}
else
{
2025-02-16 11:54:23 +01:00
// Single exception occurrence of recurring event.
// Return the item.
return item;
}
}
2025-02-16 11:54:23 +01:00
else if (isRecurringParent)
{
// Parent recurring events are never listed.
Debugger.Break();
return null;
}
else
{
2025-02-16 11:54:23 +01:00
// Single event.
return item;
}
}
else
{
// Series.
2025-02-16 11:54:23 +01:00
if (isRecurringChild)
{
// Return the parent.
return await GetCalendarItemAsync(targetDetails.Item.RecurringCalendarItemId.Value).ConfigureAwait(false);
}
else if (isRecurringParent)
return item;
else
{
// NA. Single events don't have series.
Debugger.Break();
return null;
}
}
}
}