Lingara Lingara المستندات أدلة التعلّم API المكتبات التطبيقات البناء تطبيق الويب
اللغة: العربية

خطافات الويب والأحداث

إصدار API 2026-10-affable-towhee

تسجّل Lingara ما يطرأ على خطط الدروس واستخدام حسابك في صورة أحداث، وتستقبل الأحداث من لعبتك أو تطبيقك. لكل حدث، أيًّا كان الطريق الذي يسلكه، الغلاف نفسه، ويسرد كتالوج الأحداث جميعها.

الغلاف

يحمل كل حدث ستة حقول. يبدأ id بـ lgr_evt_، وهو فريد، وهو المفتاح الذي تُزال التكرارات على أساسه. يسمّي type الحدث. وcreated_at وقت وقوعه. وapi_version هو الإصدار الذي يتشكّل عليه data: الإصدار المثبَّت عليه عميلك، أو، في الموجز والبث، الإصدار الذي سمّاه طلبك في Lingara-Version. يبدأ subject بـ lgr_sub_ ويقول عمّن يدور الحدث: وهو ثابت لعميلك لكنه يختلف من عميل إلى آخر، ولا يكون أبدًا بريدًا إلكترونيًا أو اسمًا أو معرّف حساب. أما data فصغير، يسمّي الموارد بدل أن ينسخها: اجلب المورد بالنطاق الذي يحتاجه.

نوعان يحتاج كلٌّ منهما إلى جملة. قد يصل lesson_plan.ready مرتين للخطة الواحدة، أولًا وdata.status قيمته partial ثم complete: تصرّف عند الأول إن أردت خطة قابلة للاستخدام، أو انتظر complete لتحصل على كل المجموعات. لا يُرسَل usage.threshold_reached إلا للحسابات والعملاء ذوي الدفع حسب الاستخدام، وإذا تجاوز الاستخدام عدة عتبات دفعة واحدة فلا يُبلَّغ إلا عن أعلى عتبة تُجووزت، فلا تتوقّع حدثًا لكل عتبة.

سجلّ واحد وثلاث طرق لسماعه

تناسب خطافات الويب خادمًا لديه نقطة نهاية HTTPS عامة. ويناسب الموجز والبث برنامجًا لا يملك واحدة، مثل لعبة على جهاز اللاعب. الغلاف واحد على كل باب، فيستطيع البرنامج أن يبدأ بالموجز وينتقل إلى خطافات الويب لاحقًا دون أن يغيّر طريقة قراءته للحدث. تجيب مسارات الأحداث البرامج الأصلية (native). أما اللعبة التي تعمل في متصفح فلا تستطيع استدعاءها بعد، لأن /v1/ لا يجيب أي طلب تمهيدي (preflight) عابر للأصول.

من يسمع الحدث

يسمع العميل حدثًا حين يحمل events:read والنطاق الخاص بنوع الحدث، الذي يسرده الكتالوج، وحين يتعلّق الحدث بمالك العميل. وفي الموجز والبث تضيّق نطاقات رمز الوصول ذلك أكثر، ويضيّقه types إلى الأنواع التي تسمّيها. لا يذهب webhook.test إلا إلى نقطة النهاية التي أُرسل إليها، ولا يذهب إلى الموجز أبدًا، ولا يمكن الاشتراك فيه. ولا يذهب app.installed وapp.uninstalled إلا إلى عميل التطبيق نفسه، ولا يذهبان أبدًا إلى عميل آخر للحساب نفسه.

سجّل نقطة نهاية

سجّل نقطة النهاية في صفحة خطافات الويب في تطبيق Lingara على الويب، على العنوان app.getlingara.com/admin/webhooks، بعد أن تختار العميل أولًا. يجب أن يستخدم عنوان URL الخاص بها https على المنفذ 443، وألّا يُحلّ اسم مضيفها إلا إلى عناوين عامة. اختر الأحداث المُرسَلة: لا تُعرض إلا الأنواع التي تسمح بها نطاقات العميل. لا يمكن تعديل عنوان URL والأحداث لاحقًا: أضف نقطة نهاية جديدة واحذف القديمة. يبدأ سرّ التوقيع بـ lgr_whsec_ ولا يُعرض إلا مرة واحدة.

