أدير منتدى صغيرًا وثلاثة مواقع أخرى، وكنت ألاحظ دائمًا نفس الشيء. كان الناس سعداء بالتحدث في الدردشة بمجرد وجودهم على المنتدى، لكن لا أحد يذهب إلى منتدى لطرح سؤال سريع. إما أن يسألوه في المكان الذي يتواجدون فيه بالفعل، أو لا يسألونه على الإطلاق.
لذلك، قمت ببناء إضافة (Plugin) تتيح وضع دردشة المنتدى على تلك المواقع الأخرى. مجرد وسم سكربت واحد، وتظهر فقاعة في الزاوية. يقوم الزوار بتسجيل الدخول باستخدام حساب المنتدى الذي يملكونه بالفعل، عبر شاشة تسجيل الدخول الخاصة بالمنتدى، ويتحدثون في نفس القنوات التي كانوا سيشاهدونها على المنتدى. الرسائل التي يرسلونها تصل إلى دردشة المنتدى مثل أي رسالة أخرى، لأنها في الحقيقة كذلك.
المستودع: GitHub - capodieci/discourse-chat-bridge: Allows to install discourse chat as a plugin in other websites · GitHub (MIT)
<script src="https://forum.example.com/chat-bridge/widget.js"
data-site-key="your-site-key" defer></script>
لماذا هي إضافة (Plugin) وليس خدمة خارجية
بدأت بتصميم جسر خارجي يتواصل مع Discourse عبر واجهة برمجة التطبيقات (API)، ثم قرأت الكود المصدري. غيّر ذلك التصميم بالكامل، وقد تكون الأسباب مفيدة لأي شخص يفكر في شيء مشابه.
تسجّل إضافة الدردشة نطاقًا دقيقًا واحدًا فقط لواجهة برمجة التطبيقات (API)، وهو create_message. أي شيء يتجاوز النشر، مثل قراءة القنوات، أو جلب السجل، أو فتح رسالة مباشرة، يتطلب مفتاح نطاق شامل (Global Scope Key)، ومفتاح نطاق شامل لكل مستخدم يعني حزمة كبيرة من بيانات الاعتماد يجب الاحتفاظ بها. حدود المعدل الافتراضية هي 60 طلبًا في الدقيقة على سلة المدير (Admin Bucket) و20 على سلة المستخدم (User Bucket)، وهو ما تستهلكه عميل الدردشة دون حتى محاولة. كما تحمل خطافات الويب (Webhooks) الخاصة بالدردشة أربعة أحداث للرسائل فقط ولا شيء آخر، لذا فإن التفاعلات (Reactions) والحالة (Presence) وحالة القراءة ببساطة غير متاحة للتحويل.
تختفي كل واحدة من هذه المشاكل عندما يعمل الكود داخل Discourse ويمكنه طرح سؤال مباشرة على Guardian. لا مفاتيح، ولا سقف لحدود المعدل، ومصدر حقيقة واحد بدلاً من ذاكرة مؤقتة (Cache) قد تتعارض مع المنتدى.
ما الذي يعمل
القنوات، وسجل الرسائل، والإرسال، والرسائل المباشرة مع بحث عن الأشخاص، ورسائل صوتية مسجلة في المتصفح. تظهر الرسائل الصوتية كمشغل صوتي عادي للأعضاء الذين يقرؤون دردشة المنتدى الخاصة به، وليس كرابط تحميل، وهو ما تطلب بعض العناية لتحقيقه بشكل صحيح.
يحصل كل موقع على لون مميز (Accent Colour) وزاوية وعنوان لوحة خاصين به، بحيث يمكن أن تبدو ثلاثة مواقع وكأنها ثلاثة منتجات مختلفة بدلاً من ثلاثة نسخ من نفس الودجت (Widget). يحصل الزوار على صوت إشعار يمكنهم تعطيله، وتجاوز (Override) لوضع الفاتح أو الداكن، والقدرة على كتم محادثة، مما يُكتب في عضويتهم الفعلية في Discourse ليتبعهم مرة أخرى إلى المنتدى.
يتم عرض كل شيء داخل Shadow DOM. يعمل هذا على صفحات لا أتحكم بها، ولا ينبغي أن يكون بإمكان أي من الطرفين (الموقع المضيف أو الودجت) كسر CSS الخاص بالآخر.
ما الذي لا يعمل، ولماذا
لا توجد مكالمات صوتية أو فيديو. هذا متعمد وخارج النطاق.
التسليم ليس فوريًا. تصل الرسائل في حوالي ثلاث ثوانٍ بينما تكون اللوحة مفتوحة. يستحق هذا شرحًا، لأن Discourse ينشر أحداث الدردشة إلى MessageBus ويبدو أنه يجب أن يعمل ببساطة.
لا يمكن لمتصفح على نطاق (Domain) آخر المصادقة على نقطة نهاية MessageBus. يسمح سياسة CORS بأربعة ترويسات طلب (Request Headers) ولا يحمل أي منها رمز حامل (Bearer Token)، ومسار معامل الاستعلام (Query Parameter) إلى مصادقة Discourse مقيد بنقاط نهاية RSS والتقويم. الترويسة الوحيدة التي تعمل، X-Shared-Session-Key، تحل إلى UserAuthToken وبالتالي تمكّن أي طلب يحملها، وليس فقط طلبات MessageBus. تسليم ذلك لصفحة تضمين (Embedding Page) سيحوّل ثغرة حقن جافاسكربت عبر المواقع (XSS) على موقع تسويقي إلى اختطاف كامل لحساب المنتدى. ثلاث ثوانٍ هي الصفقة الأفضل.
يوجد مسار أكثر أمانًا للوقت الحقيقي موثق في المستودع، يستخدم قناة MessageBus يكون اسمها غير القابل للتخمين هو نفسه القدرة (Capability)، ومحدودة النطاق لجلسة الودجت الواحدة بدلاً من حساب المستخدم. لم أبنيه بعد. إذا أراد أحدهم ذلك، فالمنطق موجود في docs/decisions.md.
الشيء الذي يجب فهمه قبل التثبيت
تسجيل موقع ويب يمنح ذلك الموقع وصولًا عبر المصدر (Cross-Origin) إلى منتدك حاملًا بيانات اعتماد أعضائك. هذا ليس أثرًا جانبيًا، بل هو الآلية نفسها.
إذا تم اختراق موقع قمت بتسجيله، يمكن لمهاجم قادر على تشغيل جافاسكربت هناك التصرف نيابة عن أي عضو يزوره. ليس فقط في الدردشة. أي شيء يمكن لهذا العضو فعله على المنتدى.
لذلك، سجّل فقط المواقع التي تتحكم بها. تسجيل موقع شريك أو عميل يعني قبول أمنهم كأمنك الخاص. تقول الإضافة هذا على صفحة الإدارة بجانب الحقل الذي تكتب فيه المصدر (Origin)، بدلاً من وضعه في مستند لا يفتحه أحد، وتشرح SECURITY.md ما تفعله الإضافة لتقليل نطاق التأثير (Blast Radius) وما تتعمد عدم فعله.
التثبيت
أضفه إلى تعريف الحاوية (Container Definition) وأعد البناء مرة واحدة:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone --depth 1 https://github.com/capodieci/discourse-chat-bridge.git
env:
DISCOURSE_ENABLE_CORS: true
ثم فعّل chat_bridge_enabled في الإدارة، الإعدادات، وكل شيء آخر موجود على صفحة واحدة في /chat-bridge/admin، حيث تضيف موقعًا ويب وسيمنحك وسم السكربت.
ملاحظتان ستوفران على أحدهم بعدًا من الوقت. تحتاج الرسائل الصوتية إلى تنسيقات صوتية في authorized_extensions، والتي لا تحتوي على أي شيء افتراضيًا، لذا تخبرك صفحة الإدارة بالضبط أيها مفقود. كما أن chat_allowed_groups افتراضيًا على مستوى الثقة 1، لذا لا يمكن للحسابات الجديدة تمامًا الدردشة حتى تكسبه، وهو ما يشرحه الودجت بدلاً من الفشل بصمت.
التوافق
تم الاختبار ضد إصدارات Discourse 2026.9.0 و2026.8.0، وفي الاستخدام الإنتاجي على منتدى واحد.
يعتمد هذا على كائنات خدمة الدردشة التي ليست واجهة برمجة تطبيقات عامة (Public API)، لذا يمكن لإصدار Discourse أن يحرّكها. يحتوي المستودع على سكربت فحص مسبق للقراءة فقط (Read-only Pre-flight Script) يتحقق من استمرار وجود كل واحد منها ويبلّغ في ثوانٍ بدلاً من عند أول طلب. يستحق التشغيل بعد الترقية.
ما أودّ الحصول عليه
أن يقوم أحدهم بتثبيته على منتدى ليس ملكي وإخباري بما انكسر. كل شيء حتى الآن تم التحقق منه ضد Discourse واحد، على خادم واحد، بواسطة شخص واحد، وهذا هو أضعف جانب فيه.
أود أيضًا أن أسمع من أي شخص يعرف إجابة أفضل لمشكلة MessageBus من تلك التي استقررت عليها.
