ICS → مستورد Discourse عبر واجهة برمجة تطبيقات REST

لقد قمت ببناء أداة مساعدة صغيرة تقوم بمزامنة الأحداث باستمرار من موجز iCalendar (ICS) إلى فئة Discourse عبر واجهة برمجة تطبيقات REST.

هذا ليس مكونًا إضافيًا كاملاً لـ Discourse - فهو يعمل بجانب تثبيت Discourse الخاص بك - لذا فهو ينتمي هنا في #extras. إذا كنت ترغب في عرض أحداث التقويم من مصدر خارجي (مثل تقويم Google، وجداول أوقات الجامعات، وما إلى ذلك) داخل مواضيع Discourse، فسيكون هذا مفيدًا.

المستودع

كيف يعمل

  • يقرأ الأحداث من موجز ICS معين
  • يطابقها مع المواضيع الموجودة (حسب UID أو بالرجوع إلى الوقت/الموقع)
  • ينشئ أو يحدث المواضيع في فئتك المختارة
  • يمكن تشغيله باستمرار كخدمة systemd (آمن ضد التنفيذ المكرر عبر flock)

المتطلبات

  • Ubuntu 24.04 LTS (تم اختباره)
  • Python 3 (موجود بالفعل في Ubuntu 24.04 LTS)
  • مفتاح واجهة برمجة تطبيقات Discourse
  • معرف فئة لاستهداف مواضيع الأحداث

مثال على المخرجات

إليك كيف يبدو الأمر عند مزامنة موجز جدول أوقات الجامعة لـ ICS في Discourse:

البدء السريع

استنسخ المستودع وقم بتثبيت المتطلبات:

git clone https://github.com/Ethsim12/Discourse-ICS-importer-by-REST-API.git /opt/ics-sync
cd /opt/ics-sync
pip install -r requirements.txt

قم بتشغيل مزامنة مرة واحدة يدويًا:

python3 ics_to_discourse.py \
  --ics "https://example.com/feed.ics" \
  --category-id 4 \
  --site-tz "Europe/London" \
  --static-tags "events,ics"

قم بالإعداد كخدمة/مؤقت systemd للمزامنة المستمرة (أمثلة على التكوينات في المستودع).

3 إعجابات

كانت العلامات تزعجني، لذا تأكدت من أن search.json يبحث عن المحتوى المفهرس للحدث - المنشور الأول لكل موضوع/حدث

إعجاب واحد (1)

شكراً لك مرة أخرى على المشاركة، هذه التقويم يتطور أكثر فأكثر، ويحصل على ميزات جديدة بفضل أشخاص مثلك. أتساءل كيف سيكون الوضع بعد 3-5 سنوات :slight_smile:

إعجاب واحد (1)

رائع! شكراً لك على اختباره. أي شخص آخر يرغب في تجربة مزامنة موجز ICS مع Discourse، أود الحصول على ملاحظات حول ما إذا كانت خلاصاتك تتصرف بنفس الطريقة.

إعجابَين (2)

بعض التعليقات.

لو كان لدي أي وقت، ربما كنت سأحاول تحويل هذا إلى إضافة (plugin) مناسبة. أعتقد أنه لا ينبغي أن يكون من الصعب جدًا إنشاء بعض الإعدادات وتحويل بايثون إلى روبي ووضعه في مهمة (job).

فكرة أخرى، يمكن أن تكون مفيدة للأشخاص الذين يتم استضافتهم ويرغبون في استخدام هذا، وهي تحويل المهمة إلى إجراء في GitHub (github action) وجعلها تقوم بتشغيل المهمة يوميًا. لقد فعلت هذا لبعض النصوص البرمجية التي احتاج عميل مستضاف إلى تشغيلها يوميًا منذ فترة وهي تعمل بشكل جيد جدًا. إنها في آن واحد أصعب (تتطلب تعلم سير عمل GitHub وكيفية التعامل مع الأسرار بدلاً من وظيفة cron قديمة) وأسهل (لا يتعين عليك تعلم كيفية التعامل مع تثبيت الأشياء على جهاز عبر واجهة سطر الأوامر).

إعجابَين (2)

لم أقم باختباره مؤخرًا، ولكني قمت بتغليف تحليل أحداث bbcode في آخر تحديث لي لـ

نعم، على الرغم من أنه سيكون من الجيد لو تم تقسيم إعداد ics_feeds، حتى لا يقوم المسؤول بإدخال JSON واحد في واجهة المستخدم

إعجاب واحد (1)

