Kalender-Plugin integrieren

Ein Kalender-Plugin zeigt Termine, die aus einem anderen System stammen (zum Beispiel einer Lernplattform, einer Aufgaben- oder Klausurverwaltung), direkt im IServ-Kalender an, ohne dass diese Termine als echte Kalendereinträge in IServ gespeichert werden. Ihr System bleibt dabei die alleinige, führende Datenquelle; IServ zeigt die Termine den betroffenen Benutzern lediglich zusätzlich zu ihren übrigen Kalendern an. Damit eignen sich Kalender-Plugins vor allem für Termine,

  • die sich aus der Fachlogik Ihres Systems ergeben (z. B. Abgabetermine, Klausuren, Ausleihfristen) und dort verwaltet werden sollen,

  • die von Benutzer zu Benutzer unterschiedlich sind, weil sie von individuellen Daten wie Kursen, Gruppen oder Ausleihen abhängen, und

  • die sich häufig ändern und zeitnah aktuell angezeigt werden sollen.

Das unterscheidet ein Kalender-Plugin von den beiden anderen Wegen, wie externe Termine im IServ-Kalender erscheinen können:

  • Ein CalDAV-Abonnement (siehe Kalender) bindet eine einzelne, für alle Benutzer identische iCalendar-Datei (.ics) über eine feste URL ein. IServ lädt diese Datei nur in regelmäßigen, eher groben Abständen herunter. Das passt für seltener wechselnde, benutzerunabhängige Daten wie Ferien oder einen schulweiten Terminplan, nicht aber für Termine, die vom jeweiligen Benutzer abhängen oder zeitnah aktuell sein müssen.

  • Eine vollständige CalDAV-Synchronisation erlaubt es Benutzern, Termine direkt im IServ-Kalender anzulegen und zu bearbeiten. Das passt, wenn IServ die führende Datenquelle für die Termine sein soll – nicht, wenn die Termine ohnehin schon in Ihrem System entstehen und dort verwaltet werden.

  • Ein Kalender-Plugin liefert die Termine dagegen individuell je Benutzer über eine eigene HTTP-Schnittstelle und kann IServ aktiv benachrichtigen, sobald sich etwas ändert (siehe Schritt 4). Die Termine bleiben dabei nur lesbar und werden ausschließlich von Ihrem System verwaltet. Ein Kalender-Plugin benötigt zudem keine Einrichtung in der Benutzeroberfläche, sondern wird automatisch an alle Benutzer ausgeliefert, für die es nicht deaktiviert wurde.

Diese Anleitung richtet sich an Entwickler, die ein eigenes Kalender-Plugin an IServ anbinden möchten. Programmierkenntnisse (HTTP-APIs, OAuth, JSON, Message Queuing) werden vorausgesetzt, Kenntnisse der IServ-internen Mechanismen dagegen nicht – alle dafür nötigen Begriffe werden erklärt.

Siehe auch

Wie sich Kalender-Plugins aus Sicht eines Benutzers im Kalender darstellen, ist unter Kalender beschrieben.

Funktionsweise im Überblick

Die Anbindung folgt einem asynchronen Hol-und-Cache-Prinzip: IServ fragt Ihre Termine ab und speichert sie zwischen, statt sie bei jedem Seitenaufruf live zu laden. Vereinfacht läuft das so ab:

  1. Sie registrieren Ihr Plugin über eine kleine JSON-Datei, das sogenannte Manifest. Darüber erfährt IServ, dass es Ihr Kalender-Plugin gibt und unter welcher URL es erreichbar ist.

  2. IServ ruft regelmäßig einen HTTP-Endpunkt bei Ihnen ab, der die Termine eines Benutzers für einen Zeitraum liefert, und speichert das Ergebnis in einem lokalen Zwischenspeicher (Cache).

  3. Ändern sich Termine in Ihrem System, benachrichtigen Sie IServ aktiv über eine Nachricht (Cache-Invalidierung), damit die betroffenen Termine neu abgerufen werden.

Die folgenden vier Schritte bauen aufeinander auf.