تحقّق من التسليم

كل تسليم طلب POST بثلاث ترويسات، وفق مواصفة Standard Webhooks: webhook-id (قيمة id للحدث)، وwebhook-timestamp، وwebhook-signature. مفتاح HMAC هو الجزء الذي يلي lgr_whsec_ من السرّ بعد فكّ ترميز base64، ولا يكون أبدًا السرّ بصفته سلسلة نصية. تحقّق قبل تحليل المتن، على بايتاته الخام، كما في الأسفل. ارفض التسليم الذي يبعد طابعه الزمني أكثر من خمس دقائق عن الآن، وهي القيمة الافتراضية لمكتبات Standard Webhooks: فهذا يمنع إعادة تشغيل تسليم ملتقَط.

signed   = webhook-id + "." + webhook-timestamp + "." + raw request body
key      = base64_decode(the secret after its prefix)
expected = "v1," + base64(hmac_sha256(key, signed))
accept   if |now - webhook-timestamp| <= 5 minutes
         and some entry of webhook-signature (space-separated) equals expected
             (compare in constant time)

تنشر Standard Webhooks أدوات تحقّق لمعظم اللغات. وهي تتوقّع سرًّا مكتوبًا على شكل whsec_ يليه base64، أو base64 مجرّدًا، فمرّر إليها الجزء الذي يلي lgr_whsec_ من سرّ Lingara. أما مكتبات Lingara نفسها فتقبل السرّ كاملًا.

أجب بسرعة، وتوقّع إعادة المحاولات

أجب بأي 2xx في غضون 10 ثوانٍ، وأنجز العمل بعد ذلك. أي شيء آخر، بما في ذلك انتهاء المهلة أو 3xx (لا تُتبَع عمليات إعادة التوجيه)، يُعاد بمهل متزايدة لمدة يوم تقريبًا. الرد بـ 410 على تسليم تلقائي يعطّل نقطة النهاية فورًا؛ أما الرد بـ 410 على حدث تجريبي أو على إعادة تسليم فلا يعطّلها. وبعد خمسة أيام من فشل التسليم تُعطَّل نقطة النهاية أيضًا. وفي الحالتين يُرسَل بريد إلكتروني إلى مالكها. ولا يُحتسب أبدًا عطل من جانب Lingara في تعطيل نقطة النهاية. من صفحة خطافات الويب يمكنك إرسال حدث تجريبي، أو إعادة تسليم أي تسليم من آخر 30 يومًا. كلٌّ منهما محاولة واحدة لا تُعاد أبدًا، ويُرسَل حتى إلى نقطة نهاية معطّلة.

التسليم مرة واحدة على الأقل ومن دون ترتيب. قد يصل الحدث نفسه مرتين، وقد تصل إعادة محاولة بعد حدث لاحق. تبقى webhook-id نفسها في كل إعادة محاولة، وفي إعادة التسليم حتى 30 يومًا لاحقًا. سجّل كل id عالجته لمدة 30 يومًا، وتجاهل التكرار. رتّب حسب created_at إن كان الترتيب مهمًا.

دوّر سرّ توقيع

يمكن لنقطة النهاية أن تحمل سرَّي توقيع في آنٍ واحد. وما دام كلاهما ساريًا، تحمل webhook-signature مدخلين v1,، ويستمر المستقبِل الذي يقبل أيًّا منهما في العمل. أضف السرّ الجديد إلى خادمك، وانشره، ثم ألغِ القديم.

الموجز

يعيد GET /v1/events برمز وصول العميل items (أغلفة) وnext_cursor وhas_more. يحتاج الرمز إلى events:read وإلى نطاق كل نوع تريد سماعه: مع events:read وحده يكون الموجز فارغًا. يبدأ من الآن. مرّر start=oldest لتحصل على أحداث آخر 30 يومًا تقريبًا. لا يحتاج إلى نقطة نهاية عامة ولا إلى سرّ توقيع: رمز الوصول يثبت من يسأل. يطلب الاستبدال أدناه كلا النطاقين اللذين تحتاجهما أحداث خطة الدرس.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=events:read lesson_plans:read" | jq -r '.access_token // error(.error)')"
curl "https://api.getlingara.com/v1/events" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

