J’ai créé un petit utilitaire qui synchronise en continu les événements d’un flux iCalendar (ICS) dans une catégorie Discourse via l’API REST.
Ce n’est pas un plugin Discourse complet — il s’exécute parallèlement à votre installation Discourse — il appartient donc ici dans #extras. Si vous souhaitez afficher des événements de calendrier provenant d’une source externe (par exemple, Google Agenda, flux d’horaires universitaires, etc.) dans des sujets Discourse, cela vous sera utile.
Dépôt
Comment ça marche
Lit les événements d’un flux ICS donné
Les fait correspondre aux sujets existants (par UID ou en dernier recours par heure/lieu)
Crée ou met à jour des sujets dans votre catégorie choisie
Peut s’exécuter en continu en tant que service systemd (sécurisé contre l’exécution en double via flock)
Exigences
Ubuntu 24.04 LTS (testé)
Python 3 (déjà inclus dans Ubuntu 24.04 LTS)
Une clé API Discourse
Un ID de catégorie cible pour les sujets d’événements
Exemple de sortie
Voici à quoi ressemble la synchronisation d’un flux ICS d’horaires universitaires dans Discourse :
les balises me dérangeaient, j’ai donc fait en sorte que search.json recherche le contenu indexé de l’événement - premier post de chaque sujet/événement
Merci encore pour le partage, ce calendrier évolue de plus en plus, avec de nouvelles fonctionnalités grâce à des personnes comme vous. Je me demande à quoi il ressemblera dans 3 à 5 ans
Génial ! Merci de l’avoir testé. Si quelqu’un d’autre veut essayer de synchroniser un flux ICS dans Discourse, j’aimerais avoir vos retours pour savoir si vos flux se comportent de la même manière.
Si j’avais du temps, j’essaierais probablement de convertir cela en un plugin approprié. Je pense que ce ne devrait pas être trop difficile de créer quelques paramètres, de convertir le Python en Ruby et de le mettre dans un job.
Une autre idée, qui pourrait être utile pour les personnes hébergées qui souhaitent l’utiliser, serait de convertir la tâche en une action GitHub et de la faire exécuter quotidiennement. J’ai fait cela pour certains scripts qu’un client hébergé avait besoin d’exécuter quotidiennement il y a quelque temps et cela fonctionne très bien. C’est à la fois plus difficile (cela demande d’apprendre les flux de travail GitHub et comment gérer les secrets au lieu d’un bon vieux cron job) et plus facile (vous n’avez pas à apprendre comment manipuler l’installation de choses sur une machine via une interface en ligne de commande).
Je ne l’ai pas testé récemment, mais j’ai regroupé l’analyse du bbcode d’événements dans mon dernier commit sur
oui, bien que ce serait bien si le paramètre ics_feeds était décomposé, afin que l’administrateur ne saisisse pas un seul JSON dans l’interface utilisateur.
Haha, pour être clair, je voulais dire que l’interface graphique est le vrai luxe - l’interface en ligne de commande est la compétence vers laquelle je dois tendre.
Notes de comportement issues des tests de ics_to_discourse.py
J’ai effectué une série de tests sur ce script (avec et sans --time-only-dedupe) et j’ai pensé qu’il serait utile de documenter en détail le flux de mise à jour/adoption.
1. Comment l’unicité est déterminée
Mode par défaut : l’adoption nécessite que début + fin + lieu correspondent exactement.
Avec --time-only-dedupe : l’adoption ne nécessite que début + fin ; le lieu est considéré comme “suffisamment proche”.
Si aucun sujet existant ne correspond à ces règles, un nouveau sujet est créé.
2. Le rôle du marqueur UID
Chaque sujet d’événement reçoit un marqueur HTML caché dans le premier message :
<!-- ICSUID:xxxxxxxxxxxxxxxx -->
Lors des exécutions suivantes, le script recherche d’abord ce marqueur.
S’il est trouvé, le sujet est considéré comme une correspondance UID et est mis à jour directement, quelle que soit la quantité de bruit ou l’ancienneté du texte DESCRIPTION.
Cela fait de l’UID la véritable clé d’identité. Les champs de description visibles n’affectent pas la correspondance.
3. Flux de mise à jour avec correspondance UID
Le script récupère le premier message et supprime le marqueur :
Si old_clean == fresh_clean : pas de mise à jour (évite le roulement).
S’ils diffèrent : vérifie si le changement est “significatif” :
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"))
)
Si meaningful = True → mise à jour avec “bump” (le sujet remonte dans les Derniers).
Si meaningful = False → mise à jour silencieuse (bypass_bump=True → révision uniquement, pas de “bump”).
Les tags sont fusionnés (garantit la présence des tags statiques/par défaut, ne supprime jamais les tags de modérateur/manuels).
Le titre et la catégorie ne sont jamais modifiés lors d’une mise à jour.
Flux de mise à jour sans correspondance UID
Le script tente l’adoption :
• Construit des triplets candidats de début/fin/lieu (ou début/fin uniquement avec --time-only-dedupe).
• Recherche dans /search.json et /latest.json un événement existant avec des attributs correspondants.
• Si trouvé → adopte ce sujet, ajoute le marqueur UID + les tags (le corps reste inchangé à ce stade).
• Si non trouvé → crée un tout nouveau sujet avec le marqueur et les tags.
Une fois adopté ou créé, toutes les synchronisations futures seront résolues directement par UID.
⸻
Conséquences pratiques
• Changements d’heure
• Par défaut : l’adoption échoue (les heures diffèrent) → nouveau sujet créé.
• Avec --time-only-dedupe : l’adoption échoue de la même manière ; nouveau sujet créé.
• Changements de lieu
• Par défaut : l’adoption échoue (le lieu diffère) → nouveau sujet créé.
• Avec --time-only-dedupe : l’adoption réussit (les heures correspondent), mais la différence de lieu est signalée comme “significative” → mise à jour avec “bump”.
• Changements de description
• Si le texte DESCRIPTION change mais que le début/la fin/le lieu ne changent pas :
• Le corps est mis à jour silencieusement (bypass_bump=True).
• Une révision du sujet est créée, mais pas de “bump” dans les Derniers.
• Si DESCRIPTION est inchangé (ou seulement du bruit comme Last Updated: qui se normalise), aucune mise à jour n’est effectuée.
• Marqueur UID
• Garantit une correspondance fiable lors des futures synchronisations.
• Signifie que les champs DESCRIPTION bruyants n’affectent pas la recherche du bon sujet.
⸻
Pourquoi la DESCRIPTION reste parfois “la même”
Le script compare l’intégralité du corps (moins le marqueur UID).
Si seule une ligne volatile comme Last Updated: est différente, mais qu’elle se normalise (par exemple, espaces blancs, fins de ligne, Unicode), old_clean et fresh_clean apparaissent identiques → aucune mise à jour n’est effectuée.
C’est intentionnel, pour éviter le roulement dû au bruit du flux.
⸻
Résumé
• L’heure définit l’unicité (crée toujours un nouveau sujet lorsque les heures changent).
• Changements de lieu → “bump” visible (pour que les utilisateurs remarquent les mises à jour de lieu).
• Changements de description → mise à jour silencieuse (révision mais pas de “bump”).
• Le marqueur UID = clé d’identité fiable, garantit que le bon sujet est toujours trouvé, même si la DESCRIPTION est obsolète ou bruyante.
Cela établit un bon équilibre : les changements importants apparaissent dans les Derniers, le roulement sans importance reste invisible.
Avec le recul, c’est assez hilarant de voir comment toute cette saga s’est déroulée.
Le script d’importation lui-même est maintenant extrêmement fiable : marqueurs d’UID, logique de déduplication, mises à jour significatives vs silencieuses, espaces de noms de balises… tout ce que vous voudriez vraiment en production. Les comportements correspondent parfaitement aux notes que j’ai publiées — les heures définissent l’unicité, les lieux déclenchent une augmentation, les descriptions se mettent à jour silencieusement et les marqueurs d’UID maintiennent tout ancré. C’est élégant, c’est prévisible, c’est fait.
Pendant ce temps, le pauvre sujet Meta qui a tout hébergé était… eh bien, condamné.
Il a commencé sa vie en répondant comme un faux compte (bon début ), a gonflé pour devenir un fil de discussion Frankenstein de déversements de code et de captures d’écran, puis a évolué en un pseudo-journal des modifications avec plus de commits que le dépôt lui-même. Et juste au moment où le script est finalement devenu stable ? Il a été programmé pour être supprimé.
Honnêtement, c’est poétique. Le but même du script est d’empêcher les événements dupliqués d’encombrer votre forum. Le sujet lui-même ? Considéré comme un doublon, discrètement marqué pour la collecte des déchets. Le sort même qu’il a été conçu pour prévenir est devenu sa destinée.
Alors, levons nos verres au sujet condamné :
Tu n’as pas augmenté le “Latest”, mais tu as augmenté nos cœurs.
Comment s’est passée la migration vers un plugin Discourse ? Ou mieux encore, sous forme de PR sur le plugin existant Discourse Calendar (and Event) ?
Je suis réticent à me lancer dans la configuration et la maintenance requises pour exécuter votre script à l’allure impressionnante tel quel (et je soupçonne que de nombreux auto-hébergeurs seraient dans le même cas).
Comment ce script est-il meilleur que le plugin ? (Oh, peut-être que vous ne pouvez pas installer de plugins ?) Si le plugin ne fait pas ce qui est requis, peut-être soumettre une PR ?
État rapide : J’exécute actuellement trois instances de mon importateur Python ICS → Discourse (horaire universitaire, réservations du centre sportif et calendrier Outlook). J’ai commencé à l’intégrer en tant que plugin Discourse, mais la version plugin n’a pas atteint l’ensemble des fonctionnalités du script, principalement parce que chaque flux nécessite une gestion sur mesure (bizarreries d’UID, mises à jour partielles, annulations, révisions bruyantes, etc.). Le plugin d’Angus est excellent dans de nombreux cas ; mes cas d’utilisation semblent plus « spécifiques au flux ».
J’ai également une PR ouverte contre le cœur visant à réduire le bruit du bouton bleu « Latest » lors de mises à jour ICS importantes/soudaines. Avec des flux chargés (comme les horaires universitaires), un lot d’éditions de faible valeur peut faire rebondir « Latest » ; la PR désactive efficacement le bouton « New Topics » lorsque Latest est resté ouvert pendant qu’un lot automatisé s’exécute. Je suis heureux de lier cette PR ici si cela est utile.
À plus long terme : Je suis actuellement sur IONOS auto-hébergé. Si je passe à l’hébergement officiel plus tard, j’aimerais toujours avoir un moyen de conserver le flux Python (ou un équivalent) sans avoir besoin de fonctionnalités Enterprise, si ICS inbound existe là-bas. Je soupçonne qu’une solution générique de cœur/plugin pourrait fonctionner si elle permettait des « adaptateurs » enfichables par flux tout en conservant une forte idempotence (UID ICS), la gestion des annulations et la sémantique d’édition sans rebond.
S’il y a de l’intérêt, je peux esquisser une interface d’adaptateur minimale et un chemin de migration de mon script Python vers un job Ruby, ou contribuer des pièces indépendantes du flux (mappage d’UID, mises à jour de débat/sans rebond, logique d’annulation) au plugin calendrier/événements.
C’est une bonne question, Nathan — et je pense qu’il y a définitivement de la place pour une approche minimale et indépendante du flux qui pourrait exister soit comme une petite extension du plugin Calendar/Event, soit comme une tâche légère au cœur du système.
Pour qu’une PR soit généralement utile, la clé semble être de rendre l’importateur basé sur des adaptateurs plutôt que spécifique au flux. Quelque chose comme :
Chaque flux définit un petit adaptateur (qui pourrait être en Python, YAML ou Ruby) qui mappe les champs ICS → champs de sujet Discourse (title, body, tags, start, end, location, etc.).
Le cœur gère l’idempotence (mappage UID ↔ ID de sujet), les annulations (STATUS:CANCELLED), et les modifications silencieuses (mise à jour sans faire remonter le dernier).
Les plugins ou les paramètres du site pourraient configurer l’intervalle d’interrogation, les mappages de tags et la politique de remontée (always, never, on major change).
De cette façon, les institutions ayant des flux bruyants ou complexes (horaires universitaires, réservations de salles, calendriers Outlook, etc.) peuvent fournir un adaptateur adapté à leurs données sans coder en dur quoi que ce soit dans le cœur du système.
S’il y a de l’intérêt, je serais heureux de décrire cette interface d’adaptateur ou de prototyper l’aide principale “ICS upsert” sous forme de tâche Ruby sur laquelle d’autres pourront s’appuyer — afin que cela puisse évoluer progressivement des scripts Python autonomes vers quelque chose de maintenable et de générique au sein de l’écosystème de Discourse.