from uuid import uuid4 from icalendar.cal import Calendar, FreeBusy import icalendar import urllib.parse import requests from flask import Flask from datetime import date, datetime, timedelta, time import zoneinfo from dataclasses import dataclass from danoan.toml_dataclass import TomlDataClassIO from os import environ @dataclass class CalendarConfig(TomlDataClassIO): file: str = "" url: str = "" @dataclass class Config(TomlDataClassIO): name: str email: str timezone: str calendar: CalendarConfig def load_config(file_path): with open(file_path, "r") as fr: return Config.read(fr) app = Flask(__name__) config = load_config(environ.get("CONFIG_FILE", "config.toml")) tz = zoneinfo.ZoneInfo(config.timezone) def fetch_calendar(calendar_url): return requests.get(calendar_url).content def read_calendar_file(calendar_file): with open(calendar_file, 'rb') as f: return f.read() def get_calendar(calendar_config): if calendar_config.file != "": return read_calendar_file(calendar_config.file) else: if calendar_config.url != "": u = urllib.parse.urlsplit(calendar_config.url) if u.scheme == "webcal": u = u._replace(scheme="https") return fetch_calendar(urllib.parse.urlunsplit(u)) raise ValueError("Calendar URL not configured.") def fixup_date(dt): if type(dt) == date: return datetime.combine(dt, time.min, tzinfo=tz) elif dt.tzinfo is None: return dt.replace(tzinfo=tz) else: return dt.astimezone(tz) def looks_tentative(component): summary = str(component.get('summary')) return component.get('status') == 'TENTATIVE' or 'TBD' in summary or summary.endswith("?") @app.route(f'/{str.lower(config.name)}.ics') def index(): today = date.today() start_of_week = today - timedelta(days=today.weekday()) start_date = datetime.combine(start_of_week, time.min, tzinfo=tz) end_date = start_date + timedelta(days=30) organizer = icalendar.prop.vCalAddress(f"mailto:{config.email}") organizer.name = config.name domain = config.email.split('@')[1] output = Calendar() output.add('prodid', '-//Calendar Anonymiser//alin.ovh//') output.add('version', '2.0') # Parse with icalendar try: input = Calendar.from_ical(get_calendar(config.calendar)) ## causes parse error in go code # vtimezone = icalendar.Timezone.from_tzid(config.timezone) # output.add_component(vtimezone) busy = FreeBusy(uid=f'{uuid4()}@{domain}') busy.add('organizer', organizer) busy.add('dtstamp', datetime.now(tz=tz)) busy.add('dtstart', start_date) busy.add('dtend', end_date) for component in input.walk(): if component.name == "VEVENT": dtstart = fixup_date(component.get('dtstart').dt) dtend = fixup_date(component.get('dtend', component.get('dtstart')).dt) if dtstart >= start_date and dtend <= end_date: vp = icalendar.prop.vPeriod([ dtstart, dtend ]) if looks_tentative(component): vp.FBTYPE = icalendar.enums.FBTYPE.BUSY_TENTATIVE else: vp.FBTYPE = icalendar.enums.FBTYPE.BUSY_UNAVAILABLE busy.add('freebusy', vp) output.add_component(busy) return output.to_ical(), { "Content-Type": "text/calendar" } except Exception as e: print("error:", e) return f"Error parsing with icalendar: {str(e)}", 500 if __name__ == '__main__': app.run(debug=True)