تخطي إلى المحتوى
PollsLive

June 19, 2026 · 10 min read

لوحة نتائج مباشرة مع webhooks من PollsLive

دليل المطورين لـ webhooks في PollsLive: تحقق من توقيع HMAC، واستجب مباشرة لـ vote.created وأحداث الجلسة، وارجع إلى نقطة النتائج عند الحاجة.

By the PollsLive team·DevelopersHow-toTeams & meetings

نشغّل لوحة معلومات مثبّتة على الجدار في مكتبنا تعرض ما يحدث عبر المنتج. أردت نتائج الاستطلاع المباشرة عليها أثناء اجتماع الشركة العام، دون أن يضطر أحد إلى تشغيلها أو إيقافها يدويًا. تُخبرها webhooks الخاصة بـ PollsLive متى تبدأ الجلسة ومتى تنتهي، وتأتي الأرقام من نقطة النتائج. إليك التكامل، بما في ذلك التحقّق من التوقيع، لأنه لا يجب أن تثق أبدًا بـ webhook غير مُتحقَّق منه.

الأحداث التي يمكنك الاشتراك فيها

تسجّل عنوان URL لنقطة نهاية ومجموعة من الأحداث في Studio ← Developers. الأحداث التي تهمّني للوحة معلومات مباشرة:

  • `session.started` / `session.ended`: اعرض أو أزل البطاقة المباشرة.
  • `response.created`: إجابة غير متزامنة على شريحة (الحدث عالي التردّد). يذكر الاستطلاع والشريحة، لكن دون مجاميع.
  • `vote.created`: صوت جديد على استطلاع غير متزامن. يتضمّن `totalVotes` و`voteCounts`.
  • `poll.published` / `poll.closed`: دورة الحياة، مفيدة للأرشفة.

أمر واحد يحدّد التصميم كله: الإجابات في الجلسة المباشرة لا ترسل أي حدث. لا يُطلَق `response.created` و`vote.created` إلا للاستطلاعات غير المتزامنة، التي يجيب عنها الناس من رابط في وقتهم. ترسل الجلسة المباشرة `session.started` و`session.ended` ولا شيء بينهما، لذا يبدأ هذان الحدثان البطاقة ويوقفانها، وتأتي الأرقام من نقطة النتائج.

كل عملية تسليم هي طلب `POST` بصيغة JSON بنفس المظروف: اسم `event` وطابع زمني `createdAt` وكائن `data`. يختلف محتوى `data` بحسب الحدث. يضع `vote.created` و`response.created` الحقل `pollId` في المستوى الأعلى، وتضعه أحداث الجلسة في `data.session.pollId`، وترسل أحداث الاستطلاع الاستطلاع في `data.poll`. يحمل تسليم `response.created` الحقول `pollId` و`slug` و`slideId` و`kind`، و`isCorrect` لإجابات الاختبارات.

POST /webhooks/pollslive  (from PollsLive → your server)
X-PollsLive-Signature: t=1750320000,v1=4f9a…c2

{
  "event": "vote.created",
  "createdAt": "2026-06-19T16:00:00.000Z",
  "data": {
    "pollId": "poll_9aZ2kP",
    "slug": "hack-week",
    "optionIds": ["ai"],
    "totalVotes": 193,
    "voteCounts": { "ai": 79, "debt": 64, "tools": 37, "wild": 13 }
  }
}

تحقّق من التوقيع أولًا، دائمًا

كل طلب يحمل `X-PollsLive-Signature: t=<unix>,v1=<hex>`. قيمة `v1` هي HMAC-SHA256 لـ `<t>.<rawBody>` باستخدام سرّ التوقيع الخاص بنقطة النهاية. يجب أن تحسبها على جسم الطلب الخام (وليس JSON المُحلَّل)، وأن تقارن في وقت ثابت، وأن ترفض أي شيء يكون طابعه الزمني قديمًا جدًا لمنع إعادة التشغيل.

import crypto from "node:crypto";

const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → API للمطورين

export function verifyPollsLive(rawBody, header, toleranceSec = 300) {
  // header looks like: "t=1750320000,v1=4f9a…c2"
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

الفخّ الوحيد في Express: تحتاج إلى البايتات الخام، لذا سجّل معالج التحقّق بـ `express.raw()` (أو التقط الـ buffer) بدلًا من `express.json()` لذلك المسار. ثم أقرّ بالاستلام بسرعة: أعِد `2xx` فورًا وقم بالعمل الفعلي بشكل غير متزامن، وإلا سيعتبر PollsLive الاستجابة البطيئة تسليمًا فاشلًا.

import express from "express";
import { verifyPollsLive } from "./verify.mjs";

const app = express();

app.post(
  "/webhooks/pollslive",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.get("X-PollsLive-Signature") ?? "";
    if (!verifyPollsLive(req.body.toString("utf8"), sig)) {
      return res.status(400).send("bad signature");
    }
    const evt = JSON.parse(req.body.toString("utf8"));

    // ACK first, then process out of band.
    res.sendStatus(200);
    queue.add(evt); // e.g. push to a worker / pub-sub / websocket fan-out
  },
);

app.listen(3000);

تحويل الأحداث إلى بطاقة مباشرة

