From: Bernhard Tittelbach Date: Thu, 29 Oct 2026 01:17:56 +0000 (+0100) Subject: wip: deploy lauti-to-mobilizon on lauti.realraum.at X-Git-Url: https://git.realraum.at/?a=commitdiff_plain;h=13d7e01556b29838c2fb7c1dbc52ccaed4eedf49;p=noc.git wip: deploy lauti-to-mobilizon on lauti.realraum.at --- diff --git a/nixos/lauti.http.realraum.at/etc/nixos/configuration.nix b/nixos/lauti.http.realraum.at/etc/nixos/configuration.nix index f8b2e01..30c1d9c 100644 --- a/nixos/lauti.http.realraum.at/etc/nixos/configuration.nix +++ b/nixos/lauti.http.realraum.at/etc/nixos/configuration.nix @@ -1,5 +1,142 @@ { config, lib, pkgs, ... }: +let + mobilizonCalendarMirror = { + user = "calendarbot"; + group = "calendarbot"; + home = "/home/calendarbot"; + repoDir = "/home/calendarbot/lauti-to-mobilizon"; + repository = "https://github.com/realraum/grical-to-mobilizon.git"; + branch = "lauti-to-mobilizon"; + fallbackBranch = "master"; + }; + + nodejs = pkgs.nodejs_22 or pkgs.nodejs; + pnpm = pkgs.pnpm_10 or pkgs.pnpm; + + mobilizonCalendarMirrorConfig = { + usernames = [ "realraum" ]; + defaultPicture = "5d5efceb-8c08-436b-a906-8172c5008366"; + addressID = "91"; + mobilizon = "https://events.graz.social/api"; + lauti = "https://r3.at/public_events.ics"; + lautiHosts = [ "laut.realraum.at" ]; + maxFutureDuration = { + months = 4; + }; + _predefinedHint = "Can contain: { organizer, summary, picture: { mediaUuid }, title, tags (added to lauti tags) }"; + predefined = [ + [ + "Mitgliedertreffen" + { + title = "realraum Mitgliedertreffen"; + summary = "

Worum geht’s?

Der realraum ist ein Treffpunkt zum Basteln, Experimentieren und vielem mehr – ein Hackerspace für die Bereiche Software, Hardware, Computer und Elektronik, Molekularbiologie und Chemie. Lies mehr über uns

"; + } + ] + [ + "Funkfeuer" + { + organizer = "funkfeuer"; + summary = "

Funkfeuer Graz ist ein freies, experimentelles Funk-Netzwerk in Graz. Funkfeuer ist offen für Jeden und Jede, der/die Interesse hat und bereit ist mitzuarbeiten. Funkfeuer ist ein nicht reguliertes Netzwerk, das den digitalen Graben zwischen den sozialen Schichten überbrückt und Infrastruktur und Wissen zur Verfügung zu stellen.

Funkfeuer wird von computerbegeisterten Menschen mit unterschiedlicher Motivation und Interessen betrieben. Das Projekt verfolgt keine kommerziellen Interessen.

Funkfeuer ist auch ein Soziales Experiment denn es versucht Arbeitsweisen aus der Open Source Welt in einem gesellschaftlichen Kontext zu etablieren.

Es gibt keine Zentrale Institution, wir bauen uns unser Netzwerk selber!

