Ethsim2 introduces a Python utility that syncs iCalendar (ICS) feeds into Discourse categories via the REST API. Running alongside Discourse rather than as a plugin, it creates or updates topics based on event UIDs, handling deduplication and continuous synchronization via systemdEthsim2. The tool supports Ubuntu 24.04 LTS, Python 3, and requires an API key and category ID Ethsim2. Subsequent updates improved search indexing to include event content, enhancing discoverability Ethsim2.
opcourdis praises the evolving features, while pfaffman suggests converting the script into a proper Discourse plugin or a GitHub Action for hosted users. Ethsim2 acknowledges the plugin idea but notes challenges with settings management and prefers systemd over cronEthsim2. RGJ points to an existing plugin by angus.
Ethsim2 provides detailed documentation on the script’s behavior, explaining how UID markers ensure reliable topic matching and how updates are handled. Meaningful changes (time/location) trigger bumps, while description-only changes update quietly to reduce noise Ethsim2. The author humorously reflects on the meta-discussion’s chaotic history compared to the script’s stability Ethsim2.
nathank and pfaffman inquire about plugin integration. Ethsim2 explains that the current Python script handles complex, feed-specific needs better than existing plugins. They propose an adapter-based approach where core handles idempotency and quiet updates, while adapters manage feed-specific logic Ethsim2. Recent commits address bump date resets and refine --time-only-dedupe logic to treat location changes as meaningful signals Ethsim2(Ethsim2(Ethsim2.
I’ve built a small utility that continuously syncs events from an iCalendar (ICS) feed into a Discourse category via the REST API.
This isn’t a full Discourse plugin — it runs alongside your Discourse install — so it belongs here in Customization > Extras. If you want to display calendar events from an external source (e.g. Google Calendar, University timetable feeds, etc.) inside Discourse topics, this will be useful.
Repository
How it works
Reads events from a given ICS feed
Matches them against existing topics (by UID or fallback to time/location)
Creates or updates topics in your chosen category
Can run continuously as a systemd service (safe against duplicate execution via flock)
Requirements
Ubuntu 24.04 LTS (tested)
Python 3 (already in Ubuntu 24.04 LTS)
A Discourse API key
A category ID to target for event topics
Example output
Here’s what it looks like when syncing a University timetable ICS feed into Discourse:
Thank you again for the share, this calendar is evolving more and more, getting new features thanks to people like you. I wonder how it will be like in 3-5 years
אם היה לי קצת זמן, כנראה שהייתי מנסה להמיר את זה לתוסף אמיתי. אני חושב שלא יהיה קשה מדי ליצור כמה הגדרות ולהמיר את הפייתון לרובי ולשים את זה במשימה.
רעיון נוסף, שיכול להיות שימושי עבור אנשים שמארחים ורוצים להשתמש בזה, יהיה להמיר את המשימה לפעולת גיטהאב ולגרום לה להריץ את המשימה מדי יום. עשיתי זאת עבור כמה סקריפטים שלקוח מארח היה צריך להריץ מדי יום לפני זמן מה וזה עובד די טוב. זה קשה יותר (זה דורש ללמוד זרימות עבודה של גיטהאב ואיך להתמודד עם סודות במקום משימת cron ישנה וטובה) וקל יותר (אתה לא צריך ללמוד איך להתעסק עם התקנת דברים על מכונה דרך ממשק שורת פקודה).
אם old_clean == fresh_clean: אין עדכון (מונע תנודתיות).
אם הם שונים: בודק אם השינוי הוא “משמעותי”:
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"))
)
אם meaningful = True → עדכון עם דחיפה (הנושא עולה ב’הכי חדש’).
אם meaningful = False → עדכון בשקט (bypass_bump=True → רק תיקון, ללא דחיפה).
תגיות משולבות (מבטיח שתגיות סטטיות/ברירת מחדל קיימות, לעולם לא מסיר תגיות מנהל/ידניות).
כותרת וקטגוריה לעולם אינם משתנים בעדכון.
זרימת עדכון ללא התאמת UID
הסקריפט מנסה אימוץ:
• בונה שלשות מועמדות של התחלה/סיום/מיקום (או התחלה/סיום בלבד עם --time-only-dedupe).
• מחפש ב-/search.json וב-/latest.json אירוע קיים עם תכונות תואמות.
• אם נמצא → מאמץ את הנושא, מתקין מחדש את סמן ה-UID + תגיות (גוף נשאר ללא שינוי בשלב זה).
• אם לא נמצא → יוצר נושא חדש לגמרי עם הסמן והתגיות.
לאחר אימוץ או יצירה, כל סנכרונים עתידיים יפתרו ישירות לפי UID.
⸻
השלכות מעשיות
• שינויי זמן
• ברירת מחדל: אימוץ נכשל (זמנים שונים) → נוצר נושא חדש.
• עם --time-only-dedupe: אימוץ נכשל באותה צורה; נוצר נושא חדש.
• שינויי מיקום
• ברירת מחדל: אימוץ נכשל (מיקום שונה) → נוצר נושא חדש.
• עם --time-only-dedupe: אימוץ מצליח (זמנים תואמים), אך הבדל במיקום מסומן כ"משמעותי" → עדכון עם דחיפה.
• שינויי תיאור
• אם טקסט ה-DESCRIPTION משתנה אך התחלה/סיום/מיקום לא:
• הגוף מתעדכן בשקט (bypass_bump=True).
• נוצר תיקון לנושא, אך ללא דחיפה ב’הכי חדש’.
• אם DESCRIPTION ללא שינוי (או רק רעש כמו Last Updated: שמתנרמל), לא מתבצע כלל עדכון.
• סמן UID
• מבטיח התאמה אמינה בסנכרונים עתידיים.
• פירושו ששדות DESCRIPTION רועשים אינם משפיעים על מציאת הנושא הנכון.
⸻
מדוע ה-DESCRIPTION לפעמים “נשאר זהה”
הסקריפט משווה את כל הגוף (ללא סמן ה-UID).
אם רק שורה תנודתית כמו Last Updated: שונה, אך היא מתנרמלת (למשל, רווחים, סיומות שורה, יוניקוד), old_clean ו-fresh_clean נראים זהים → לא מתבצע עדכון.
זה מכוון, כדי למנוע תנודתיות מרעש הזנה.
⸻
סיכום
• זמן מגדיר ייחודיות (תמיד יוצר נושא חדש כאשר זמנים משתנים).
• שינויי מיקום → דחיפה גלויה (כדי שמשתמשים ישימו לב לעדכוני מיקום).
• שינויי תיאור → עדכון שקט (תיקון אך ללא דחיפה).
• סמן UID = מפתח זהות אמין, מבטיח שהנושא הנכון תמיד יימצא, גם אם ה-DESCRIPTION מיושן או רועש.
זה מכה איזון טוב: שינויים חשובים מופיעים ב’הכי חדש’, תנודתיות לא חשובה נשארת בלתי נראית.
Looking back, it’s kind of hilarious how this whole saga unfolded.
The importer script itself is now rock-solid: UID markers, dedupe logic, meaningful vs. quiet updates, tag namespaces… all the stuff you’d actually want in production. The behaviours line up perfectly with the notes i posted — times define uniqueness, locations trigger a bump, descriptions update quietly, and UID markers keep everything anchored. It’s elegant, it’s predictable, it’s done.
Meanwhile, the poor Meta topic that hosted it all was… well, doomed.
It began life replying as a sockpuppet (strong start ), ballooned into a Frankenstein thread of code dumps and screenshots, then evolved into a pseudo-changelog with more commits than the repo itself. And just as the script finally became stable? Scheduled for deletion.
Honestly, it’s poetic. The script’s entire purpose is to stop duplicate events from cluttering up your forum. The topic itself? Seen as a duplicate, quietly marked for garbage collection. The very fate it was built to prevent became its destiny.
So here’s to the doomed topic:
You didn’t bump Latest, but you bumped our hearts.
How did you get on with moving it to a Discourse plugin? Or better yet, as a PR on the existing Discourse Calendar and Events Plugin?
I’m reluctant to jump into the config and maintenance required to run your awesome looking script as is (and suspect that many self-hosters would be in the same boat).
סטטוס מהיר: כרגע אני מריץ שלוש מופעים של ייבוא ה-Python שלי מ-ICS ל-Discourse (לוח זמנים של Uni, הזמנות של מרכז הספורט ולוח שנה של Outlook). התחלתי לעטוף אותו כתוסף Discourse, אבל גרסת התוסף לא עמדה במערך התכונות של הסקריפט - בעיקר מכיוון שכל הזנה דורשת טיפול מותאם אישית (מוזרויות UID, עדכונים חלקיים, ביטולים, שינויים רועשים וכו’). התוסף של אנגוס מצוין למקרים רבים; מקרי השימוש שלי נראים יותר “ספציפיים להזנה”.
יש לי גם PR פתוח נגד הליבה שמטרתו להפחית את הרעש של הכפתור הכחול “הכי חדש” במהלך עדכוני ICS גדולים/מתפרצים. עם הזנות עמוסות (כמו לוחות זמנים של אוניברסיטאות), קבוצת עריכות בעלות ערך נמוך יכולה להמשיך להקפיץ את “הכי חדש”; ה-PR למעשה מבטל את פעולת כפתור “נושאים חדשים” כאשר “הכי חדש” נשאר פתוח בזמן שקבוצה אוטומטית פועלת. אשמח לקשר את ה-PR הזה כאן אם זה מועיל.
לטווח ארוך יותר: אני כרגע ב-IONOS בהוסט עצמי. אם אעבור להוסט רשמי מאוחר יותר, עדיין אשמח למצוא דרך לשמור על זרימת ה-Python (או מקבילה) מבלי להזדקק לתכונות Enterprise, אם ICS inbound קיים שם. אני חושב שפתרון כללי של ליבה/תוסף יכול לעבוד אם הוא יאפשר “מתאמים” ניתנים לחיבור לכל הזנה תוך שמירה על איתנות אידמפוטנטיות (ICS UID), טיפול בביטולים, וסמנטיקה של עריכה ללא קפיצה.
אם יש עניין, אני יכול לשרטט ממשק מתאם מינימלי ונתיב הגירה מהסקריפט שלי ל-Ruby job, או לתרום חלקים שאינם תלויים בהזנה (מיפוי UID, השהייה/עדכונים ללא קפיצה, לוגיקת ביטול) לתוסף לוח השנה/אירועים.
זו שאלה טובה, ניית’ן — ואני חושב שיש בהחלט מקום לגישה מינימלית, שאינה תלויה ב-feed, שיכולה להתקיים כהרחבה קטנה לפלאגין Calendar/Event או כ-job ליבה קל משקל.
כדי ש-PR יהיה שימושי באופן כללי, המפתח נראה כמבצע את הייבוא מבוסס מתאם (adapter-based) ולא ספציפי ל-feed. משהו כמו:
כל feed מגדיר מתאם קטן (יכול להיות Python, YAML, או Ruby) שממפה שדות ICS → שדות נושא Discourse (title, body, tags, start, end, location, וכו’).
פלאגינים או הגדרות אתר יכולים להגדיר מרווח סקר, מיפויי תגים, ומדיניות דחיפה (always, never, on major change).
בצורה זו, מוסדות עם feeds רועשים או מורכבים (לוחות זמנים של אוניברסיטאות, הזמנות חדרים, יומני Outlook, וכו’) יכולים לספק מתאם המתאים לנתונים שלהם מבלי לקודד שום דבר בליבה.
אם יש עניין, אשמח לשרטט את ממשק המתאם הזה או ליצור אב-טיפוס של ה-helper “ICS upsert” בליבה כ-job Ruby שאחרים יוכלו לבנות עליו — כך שזה יוכל להתפתח בהדרגה מסקריפטים עצמאיים של Python למשהו שניתן לתחזק וגנרי בתוך האקוסיסטם של Discourse.