تخطّي إلى المحتوى
تكاملات المطوّرين

اربط Puzzel بمنصتك

يمكن لـ Puzzel تسليم نتيجة اللاعب مباشرة إلى نظام آخر — نقاطه، ومدى تقدّمه، وما أجاب به — دون أن يكون ذلك النظام نظام LMS كاملاً. تضم هذه الصفحة كل قناة متاحة: ما الذي يخرج، ومتى يخرج، وأصغر ما عليك بناؤه لاستقباله.

تخرج النتائج عبر
webhook، أو رسالة إلى الصفحة المحيطة بالنشاط
تستطيع صفحتك أن
تسجّل دخول اللاعب وتعيد ضبط النشاط
يُفعَّل
لكل نشاط على حدة، ضمن قائمة المطوّر في المحرِّر
متاح مع
خطة مدفوعة — هذه الإعدادات مُعطّلة في الحساب المجاني

ما القناة التي تحتاجها؟

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

خارج Puzzel
داخل Puzzel

webhook النتائج

فعّل "إرسال النتائج إلى webhook" في المحرِّر وأدخل رابط URL. ومنذ تلك اللحظة، في كل مرة يُحفظ فيها تقدّم اللاعب، يُرسل متصفحه النتيجة كاملة بطلب POST إلى ذلك الرابط بصيغة JSON.

الإعداد
  1. 1 افتح النشاط في المحرِّر وانتقل إلى قائمة المطوّر.
  2. 2 فعّل "إرسال النتائج إلى webhook" والصق رابط نقطة النهاية في الحقل الذي يليها. يجب أن يكون رابط URL كاملاً — فالنطاق المجرّد مرفوض — ويجب أن يكون https، لأن المتصفح يمنع أي طلب http عادي صادر من صفحة تُقدَّم عبر https.
  3. 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.

index.html
<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: الحقول نفسها، تُرسَل في اللحظات نفسها. وكل ما ورد تحت "ما الذي تحتويه النتيجة" ينطبق هنا أيضًا.

إشارة الإتمام

أصغر قناة، لما لا تعنيك النتيجة نفسها في شيء: فعّل "إرسال إشارة إتمام" وتحصل صفحتك على رسالة واحدة لحظة انتهاء اللاعب.

index.html
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"
}
تكون النتيجة محفوظة بالفعل عند وصولها

تُرسَل الإشارة عمدًا بعد أن تكتمل عملية الحفظ التي تُنهي النشاط، لذا فإن الصفحة التي تتفاعل بقراءة النتيجة مرة أخرى ستجدها هناك.

فقط داخل iframe

تُرسل كلتا قناتي الرسائل إلى الصفحة التي تُؤطّر النشاط. وعند فتحه في تبويب مستقل لا يوجد من يُخبَر، فلا تُرسل أي رسالة.

ما الذي تحتويه النتيجة

شكل واحد، أيًا كانت القناة التي تحمله. تُفهرس إجابات اللاعب بمعرّفات عناصر النشاط نفسها، لذا تظهر المفاتيح نفسها في 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 أو صفر. وبهذا يُميَّز النوع الذي لا يُسجّل نقاطًا عن التشغيلة التي سجّلت صفرًا — لذا اقرأ القيمة بافتراض قيمة بديلة، ولا تفترض أبدًا وجود المفتاح.

إعادة ضبط النشاط من صفحتك

تسير تعليمة واحدة في الاتجاه المعاكس. عند تفعيل "قبول المحفّزات من الصفحة الأصل"، تستطيع الصفحة القائمة بالتضمين مسح إجابات اللاعب وإعادة النشاط إلى البداية — لأجل زر "أعد المحاولة" خارج الإطار.

reset
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 الذي أصدره مزوّد الهوية بدلاً من الحقول نفسها. نتحقق منه مقابل الجهات المُصدِرة المُعدَّة لحسابك قبل السماح للاعب بالدخول، فتكون الهوية مؤكَّدة لا مجرد مصرَّح بها — وهذا هو الخيار المناسب عندما يجب أن تكون النتيجة موثوقة.

index.html
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 حقيقيًا، فمن المرجح أنك لا تحتاج إلى أي مما سبق. يمكن أن تعود الدرجات إلى دفتر درجاته تلقائيًا، دون أي شيء عليك استضافته.

ليست أيًا من هذه؟

منصات الدورات، ومواقع العضويات، والشبكات الداخلية، وأي شيء بنيته بنفسك — هذا بالضبط ما صُممت له القنوات في هذه الصفحة. تضمينٌ مع إشارة الإتمام يغطي معظم الحالات.

ما لا يوجد

حتى لا تبحث عنه:

  • لا توجد نقطة نهاية لقراءة النتائج مرة أخرى. ينشئ API الأنشطة؛ وتخرج النتائج عبر القنوات في هذه الصفحة، أو عبر التصدير في لوحة التحكم.
  • لا يوجد توقيع على الـ webhook. لا يوجد ما يُتحقَّق من الطلب مقابله — ولهذا لا ينبغي أن تكون النتيجة وحدها الضامن لأي شيء مهم.
  • لا يوجد webhook على مستوى الحساب بأكمله. الرابط إعداد خاص بالنشاط، فالنشاط الذي تنسخه يحمله معه، والنشاط الجديد يبدأ بدونه.
  • لا شيء في لعبة الفرق أو الغرفة المباشرة. قناتا الرسائل والـ webhook مُخصصتان للعب الفردي، وتُعطَّل الإعدادات تلقائيًا عند تفعيل اللعب الجماعي.
  • لا توجد قائمة انتظار للتسليم. لا شيء يُخزَّن ويُعاد إرساله — الحفظ التالي هو إعادة المحاولة، وآخر استدعاء في التشغيلة هو ما يهم.

هل هناك خلل ما؟

أرسل الطلب الذي جرّبته والخطأ الذي حصلت عليه، وستحصل على إجابة حقيقية من الشخص الذي كتب نقطة النهاية.

راسل الدعم عبر البريد الإلكتروني