بصراحة، أنا لا أستخدم cron الآن، بل أستخدم systemd على خادم Ubuntu 24.04 LTS.

إعجاب واحد (1)

هذه رفاهية سأتعلم تحقيقها بمجرد أن يتوفر لدي الوقت :wink::face_exhaling:

عدم الوصول إلى سطر الأوامر، في رأيي المتواضع، ليس رفاهية على الإطلاق! :rofl:

إعجاب واحد (1)

هاها، للتوضيح، كنت أعني أن واجهة المستخدم الرسومية هي الرفاهية الحقيقية - واجهة سطر الأوامر هي المهارة التي أحتاج إلى العمل نحوها.

إعجاب واحد (1)

أعتقد أن @angus سبقك إلى ذلك ببضع سنوات

3 إعجابات

ملاحظات سلوكية من اختبار ics_to_discourse.py

لقد أجريت سلسلة من الاختبارات على هذا البرنامج النصي (مع وبدون --time-only-dedupe) واعتقدت أنه سيكون من المفيد توثيق تدفق التحديث/التبني بالتفصيل.


1. كيفية تحديد التفرد

  • الوضع الافتراضي: يتطلب التبني تطابق البداية + النهاية + الموقع بالضبط.
  • مع --time-only-dedupe: يتطلب التبني فقط البداية + النهاية؛ يُعامل الموقع على أنه “قريب بما فيه الكفاية”.

إذا لم يتطابق أي موضوع موجود مع هذه القواعد، يتم إنشاء موضوع جديد.


2. دور علامة UID

  • يحصل كل موضوع حدث على علامة HTML مخفية في المنشور الأول:
  <!-- ICSUID:xxxxxxxxxxxxxxxx -->
  • في عمليات التشغيل اللاحقة، يبحث البرنامج النصي عن تلك العلامة أولاً.
  • إذا تم العثور عليها، يُعتبر الموضوع مطابقًا لـ UID ويتم تحديثه مباشرة، بغض النظر عن مدى صخب أو قدم نص الوصف.
  • هذا يجعل UID مفتاح الهوية الحقيقي. حقول الوصف المرئية لا تؤثر على المطابقة.

3. تدفق التحديث مع مطابقة UID

  1. يجلب البرنامج النصي المنشور الأول ويزيل العلامة:
 old_clean = strip_marker(old_raw)
 fresh_clean = strip_marker(fresh_raw)
  1. إذا كان old_clean == fresh_clean: لا يوجد تحديث (يتجنب التغيير).
  2. إذا اختلفا: تحقق مما إذا كان التغيير “ذا مغزى”:
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 → مراجعة فقط، لا دفعة).
  1. يتم دمج العلامات (يضمن وجود العلامات الثابتة/الافتراضية، ولا يزيل أبدًا العلامات اليدوية/الخاصة بالمشرف).
  2. لا يتم تغيير العنوان والفئة أبدًا عند التحديث.

  1. تدفق التحديث بدون مطابقة UID
  2. يحاول البرنامج النصي التبني:
    • يبني ثلاثيات مرشحة للبداية/النهاية/الموقع (أو البداية/النهاية فقط مع --time-only-dedupe).
    • يبحث في /search.json و /latest.json عن حدث موجود بسمات متطابقة.
    • إذا تم العثور عليه → تبني هذا الموضوع، وتزويده بعلامة UID والعلامات (يُترك الجسم دون تغيير في هذه المرحلة).
    • إذا لم يتم العثور عليه → إنشاء موضوع جديد تمامًا بالعلامة والعلامات.
  3. بمجرد تبنيه أو إنشائه، ستحل جميع المزامنات المستقبلية مباشرة عن طريق UID.

  1. العواقب العملية
    • تغييرات الوقت
    • افتراضي: يفشل التبني (الأوقات مختلفة) → يتم إنشاء موضوع جديد.
    • مع --time-only-dedupe: يفشل التبني بنفس الطريقة؛ يتم إنشاء موضوع جديد.
    • تغييرات الموقع
    • افتراضي: يفشل التبني (الموقع مختلف) → يتم إنشاء موضوع جديد.
    • مع --time-only-dedupe: ينجح التبني (الأوقات متطابقة)، ولكن يتم تمييز اختلاف الموقع على أنه “ذو مغزى” → تحديث مع دفعة.
    • تغييرات الوصف
    • إذا تغير نص الوصف ولكن لم تتغير البداية/النهاية/الموقع:
    • يتم تحديث الجسم بهدوء (bypass_bump=True).
    • يتم إنشاء مراجعة للموضوع، ولكن لا توجد دفعة في الأحدث.
    • إذا لم يتغير الوصف (أو كان مجرد ضوضاء مثل Last Updated: التي يتم تطبيعها بعيدًا)، فلن يحدث أي تحديث على الإطلاق.
    • علامة UID
    • يضمن المطابقة الموثوقة في المزامنات المستقبلية.
    • يعني أن حقول الوصف الصاخبة لا تؤثر على ما إذا كان الموضوع الصحيح سيتم العثور عليه.

  1. لماذا يبقى الوصف “كما هو” أحيانًا
    يقارن البرنامج النصي الجسم بأكمله (باستثناء علامة UID).
    إذا كان سطر متقلب فقط مثل Last Updated: مختلفًا، ولكنه يتم تطبيعه بعيدًا (مثل المسافات البيضاء، نهايات الأسطر، Unicode)، فإن old_clean و fresh_clean يبدوان متطابقين → لا يتم إجراء أي تحديث.
    هذا عن قصد، لمنع التغيير من ضوضاء التغذية.

