Ich habe ein kleines Dienstprogramm erstellt, das kontinuierlich Ereignisse aus einem iCalendar (ICS)-Feed über die REST-API in eine Discourse-Kategorie synchronisiert.
Dies ist kein vollständiges Discourse-Plugin – es läuft parallel zu Ihrer Discourse-Installation – daher gehört es hierher in #extras. Wenn Sie Kalenderereignisse aus einer externen Quelle (z. B. Google Kalender, Stundenpläne von Universitäten usw.) in Discourse-Themen anzeigen möchten, ist dies nützlich.
Repository
Funktionsweise
Liest Ereignisse aus einem gegebenen ICS-Feed
Gleicht sie mit vorhandenen Themen ab (nach UID oder als Fallback nach Zeit/Ort)
Erstellt oder aktualisiert Themen in Ihrer gewählten Kategorie
Kann kontinuierlich als systemd-Dienst ausgeführt werden (sicher gegen doppelte Ausführung durch flock)
Anforderungen
Ubuntu 24.04 LTS (getestet)
Python 3 (bereits in Ubuntu 24.04 LTS enthalten)
Ein Discourse API-Schlüssel
Eine Kategorie-ID, die für Ereignisthemen als Ziel dient
Beispielausgabe
So sieht es aus, wenn ein Stundenplan-ICS-Feed einer Universität in Discourse synchronisiert wird:
die Tags haben mich genervt, also habe ich dafür gesorgt, dass search.json nach indizierten Inhalten des Events sucht – erster Beitrag jedes Themas/Events
Vielen Dank nochmals für das Teilen, dieser Kalender entwickelt sich immer weiter und erhält dank Menschen wie Ihnen neue Funktionen. Ich frage mich, wie er in 3-5 Jahren sein wird
Brillant! Danke fürs Ausprobieren. Jeder andere, der versuchen möchte, einen ICS-Feed mit Discourse zu synchronisieren, ich würde mich über Feedback freuen, ob sich Ihre Feeds gleich verhalten.
Wenn ich Zeit hätte, würde ich wahrscheinlich versuchen, dies in ein richtiges Plugin umzuwandeln. Ich denke, es sollte nicht allzu schwer sein, einige Einstellungen zu erstellen, das Python in Ruby umzuwandeln und es in einen Job zu packen.
Eine weitere Idee, die für Leute nützlich sein könnte, die gehostet werden und dies nutzen möchten, wäre, die Aufgabe in eine GitHub-Aktion umzuwandeln und sie täglich ausführen zu lassen. Ich habe dies vor einiger Zeit für einige Skripte getan, die ein gehosteter Kunde täglich ausführen musste, und es funktioniert ziemlich gut. Es ist gleichzeitig schwieriger (es erfordert das Erlernen von GitHub-Workflows und den Umgang mit Geheimnissen anstelle eines guten alten Cron-Jobs) und einfacher (Sie müssen nicht lernen, wie man Dinge über eine Befehlszeilenschnittstelle auf einer Maschine installiert).
Ich habe es in letzter Zeit nicht getestet, aber die Ereignis-BBCode-Analyse in meinem letzten Commit zu
Ja, obwohl es schön wäre, wenn die Einstellung ics_feeds aufgeschlüsselt würde, damit der Administrator nicht ein einzelnes JSON in die Benutzeroberfläche eingibt.
Verhaltenshinweise aus Tests von ics_to_discourse.py
Ich habe eine Reihe von Tests mit diesem Skript durchgeführt (mit und ohne --time-only-dedupe) und dachte, es wäre nützlich, den Update-/Übernahmefluss im Detail zu dokumentieren.
1. Wie die Einzigartigkeit bestimmt wird
Standardmodus: Die Übernahme erfordert, dass Start + Ende + Ort exakt übereinstimmen.
Mit --time-only-dedupe: Die Übernahme erfordert nur Start + Ende; der Ort wird als „nahe genug“ behandelt.
Wenn kein vorhandenes Thema diesen Regeln entspricht, wird ein neues Thema erstellt.
2. Die Rolle des UID-Markers
Jedes Ereignisthema erhält einen versteckten HTML-Marker im ersten Beitrag:
<!-- ICSUID:xxxxxxxxxxxxxxxx -->
Bei nachfolgenden Ausführungen sucht das Skript zuerst nach diesem Marker.
Wenn er gefunden wird, wird das Thema als UID-Übereinstimmung betrachtet und direkt aktualisiert, unabhängig davon, wie unübersichtlich oder veraltet der BESCHREIBUNGstext sein mag.
Dies macht die UID zum wahren Identitätsschlüssel. Sichtbare Beschreibungsfelder beeinflussen die Übereinstimmung nicht.
3. Update-Fluss mit UID-Übereinstimmung
Das Skript ruft den ersten Beitrag ab und entfernt den Marker:
Wenn old_clean == fresh_clean: kein Update (verhindert Änderungen).
Wenn sie sich unterscheiden: Prüfen, ob die Änderung „bedeutsam“ ist:
meaningful = (
_norm_time(old_attrs.get("start")) != _norm_time(new_attrs.get("start"))
or _norm_time(old_attrs.get("end")) != _norm_time(new_attrs.get("end"))
or _norm_loc(old_attrs.get("location")) != _norm_loc(new_attrs.get("location"))
)
Wenn meaningful = True → Update mit Bump (Thema steigt in „Neueste“ auf).
Wenn meaningful = False → Update leise (bypass_bump=True → nur Überarbeitung, kein Bump).
Tags werden zusammengeführt (stellt sicher, dass statische/Standard-Tags vorhanden sind, entfernt niemals Moderator-/manuelle Tags).
Titel und Kategorie werden bei einem Update niemals geändert.
Update-Fluss ohne UID-Übereinstimmung
Das Skript versucht die Übernahme:
• Erstellt Kandidaten-Tripel aus Start/Ende/Ort (oder nur Start/Ende mit --time-only-dedupe).
• Sucht in /search.json und /latest.json nach einem vorhandenen Ereignis mit übereinstimmenden Attributen.
• Wenn gefunden → Übernimmt dieses Thema, rüstet es mit UID-Marker + Tags nach (Text bleibt in diesem Stadium unverändert).
• Wenn nicht gefunden → Erstellt ein brandneues Thema mit dem Marker und den Tags.
Sobald übernommen oder erstellt, werden alle zukünftigen Synchronisierungen direkt über die UID aufgelöst.
⸻
Praktische Konsequenzen
• Zeitänderungen
• Standard: Übernahme schlägt fehl (Zeiten unterscheiden sich) → neues Thema wird erstellt.
• Mit --time-only-dedupe: Übernahme schlägt auf die gleiche Weise fehl; neues Thema wird erstellt.
• Ortsänderungen
• Standard: Übernahme schlägt fehl (Ort unterscheidet sich) → neues Thema wird erstellt.
• Mit --time-only-dedupe: Übernahme gelingt (Zeiten stimmen überein), aber die Ortsänderung wird als „bedeutsam“ gekennzeichnet → Update mit Bump.
• Beschreibungsänderungen
• Wenn sich der BESCHREIBUNGstext ändert, aber Start/Ende/Ort nicht:
• Der Text wird leise aktualisiert (bypass_bump=True).
• Eine Überarbeitung des Themas wird erstellt, aber kein Bump in „Neueste“.
• Wenn die BESCHREIBUNG unverändert ist (oder nur Rauschen wie „Zuletzt aktualisiert:“ enthält, das normalisiert wird), erfolgt überhaupt keine Aktualisierung.
• UID-Marker
• Stellt eine zuverlässige Übereinstimmung bei zukünftigen Synchronisierungen sicher.
• Bedeutet, dass unübersichtliche BESCHREIBUNGsfelder nicht beeinflussen, ob das richtige Thema gefunden wird.
⸻
Warum die BESCHREIBUNG manchmal „gleich bleibt“
Das Skript vergleicht den gesamten Text (abzüglich des UID-Markers).
Wenn nur eine volatile Zeile wie „Zuletzt aktualisiert:“ unterschiedlich ist, diese aber normalisiert wird (z. B. Leerzeichen, Zeilenumbrüche, Unicode), erscheinen old_clean und fresh_clean identisch → es wird keine Aktualisierung vorgenommen.
Dies ist beabsichtigt, um Änderungen durch Rauschen im Feed zu verhindern.
⸻
Zusammenfassung
• Zeit bestimmt die Einzigartigkeit (erstellt immer ein neues Thema, wenn sich die Zeiten ändern).
• Ortsänderungen → sichtbarer Bump (damit Benutzer Veranstaltungsort-Updates bemerken).
• Beschreibungsänderungen → leises Update (Überarbeitung, aber kein Bump).
• UID-Marker = zuverlässiger Identitätsschlüssel, stellt sicher, dass das richtige Thema immer gefunden wird, auch wenn die BESCHREIBUNG veraltet oder unübersichtlich ist.
Dies stellt eine gute Balance dar: wichtige Änderungen werden in „Neueste“ angezeigt, unwichtige Änderungen bleiben unsichtbar.
Rückblickend ist es irgendwie urkomisch, wie sich diese ganze Saga entwickelt hat. Das Import-Skript selbst ist jetzt absolut stabil: UID-Marker, Deduplizierungslogik, aussagekräftige vs. stille Updates, Tag-Namensräume … all die Dinge, die man sich in der Produktion tatsächlich wünscht. Das Verhalten stimmt perfekt mit den von mir geposteten Notizen überein – Zeiten definieren Einzigartigkeit, Orte lösen ein Hochstufen aus, Beschreibungen werden still aktualisiert und UID-Marker halten alles verankert. Es ist elegant, es ist vorhersehbar, es ist fertig.
In der Zwischenzeit war das arme Meta-Thema, das alles beherbergte … nun ja, zum Scheitern verurteilt. Es begann als Antwort eines Sockenpuppen-Accounts (starker Start ), schwoll zu einem Frankenstein-Thread aus Code-Dumps und Screenshots an, entwickelte sich dann zu einem Pseudo-Changelog mit mehr Commits als das Repository selbst. Und gerade als das Skript endlich stabil wurde? Zur Löschung vorgesehen.
Ehrlich gesagt, es ist poetisch. Der gesamte Zweck des Skripts ist es, zu verhindern, dass doppelte Ereignisse Ihr Forum überladen. Das Thema selbst? Wurde als Duplikat angesehen, still für die Müllabfuhr markiert. Das Schicksal, das es verhindern sollte, wurde sein eigenes Schicksal.
Also, Prost an das zum Scheitern verurteilte Thema: Du hast Latest nicht hochgestuft, aber unsere Herzen.
Wie sind Sie mit der Umwandlung in ein Discourse-Plugin vorangekommen? Oder noch besser, als PR für das bestehende Plugin Discourse Calendar (and Event)?
Ich bin zurückhaltend, mich mit der Konfiguration und Wartung zu befassen, die für die Ausführung Ihres großartig aussehenden Skripts erforderlich ist (und vermute, dass viele Self-Hosters im selben Boot sitzen würden).
Wie ist dieses Skript besser als das Plugin? (Oh, vielleicht können Sie keine Plugins installieren?) Wenn das Plugin nicht das tut, was erforderlich ist, vielleicht einen PR einreichen?
Kurzer Status: Ich führe derzeit drei Instanzen meines Python ICS→Discourse-Importers aus (Uni-Stundenplan, Sportzentrum-Buchungen und einen Outlook-Kalender). Ich habe begonnen, ihn als Discourse-Plugin zu verpacken, aber die Plugin-Version blieb hinter dem Funktionsumfang des Skripts zurück – hauptsächlich, weil jeder Feed eine individuelle Behandlung benötigt (UID-Eigenheiten, Teilaktualisungen, Stornierungen, viele Revisionen usw.). Angus’ Plugin ist für viele Fälle großartig; meine Anwendungsfälle scheinen eher „Feed-spezifisch“ zu sein.
Ich habe auch einen offenen PR gegen den Kern, der darauf abzielt, das Rauschen des blauen „Latest“-Buttons bei großen/burstigen ICS-Updates zu reduzieren. Bei stark frequentierten Feeds (wie Universitätsstundenplänen) kann eine Charge von geringwertigen Bearbeitungen den „Latest“-Button ständig aufpoppen lassen; der PR deaktiviert effektiv den „New Topics“-Button, wenn „Latest“ geöffnet war, während eine automatisierte Charge läuft. Gerne verlinke ich diesen PR hier, wenn er nützlich ist.
Langfristig: Ich bin derzeit auf selbst gehostetem IONOS. Wenn ich später zu einem offiziellen Hosting wechsle, würde ich mir immer noch wünschen, den Python-Flow (oder ein Äquivalent) beibehalten zu können, ohne Enterprise-Funktionen zu benötigen, falls ICS-Inbound dort existiert. Ich vermute, eine generische Kern-/Plugin-Lösung könnte funktionieren, wenn sie austauschbare „Adapter“ pro Feed zulässt und gleichzeitig eine starke Idempotenz (ICS-UID), Stornierungsbehandlung und Bearbeitung-ohne-Bump-Semantik beibehält.
Wenn Interesse besteht, kann ich eine minimale Adapter-Schnittstelle und einen Migrationspfad von meinem Python-Skript zu einem Ruby-Job skizzieren oder Feed-unabhängige Teile (UID-Mapping, Debounce/No-Bump-Updates, Stornierungslogik) zum Kalender-/Event-Plugin beitragen.
Das ist eine gute Frage, Nathan – und ich denke, es gibt definitiv Raum für einen minimalen, Feed-unabhängigen Ansatz, der entweder als kleine Erweiterung des Calendar/Event-Plugins oder als leichtgewichtiger Core-Job existieren könnte.
Damit ein PR allgemein nützlich ist, scheint der Schlüssel darin zu liegen, den Importeur adapterbasiert und nicht Feed-spezifisch zu gestalten. Etwas wie:
Jeder Feed definiert einen kleinen Adapter (kann Python, YAML oder Ruby sein), der ICS-Felder → Discourse-Topic-Felder (title, body, tags, start, end, location usw.) abbildet.
Core kümmert sich um Idempotenz (UID ↔ Topic-ID-Mapping), Stornierungen (STATUS:CANCELLED) und stille Bearbeitungen (Update ohne Bumpen von Latest).
Plugins oder Site-Einstellungen könnten das Abfrageintervall, Tag-Mappings und die Bump-Richtlinie (always, never, on major change) konfigurieren.
Auf diese Weise können Institutionen mit verrauschten oder komplexen Feeds (Universitätsstundenpläne, Raumbuchungen, Outlook-Kalender usw.) einen Adapter bereitstellen, der auf ihre Daten zugeschnitten ist, ohne etwas im Core fest zu codieren.
Wenn Interesse besteht, würde ich gerne diese Adapter-Schnittstelle skizzieren oder den Core “ICS upsert”-Helfer als Ruby-Job prototypisieren, auf dem andere aufbauen können – damit sich dies schrittweise von eigenständigen Python-Skripten zu etwas Wartbarem und Generischem innerhalb des Discourse-Ökosystems entwickeln kann.