Files
dotfiles/system-modules/calendar-publish.nix
Lennart J. Kurzweg (Nx2) d2d1ce7830 overlays refactor
2025-06-17 19:48:31 +02:00

139 lines
5.9 KiB
Nix

{ pkgs, ... }@all: with all;
let
radicale-root = "/var/lib/radicale";
web-root = "/var/nginx/webroot";
in
{
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 = with pkgs; let
nx_cal_publish = (writers.writePython3Bin "nx_cal_publish" {
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/${hyper.user}/preservation",
"${radicale-root}/collections/collection-root/${hyper.user}/effort",
"${radicale-root}/collections/collection-root/${hyper.user}/experience",
"${radicale-root}/collections/collection-root/${hyper.user}/exposure",
"${radicale-root}/collections/collection-root/${hyper.user}/engagement",
]
# Path to the output .ics file
OUTPUT_FILE = "${web-root}/schedule.ics"
combine_ics_from_directories(DIRECTORIES, OUTPUT_FILE)
'');
in ''
${nx_cal_publish}/bin/nx_cal_publish
'';
serviceConfig = {
Type = "oneshot";
User = hyper.user;
};
};
}