نقاط النهاية
`webhooks.list` و`list_all` و`iterate` و`get` و`create` و`update` و`delete` و`rotate_secret` و`test` و`get_delivery` و`replay_delivery`، وسجلات التسليم والنشاط.
كل الدوالّ
endpoint = client.webhooks.create( url: "https://acme.com/hooks/mail", eventTypes: ["email.sent", "email.bounced"], description: "Billing service") File.write(".openemail-webhook-secret", endpoint[:secret]) client.webhooks.listclient.webhooks.get(endpoint[:id])client.webhooks.update(endpoint[:id], enabled: false)client.webhooks.test(endpoint[:id])latest = client.webhooks.list_deliveries(endpoint[:id], limit: 1).items.firstclient.webhooks.get_delivery(endpoint[:id], latest[:id])client.webhooks.replay_delivery(endpoint[:id], latest[:id])rotated = client.webhooks.rotate_secret(endpoint[:id])File.write(".openemail-webhook-secret", rotated[:secret])client.webhooks.delete(endpoint[:id])استدعاء create هو المرة الوحيدة التي يُعاد فيها السر، عدا rotate_secret. ولا تعيده القراءة أبدًا، فخزّنه قبل أي شيء آخر. وأغفل eventTypes لتحصل على المجموعة الافتراضية، أي كل حدث email.* عدا email.replied. ولا تصل email.replied وdomain.* وsuppression.* وfile.* وform.* إلى نقطة نهاية إلا إذا سمّتها.
ليس لـ rotate_secret نافذة تداخل. فالسر القديم يتوقف عن العمل فورًا، فانشر الجديد قبل التدوير. ولا تُعاد محاولته تلقائيًا أبدًا: فإعادة المحاولة ستدوّر مرة ثانية وتُبطل السر الذي أعادته المحاولة الأولى.
ولا تُعاد محاولة create أيضًا، فقد يترك فشل في الشبكة نقطة نهاية منشأة بسر لم تره قط. افحص list قبل أن تنشئها مجددًا. وتضم مساحة العمل 10 نقاط نهاية افتراضيًا، والتالية بعد بلوغ الحد تعطي 422 workspace_limit_reached.
ما يمكنك الاشتراك فيه
OpenEmail::WEBHOOK_EVENTS هو Hash مجمَّد بكل أسماء الأحداث، فتستطيع عرض القائمة دون طلب، ويعيد webhooks.list_events الأسماء نفسها مع جملة لكل منها، إضافة إلى الحدود التي تلتزم بها نقطة النهاية. والأحداث أحداث **صندوق البريد** لا أحداث هذه الواجهة: فـ email.received ينطلق للبريد الذي يصل إلى التطبيق، وemail.sent ينطلق لرسالة أرسلها محرّر الرسائل. والاشتراك ليس كمراقبة حركة API الخاصة بك.
ينطلق file.uploaded عندما يوضع ملف على صفحة الملفات، وfile.deleted عندما يُحذف ملف. ويحمل data الخاص بهما fileId وfilename وmimeType وsizeBytes وdirection وto وthreadId وmessageId، وuploadedAt أو deletedAt. وto هو العنوان الذي ينتمي إليه الملف، أو nil لملف ينتمي إلى مساحة العمل كلها.
أحداث الملفات ليست في المجموعة الافتراضية، فلا تستقبلها نقطة النهاية إلا إذا سمّتها في eventTypes. ونقطة النهاية المقيّدة ببعض العناوين لا تُبلَّغ إلا عن ملفات تلك العناوين، فالرفع الخاص بمساحة العمل كلها، مع to بقيمة nil، لا يُرسل إليها.
ينطلق form.submitted عندما يشترك شخص عبر أحد نماذجك، وform.confirmed عندما ينضم اشتراك معلّق إلى الجماهير، لأن الشخص فتح رابط التأكيد أو لأنك وافقت عليه. ويحمل data الخاص بـ form.submitted الحقول formId وformName وsubmissionId وemail وstatus وanswers وaudienceIds وsourceUrl وsubmittedAt. ويحمل data الخاص بـ form.confirmed الحقول formId وformName وsubmissionId وemail وaudienceIds وvia، وقيمته link أو approval، وconfirmedAt.
الاشتراك في نموذج بلا تأكيد مزدوج يرسل form.submitted مع status بقيمة added ولا يرسل form.confirmed، لذا عامل هذا الزوج على أنه لحظة انضمام الشخص. ومن يشترك مرة أخرى قبل التأكيد يحتفظ بنفس submissionId، ولا يُرسل form.submitted مجددًا إلا إذا تغيّرت إجاباته. وأحداث النماذج ليست في المجموعة الافتراضية، ونقطة النهاية المقيّدة ببعض العناوين لا تتلقاها أبدًا، لأن الاشتراكات تخص مساحة العمل كلها.
إثبات أنه يعمل
result = client.webhooks.test("whe_3f9c2a7b1e4d8f60a5c7b92d")puts result.dig(:delivery, :status), result.dig(:delivery, :responseCode) client.webhooks.iterate_deliveries("whe_3f9c2a7b1e4d8f60a5c7b92d") do |delivery| puts "#{delivery[:eventType]} #{delivery[:status]} #{delivery[:responseCode]} #{delivery[:error]}"endيرسل test حدث email.sent اصطناعيًا موقَّعًا وينتظر انتهاء المحاولة. ويعود بشكل طبيعي أيًّا كان ما أجاب به مستقبِلك، فتفرّع على delivery[:status]، لا على ما إذا كان الاستدعاء قد رفع خطأً. والرمز 4xx جواب مفيد: فعنوان URL قابل للوصول والرفض جاء من معالجك أنت، وغالبًا من فحصه للتوقيع.
قيمة responseCode بـ nil تعني أنه لم تكن هناك استجابة على الإطلاق (DNS، أو TLS، أو انتهاء مهلة)، وهي حقيقة مختلفة عن استجابة قالت 0. ويحمل كل صف attempt وmaxAttempts، فقد تصف عدة صفوف حدثًا واحدًا: فـ eventId المشترك بينها هو الحدث، ورقم المحاولة هو المحاولة. ويقول nextAttemptAt متى تحين إعادة المحاولة التلقائية بعد الصف.
إرساله مرة أخرى
detail = client.webhooks.get_delivery("whe_3f9c2a7b1e4d8f60a5c7b92d", "whd_8c1e4a7f2b9d3e6a0c5f1b28")p detail[:payload], detail[:responseBody], detail[:replayRefusal] replay = client.webhooks.replay_delivery("whe_3f9c2a7b1e4d8f60a5c7b92d", "whd_8c1e4a7f2b9d3e6a0c5f1b28")p replay.dig(:delivery, :status), replay.dig(:delivery, :responseCode)التسليم الذي يستمر في الفشل يُجرَّب حتى 8 مرات: فور حدوثه، ثم بعد دقيقة واحدة، و5 دقائق، و30 دقيقة، وساعتين، و5 ساعات، و10 ساعات، و10 ساعات أخرى، أي نحو 27 ساعة ونصف في المجموع. ولا يُكرَّر إلا الفشل الذي يستحق التكرار: لا استجابة، أو 408 أو 425 أو 429 أو 5xx. وإعادة التشغيل ترسل الحدث المخزَّن مرة أخرى بالقيم نفسها لـid وtype وcreatedAt وdata، فالمستقبِل الذي يُسقط المعرّفات التي عالجها من قبل يعامله على أنه الحدث الذي يعرفه. الجديد هو التوقيع وحده.
- يرسل
replay_deliveryحدثًا واحدًا الآن ويعيد ما أجاب به خادمك. ويعمل على محاولة سُلِّمت أيضًا، ولا تُعاد محاولته أبدًا. وقبل أن يرسل، تُوقَف مؤقتًا إعادات المحاولة التلقائية لذلك الحدث التي لم تبدأ بعد: تبقى ملغاة إذا سُلِّمت إعادة التشغيل، وتُستأنف في موعدها إذا فشلت. - إذا كانت إعادة محاولة تلقائية للحدث نفسه تُرسَل في تلك اللحظة، فلا يرسل
replay_deliveryشيئًا ويرفع 409retry_in_progress، وما دامت إعادة تشغيل أخرى له قيد الإرسال يرفع 409replay_in_progress، فلا يتلقى مستقبِلك نسختين في الوقت نفسه أبدًا، حتى من إعادتي تشغيل أُرسلتا في اللحظة نفسها. انتظر بضع ثوانٍ واقرأget_delivery، فقد تُسلِّم تلك المحاولة أو إعادة التشغيل الحدث. وإعادة التشغيل حدث واحد في كل مرة: لا يوجد استدعاء يعيد إرسال كل تسليم فاشل. - ويرفع أيضًا 409 لنقطة نهاية معطّلة (
webhook_disabled)، أو لحدث لم تعد نقطة النهاية تستمع إليه (event_not_subscribed) أو لم تعد تغطيه (event_out_of_scope)، أو لمحاولة لا حدث مخزَّنًا لها (delivery_not_replayable). ويبلّغget_deliveryعن ذلك الجواب مسبقًا بوصفهreplayRefusal.
لا يعيد الـ gem محاولة replay_delivery من تلقاء نفسه أبدًا، لأن إعادة المحاولة بعد استجابة ضائعة سترسل الحدث مجددًا.
المعاملات: webhooks.create
urlStringمطلوب- الوجهة التي تُرسَل إليها التسليمات بـ POST. بـ HTTPS حصرًا، ولا يجوز أن يكون المضيف `localhost` ولا اسمًا من نوع `.localhost` أو `.local` أو `.internal`، ولا عنوان IP حرفيًا من نوع loopback أو خاص أو CGNAT أو link-local. فهذا طلب من جهة الخادم إلى عنوان تزوّده أنت، ولذلك تعطي تلك الحالات 422 `invalid_webhook_url` على `url`. ويقرأ الفحص اسم المضيف كما كُتب، وكل تسليم يحلّ المضيف مجددًا ويرفض الإرسال إلى عنوان في أحد تلك النطاقات. ولا تتبع التسليمات عمليات إعادة التوجيه أبدًا، فسجّل العنوان النهائي. والمخزَّن هو ما يسلسله محلّل URL لما أرسلته، فـ `https://acme.com` يُقرأ مجددًا `https://acme.com/`.
eventTypesArray<String>- الأحداث التي تصل إلى نقطة النهاية هذه: أي من القيم في `OpenEmail::WEBHOOK_EVENTS`. ويحدّ `create` الـ Array بعدد الأحداث الموجودة، فواحد فوق ذلك يعطي 422 على `eventTypes`، أما `update` فلا يحدّها. والمحدود هو الطول فقط، والاسم المكرر يُخزَّن ويُقرأ تمامًا كما أرسلته. والإغفال أو الفراغ يُخزَّن كقائمة فارغة، ولهذا يُقرأ مجددًا `["*"]`، وهو يعني كل حدث `email.*` عدا `email.replied`، أي أربعة عشر حدثًا اليوم، ولا يعني أبدًا عائلات النطاقات أو الحظر أو الملفات أو النماذج. والعائلة المضافة لاحقًا لا تصل أبدًا إلى نقطة نهاية لم تسمّها، فلا يمكن لتكامل أن يبدأ باستقبال شكل لم يره قط بسبب إصدار جديد.
descriptionString- تسمية لنقطة النهاية، بحد أقصى 200 حرف، كي تُقرأ قائمة الـ webhooks كأسماء لا كعمود من عناوين URL. وإن أُغفلت، خُزّنت وأُعيدت كـ nil.
addressAllowlistArray<String>- عناوين مفردة تُبلَّغ عنها نقطة النهاية هذه. ويُسلَّم الحدث حين يكون العنوان الذي يخصه في هذه القائمة، أو حين يكون نطاقه في `domainAllowlist`. اترك القائمتين فارغتين فتُبلَّغ نقطة النهاية عن كل عنوان تملكه مساحة العمل. بحد أقصى 50، والعنوان الذي لا تملكه مساحة العمل هذه يعطي 422 `invalid_parameter`.
domainAllowlistArray<String>- نطاقات كاملة تُبلَّغ عنها نقطة النهاية هذه، بما في ذلك العناوين المضافة إليها لاحقًا. ويحمل النطاق أيضًا أحداث `domain.*` الخاصة به. بحد أقصى 25.
api_keyString- ينشئ نقطة النهاية بهذا المفتاح بدل مفتاح العميل.
الاستجابة: نقطة النهاية المُنشأة
Hash بمفاتيح من نوع Symbol. ويعيد get وlist وupdate الشكل نفسه دون secret.
objectString- دائمًا `webhook`، وهو المميِّز نفسه الذي تعيده القراءة العادية، لأن السر مفتاح إضافي واحد على الشكل العادي لا نوع كائن مستقل. ووجود `secret` من عدمه يحدّده التابع الذي استدعيته، لا هذا الحقل.
idString- معرّف نقطة النهاية: `whe_` متبوعًا بـ 24 حرفًا ست عشريًا. ويأخذه كل استدعاء webhook آخر: `get` و`update` و`delete` و`rotate_secret` و`test` و`list_deliveries` و`list_all_deliveries` و`iterate_deliveries` و`get_delivery` و`replay_delivery`.
urlString- نقطة النهاية كما خُزّنت، بعد اجتياز فحص HTTPS وفحص المضيفات المحظورة. وهي عنوان URL المحلَّل بعد إعادة تسلسله، فقارن بهذه القيمة لا بالـ String الذي أرسلته.
descriptionString or nil- التسمية التي أعطيتها لها، أو nil إن لم تعطها تسمية. و`update` الذي يرسل `description: nil` يمسحها.
eventTypesArray<String>- الأحداث المشترَك فيها، أو `["*"]` حين لم تسمِّ نقطة النهاية أي حدث. والقيمة `["*"]` هي طريقة عرض قائمة مخزَّنة فارغة عند القراءة ولا يمكن إرسالها مجددًا، وهي تمثّل أحداث الرسائل الأربعة عشر لا الفهرس كله. ولا يقبل `create` و`update` إلا أسماء الأحداث الحرفية.
enabledBoolean- ما إذا كانت التسليمات تُحاوَل. فنقطة النهاية المعطّلة تُتخطى عند إرسال الأحداث وتحتفظ بسرّها وبسجل تسليماتها. وهي true دائمًا هنا، لأن `update` وحده يأخذ `enabled`.
disabledAtString or nil- متى عطّل الخادم نقطة النهاية بعد 100 تسليم فاشل متتالٍ. nil ما دامت مفعّلة، وكذلك حين تكون قد عطّلتها بنفسك.
disabledReasonString or nil- لماذا عطّلها الخادم. nil كلما كانت قيمة `disabledAt` هي nil.
consecutiveFailuresInteger- التسليمات الفاشلة المتتالية. وأي حدث مُسلَّم يعيدها إلى 0، وكذلك يفعل `update` مع `enabled: true`.
addressAllowlistArray<String>- العناوين المفردة التي تُبلَّغ عنها نقطة النهاية هذه.
domainAllowlistArray<String>- النطاقات الكاملة التي تُبلَّغ عنها نقطة النهاية هذه. وخلوّ القائمتين يعني كل عنوان تملكه مساحة العمل.
lastDeliveryAtString or nil- طابع وقت بصيغة ISO 8601 لآخر محاولة تسليم، لا لآخر نجاح. ويُختم بعد طلب POST فاشل أيضًا، فهو يخبرك بأن نقطة النهاية جُرّبت، و`list_deliveries` يخبرك كيف سارت المحاولة. ويكون nil حتى المحاولة الأولى، ولذلك هو nil دائمًا في `create`.
createdAtString- طابع وقت بصيغة ISO 8601 لوقت تسجيل نقطة النهاية. ويعيد `list` نقاط النهاية من الأحدث إلى الأقدم بحسب هذا الحقل.
secretString- مفتاح HMAC-SHA-256 الذي يوقّع ترويسة `X-OpenEmail-Signature` في كل تسليم: `whsec_` متبوعًا بـ 43 حرفًا بترميز base64url، وهو ما تمرّره إلى `OpenEmail.verify_webhook_signature`، مع البادئة. ويعيده `create` و`rotate_secret` ولا شيء غيرهما. ولا تعيده القراءة أبدًا، فخزّنه الآن. والسر الضائع لا يمكن استبداله إلا بـ `rotate_secret`، الذي يُبطل القديم فورًا.
ترشيح السجلات
failed = client.webhooks.list_workspace_deliveries(status: "failed", since: Time.now - 86_400)p failed.items.map { |delivery| [delivery[:endpointId], delivery[:eventType], delivery[:responseCode]] } history = client.webhooks.list_activity("whe_3f9c2a7b1e4d8f60a5c7b92d")p history.items.map { |change| [change[:type], change.dig(:actor, :label)] }يقرأ list_deliveries نقطة نهاية واحدة، ويقرأ list_workspace_deliveries كل نقاط النهاية أو تلك التي يسمّيها endpoint_ids:، ويأخذ كلاهما status: وsince: وuntil:، وهي مرشِّحات تبويب التسليمات في لوحة التحكم. ويقرأ list_activity وlist_workspace_activity سجل التدقيق: من أنشأ ماذا أو غيّره أو بدّل حالته أو دوّره أو اختبره أو أعاد تشغيله أو أزاله. ولكل منها نسخة list_all_ ونسخة iterate_ إلى جانبه، وكل صف من سجل مساحة العمل يحمل endpointId. ويعيد webhooks.stats الأرقام التي خلف تبويب التحليلات لنافذة زمنية تختارها.
يأخذ since: وuntil: قيمة Time أو DateTime أو لحظة ISO 8601 في صورة String، وDate في Ruby يعني منتصف الليل UTC في ذلك اليوم. وuntil كلمة محجوزة في Ruby، لكنه يعمل كوسيط مسمّى مثل أي وسيط آخر: list_deliveries(id, since: start, until: finish).