خطافات الويب ونشر الملخصات في Slack وTeams
يرسل قرار حدثًا موقّعًا إلى عنوان تختاره كلما اكتمل اجتماع أو جهزت ملاحظاته أو تغيّرت مهمة، وينشر ملخصًا منسّقًا في قناة Slack أو Teams. لا يحتاج شيء من ذلك إلى تطبيق في متجر أو إلى صلاحيات OAuth: تلصق عنوانًا في الإعدادات فقط.
الإعداد
يضيف مالك مساحة العمل أو مشرفوها الوجهات من «الإعدادات ← التكاملات». لا يرى الأعضاء الآخرون هذه الصفحة، ويُسجَّل كل إنشاء وتعديل وتدوير للسر وحذف في سجل نشاط مساحة العمل.
لكل مساحة عمل حتى ١٠ وجهات من ثلاثة أنواع: خطاف ويب موقّع (webhook)، وعنوان Incoming Webhook من Slack (slack)، وعنوان Workflows من Microsoft Teams (teams). تختار لكل وجهة الأحداث التي تصلها، ومستوى المحتوى، ولغة رسائل Slack وTeams.
حين تنشئ خطاف ويب يظهر سرّ التوقيع (whsec_…) مرة واحدة فقط؛ احفظه في مدير الأسرار لديك. «تدوير السر» ينشئ سرًا جديدًا، ويبقى القديم صالحًا للتوقيع ٢٤ ساعة كي تحدّث خادمك دون أن تفقد أي حدث.
زر «إرسال تجريبي» يرسل حدث ping مرة واحدة ويعرض النتيجة فورًا، وزر «إيقاف» يوقف الإرسال دون حذف الوجهة. يعرض سجل التسليم آخر ٥٠ محاولة بحالتها ورمز الرد ومدتها.
Zapier وMake وn8n
- Zapier
- أنشئ Zap يبدأ بـ «Webhooks by Zapier» ثم «Catch Hook»، وانسخ العنوان الذي يعطيك إياه إلى وجهة من نوع خطاف ويب، ثم اضغط «إرسال تجريبي» ليتعرّف Zapier على الحقول.
- Make
- ابدأ السيناريو بتطبيق «Webhooks» ثم وحدة «Custom webhook»، وانسخ عنوانها، ثم أرسل حدثًا تجريبيًا ليحدد Make بنية البيانات.
- n8n
- أضف عقدة Webhook بطريقة POST واستعمل عنوان الإنتاج (Production URL) لا عنوان الاختبار. إن كان n8n مستضافًا لديك فيجب أن يكون متاحًا على الإنترنت عبر HTTPS والمنفذ 443.
لا تتحقق هذه الأدوات من التوقيع تلقائيًا. إن احتجت إلى ذلك فأضف خطوة شيفرة تستعمل مثال Node أو Python أدناه، أو اجعل العنوان نفسه سرًا لا يُشارك.
الأحداث
| الحدث | متى يُرسل |
|---|---|
meeting.completed | حين يكتمل تفريغ الاجتماع. قد تصل الملاحظات بعده بدقائق في حدث notes.ready. |
notes.ready | حين تُكتب ملاحظات الاجتماع، ومرة أخرى كلما أُعيدت كتابتها (بعد تسمية متحدث أو تغيير إعدادات الملاحظات). |
task.created | لكل مهمة جديدة تظهر في الملاحظات. |
task.completed | حين يضع أحدهم علامة «منجزة» على مهمة. |
task.assigned | حين تُسند مهمة إلى عضو في مساحة العمل. إلغاء الإسناد لا يرسل حدثًا. |
ping | من زر «إرسال تجريبي» فقط، ولا يُعاد. |
الحمولة
كل حدث كائن JSON بترميز UTF-8، بالغلاف نفسه: id وtype وapi_version وcreated (ثوانٍ منذ ١٩٧٠) وworkspace، ثم data الخاصة بالحدث. النص العربي يصل كما هو بلا ترميز \u. التواريخ بصيغة ISO 8601 بتوقيت UTC، وتاريخ الاستحقاق due_date بصيغة YYYY-MM-DD.
في المهمة: owner هو الاسم كما ورد في الملاحظات، وassignee عضو مساحة العمل الذي أُسندت إليه، وdeadline الموعد كما قيل في الاجتماع، وtimestamp لحظة ذكرها في التسجيل. عنوان url هو صفحة الاجتماع في التطبيق (مع /en/ لوجهة إنجليزية)، ولا يفتحه إلا من يملك حسابًا في قرار وصلاحية على الاجتماع.
meeting.completed
{
"id": "evt_1a1a1a1a1a1a1a1a1a1a1a1a",
"type": "meeting.completed",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"title": "مراجعة خطة الإطلاق",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"source": "upload",
"recorded_at": "2026-09-27T09:00:00Z",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
}
}
}notes.ready
{
"id": "evt_2b2b2b2b2b2b2b2b2b2b2b2b",
"type": "notes.ready",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"title": "مراجعة خطة الإطلاق",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"source": "upload",
"recorded_at": "2026-09-27T09:00:00Z",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"notes": {
"language": "ar",
"summary": [
"راجع الفريق خطة الإطلاق في السعودية واتفق على موعد نهاية الربع.",
"ميزانية الإعلانات أعلى من المتوقع وتحتاج إلى موافقة."
],
"decisions": [
"اعتماد ميزانية الإعلانات في السعودية للربع القادم."
],
"tasks": [
{
"id": "tk_a3b4c5d6a3b4c5d6a3b4c5d6",
"text": "إرسال الخطة التفصيلية إلى الفريق",
"owner": "لينا",
"assignee": {
"email": "lina@noor.example"
},
"due_date": "2026-10-01",
"deadline": "غدًا صباحًا",
"status": "open",
"timestamp": "00:46",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=46"
},
{
"id": "tk_e7f8a9b0e7f8a9b0e7f8a9b0",
"text": "إصلاح خلل صفحة الدفع",
"owner": "كريم",
"assignee": null,
"due_date": null,
"deadline": "قبل الخميس",
"status": "open",
"timestamp": "00:23",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=23"
}
]
}
}
}task.created
{
"id": "evt_3c3c3c3c3c3c3c3c3c3c3c3c",
"type": "task.created",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"title": "مراجعة خطة الإطلاق",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"source": "upload",
"recorded_at": "2026-09-27T09:00:00Z",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"task": {
"id": "tk_a3b4c5d6a3b4c5d6a3b4c5d6",
"text": "إرسال الخطة التفصيلية إلى الفريق",
"owner": "لينا",
"assignee": null,
"due_date": null,
"deadline": "غدًا صباحًا",
"status": "open",
"timestamp": "00:46",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=46"
}
}
}task.completed
{
"id": "evt_4d4d4d4d4d4d4d4d4d4d4d4d",
"type": "task.completed",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"title": "مراجعة خطة الإطلاق",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"source": "upload",
"recorded_at": "2026-09-27T09:00:00Z",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"task": {
"id": "tk_a3b4c5d6a3b4c5d6a3b4c5d6",
"text": "إرسال الخطة التفصيلية إلى الفريق",
"owner": "لينا",
"assignee": {
"email": "lina@noor.example"
},
"due_date": "2026-10-01",
"deadline": "غدًا صباحًا",
"status": "done",
"timestamp": "00:46",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=46"
},
"completed_by": {
"email": "lina@noor.example"
}
}
}task.assigned
{
"id": "evt_5e5e5e5e5e5e5e5e5e5e5e5e",
"type": "task.assigned",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"title": "مراجعة خطة الإطلاق",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"source": "upload",
"recorded_at": "2026-09-27T09:00:00Z",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"task": {
"id": "tk_a3b4c5d6a3b4c5d6a3b4c5d6",
"text": "إرسال الخطة التفصيلية إلى الفريق",
"owner": "لينا",
"assignee": {
"email": "lina@noor.example"
},
"due_date": "2026-10-01",
"deadline": "غدًا صباحًا",
"status": "open",
"timestamp": "00:46",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=46"
},
"assigned_by": {
"email": "mona@noor.example"
}
}
}ping
{
"id": "evt_6f6f6f6f6f6f6f6f6f6f6f6f",
"type": "ping",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5",
"name": "Studio Nour"
},
"data": {
"destination": {
"id": "dst_0c1d2e3f0c1d2e3f0c1d2e3f",
"name": "Zapier"
},
"message": "Qarar can reach this endpoint."
}
}ما يُرسل وما لا يُرسل
حين تضيف وجهة فأنت تنقل محتوى اجتماعاتك إلى طرف ثالث تختاره: Zapier أو Slack أو Microsoft أو خادمك. يصل كل حدث من اجتماعات مساحة العمل إلى وجهاتها، أيًّا كان منشئ الاجتماع؛ واجتماعات مساحتك الشخصية تصل إلى وجهات مساحتك الشخصية فقط. لذلك تختار لكل وجهة أحد مستويين:
- المحتوى الكامل (
full) - عنوان الاجتماع، ونقاط الملخص، والقرارات، ونص المهام وأسماء أصحابها كما في الملاحظات، وبريد من أُسندت إليه المهمة ومن أنجزها أو أسندها، واسم مساحة العمل.
- الحد الأدنى (
minimal) - المعرّفات والروابط والأوقات والمدة والحالة وتاريخ الاستحقاق وأعداد النقاط فقط: لا عناوين ولا نصوص ولا أسماء ولا عناوين بريد. يناسب من يريد تنبيهًا يعود به إلى قرار.
لا يُرسل أبدًا: نص التفريغ، والصوت، وبصمات الصوت، وعناوين بريد الحاضرين من التقويم، وروابط المشاركة. ولا يصل إلى أي وجهة اجتماعٌ من مساحة عمل أخرى.
الحدث نفسه بمستوى الحد الأدنى: notes.ready
{
"id": "evt_2b2b2b2b2b2b2b2b2b2b2b2b",
"type": "notes.ready",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"notes": {
"language": "ar",
"summary_count": 2,
"decisions_count": 1,
"tasks_count": 2
}
}
}الحدث نفسه بمستوى الحد الأدنى: task.completed
{
"id": "evt_4d4d4d4d4d4d4d4d4d4d4d4d",
"type": "task.completed",
"api_version": "2026-09-27",
"created": 1790503934,
"workspace": {
"id": "ws_7d20e57d20e57d20e57d20e5"
},
"data": {
"meeting": {
"id": "mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b",
"completed_at": "2026-09-27T09:52:14Z",
"duration_seconds": 2710
},
"task": {
"id": "tk_a3b4c5d6a3b4c5d6a3b4c5d6",
"status": "done",
"due_date": "2026-10-01",
"url": "https://getqarar.com/m/mt_4f1c9a2b4f1c9a2b4f1c9a2b4f1c9a2b#t=46"
}
}
}الترويسات
| الترويسة | القيمة |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | Qarar-Webhooks/1 (+https://getqarar.com/en/developers/webhooks/) |
Qarar-Event-Id | معرّف الحدث (evt_…)، وهو نفسه id في الحمولة. ثابت عبر المحاولات. |
Qarar-Event-Type | نوع الحدث، مثل notes.ready. |
Qarar-Delivery-Id | معرّف عملية التسليم (dl_…)، كما يظهر في سجل التسليم. |
Qarar-Signature | الطابع الزمني والتوقيع، مثل: t=1790503934,v1=b833a37f88eed2326856a6484b01e3be64888c6c5c07f57f2f566c5015e08b1d |
التحقق من التوقيع
- افصل الترويسة
Qarar-Signatureعند الفواصل:tهو وقت الإرسال بالثواني، وv1توقيع بالنظام الست عشري. في الساعات الأربع والعشرين التي تلي تدوير السر تحمل الترويسة قيمتيv1، واحدة لكل سر. - احسب HMAC-SHA256 بمفتاح هو السر كاملًا كما هو (
whsec_…)، ورسالة هيtثم نقطة ثم جسم الطلب الخام كما وصل، قبل تحليل JSON أو إعادة ترتيبه. - قارن ناتجك بكل قيمة
v1مقارنةً ثابتة الزمن، واقبل الطلب إن طابقت إحداها. ارفض الطلب إن ابتعدtأكثر من ٥ دقائق عن ساعتك، فذلك يمنع إعادة إرسال طلب قديم التُقط.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the exact bytes Qarar sent (a Buffer or a string), before any JSON parsing.
export function verifyQararSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = String(header).split(',').map(part => part.trim().split('='));
const t = Number(parts.find(([key]) => key === 't')?.[1]);
const signatures = parts.filter(([key]) => key === 'v1').map(([, value]) => value);
if (!Number.isInteger(t) || !signatures.length) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
return signatures.some(signature => {
const given = Buffer.from(signature, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
import { createServer } from 'node:http';
createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
if (!verifyQararSignature(rawBody, request.headers['qarar-signature'] ?? '', process.env.QARAR_WEBHOOK_SECRET)) {
response.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody);
// Skip event.id if you have already handled it, then act on event.type.
response.writeHead(200).end();
}).listen(8080);import hashlib
import hmac
import time
def verify_qarar_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
items = [part.strip().split("=", 1) for part in header.split(",") if "=" in part]
timestamps = [value for key, value in items if key == "t"]
signatures = [value for key, value in items if key == "v1"]
if not timestamps or not signatures or not timestamps[0].isdigit():
return False
t = int(timestamps[0])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, signature) for signature in signatures)في Flask استعمل request.get_data()، وفي Django request.body، وفي FastAPI await request.body(): الجسم الخام لا الكائن المحلَّل.
مثال محسوب للاختبار: بهذا السر وهذا الطابع الزمني وهذا الجسم يجب أن تحصل على التوقيع نفسه.
secret whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
t 1790503934
body {"id":"evt_6f6f6f6f6f6f6f6f6f6f6f6f","type":"ping","api_version":"2026-09-27","created":1790503934,"workspace":{"id":"ws_7d20e57d20e57d20e57d20e5","name":"Studio Nour"},"data":{"destination":{"id":"dst_0c1d2e3f0c1d2e3f0c1d2e3f","name":"Zapier"},"message":"Qarar can reach this endpoint."}}
signature b833a37f88eed2326856a6484b01e3be64888c6c5c07f57f2f566c5015e08b1dالتسليم وإعادة المحاولة
تُعد المحاولة ناجحة حين يرد عنوانك بـ 2xx خلال ١٠ ثوانٍ. ردّ بسرعة وأجّل العمل الطويل إلى ما بعد الرد.
| الرد | ما يحدث |
|---|---|
| 2xx | تم التسليم. |
| 408 و429 و5xx، وانتهاء المهلة وأخطاء الشبكة | تُعاد المحاولة بعد دقيقة، ثم ٥ دقائق، ثم ٣٠ دقيقة، ثم ساعتين، ثم ٦ ساعات، ثم ١٢ ساعة، ثم ٢٤ ساعة: ٨ محاولات في نحو ٤٥ ساعة. |
| 410 Gone | تفشل المحاولة وتُوقف الوجهة. |
| أي 4xx آخر | تفشل دون إعادة. |
| 3xx | لا تُتبع إعادة التوجيه، وتُعد المحاولة فاشلة. |
- حين تفشل ١٠ أحداث متتالية نهائيًا توقف الوجهة، ويظهر السبب في الإعدادات حتى تعيد تشغيلها.
- قد يصل الحدث نفسه أكثر من مرة (بعد مهلة انتهت مثلًا بينما نجح عنوانك)، فتجاهل أي
Qarar-Event-Idعالجته من قبل. - لا يُضمن الترتيب: قد تصل
task.createdقبلnotes.ready. اعتمد علىcreatedوعلى الحالة في الحمولة. - تحتفظ كل وجهة بآخر ٥٠ عملية تسليم في سجلها.
قواعد عنوان الوجهة
يتحقق قرار من العنوان عند حفظه وقبل كل محاولة، حتى لا يُستعمل للوصول إلى شبكات داخلية:
- HTTPS فقط، على المنفذ 443، وبلا اسم مستخدم أو كلمة مرور داخل العنوان.
- يجب أن يشير اسم النطاق إلى عناوين عامة فقط. تُرفض العناوين الخاصة وعنوان الحلقة المحلية (loopback) والعناوين المحلية للرابط (link-local) وعناوين CGNAT والبث المتعدد وعناوين التوثيق، سواء كُتبت أرقامًا أو ظهرت في DNS.
- تُرفض الأسماء
localhostوما ينتهي بـ.localأو.internal، وأسماء خدمات البيانات الوصفية السحابية، ونطاق قرار نفسه. - عنوان Slack يجب أن يبدأ بـ
https://hooks.slack.com/services/، وعنوان Teams يجب أن يكون على*.webhook.office.comأو*.logic.azure.comأو*.powerplatform.com.
Slack
في Slack أنشئ تطبيقًا لمساحة عملك وفعّل Incoming Webhooks، أو استعمل تطبيقًا لديك، ثم اختر «Add New Webhook to Workspace» والقناة، وانسخ العنوان الذي يبدأ بـ https://hooks.slack.com/services/. لا يحتاج قرار إلى أي تطبيق في دليل Slack.
تصل رسالة بتنسيق Block Kit: عنوان الاجتماع، ونقاط الملخص، والقرارات، والمهام مع المسؤول وتاريخ الاستحقاق، وزر يفتح الاجتماع. تبدأ الأسطر العربية بعلامة الاتجاه من اليمين (U+200F) كي تُعرض من اليمين إلى اليسار.
Microsoft Teams
توقف Microsoft موصلات Microsoft 365 (Office 365 Connectors) تدريجيًا، والبديل هو تطبيق Workflows. في القناة اختر «…» ثم «Workflows»، وابدأ من قالب «Send webhook alerts to a channel» أو من المشغّل «When a Teams webhook request is received»، واحفظ، ثم انسخ عنوان الخطاف.
يصل منشور بطاقة Adaptive Card (الإصدار 1.5، باتجاه من اليمين إلى اليسار حين تكون اللغة العربية) فيها الملخص والقرارات والمهام وزر «فتح الاجتماع». تقبل هذه الصيغة عناوين Workflows وعناوين الموصلات القديمة ما دامت تعمل. يرتبط سير العمل بمن أنشأه، فأضف مالكًا مشاركًا كي لا يتوقف إن غادر.