ملخص
• الوقت يحدد التفرد (ينشئ دائمًا موضوعًا جديدًا عند تغيير الأوقات).
• تغييرات الموقع → دفعة مرئية (حتى يلاحظ المستخدمون تحديثات المكان).
• تغييرات الوصف → تحديث هادئ (مراجعة ولكن لا دفعة).
• علامة UID = مفتاح هوية موثوق، يضمن العثور دائمًا على الموضوع الصحيح، حتى لو كان الوصف قديمًا أو صاخبًا.

هذا يحقق توازنًا جيدًا: التغييرات المهمة تظهر في الأحدث، والتغييرات غير المهمة تبقى غير مرئية.

بالنظر إلى الوراء، من المضحك نوعًا ما كيف تطورت هذه الملحمة بأكملها.
البرنامج النصي للمستورد نفسه قوي الآن: علامات UID، منطق إزالة التكرار، التحديثات الهادفة مقابل الصامتة، مساحات أسماء العلامات… كل الأشياء التي تريدها بالفعل في بيئة الإنتاج. تتوافق السلوكيات تمامًا مع الملاحظات التي نشرتها - تحدد الأوقات التفرد، وتشغل المواقع زيادة، وتحدث الأوصاف بصمت، وتحافظ علامات UID على كل شيء مثبتًا. إنه أنيق، إنه متوقع، لقد تم الانتهاء منه. :white_check_mark:

في غضون ذلك، كان موضوع Meta المسكين الذي استضاف كل شيء… حسنًا، محكومًا عليه بالفشل.
بدأ حياته بالرد كحساب وهمي (بداية قوية :socks:)، وتضخم إلى سلسلة من أكوام التعليمات البرمجية ولقطات الشاشة، ثم تطور إلى سجل تغييرات زائف به عدد من الالتزامات أكثر من المستودع نفسه. وفقط عندما أصبح البرنامج النصي مستقرًا أخيرًا؟ تم جدولته للحذف. :skull:

بصراحة، إنه أمر شاعري. الغرض الكامل للبرنامج النصي هو منع الأحداث المكررة من تلويث منتدىك. الموضوع نفسه؟ يُنظر إليه على أنه مكرر، ويتم تمييزه بصمت لجمع القمامة. المصير نفسه الذي بُني لمنعه أصبح قدره. :wastebasket:

لذا، إليكم هذا الموضوع المحكوم عليه بالفشل:
لم تقم بزيادة “الأحدث”، لكنك زدت قلوبنا. :heart:

إعجابَين (2)

كيف كان تقدمك في نقله إلى إضافة Discourse؟ أو الأفضل من ذلك، كمساهمة في الإضافة الحالية https://meta.discourse.org/t/discourse-calendar-and-event/97376؟

أنا متردد في الخوض في الإعداد والصيانة المطلوبة لتشغيل البرنامج النصي الرائع الخاص بك كما هو (وأشتبه في أن العديد من المستضيفين الذاتيين سيكونون في نفس القارب).

إعجاب واحد (1)

كيف هذا السكربت أفضل من الإضافة؟ (أوه، ربما لا يمكنك تثبيت الإضافات؟) إذا لم تفعل الإضافة ما هو مطلوب، ربما تقدم طلب سحب؟

شكرًا على التنبيه!