يتعامل الـ worker مع الحالتين بشكل منفصل. في الجلسة المباشرة، يبدأ `session.started` تحديثًا كل خمس ثوانٍ ويوقفه `session.ended`. وفي الاستطلاع غير المتزامن، كل `vote.created` أو `response.created` هو تنبيه يقول "حدث تغيّر ما"، ويُهدّئ الـ worker دفعة منها (حدث لكل إجابة قد يكون كثيرًا) في تحديث واحد. يحمل `vote.created` الأعداد فعلًا، لكن بقية الأحداث لا تحملها، لذا ينتهي كل مسار بالاستدعاء نفسه لـ `GET /polls/{id}/results`. هذا يُبقي عدد الطلبات منخفضًا حتى مع استطلاع مزدحم، والأرقام الحالية في صفحة حدود المعدّل.

const API = process.env.POLLSLIVE_API;
const auth = { Authorization: `Bearer ${process.env.POLLSLIVE_KEY}` };
const pending = new Map(); // pollId → debounce timer
const loops = new Map(); // pollId → refresh loop for a live session

function onEvent(evt) {
  switch (evt.event) {
    case "session.started":
      return startLoop(evt.data.session.pollId);
    case "session.ended":
      return stopLoop(evt.data.session.pollId);
    case "vote.created":
    case "response.created":
      return nudge(evt.data.pollId);
  }
}

function startLoop(pollId) {
  if (loops.has(pollId)) return; // a retried delivery can arrive twice
  refresh(pollId);
  loops.set(pollId, setInterval(() => refresh(pollId), 5000));
}

function stopLoop(pollId) {
  clearInterval(loops.get(pollId));
  loops.delete(pollId);
  teardownTile(pollId);
}

function nudge(pollId) {
  if (pending.has(pollId)) return; // a refresh is already due: debounce bursts
  pending.set(pollId, setTimeout(() => {
    pending.delete(pollId);
    refresh(pollId);
  }, 750));
}

async function refresh(pollId) {
  const res = await fetch(`${API}/polls/${pollId}/results`, { headers: auth });
  if (res.status === 429) {                 // backed-off retry on rate limit
    return setTimeout(() => refresh(pollId), 2000);
  }
  const { data } = await res.json();
  pushToDashboard(pollId, data.slides); // websocket → the wall display
}

يحتاج المفتاح إلى النطاق `results:read`. تقرأ البطاقة `slides` من النتائج، لا `voteCounts` في المستوى الأعلى: كل عنصر في `slides` يجمع الإجابات المباشرة والأصوات غير المتزامنة معًا، بينما لا يعدّ `voteCounts` إلا الأصوات غير المتزامنة ويبقى فارغًا لاستطلاع لم يُشغَّل إلا مباشرة. وإذا عُرض الاستطلاع أكثر من مرة، يجمع `slides` كل جلساته.

ينتهي الأمر بالجدار يعرض بطاقة مثل التي أدناه. أثناء الجلسة المباشرة تتحدّث كل خمس ثوانٍ، وهذا قريب بما يكفي من القاعة لشاشة جدارية:

Live poll

أين يجب أن نضع أسبوع الهاكاثون هذا العام؟

ميزات الذكاء الاصطناعي للمنتج الأساسي41% · 79
سداد الدين التقني / الموثوقية33% · 64
أدوات داخلية توفّر علينا الوقت19% · 37
بطاقة حرّة: أي شيء مقبول7% · 13
بطاقة توضيحية، تُملأ من GET /polls/{id}/results.

احتفظ دائمًا بخطة بديلة

قد تتأخّر الـ webhooks أو تُفقد. الشبكات شبكات. لذلك تعمل نفس دالة `refresh()` أيضًا على فترة أمان بطيئة (كل 30 ثانية) لكل استطلاع على الجدار. حزام وحمّالة: webhooks للتحديثات السريعة، واستدعاء لطيف كي لا تكون البطاقة خاطئة لمدة طويلة. إن فعلت واحدًا فقط، فليكن التحقّق؛ وإن فعلت اثنين، فأضف الخطة البديلة.

تعامل مع الـ webhook كإشارة "اذهب وانظر"، لا كمصدر للحقيقة. تحقّق منه، وهدّئه، ثم اقرأ نقطة النتائج، فتحصل على لوحة معلومات محدّثة وصامدة.

القائمة الكاملة للأحداث وصيغة التوقيع موجودة في دليل المطوّرين وفي مرجع الواجهة البرمجية التفاعلي. وإن كنت تؤتمت جانب *الإنشاء* أيضًا، فإن مقال زميلتي حول تشغيل اجتماعات الاستعراض من الواجهة البرمجية هو المقال المرافق.

Tip

حمولات webhook إشارات، لا مصدر للنتائج. معظمها بلا أعداد، لذا هدّئها ثم اقرأ نقطة النتائج للحصول على الأرقام.

اقرأ النتائج في Studio مع قراءة وتصدير نتائج الاستطلاع. راجع دليل التكاملات.

ابنِ على PollsLive: أنشئ استطلاعات، وشغّل جلسات مباشرة، واسحب النتائج، واستقبل الـ webhooks من كودك الخاص.

اقرأ وثائق المطوّرين

نستخدم ملفات تعريف ارتباط ضرورية لإبقائك مسجّل الدخول ولمنع التصويت المكرر. وبإذنك، نستخدم أيضًا Cloudflare Web Analytics لإحصاء مشاهدات الصفحات وWhop Pixel لمعرفة الزيارات التي تؤدي إلى عملية شراء. سياسة ملفات تعريف الارتباط