next_cursor موجود دائمًا: خزّنه ومرّره مجددًا بوصفه cursor. وهو معتم. تعني قيمة has_more true أن تستدعي مجددًا الآن، وتعني false أنك لحقت بكل شيء: استطلع لاحقًا، أو افتح البث. المؤشر الأقدم من 30 يومًا يُرفض بـ 410 وcursor_expired. ومن دون مؤشر يبدأ الموجز من الآن، وتُتخطّى الأحداث الواقعة بينهما. لاستعادتها، استدعِ مع start=oldest، الذي يعود إلى أقدم ما تُحفظ الأحداث، وتخطَّ قيم id التي عالجتها من قبل.

البث

يحمل GET /v1/events/stream الأحداث نفسها بوصفها أحداثًا مرسَلة من الخادم (server-sent events). قيمة data في كل إطار event غلاف واحد، وid: في كل إطار مؤشر، وهو القيمة نفسها التي في next_cursor، فتستطيع التنقّل بين الموجز والبث دون فجوة. بعد انقطاع الاتصال، أو إطار done (ينهي البث نفسه من حين لآخر)، أو إطار error، أعد الاتصال مع ضبط Last-Event-ID على آخر id: تلقّيته. تفعل معظم عملاء SSE ذلك نيابةً عنك، وكذلك tailEvents في مكتبات Lingara (أما streamEvents فيها فاتصال واحد). إنه مؤشر، لا id الحدث. يرسل البث نبضات دورية، فالاتصال الصامت اتصال ميت.

curl -N "https://api.getlingara.com/v1/events/stream" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

أرسل حدثًا إلى Lingara

يرسل POST /v1/events مع events:write حدثًا إلى Lingara بالشكل {type, data}: إما world.context_changed (مع scene وsource_lang وtarget_lang وlevel، واختياريًا npc له name وpersona، وtags)، وإما world.practice_requested (مع topic واللغتين والمستوى أنفسها). الترويسة Idempotency-Key مطلوبة: حتى 255 حرفًا مرئيًا من ASCII، مثل UUID. ومن دونها يكون الرد 400 وidempotency_key_required. اضبطها مرة واحدة لكل حدث، وأرسل المفتاح نفسه عند إعادة المحاولة. المفتاح الواحد حدث واحد: في غضون يوم، يحصل الطلب الثاني بالمفتاح نفسه على الرد الأول (متطابقًا بوصفه JSON، لا بايتًا ببايت)، حتى لو اختلف متنه، وبعد ذلك يحصل على الحدث نفسه، كما في الأسفل. الأحداث الواردة غير موقَّعة: رمز الوصول الخاص بك هو الإثبات. صِف العالم، لا اللاعب أبدًا: لا أسماء ولا محادثات في scene أو npc أو topic أو tags.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=events:write lesson_plans:write" | jq -r '.access_token // error(.error)')"
curl -X POST "https://api.getlingara.com/v1/events" \
  -H "Authorization: Bearer $LINGARA_TOKEN" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"world.context_changed","data":{"scene":"A night market in Taipei, just after rain","npc":{"name":"Auntie Lin","persona":"a street-food vendor who likes to haggle"},"source_lang":"en","target_lang":"zh","level":3,"tags":["market","food","chapter-2"],"generate":true}}'

كل حقل نصي سطر واحد من الأحرف المرئية، يُعدّ بعد التشذيب: scene وtopic حتى 160، وnpc.name حتى 32، وnpc.persona حتى 120. تُرفض فواصل الأسطر وعلامات الجدولة وسائر أحرف التحكم، وكذلك الأحرف غير المرئية وأحرف التنسيق: أحرف تجاوز الاتجاه، والأحرف ذات العرض الصفري عدا أحرف الوصل التي تحتاجها بعض أنظمة الكتابة والرموز التعبيرية، وكتلة الوسوم (tag block)، وأحرف الاستخدام الخاص. يحمل tags حتى 8 رموز آلية بأحرف صغيرة، طول كلٍّ منها حتى 24 حرفًا، ولا يصل أبدًا إلى خطة الدرس. قيمة level من 1 إلى 9، ويجب أن تختلف اللغتان. الطلب الخارج عن هذه الحدود يُرفض بـ 400 ولا يُسجَّل له أي حدث.