ملخص سريع: أقوم حاليًا بتشغيل ثلاث نسخ من برنامج استيراد Python الخاص بي من ICS إلى Discourse (جدول مواعيد الجامعة، حجوزات مركز الرياضة، وتقويم Outlook). بدأت في تغليفه كمكون إضافي لـ Discourse، لكن إصدار المكون الإضافي لم يصل إلى مجموعة ميزات البرنامج النصي - ويرجع ذلك أساسًا إلى أن كل تغذية تحتاج إلى معالجة مخصصة (غرائب معرفات فريدة، تحديثات جزئية، إلغاءات، مراجعات مزعجة، إلخ). المكون الإضافي الخاص بـ Angus رائع للعديد من الحالات؛ حالات الاستخدام الخاصة بي تبدو أكثر “خاصة بالتغذية”.

لدي أيضًا طلب سحب مفتوح ضد النواة يهدف إلى تقليل ضوضاء زر “الأحدث” الأزرق أثناء تحديثات ICS الكبيرة/المتقطعة. مع التغذيات المزدحمة (مثل جداول مواعيد الجامعات)، يمكن لمجموعة من التعديلات ذات القيمة المنخفضة أن تبقي “الأحدث” في حالة اهتزاز؛ يقوم طلب السحب بتنفيذ زر “المواضيع الجديدة” بشكل فعال عندما يكون “الأحدث” مفتوحًا أثناء تشغيل دفعة آلية. يسعدني ربط طلب السحب هذا هنا إذا كان مفيدًا.

على المدى الطويل: أنا حاليًا على IONOS المستضاف ذاتيًا. إذا انتقلت إلى الاستضافة الرسمية لاحقًا، فلا يزال بإمكاني الاستمتاع بطريقة للحفاظ على تدفق Python (أو ما يعادله) دون الحاجة إلى ميزات Enterprise، إذا كان ICS الوارد موجودًا هناك. أشك في أن حلاً عامًا للنواة/المكون الإضافي يمكن أن يعمل إذا سمح بـ “محولات” قابلة للتوصيل لكل تغذية مع الحفاظ على التماثل القوي (معرف فريد لـ ICS)، ومعالجة الإلغاء، ودلالات التعديل بدون رفع.

إذا كان هناك اهتمام، يمكنني رسم واجهة محول بسيطة ومسار ترحيل من برنامج Python النصي الخاص بي إلى مهمة Ruby، أو المساهمة بقطع مستقلة عن التغذية (تعيين معرف فريد، إلغاء الضوضاء/التحديثات بدون رفع، منطق الإلغاء) في المكون الإضافي للتقويم/الأحداث.

إعجاب واحد (1)

هذا سؤال جيد يا ناثان - وأعتقد أن هناك بالتأكيد مجالًا لنهج بسيط وغير مرتبط بالخلاصة يمكن أن يعيش إما كتوسيع صغير لمكون الإضافي للتقويم/الحدث أو كوظيفة أساسية خفيفة الوزن.

لكي يكون طلب السحب مفيدًا بشكل عام، يبدو أن المفتاح هو جعل المستورد قائمًا على المحول بدلاً من كونه خاصًا بالخلاصة. شيء مثل:

  • تحدد كل خلاصة محولًا صغيرًا (يمكن أن يكون بايثون أو YAML أو روبي) يقوم بتعيين حقول ICS → حقول موضوع Discourse (title، body، tags، start، end، location، إلخ).
  • تتعامل الوظيفة الأساسية مع التكرار (تعيين UID ↔ معرف الموضوع)، والإلغاء (STATUS:CANCELLED)، والتعديلات الهادئة (التحديث دون تخطي الأحدث).
  • يمكن للمكونات الإضافية أو إعدادات الموقع تكوين فترة الاستطلاع، وتعيينات العلامات، وسياسة التخطي (always، never، on major change).

بهذه الطريقة، يمكن للمؤسسات ذات الخلاصات الصاخبة أو المعقدة (جداول الجامعات، حجوزات الغرف، تقويمات Outlook، إلخ) توفير محول مناسب لبياناتها دون ترميز أي شيء بشكل ثابت في الوظيفة الأساسية.

إذا كان هناك اهتمام، فسأكون سعيدًا بتحديد واجهة المحول هذه أو إنشاء نموذج أولي لمساعد “ICS upsert” الأساسي كوظيفة روبي يمكن للآخرين البناء عليها - حتى يتطور هذا تدريجيًا من نصوص بايثون مستقلة إلى شيء قابل للصيانة وعام ضمن نظام Discourse البيئي.

إعجابَين (2)

لم يعد الأمر كذلك مع الالتزام التالي، شكرًا Discourse!

3 إعجابات