Schritt 1: Kalender-Plugin-Manifest registrieren

Damit IServ Ihr Kalender-Plugin überhaupt kennt, legen Sie eine JSON-Manifest-Datei auf dem IServ-Server unter /usr/share/iserv/shared/calendar/plugins/ ab, zum Beispiel als mein-plugin.json. Die Datei muss auf .json enden. Gibt es mehrere Manifeste mit derselben id, gewinnt das zuerst gefundene.

{
  "id": "mein-plugin",
  "name": {
    "en": "My Plugin",
    "de": "Mein Plugin"
  },
  "url": "https://server.example.com/api/calendar/v1/events",
  "publishable": true
}

Die Felder im Einzelnen:

  • id: eindeutiger, über Updates hinweg stabiler Bezeichner Ihres Plugins. Er taucht auch im plugin-Feld der später gelieferten Termine wieder auf (siehe Schritt 2).

  • name: Anzeigename für die Kalender-Oberfläche, als Objekt mit Sprachkürzeln (z. B. en, de) als Schlüssel.

  • url: absolute URL Ihres Termin-Endpunkts (siehe Schritt 2).

  • publishable (optional, Standard: true): legt fest, ob die Termine auch über einen öffentlichen iCal-Feed-Link abgerufen werden dürfen. Setzen Sie den Wert auf false, wenn das für Ihre Termine nicht gewünscht ist.

Manifest wirksam werden lassen

Nachdem das Manifest an Ort und Stelle liegt, liest IServ es nicht sofort automatisch ein. Auf dem betreffenden Server muss einmalig iservchk calendar ausgeführt werden (als root), damit IServ die Termine der neuen Quelle abruft. Danach hält ein regelmäßig laufender Abgleich den Cache aktuell.

Schritt 2: Termin-Endpunkt implementieren

Die url aus Ihrem Manifest muss einen HTTP-GET-Endpunkt bereitstellen, den IServ in etwa so aufruft:

GET <url>?userId=<uuid>&start=YYYY-MM-DD&end=YYYY-MM-DD
Authorization: Bearer <token>
  • userId: die UUID des Benutzers, für den Termine geliefert werden sollen.

  • start / end: der angefragte Zeitraum. IServ fragt bewusst großzügige Zeitfenster ab und cacht das Ergebnis – eine exakte Behandlung der Grenzen ist daher nicht kritisch. Liefern Sie im Zweifel lieber einen Termin zu viel als zu wenig, etwa wenn Zeitzonen die Grenze uneindeutig machen.

Der Endpunkt muss öffentlich (bzw. für IServ) erreichbar sein.

Bemerkung

Der Kalender läuft in einem Docker Container und damit dieser Ihren Endpunkt erreichen kann, ohne den regulären Webserver-Prozess-Pool zu blockieren, ist eine nginx-Proxy-Regel nötig, die direkt auf den internen Webserver-Port 982 verweist. Legen Sie dazu ein Template unter iconf/etc/nginx/server-iserv/40<modul>_plugin-calendar.templ an. Da iconf-Templates eine $-Ersetzung durchführen, müssen nginx-Variablen, die unverändert übernommen werden sollen, mit einem Backslash maskiert werden (\$host statt $host):

location ^~ /iserv/public/mein-modul/api/calendar-plugin/v1/ {
    proxy_pass http://localhost:982;
    proxy_set_header Host \$host;
    proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_set_header X-Real-IP \$remote_addr;
    proxy_read_timeout 5m;
    proxy_buffering off;
}

Das location-Muster muss zum Pfad-Präfix Ihres Endpunkts passen. Betreiben Sie Ihren Endpunkt dagegen auf einem eigenen, von IServ unabhängigen Server, entfällt dieser Schritt.

Antwortformat

Der Endpunkt muss mit HTTP 200 antworten und dabei einen von zwei Antworttypen liefern.

Aktiv – das Plugin liefert Termine (auch eine leere Liste ist gültig) für diesen Benutzer:

