From 2206e5472bb607b749eca591ce88dc9b73709683 Mon Sep 17 00:00:00 2001 From: "Lennart J. Kurzweg (Nx2)" Date: Mon, 27 Jan 2025 22:20:27 +0100 Subject: [PATCH] calendar public --- configuration.nix | 2 + system-modules/calendar-publish.nix | 138 +++++++++++++++++++ system-modules/nx2site/open-web-calendar.nix | 15 ++ 3 files changed, 155 insertions(+) create mode 100644 system-modules/calendar-publish.nix create mode 100644 system-modules/nx2site/open-web-calendar.nix diff --git a/configuration.nix b/configuration.nix index 3bbb61b..3c2c630 100644 --- a/configuration.nix +++ b/configuration.nix @@ -42,8 +42,10 @@ ./system-modules/nx2site.nix ./system-modules/postgres.nix ./system-modules/nx2site/proxy.nix + ./system-modules/calendar-publish.nix ./system-modules/nx2site/audiobookshelf.nix ./system-modules/nx2site/gitea.nix + ./system-modules/nx2site/open-web-calendar.nix ./system-modules/nx2site/radicale.nix # ./system-modules/nx2site/nextcloud.nix ./system-modules/nx2site/vaultwarden.nix diff --git a/system-modules/calendar-publish.nix b/system-modules/calendar-publish.nix new file mode 100644 index 0000000..c7b706f --- /dev/null +++ b/system-modules/calendar-publish.nix @@ -0,0 +1,138 @@ +{ config, pkgs, user, ... }: +{ + environment.systemPackages = with pkgs; let + radicale-root = "/var/lib/radicale"; + web-root = "/var/nginx/webroot"; + in [ + (writers.writePython3Bin "nx_cal_pub" { + libraries = with python3Packages; [ + ical + ics + requests + dateutils + ]; + flakeIgnore = [ "E302" "E305" "E226" "E501" ]; + } /*python */ '' +import pytz +import os +from ics import Calendar, Event +from ics.grammar.parse import ContentLine +from dateutil.rrule import rrulestr +from ics.event import datetime, timedelta + +def combine_ics_from_directories(directories, output_file): + """ + Combine all .ics events from a list of directories into one .ics file, supporting recurring events. + + :param directories: List of directories containing .ics files. + :param output_file: Path to the output .ics file. + """ + combined_calendar = Calendar() + + for directory in directories: + if not os.path.exists(directory): + print(f"Directory '{directory}' does not exist. Skipping.") + continue + + for filename in os.listdir(directory): + if filename.endswith(".ics"): + file_path = os.path.join(directory, filename) + try: + with open(file_path, 'r') as file: + calendar = Calendar(file.read()) + for event in calendar.events: + # Handle recurring events + rrule_line = None + for line in event.extra: + if isinstance(line, ContentLine) and line.name == "RRULE": + rrule_line = line + break + + if rrule_line: + # Convert UNTIL to UTC if DTSTART is timezone-aware + rrule_params = rrule_line.value.split(";") + rrule_dict = {} + for param in rrule_params: + key, value = param.split("=") + rrule_dict[key] = value + + if "UNTIL" in rrule_dict and event.begin.tzinfo: + until = datetime.fromisoformat(rrule_dict["UNTIL"]) + if until.tzinfo is None: # If UNTIL is naive, make it UTC + until = until.astimezone(pytz.UTC) + rrule_dict["UNTIL"] = until.astimezone(pytz.UTC).strftime("%Y%m%dT%H%M%SZ") + + # Reconstruct RRULE string + rrule_fixed = ";".join(f"{key}={value}" for key, value in rrule_dict.items()) + rrule = rrulestr(rrule_fixed, dtstart=event.begin.astimezone(pytz.timezone('CET'))) + + # Expand recurring events and filter based on the date + for occurrence in rrule: + notTooOld = occurrence.date() >= (datetime.now().astimezone(pytz.UTC) - timedelta(days=1)).date() + notTooFuturisic = occurrence.date() < (datetime.now().astimezone(pytz.UTC) + timedelta(days=60)).date() + if notTooOld and notTooFuturisic: + new_event = Event( + name="", + begin=occurrence, + end=occurrence + (event.end - event.begin), + transparent=event.transparent or True, + ) + combined_calendar.events.add(new_event) + else: + # Regular events, directly add if within date range + if event.begin.astimezone(pytz.timezone('CET')).date() >= (datetime.now().astimezone(pytz.timezone('CET')) - timedelta(days=1)).date(): + new_event = Event( + name="", + begin=event.begin, + end=event.end, + transparent=event.transparent or True, + ) + combined_calendar.events.add(new_event) + + except Exception as e: + print(f"Error reading file '{file_path}': {e}") + exit(1) + + try: + with open(output_file, 'w') as file: + file.writelines(combined_calendar.serialize_iter()) + print(f"Combined .ics file saved to '{output_file}'") + except Exception as e: + print(f"Error saving combined .ics file: {e}") + +if __name__ == "__main__": + # List of directories containing .ics files + DIRECTORIES = [ + "${radicale-root}/collections/collection-root/${user}/preservation", + "${radicale-root}/collections/collection-root/${user}/effort", + "${radicale-root}/collections/collection-root/${user}/experience", + "${radicale-root}/collections/collection-root/${user}/exposure", + "${radicale-root}/collections/collection-root/${user}/engagement", + ] + + # Path to the output .ics file + OUTPUT_FILE = "${web-root}/schedule.ics" + + combine_ics_from_directories(DIRECTORIES, OUTPUT_FILE) +'') + ]; + systemd.timers."nx_cal_publish" = { + enable = true; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "2m"; + OnUnitActiveSec = "6h"; + Unit = "nx_cal_publish.service"; + }; + }; + + systemd.services."nx_cal_publish" = { + script = '' + nx_cal_publish + ''; + serviceConfig = { + Type = "oneshot"; + User = "nx2"; + }; + }; +} diff --git a/system-modules/nx2site/open-web-calendar.nix b/system-modules/nx2site/open-web-calendar.nix new file mode 100644 index 0000000..056f663 --- /dev/null +++ b/system-modules/nx2site/open-web-calendar.nix @@ -0,0 +1,15 @@ +{ pkgs, domain, ... }: +{ + services = { + open-web-calendar = { + enable = true; + domain = "cal.${domain}"; + package = pkgs.open-web-calendar; + settings = { + # PORT = 21342; + }; + calendarSettings = { + }; + }; + }; +}