وبهوکها و رویدادها
نسخهٔ 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 عمومی دارد. فید و جریان برای پروگرامی مناسباند که چنین چیزی ندارد، مانند بازیای روی دستگاه بازیکن. پاکت در همهٔ دروازهها یکسان است، پس پروگرام میتواند با فید شروع کند و بعدها بدون تغییر در شیوهٔ خواندن رویداد به وبهوکها برود. مسیرهای رویداد به پروگرامهای بومی پاسخ میدهند. بازیای که در براوزر اجرا میشود هنوز نمیتواند آنها را فرا بخواند، زیرا /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 خالی نوشته شده باشد، پس بخشی از رمز Lingara را که پس از lgr_whsec_ میآید به آنها بدهید. کتابخانههای خود Lingara تمام رمز را میپذیرند.
زود پاسخ دهید، منتظر تلاش دوباره باشید
در ظرف 10 ثانیه با هر 2xx پاسخ دهید و کار را پس از آن انجام دهید. هر پاسخ دیگری، از جمله ختم مهلت یا 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 بهتنهایی، فید خالی است. از اکنون شروع میشود. برای رویدادهای حدود 30 روز گذشته start=oldest را بفرستید. نه نقطهٔ پایانی عمومی لازم دارد و نه رمز امضا: توکن دسترسی ثابت میکند چه کسی میپرسد. تبادلهٔ زیر هر دو حوزهای را که رویدادهای پلان درسی لازم دارند درخواست میکند.
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 رویداد. جریان ضربان (heartbeat) میفرستد، پس اتصال خاموش اتصالی مرده است.
curl -N "https://api.getlingara.com/v1/events/stream" \
-H "Authorization: Bearer $LINGARA_TOKEN"فرستادن رویداد به Lingara
POST /v1/events با events:write رویدادی را به شکل {type, data} به Lingara میفرستد: 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 استفادهٔ ماه تا امروز را نشان میدهد.
اگر ترجمه با مرجع انگلیسی فرق داشته باشد، مرجع انگلیسی درست است.