{
  "status": "active",
  "events": [
    {
      "id": "evt-1",
      "title": "Mathe-Hausaufgabe",
      "start": "2026-01-15T10:00:00+00:00",
      "end": "2026-01-15T11:00:00+00:00",
      "allDay": false,
      "plugin": "mein-plugin",
      "displayFields": [
        { "label": { "en": "Location", "de": "Ort" }, "text": { "en": "Room 101", "de": "Raum 101" } }
      ]
    }
  ]
}

Inaktiv – das Plugin ist für diesen Benutzer deaktiviert, bereits zwischengespeicherte Termine werden daraufhin von IServ entfernt:

{
  "status": "inactive"
}

Die Felder eines Termins:

  • id: innerhalb Ihres Plugins eindeutiger und über mehrere Abfragen hinweg stabiler Bezeichner. Er wird für die Cache-Verwaltung benötigt.

  • title: Anzeigetitel des Termins.

  • start / end: Datum/Uhrzeit in ISO-8601/RFC-3339-Format, immer mit Zeitzonen-Offset (z. B. +02:00). Bei ganztägigen Terminen ist end exklusiv zu verstehen, entspricht also dem Beginn des Folgetages (analog zu iCalendar/FullCalendar): Ein ganztägiger Termin am 1. Januar hat start: 2026-01-01T00:00:00+00:00 und end: 2026-01-02T00:00:00+00:00.

  • allDay: ob es sich um einen ganztägigen Termin handelt.

  • plugin: muss mit der id aus Ihrem Manifest übereinstimmen.

  • displayFields (optional): zusätzliche Detailinformationen, die in der Terminansicht angezeigt werden. Jeder Eintrag besteht aus label und text, beide entweder als einfacher Text oder als Objekt mit Sprachkürzeln für mehrsprachige Inhalte (analog zu name im Manifest).

Mögliche Fehlerantworten:

Status

Bedeutung

400

Anfrage fehlerhaft, z. B. fehlender userId-Parameter oder ungültiges Datumsformat (erwartet: YYYY-MM-DD).

401

Das Bearer-Token fehlt, ist abgelaufen oder ließ sich nicht verifizieren.

403

Das Token ist gültig, trägt aber nicht den erforderlichen Scope.

404

Der angefragte Benutzer ist unbekannt.

Andere Statuscodes als 200 wertet IServ als Fehler und greift währenddessen auf zuvor zwischengespeicherte Daten zurück.

Schritt 3: Bearer-Token prüfen

IServ authentifiziert sich bei Ihrem Endpunkt mit einem OAuth-2.0-Bearer-Token, das den Scope iserv:calendar:plugin-events:read trägt. Ihr Endpunkt muss dieses Token vor jeder Antwort prüfen:

  1. Extrahieren Sie das Token aus dem Authorization-Header (Bearer <token>).

  2. Ermitteln Sie den Introspection-Endpunkt des IServ-Servers über dessen OpenID-Connect-Discovery-Dokument. Dieses liegt unter https://<servername>/iserv/auth/public/.well-known/openid-configuration und enthält unter anderem das Feld introspection_endpoint. Hartcodieren Sie den Pfad nicht selbst, da er sich ändern kann.

  3. Prüfen Sie das Token per Token-Introspection (RFC 7662), indem Sie es an den ermittelten introspection_endpoint senden:

    POST <introspection_endpoint>
    
    token=<bearer-token>
    token_type_hint=access_token
    

    Die Antwort enthält unter anderem, ob das Token aktiv ist und welche Scopes es trägt:

    {
      "active": true,
      "scope": "iserv:calendar:plugin-events:read",
      "client_id": "iserv_calendar_plugin",
      "exp": 1700000000
    }
    
  4. Antworten Sie mit 401, wenn das Token fehlt oder active false ist, und mit 403, wenn der Scope iserv:calendar:plugin-events:read nicht im (durch Leerzeichen getrennten) scope-Feld enthalten ist.

Schritt 4: Cache-Invalidierung einrichten

Ändern sich Termine in Ihrem System (Anlegen, Bearbeiten, Löschen), zeigt IServ das nicht von selbst sofort an – der Kalender-Cache muss aktiv benachrichtigt werden. Dazu veröffentlichen Sie eine Nachricht auf dem RabbitMQ-Broker, den jeder IServ-Server lokal betreibt.

