اربط Puzzel بمنصتك
يمكن لـ Puzzel تسليم نتيجة اللاعب مباشرة إلى نظام آخر — نقاطه، ومدى تقدّمه، وما أجاب به — دون أن يكون ذلك النظام نظام LMS كاملاً. تضم هذه الصفحة كل قناة متاحة: ما الذي يخرج، ومتى يخرج، وأصغر ما عليك بناؤه لاستقباله.
- تخرج النتائج عبر
- webhook، أو رسالة إلى الصفحة المحيطة بالنشاط
- تستطيع صفحتك أن
- تسجّل دخول اللاعب وتعيد ضبط النشاط
- يُفعَّل
- لكل نشاط على حدة، ضمن قائمة المطوّر في المحرِّر
- متاح مع
- خطة مدفوعة — هذه الإعدادات مُعطّلة في الحساب المجاني
ما القناة التي تحتاجها؟
هناك ثلاثة أشياء يمكن أن تخرج من النشاط وشيء واحد يمكن أن يدخل إليه. واختيار المناسب منها يعتمد على سؤال واحد: هل يوجد اللاعب داخل صفحتك، أم في مكان مختلف تمامًا؟
تُرسَل كل نتيجة محفوظة بطلب POST إلى رابط URL تملكه، بصيغة JSON.
- استخدمها عندما
- قد يكون اللاعب في أي مكان — رابط مشترك، رمز QR، موقع شخص آخر — وتريد أن تصل النتيجة إلى قاعدة بياناتك.
- تحتاج إلى
- نقطة نهاية HTTPS تقبل طلب POST عابرًا للمصدر.
save_puzzle_results_via_webhookنفس بيانات JSON، تُرسَل إلى الصفحة التي تُضمِّن النشاط بدلاً من خادم.
- استخدمها عندما
- تُضمِّن النشاط في صفحة دورتك، وتستطيع الصفحة نفسها أن تفعل شيئًا بالنتيجة.
- تحتاج إلى
- iframe في صفحتك مع مستمع رسائل. بلا خادم وبلا CORS.
save_results_iframe_postmessageرسالة واحدة عند انتهاء اللاعب، لا تحمل سوى هذه الحقيقة.
- استخدمها عندما
- كل ما تريد معرفته هو ما إذا كان قد انتهى — لتمييز الدرس كمكتمل، أو فتح الدرس التالي، أو عرض شاشتك.
- تحتاج إلى
- iframe في صفحتك مع مستمع رسائل.
send_completion_signal_when_embeddedwebhook النتائج
فعّل "إرسال النتائج إلى webhook" في المحرِّر وأدخل رابط URL. ومنذ تلك اللحظة، في كل مرة يُحفظ فيها تقدّم اللاعب، يُرسل متصفحه النتيجة كاملة بطلب POST إلى ذلك الرابط بصيغة JSON.
- 1 افتح النشاط في المحرِّر وانتقل إلى قائمة المطوّر.
- 2 فعّل "إرسال النتائج إلى webhook" والصق رابط نقطة النهاية في الحقل الذي يليها. يجب أن يكون رابط URL كاملاً — فالنطاق المجرّد مرفوض — ويجب أن يكون https، لأن المتصفح يمنع أي طلب http عادي صادر من صفحة تُقدَّم عبر https.
- 3 العب النشاط مرة بنفسك. يصل أول طلب POST بمجرد أن تجيب عن شيء ما.
import express from 'express';
const app = express();
// The POST is made by the player's browser, so the browser asks permission
// first. Answer the preflight and the real request can land.
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', 'https://puzzel.org');
res.set('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
app.post('/puzzel/results', express.json(), async (req, res) => {
const result = req.body;
// One run posts several times as it goes. Key on the pair, not on an
// insert: each call carries the whole state, so the newest simply wins.
await db.results.upsert(
{ playerUid: result.playerUid, activityKey: result.activityKey },
result
);
// Only act on the finish once.
if (result.progress >= 100 || result.hasAlternateCompletion) {
await markCourseStepComplete(result);
}
res.sendStatus(200);
});تخرج نتيجة في كل مرة يُحفظ فيها الإدخال: بعد توقف عن الكتابة، عند وضع بطاقة، عند توقف الساعة، ومرة أخرى عند إتمام النشاط. الكلمات المتقاطعة الطويلة قد تُنتج عشرات الاستدعاءات لا استدعاءً واحدًا — لذا اكتب معالجك كعملية upsert مفتاحها playerUid و activityKey بدلاً من عملية إدخال. يحمل كل استدعاء الحالة الكاملة، فيحلّ الأحدث دائمًا محل الأقدم، ويعوّض الاستدعاء التالي أي استدعاء مفقود.
progress نسبة مئوية: 100 تعني اكتمال النشاط. بعض الأنواع يمكن أن تنتهي دون الوصول إلى هذه النسبة — اختبار أُجيب عنه بالكامل، أو حقل حل تم حلّه — وتلك تحمل hasAlternateCompletion بدلاً من ذلك. عامل كلتا الحالتين على أنهما إنهاء.
يصل طلب POST من التبويب الذي يُلعب فيه النشاط، لا من خادم Puzzel. معظم نقاط النهاية المُعدّة لاستقبال الـ webhooks تقبله فعلاً. وإن لم تصل أي طلبات إلى نقطتك، فهذا هو السبب: يطلب المتصفح الإذن أولاً، لذا أجب عن طلب OPTIONS التمهيدي بترويسة Access-Control-Allow-Origin ثم يتبعه طلب POST الفعلي.
لا يحمل الطلب أي توقيع، ويصل من متصفح لا تتحكم فيه، لذا يستطيع أي شخص يطّلع على الصفحة أن يرسل لك طلبًا مماثلاً. هذا لا يشكّل مشكلة عند تعبئة شريط تقدّم أو لوحة معلومات. أما بالنسبة لأي شيء لا تسمح لطالب بتحديده بنفسه — درجة معتمدة، شهادة، دفعة مالية — فتحقّق منه مقابل النتائج في لوحة تحكم Puzzel، أو دع موصلات درجات LMS تنقل النقاط بدلاً من ذلك.
النتائج إلى الصفحة المحيطة
إذا ضمّنت النشاط، يمكنك أن تجعل نفس بيانات JSON تُرسَل إلى صفحتك بدلاً من خادم. فعّل "إرسال النتائج إلى الصفحة الأصل" واستمع للرسالة. لا شيء يغادر المتصفح، فلا حاجة لبناء نقطة نهاية ولا للتفكير في CORS.
<iframe
src="https://puzzel.org/en/quiz/embed?p=-Nq8sample_activity_key"
width="100%"
height="700"
frameborder="0"
allowfullscreen
></iframe>
<script>
window.addEventListener('message', (event) => {
if (event.origin !== 'https://puzzel.org') return;
const result = event.data;
if (!result || !result.activityKey) return;
// Same shape the webhook posts, same advice: it arrives repeatedly.
saveProgress(result);
});
</script>يستقبل المستمع كل رسالة تُرسَل إلى الصفحة، بما في ذلك من إطارات أخرى وإضافات المتصفح. قارن event.origin بـ https://puzzel.org قبل أن تثق بمحتواها.
هذه توأم الـ webhook: الحقول نفسها، تُرسَل في اللحظات نفسها. وكل ما ورد تحت "ما الذي تحتويه النتيجة" ينطبق هنا أيضًا.
إشارة الإتمام
أصغر قناة، لما لا تعنيك النتيجة نفسها في شيء: فعّل "إرسال إشارة إتمام" وتحصل صفحتك على رسالة واحدة لحظة انتهاء اللاعب.
window.addEventListener('message', (event) => {
if (event.origin !== 'https://puzzel.org') return;
if (event.data?.completed !== true) return;
// { completed: true, activityKey: '-Nq8sample_activity_key' }
unlockNextLesson(event.data.activityKey);
});{
"completed": true,
"activityKey": "-Nq8sample_activity_key"
}تُرسَل الإشارة عمدًا بعد أن تكتمل عملية الحفظ التي تُنهي النشاط، لذا فإن الصفحة التي تتفاعل بقراءة النتيجة مرة أخرى ستجدها هناك.
تُرسل كلتا قناتي الرسائل إلى الصفحة التي تُؤطّر النشاط. وعند فتحه في تبويب مستقل لا يوجد من يُخبَر، فلا تُرسل أي رسالة.
ما الذي تحتويه النتيجة
شكل واحد، أيًا كانت القناة التي تحمله. تُفهرس إجابات اللاعب بمعرّفات عناصر النشاط نفسها، لذا تظهر المفاتيح نفسها في correctUids.
| الحقل | النوع | ما يفعله |
|---|---|---|
activityKey دائمًا string | string | النشاط الذي تنتمي إليه النتيجة. المفتاح نفسه الذي تراه في رابط URL الخاص بالنشاط، بعد ?p=. |
playerUid دائمًا string | string | من لعب، كمعرّف مجهول. ثابت لهذا اللاعب على هذا الجهاز، لذا هو ما تُفهرس عليه النتائج — وهو ليس عنوان بريد إلكتروني ولا حساب Puzzel. |
player أحيانًا object | object | حقول التسجيل التي يطلبها النشاط، كما أعددتها: name, email, class, student_id وغيرها. تكون غائبة إلى أن يسجّل اللاعب، وغائبة تمامًا في نشاط لا يطلب شيئًا. |
progress دائمًا number | number | مدى التقدّم، كنسبة مئوية. 100 تعني الانتهاء. |
timePassed دائمًا number | number | الوقت المُستغرَق في النشاط، بالميلي ثانية. |
lastPlayedAt دائمًا number | number | متى حُفظت هذه النتيجة، كطابع زمني Unix بالميلي ثانية. |
createdAt أحيانًا number | number | متى بدأت المحاولة، كطابع زمني Unix بالميلي ثانية. |
playerInput أحيانًا object | object | ما أدخله اللاعب فعليًا، مُفهرسًا بمعرّف العنصر الذي ينتمي إليه. يعتمد الشكل الداخلي على نوع النشاط — كلمة، قائمة بطاقات موضوعة، أو خيار مُختار. |
correctUids أحيانًا object | object | أي من تلك العناصر صحيح، مُفهرس بالطريقة نفسها. غائب ما دام لم تتم الإجابة عن شيء بعد. |
score أحيانًا number | number | النقاط المُحرزة، في الأنواع التي تُسجّل نقاطًا للتشغيلة. غائب في كل ما عدا ذلك — بما في ذلك تشغيلة سجّلت صفرًا بالفعل، لذا تحقّق من وجود المفتاح قبل قراءته. |
performance أحيانًا number | number | مقياس النوع الخاص لجودة الأداء، حيثما يحتفظ بواحد — الكلمات في الدقيقة في تمرين الطباعة، مثلاً. |
attempts أحيانًا number | number | رقم هذه التشغيلة: 1 في المرة الأولى، ويزيد واحدًا مع كل إعادة بدء. فقط في الأنواع التي تُنهي التشغيلة مبكرًا وتحسب إعادات المحاولة. |
knockedOut أحيانًا boolean | boolean | انتهت التشغيلة عند إجابة خاطئة وهي منتهية دون اكتمال. |
hasAlternateCompletion أحيانًا boolean | boolean | انتهى النشاط بطريقة لا تصل إلى 100% — اختبار أُجيب عنه بالكامل، أو حقل حل تم حلّه. عامله كإتمام. |
missedKeys أحيانًا array | array | الأحرف التي ظلّ اللاعب يخطئ فيها، الأكثر خطأً أولاً. تمرين الطباعة فقط. |
contentVersion أحيانًا number | number | إصدار محتوى النشاط الذي جرت اللعبة عليه. يتغيّر عند تعديل المالك للأسئلة، لذا يمكن تمييز نتيجة قديمة عن نتيجة حالية. |
{
"activityKey": "-Nq8sample_activity_key",
"playerUid": "kK3r9TzSampleAnonymousUid",
"player": {
"name": "Ava Ortega",
"email": "ava@school.example",
"class": "5B"
},
"progress": 100,
"timePassed": 243120,
"lastPlayedAt": 1758297843120,
"createdAt": 1758297600000,
"playerInput": {
"-Nq8item_one": "Lisbon",
"-Nq8item_two": "1969"
},
"correctUids": {
"-Nq8item_one": true,
"-Nq8item_two": false
},
"score": 800,
"contentVersion": 1757923200000
}يُحذف الحقل غير المنطبق من JSON بدلاً من إرساله كـ null أو صفر. وبهذا يُميَّز النوع الذي لا يُسجّل نقاطًا عن التشغيلة التي سجّلت صفرًا — لذا اقرأ القيمة بافتراض قيمة بديلة، ولا تفترض أبدًا وجود المفتاح.
إعادة ضبط النشاط من صفحتك
تسير تعليمة واحدة في الاتجاه المعاكس. عند تفعيل "قبول المحفّزات من الصفحة الأصل"، تستطيع الصفحة القائمة بالتضمين مسح إجابات اللاعب وإعادة النشاط إلى البداية — لأجل زر "أعد المحاولة" خارج الإطار.
const frame = document.querySelector('iframe').contentWindow;
// Sent to the frame, from the page that embeds it — no other sender is
// accepted, and the activity has to have the trigger setting switched on.
frame.postMessage({ trigger: { type: 'reset' } }, 'https://puzzel.org');لا توجد رسالة لإرسال نتيجة، أو فتح رسالة الختام، أو الانتقال إلى سؤال. تُتجاهَل أي رسالة تطلب شيئًا غير إعادة الضبط.
يُقبل المحفّز من الصفحة التي تُؤطّر النشاط فقط ومن لا مكان آخر — لا من إطار مجاور، ولا من سكريبت في الصفحة. الإعداد مُعطّل افتراضيًا، لذا فعّله للأنشطة التي تديرها.
إحضار هوية اللاعب من منصتك
إذا كانت منصتك تعرف بالفعل من يلعب، فلا داعي لأن يسأله النشاط عن هويته مجددًا. توجد طريقتا مصافحة، كلتاهما لنشاط داخل صفحتك، وكلتاهما نُفعّلهما نحن بدلاً من المحرِّر — لأنهما تُغيّران الجهة التي تنتمي إليها النتيجة، لذا يتم إعدادهما معك بدلاً من خانة اختيار.
يُعلن النشاط عن نفسه برسالة 'app-loaded' وينتظر. تُرسل صفحتك بيانات اللاعب في المقابل، وتُسجَّل النتيجة باسمه دون أن يكتب اللاعب أي شيء أو يرى شاشة تسجيل.
نفس المصافحة، لكن صفحتك تُرسل رمز JWT الذي أصدره مزوّد الهوية بدلاً من الحقول نفسها. نتحقق منه مقابل الجهات المُصدِرة المُعدَّة لحسابك قبل السماح للاعب بالدخول، فتكون الهوية مؤكَّدة لا مجرد مصرَّح بها — وهذا هو الخيار المناسب عندما يجب أن تكون النتيجة موثوقة.
window.addEventListener('message', (event) => {
if (event.origin !== 'https://puzzel.org') return;
// The activity says it is ready for a player.
if (event.data === 'app-loaded') {
frame.postMessage(
{
player: { name: 'Ava Ortega', email: 'ava@school.example', class: '5B' },
// true starts a fresh attempt instead of resuming this player's last one
should_reset: false
},
'https://puzzel.org'
);
}
// Signed in; the board is coming.
if (event.data?.success === true) hideYourOwnSpinner();
});أخبرنا بأي الطريقتين تريد وأين ستُضمَّن الأنشطة، وسنُعِدّ حسابك ونشرح لك كل خطوة.
راسلنا عبر البريد بخصوص الهويةإنشاء الأنشطة من نظامك
كل ما سبق يتعلق بخروج النتيجة. أما في الاتجاه المعاكس — إنشاء الأنشطة نفسها من محتوى لديك بالفعل — فهذا هو Puzzle API: طلب POST واحد لكل نوع نشاط، وتحصل في المقابل على مفتاح ورابط URL للتضمين.
اطّلع على مرجع APIمتى يكون الموصل الجاهز هو الخيار الأفضل
إذا كانت المنصة في الطرف الآخر نظام LMS حقيقيًا، فمن المرجح أنك لا تحتاج إلى أي مما سبق. يمكن أن تعود الدرجات إلى دفتر درجاته تلقائيًا، دون أي شيء عليك استضافته.
يُشغَّل من داخل نظام LMS، وتُكتب النقاط تلقائيًا في دفتر درجاته.
انشر نشاطًا كواجب، وستعود الدرجات تلقائيًا.
منصات الدورات، ومواقع العضويات، والشبكات الداخلية، وأي شيء بنيته بنفسك — هذا بالضبط ما صُممت له القنوات في هذه الصفحة. تضمينٌ مع إشارة الإتمام يغطي معظم الحالات.
ما لا يوجد
حتى لا تبحث عنه:
- لا توجد نقطة نهاية لقراءة النتائج مرة أخرى. ينشئ API الأنشطة؛ وتخرج النتائج عبر القنوات في هذه الصفحة، أو عبر التصدير في لوحة التحكم.
- لا يوجد توقيع على الـ webhook. لا يوجد ما يُتحقَّق من الطلب مقابله — ولهذا لا ينبغي أن تكون النتيجة وحدها الضامن لأي شيء مهم.
- لا يوجد webhook على مستوى الحساب بأكمله. الرابط إعداد خاص بالنشاط، فالنشاط الذي تنسخه يحمله معه، والنشاط الجديد يبدأ بدونه.
- لا شيء في لعبة الفرق أو الغرفة المباشرة. قناتا الرسائل والـ webhook مُخصصتان للعب الفردي، وتُعطَّل الإعدادات تلقائيًا عند تفعيل اللعب الجماعي.
- لا توجد قائمة انتظار للتسليم. لا شيء يُخزَّن ويُعاد إرساله — الحفظ التالي هو إعادة المحاولة، وآخر استدعاء في التشغيلة هو ما يهم.
هل هناك خلل ما؟
أرسل الطلب الذي جرّبته والخطأ الذي حصلت عليه، وستحصل على إجابة حقيقية من الشخص الذي كتب نقطة النهاية.
راسل الدعم عبر البريد الإلكتروني