مع "generate": true (القيمة الافتراضية لـ world.practice_requested)، يحتاج الرمز أيضًا إلى lesson_plans:write. ومن دونه يُرفض الطلب بـ 403 ولا يُسجَّل أي حدث. ومعه تبدأ Lingara خطة درس، بالفحوص نفسها والفاتورة نفسها كما لو أنشأتها مباشرة، ويقول reaction في الرد 202 ما الذي حدث. مع started وplan_status بقيمة generating، يتبع ذلك lesson_plan.ready أو lesson_plan.failed قيمة data.plan_id فيه هي plan_id الوارد في الرد، على كل باب تستخدمه. ومع partial أو complete تكون الخطة قد جاءت من المكتبة ويمكن قراءتها الآن، ولا يُوعَد بأي حدث: قد يصل أحدها مع ذلك، فلا يستحق الانتظار إلا generating. ومع refused أو failed يبقى الحدث قائمًا. لا يُعاد بالمفتاح نفسه، فأرسل حدثًا جديدًا لتحاول مجددًا.

تحصل إعادة المحاولة في غضون يوم على الرد الأول. أما إعادة المحاولة بعد ذلك فيُعاد بناء ردها من الحدث المخزَّن، الذي يحتفظ بالخطة التي بدأها لكنه لا يحتفظ بسبب رفض التفاعل. لذا قد تجيب إعادة محاولة متأخرة بـ reaction قيمته failed وinternal: ومعنى ذلك أن النتيجة الأولى لم تُسجَّل، لا أنه لا توجد خطة. اقرأ الخطة بقيمة plan_id إن احتفظت بها، أو أرسل حدثًا جديدًا.

Tidewater Games: لعبة بلا خادم

تبني Tidewater Games، وهي استوديو خيالي، لعبة بمحرّك Godot يستكشف فيها اللاعب سوقًا ليلية. ويشغّل مطوّرها اللعبة على جهازه الخاص، بعميله الخاص.

يدخل اللاعب كشكًا للنودلز. فترسل اللعبة world.context_changed مع "generate": true، وهو الأمر الوارد في قسم الأحداث الواردة أعلاه، وتحتفظ بقيمة plan_id من الرد.

إذا كانت قيمة plan_status هي generating، تقرأ اللعبة البث، أو تستطلع الموجز، حتى يصل lesson_plan.ready يحمل plan_id ذاك. ثم تقرأ الخطة بالنطاق lesson_plans:read، كما في الأسفل. وإذا كانت الخطة complete من قبل، تقرؤها فورًا.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=lesson_plans:read" | jq -r '.access_token // error(.error)')"
curl "https://api.getlingara.com/v1/lesson-plans/$ID" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

لاحقًا يضيف الاستوديو خادمًا صغيرًا بنقطة نهاية HTTPS ويسجّله لـ lesson_plan.ready. فيصل الحدث نفسه إلى هناك، بقيمة id نفسها، ولا تتغيّر شيفرة اللعبة التي تقرؤه.

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

كم تكلّف الأحداث

يُحتسب في استخدام عميلك كل حدث وارد مقبول، وكل استدعاء للموجز، وكل بث يُفتح، كما يُحتسب كل استدعاء لـ /v1/. والحدث المرسَل مع "generate": true يُحتسب أيضًا خطةَ درس. ويُحتسب كل تسليم لخطاف ويب مرة واحدة لكل حدث ولكل نقطة نهاية، عند أول 2xx، أيًّا كانت المحاولة التي تحقّق فيها. ولا يُحتسب مجددًا أبدًا، ولا يُحتسب الحدث التجريبي أبدًا. يعرض GET /v1/usage الشهر حتى الآن.

إذا اختلفت ترجمةٌ عن المرجع الإنجليزي، فالمرجع الإنجليزي هو الصحيح.