Verbindungsdaten für den lokalen Broker (nur relevant, wenn Ihr Code auf demselben IServ-Server läuft):

Host

localhost

Port

5672

Login

iserv-core

Passwort-Datei

/var/lib/iserv/server-rabbitmq/pwd/iserv-core.pwd

Veröffentlichen Sie die Nachricht mit folgendem Routing:

Exchange

iserv.topic (Topic-Exchange)

Routing-Key

iserv.calendar.plugin.invalidate

Nachrichtenformat (Version 1):

{
  "version": 1,
  "plugin": "mein-plugin",
  "eventIds": ["evt-1", "evt-2"],
  "audience": {
    "userIds": ["7c9e6679-7425-40de-944b-e07fc1f90ae7"],
    "groupIds": ["550e8400-e29b-41d4-a716-446655440000"],
    "roleIds": null
  },
  "reason": "update"
}

Nur version und plugin sind zwingend erforderlich, alle anderen Felder sind optional und werden mit null angenommen, wenn sie fehlen.

Wie betroffene Benutzer ermittelt werden

IServ ermittelt die betroffenen Benutzer als Vereinigung zweier unabhängiger Quellen:

  • Über ``eventIds`` (sofern angegeben): alle Benutzer, die einen der genannten Termine aktuell in ihrem Cache haben. Das erfasst auch Benutzer, die den Termin vorher sehen konnten – etwa wenn eine Gruppe aus einem Termin entfernt wurde.

  • Über ``audience`` (sofern angegeben): Mitglieder der genannten Gruppen und Rollen sowie direkt aufgeführte Benutzer-UUIDs. Das erfasst neu betroffene Benutzer – etwa wenn eine Gruppe neu zu einem Termin hinzugefügt wurde.

Weil beide Quellen vereinigt werden, deckt eine einzelne Nachricht sowohl den alten als auch den neuen Empfängerkreis ab, ohne dass Sie selbst eine Änderungsliste (Diff) führen müssen.

Was invalidiert wird

  • Ist eventIds gesetzt, werden nur diese Termine für die betroffenen Benutzer aus dem Cache entfernt.

  • Ist eventIds null, werden alle zwischengespeicherten Termine dieses Plugins für die betroffenen Benutzer entfernt.

  • Sind beide Felder (eventIds und audience) null, gilt das für alle Benutzer. Das ist die teuerste Variante und sollte nur verwendet werden, wenn z. B. ein Massenimport sämtliche Termine ersetzt und eine engere Eingrenzung nicht praktikabel ist.

Tipp

Grenzen Sie Invalidierungen möglichst eng ein. Die Angabe einer großen Gruppe (z. B. aller Schüler) löst für jedes Mitglied einen erneuten Abruf aus. Bevorzugen Sie audience.userIds gegenüber audience.groupIds, wo immer das möglich ist.

Migration von der alten Schnittstelle

Bestand für Ihr Modul bereits eine Anbindung über die ältere, mittlerweile abgekündigte interface-calendar-Schnittstelle (Klassen wie CalendarPluginInterface im Namensraum IServ\CalendarInterfaceBundle), gehen Sie beim Umstieg auf die hier beschriebene API wie folgt vor:

  1. Entfernen Sie den Service-Tag iserv.calendar.plugin aus Ihrer services.yaml. Er hat Ihr Plugin bislang bei der alten Brücke registriert, die Manifeste erzeugt und Termine stellvertretend für Sie ausgeliefert hat.

  2. Entfernen Sie Ihre alte Plugin-Klasse, die CalendarPluginInterface implementiert hat. Die Methoden getId(), getName(), isActive() und getEvents() werden jetzt durch Ihr Manifest (Schritt 1) und Ihren HTTP-Endpunkt (Schritt 2) abgedeckt.

  3. Entfernen Sie die Abhängigkeit auf das Paket, das die alte Brücke bereitgestellt hat, aus Ihrer debian/control-Datei.