""" Calendar Service for generating iCalendar (.ics) data Implements RFC 5545 iCalendar format for universal calendar compatibility """ from icalendar import Calendar, Event as iCalEvent, Alarm from datetime import datetime, timedelta from zoneinfo import ZoneInfo import uuid import os class CalendarService: """Service for generating iCalendar (.ics) data compatible with all calendar apps""" def __init__(self): self.domain = os.getenv('CALENDAR_DOMAIN', 'loaf.community') self.timezone = ZoneInfo(os.getenv('CALENDAR_TIMEZONE', 'America/New_York')) def generate_event_uid(self) -> str: """ Generate unique event identifier (UUID4 hex-encoded per RFC 7986) Returns: str: Unique identifier in format {uuid}@{domain} """ return f"{uuid.uuid4().hex}@{self.domain}" def event_to_ical_event(self, event, include_reminder: bool = True): """ Convert database Event model to iCalendar Event component Args: event: Event model instance from database include_reminder: Whether to add 1-hour reminder alarm Returns: icalendar.Event: iCalendar event component """ ical_event = iCalEvent() # Required properties ical_event.add('uid', event.calendar_uid or self.generate_event_uid()) ical_event.add('dtstamp', datetime.now(self.timezone)) ical_event.add('dtstart', event.start_at) ical_event.add('dtend', event.end_at) ical_event.add('summary', event.title) # Optional properties if event.description: ical_event.add('description', event.description) if event.location: ical_event.add('location', event.location) # Metadata ical_event.add('url', f"https://{self.domain}/events/{event.id}") ical_event.add('status', 'CONFIRMED') ical_event.add('sequence', 0) # Add 1-hour reminder (VALARM component) if include_reminder: alarm = Alarm() alarm.add('action', 'DISPLAY') alarm.add('description', f"Reminder: {event.title}") alarm.add('trigger', timedelta(hours=-1)) ical_event.add_component(alarm) return ical_event def create_calendar(self, name: str, description: str = None): """ Create base calendar with metadata Args: name: Calendar name (X-WR-CALNAME) description: Optional calendar description Returns: icalendar.Calendar: Base calendar object """ cal = Calendar() cal.add('prodid', '-//LOAF Membership Platform//EN') cal.add('version', '2.0') cal.add('x-wr-calname', name) cal.add('x-wr-timezone', str(self.timezone)) if description: cal.add('x-wr-caldesc', description) cal.add('method', 'PUBLISH') cal.add('calscale', 'GREGORIAN') return cal def create_single_event_calendar(self, event) -> bytes: """ Create calendar with single event for download Args: event: Event model instance Returns: bytes: iCalendar data as bytes """ cal = self.create_calendar(event.title) ical_event = self.event_to_ical_event(event) cal.add_component(ical_event) return cal.to_ics() def create_subscription_feed(self, events: list, feed_name: str) -> bytes: """ Create calendar subscription feed with multiple events Args: events: List of Event model instances feed_name: Name for the calendar feed Returns: bytes: iCalendar data as bytes """ cal = self.create_calendar( feed_name, description="LOAF Community Events - Auto-syncing calendar feed" ) for event in events: ical_event = self.event_to_ical_event(event) cal.add_component(ical_event) return cal.to_ics()