"; + } + ] + ]; + }; + + mobilizonCalendarMirrorConfigFile = + pkgs.writeText "mobilizon-calendar-mirror-config.json" + (builtins.toJSON mobilizonCalendarMirrorConfig); + + mobilizonCalendarMirrorUpdate = pkgs.writeShellApplication { + name = "mobilizon-calendar-mirror-update"; + runtimeInputs = with pkgs; [ + binutils + cacert + coreutils + curl.dev + gcc + git + gnumake + nodejs + openssl.dev + pkg-config + pnpm + python3 + zlib.dev + ]; + text = '' + set -euo pipefail + + repoDir=${lib.escapeShellArg mobilizonCalendarMirror.repoDir} + repository=${lib.escapeShellArg mobilizonCalendarMirror.repository} + branch=${lib.escapeShellArg mobilizonCalendarMirror.branch} + fallbackBranch=${lib.escapeShellArg mobilizonCalendarMirror.fallbackBranch} + + export HOME=${lib.escapeShellArg mobilizonCalendarMirror.home} + export SSL_CERT_FILE=${lib.escapeShellArg "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"} + export GIT_SSL_CAINFO="$SSL_CERT_FILE" + + # Build native npm modules (node-libcurl) against Nix-provided libraries. + export npm_config_nodedir=${lib.escapeShellArg "${nodejs}"} + export npm_config_build_from_source=true + export CFLAGS=${lib.escapeShellArg "-I${pkgs.curl.dev}/include"} + export CXXFLAGS=${lib.escapeShellArg "-I${pkgs.curl.dev}/include"} + export PKG_CONFIG_PATH=${lib.escapeShellArg "${pkgs.curl.dev}/lib/pkgconfig:${pkgs.openssl.dev}/lib/pkgconfig:${pkgs.zlib.dev}/lib/pkgconfig"} + export NIX_CFLAGS_COMPILE=${lib.escapeShellArg "-I${pkgs.curl.dev}/include -I${pkgs.openssl.dev}/include -I${pkgs.zlib.dev}/include"} + linkFlags=${lib.escapeShellArg "-L${pkgs.curl.out}/lib -L${pkgs.openssl.out}/lib -L${pkgs.zlib.out}/lib -Wl,-rpath,${pkgs.curl.out}/lib -Wl,-rpath,${pkgs.openssl.out}/lib -Wl,-rpath,${pkgs.zlib.out}/lib"} + export NIX_LDFLAGS="$linkFlags" + export LDFLAGS="$linkFlags" + + mkdir -p "$repoDir" + + # session.json is ignored upstream, but protect the live credentials from a + # forced checkout in case upstream ever starts tracking a file with that + # name. If it does not exist, no session file is created here. + sessionFile="$repoDir/session.json" + sessionBackup= + if [ -f "$sessionFile" ]; then + sessionBackup=$(mktemp "$repoDir/.session.json.backup.XXXXXX") + cp -p "$sessionFile" "$sessionBackup" + fi + + restoreSession() { + if [ -n "$sessionBackup" ] && [ -f "$sessionBackup" ]; then + mv -f "$sessionBackup" "$sessionFile" + fi + } + trap restoreSession EXIT + + if [ ! -d "$repoDir/.git" ]; then + git -C "$repoDir" init -q + git -C "$repoDir" remote add origin "$repository" + else + git -C "$repoDir" remote set-url origin "$repository" + fi + + if git ls-remote --exit-code --heads "$repository" "refs/heads/$branch" >/dev/null 2>&1; then + selectedBranch="$branch" + else + echo "Branch '$branch' no longer exists upstream; falling back to '$fallbackBranch'." >&2 + selectedBranch="$fallbackBranch" + fi + + git -C "$repoDir" fetch --prune --force origin "+refs/heads/*:refs/remotes/origin/*" + git -C "$repoDir" checkout -f -B "$selectedBranch" "origin/$selectedBranch" + git -C "$repoDir" reset --hard "origin/$selectedBranch" + + restoreSession + trap - EXIT + + # config.json is generated from this NixOS configuration. session.json is + # intentionally left alone: it is created once (pnpm new-session) and is + # refreshed/rewritten by the application itself. + install -m 0640 ${mobilizonCalendarMirrorConfigFile} "$repoDir/config.json" + + cd "$repoDir" + pnpm install --frozen-lockfile + ''; + }; +in { # System basics imports = [ @@ -103,6 +240,9 @@ pkgs.vimPlugins.vim-suda pkgs.tmux pkgs.vimPlugins.comment-nvim + nodejs + pnpm + pkgs.git ]; # Time zone and locale @@ -171,6 +311,125 @@ }; + # Mobilizon calendar mirror (grical-to-mobilizon). + # + # The application is checked out at runtime so the lauti-to-mobilizon branch + # can be followed and automatically falls back to master once that branch is + # deleted. session.json is expected to exist in the checkout; it is never + # created or overwritten here, because the application refreshes and rewrites + # it after every successful login. + users.groups.calendarbot = { }; + + users.users.calendarbot = { + isSystemUser = true; + group = mobilizonCalendarMirror.group; + home = mobilizonCalendarMirror.home; + homeMode = "0750"; + createHome = true; + shell = "${pkgs.shadow}/bin/nologin"; + }; + + systemd.services.mobilizon-calendar-mirror-update = { + description = "Clone/update grical-to-mobilizon and install its pnpm dependencies"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + User = mobilizonCalendarMirror.user; + Group = mobilizonCalendarMirror.group; + WorkingDirectory = mobilizonCalendarMirror.home; + ExecStart = "${mobilizonCalendarMirrorUpdate}/bin/mobilizon-calendar-mirror-update"; + + # Fetching and building native node modules needs more time than a run. + TimeoutStartSec = "20min"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = false; + ReadWritePaths = [ mobilizonCalendarMirror.home ]; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ]; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + SystemCallFilter = [ "@system-service" ]; + SystemCallArchitectures = "native"; + }; + }; + + systemd.services.mobilizon-calendar-mirror = { + description = "Mobilizon calendar mirror (grical-to-mobilizon)"; + wants = [ "network-online.target" ]; + requires = [ "mobilizon-calendar-mirror-update.service" ]; + after = [ "network-online.target" "mobilizon-calendar-mirror-update.service" ]; + path = [ nodejs pnpm pkgs.coreutils ]; + environment = { + HOME = mobilizonCalendarMirror.home; + SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + NODE_EXTRA_CA_CERTS = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + }; + serviceConfig = { + Type = "oneshot"; + User = mobilizonCalendarMirror.user; + Group = mobilizonCalendarMirror.group; + WorkingDirectory = mobilizonCalendarMirror.repoDir; + + # Best-effort report first, then the actual sync. A failing report must + # not prevent the sync from running. + ExecStart = [ + "-${pnpm}/bin/pnpm report --autoremove-deleted" + "${pnpm}/bin/pnpm start" + ]; + + # Type=oneshot: TimeoutStartSec doubles as the runtime limit. + TimeoutStartSec = "10min"; + MemoryMax = "512M"; + MemoryHigh = "384M"; + TasksMax = 256; + UMask = "0077"; + + NoNewPrivileges = true; + CapabilityBoundingSet = ""; + AmbientCapabilities = ""; + ProtectSystem = "strict"; + ProtectHome = false; + ReadWritePaths = [ mobilizonCalendarMirror.repoDir mobilizonCalendarMirror.home ]; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + RemoveIPC = true; + SystemCallFilter = [ "@system-service" ]; + SystemCallArchitectures = "native"; + }; + }; + + systemd.timers.mobilizon-calendar-mirror = { + description = "Run the Mobilizon calendar mirror every 8 hours"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 00/8:00:00"; + Persistent = true; + RandomizedDelaySec = "15min"; + Unit = "mobilizon-calendar-mirror.service"; + }; + }; + # SSH access services.openssh = { enable = true;