وبهوکها و رویدادها
نسخهٔ 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 مصرف ماه تا امروز را نشان میدهد.
اگر ترجمه با مرجع انگلیسی تفاوت داشته باشد، مرجع انگلیسی معتبر است.