هل يمكن تضمين أسماء المستخدمين في حمولة webhook الخاصة بالشارات؟

هل يمكن أن تتضمن خطافات user_badge حقل username إلى جانب user_id؟ حاليًا، يبدو حمولة حدث user_badge على النحو التالي:

{
  "user_badge": {
    "id": 123456,
    "granted_at": "2026-09-25T14:54:42.073Z",
    "created_at": "2026-09-26T05:34:12.965Z",
    "post_id": 234567,
    "post_number": 1,
    "badge_id": 123,
    "user_id": 456,
    "granted_by_id": -1,
    "topic_id": 56789
  }
}

الحصول على اسم المستخدم المقابل لـ user_id (وأيضًا granted_by_id إذا لم يكن -1) يتطلب إجراء استدعاء إضافي إلى /admin/users/{user_id}.json، وهو ما لا يتطلب فقط طلبات إضافية، بل يتطلب أيضًا صلاحيات المدير على ما أعتقد.

هل هناك طريقة للحصول على username لمستخدم معين بناءً على user_id باستخدام مفتاح API دون الحاجة إلى صلاحيات المدير؟

أقوم بإنشاء خدمة خارجية كنت أأمل في تشغيلها عند منح شارة معينة أو أكثر؛ ومع ذلك، تحتاج الخدمة إلى اسم المستخدم، بينما تُرجع خطافات الشارات معرفات المستخدمين فقط فقط. :confused:

على أي حال، الحل البديل الذي أستخدمه للخطافات (Webhooks) غير القياسية أو عندما أحتاج إلى بيانات إضافية هو تنفيذ ذلك عبر Workflows ثم إرسال ذلك إلى n8n.

للتوضيح، فإن جلب اسم المستخدم من معرّف المستخدم يتطلب فقط استدعاءً واحدًا لواجهة برمجة التطبيقات (API)، ما لم تكن تمنح آلاف الشارات يدويًا في لحظة واحدة، فستكون ضمن حدود API الافتراضية دون أي مشكلة.

شكراً! سأبحث في موضوع Workflows.

هذا خبر رائع! هل يمكنك من فضلك تقديم تفاصيل عن ذلك الاستدعاء الواحد لواجهة برمجة التطبيقات الذي ليس /admin/users/{user_id}.json (والذي يتطلب صلاحيات المسؤول)؟ لم أتمكن من العثور على نقطة نهاية (endpoint) للقيام بذلك بمفتاح واجهة برمجة التطبيقات دون الحاجة إلى صلاحيات المسؤول.

ما المشكلة في مفتاح API الخاص بالمسؤول؟ هذا المفروض أن يكون مهمة في الخلفية، أليس كذلك؟

مبدأ الحد الأدنى من الامتيازات. هذه الخدمة الخارجية ستعمل على خادم آخر وتحتاج فقط إلى صلاحيات القراءة في Discourse لأداء عملها. كلما قل عدد المفاتيح الرئيسية المتداولة عبر البنية التحتية، كان ذلك أفضل.

يُثير هذا نقطة مثيرة للاهتمام، وهي لماذا لا يوجد نقطة نهاية عامة (endpoint) للحصول على مستخدم عبر المعرّف (id) تُظهر فقط الحقول العامة، إذا لم يتم اختيار login_required. أليس من المفيد ذلك؟ أتساءل لماذا لم يتم إنشاؤه. أو ربما يمكن للمستخدمين المسجّلين فقط الحصول على هذا الامتياز، ولكن في كل الأحوال، يجب أن يكون هناك طريقة للوصول إليه دون استخدام نقطة نهاية المسؤول (admin endpoint)…

@burke حل بديل أكثر تعقيدًا هو الاطلاع على /groups/trust_level_0/members.json (أو أي مجموعة أكثر تحديدًا) والبحث عن المستخدم عبر معرّفه، إذ تحتوي هذه الصفحة أيضًا على اسم المستخدم.

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

وهذا يعيدنا إلى الطلب الأصلي. يجب أن يلتزم خطاف الويب (Webhook) بهذا المفهوم وأن يرسل اسم المستخدم، وليس معرّف المستخدم.

متفق! يبدو أن هذا هو أفضل حل. يمكن أن يسمح بإضافة username و granted_by_username وإسقاط user_id و granted_by_id بفترة انتقالية غير كاسرة.

حاليًا، إذا تم تغيير الشارة بواسطة النظام (أي تلقائيًا)، فإن الـ Webhook يعيد granted_by_id بقيمة -1. ما المكافئ لـ granted_by_username؟

  • "" (نص فارغ)
  • "<system>" (قيمة سحرية لا يمكن أن تكون اسم مستخدم)
  • null
  • لا شيء – أي حذف granted_by_username من الحمولة (payload) إذا كان الحدث ناتجًا عن النظام

أفترض أن null أو لا شيء سيكون منطقيًا أكثر.

في الواقع، -1 هو معرف مستخدم صالح مرتبط باسم مستخدم (عادةً system) تمامًا مثل أي مستخدم آخر. لا يوجد سبب لمعاملة هذا المعرف بشكل مختلف.

هنا على meta https://meta.discourse.org/u/system/summary

آه. شكرًا لك على تعليمي ذلك. إذن، من المرجح أن يكون granted_by_username ببساطة "system" للتغييرات الآلية.

@putty لقد كنت أبحث في Workflows. حتى يتم تضمين أسماء المستخدمين في أحداث الشارات (Badges) (أو ربما بغض النظر عن ذلك، حيث يمكنني استخدام عوامل التصفية في Workflows لإرسال فقط التغييرات التي يحتاج خدمتي إلى معرفتها)، يبدو هذا خيارًا بديلًا رائعًا! شكرًا مرة أخرى!

مع الخطافات (Webhooks)، يوجد ترويسة X-Discourse-Event-Signature تحتوي على توقيع HMAC-SHA256 للحِمل (Payload) باستخدام سرّ (Secret) يتيح لي التحقق من المرسل. هل هناك احتمال لتوقيع طلبات HTTP (POSTs) من Workflows؟ ربما لم أجد الطريقة بعد. كنت آمل أن يكون هناك إعداد مثل “توقيع الطلب باستخدام السرّ” يمكنني تفعيله وتوفير السرّ. أقرب ما رأيته هو إضافة بيانات اعتماد header_auth لإرسال سرّ في ترويسة (على سبيل المثال، X-Discourse-Event-Secret: Discourse Rocks!). هذا أفضل من لا شيء، لكن التوقيع باستخدام سرّ (بدلاً من إرسال السرّ نفسه) سيكون تفضيلاً.

للأسف، اعتمدتُ أيضًا على إرسال المفتاح السري عبر ترويسة (Header). ربما يقترح @j.jaffeux بديلاً أفضل، وسأعتمد عليه فورًا أيضًا :sweat_smile:

أنشأت خطوة كود لحساب التوقيع. هناك واجهة برمجة تطبيقات Web Crypto API يمكنها حساب HMAC-SHA256 بسهولة أكبر بكثير، لكنها تستخدم Promise وهذا لن يعمل داخل eval. لذا، طلبت من الذكاء الاصطناعي إنشاء دالة JavaScript أصلية لحساب HMAC-SHA256، وأضفت متغيراً بمفتاح secret وقيمته هي السر، وانتهى بي الأمر إلى سكربت كود كهذا:

// المتغيرات المتاحة:
//   $input.item.json       - بيانات JSON للعنصر الحالي
//   $input.all()           - مصفوفة من جميع عناصر الإدخال
//   $json                  - اختصار لـ $input.item.json
//   $("NodeName").item     - الوصول إلى العنصر المزدوج من عقدة أخرى
//   $vars.KEY              - متغيرات سير العمل
//   $site_settings.NAME    - إعدادات الموقع
//   $execution             - بيانات وصفية للتنفيذ (المعرّف، اسم سير العمل، ...)
//   $current_user          - المستخدم الذي يشغّل سير العمل
//   $helpers.absoluteUrl(path) - تحويل مسار نسبي إلى URL مطلق
//   console.log/warn/error - التسجيل (Logging)

function hmacSha256(message, key) {
  function utf8Bytes(str) {
    const bytes = [];

    for (let i = 0; i < str.length; i++) {
      let code = str.charCodeAt(i);

      // التعامل مع أزواج Surrogate
      if (
        code >= 0xd800 &&
        code <= 0xdbff &&
        i + 1 < str.length
      ) {
        const next = str.charCodeAt(i + 1);

        if (next >= 0xdc00 && next <= 0xdfff) {
          code =
            0x10000 +
            ((code - 0xd800) << 10) +
            (next - 0xdc00);
          i++;
        }
      }

      if (code <= 0x7f) {
        bytes.push(code);
      } else if (code <= 0x7ff) {
        bytes.push(
          0xc0 | (code >> 6),
          0x80 | (code & 0x3f)
        );
      } else if (code <= 0xffff) {
        bytes.push(
          0xe0 | (code >> 12),
          0x80 | ((code >> 6) & 0x3f),
          0x80 | (code & 0x3f)
        );
      } else {
        bytes.push(
          0xf0 | (code >> 18),
          0x80 | ((code >> 12) & 0x3f),
          0x80 | ((code >> 6) & 0x3f),
          0x80 | (code & 0x3f)
        );
      }
    }

    return bytes;
  }

  function rotr(x, n) {
    return (x >>> n) | (x << (32 - n));
  }

  function sha256(bytes) {
    const K = [
      0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
      0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
      0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
      0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
      0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
      0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
      0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
      0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
      0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
      0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
      0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
      0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
      0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
      0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
      0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
      0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    ];

    const H = [
      0x6a09e667,
      0xbb67ae85,
      0x3c6ef372,
      0xa54ff53a,
      0x510e527f,
      0x9b05688c,
      0x1f83d9ab,
      0x5be0cd19
    ];

    const data = bytes.slice();
    const bitLength = data.length * 8;

    // تعبئة SHA-256
    data.push(0x80);

    while ((data.length % 64) !== 56) {
      data.push(0);
    }

    // إضافة طول الرسالة بصيغة 64 بت بنظام Big-endian.
    // أرقام JS آمنة هنا لحجم حمولة سير العمل العادية.
    const high = Math.floor(bitLength / 0x100000000);
    const low = bitLength >>> 0;

    data.push(
      (high >>> 24) & 0xff,
      (high >>> 16) & 0xff,
      (high >>> 8) & 0xff,
      high & 0xff,
      (low >>> 24) & 0xff,
      (low >>> 16) & 0xff,
      (low >>> 8) & 0xff,
      low & 0xff
    );

    const w = new Array(64);

    for (let offset = 0; offset < data.length; offset += 64) {
      for (let i = 0; i < 16; i++) {
        const j = offset + i * 4;

        w[i] =
          ((data[j] << 24) |
            (data[j + 1] << 16) |
            (data[j + 2] << 8) |
            data[j + 3]) >>> 0;
      }

      for (let i = 16; i < 64; i++) {
        const s0 =
          rotr(w[i - 15], 7) ^
          rotr(w[i - 15], 18) ^
          (w[i - 15] >>> 3);

        const s1 =
          rotr(w[i - 2], 17) ^
          rotr(w[i - 2], 19) ^
          (w[i - 2] >>> 10);

        w[i] =
          (
            w[i - 16] +
            s0 +
            w[i - 7] +
            s1
          ) >>> 0;
      }

      let a = H[0];
      let b = H[1];
      let c = H[2];
      let d = H[3];
      let e = H[4];
      let f = H[5];
      let g = H[6];
      let h = H[7];

      for (let i = 0; i < 64; i++) {
        const S1 =
          rotr(e, 6) ^
          rotr(e, 11) ^
          rotr(e, 25);

        const ch = (e & f) ^ (~e & g);

        const temp1 =
          (
            h +
            S1 +
            ch +
            K[i] +
            w[i]
          ) >>> 0;

        const S0 =
          rotr(a, 2) ^
          rotr(a, 13) ^
          rotr(a, 22);

        const maj =
          (a & b) ^
          (a & c) ^
          (b & c);

        const temp2 = (S0 + maj) >>> 0;

        h = g;
        g = f;
        f = e;
        e = (d + temp1) >>> 0;
        d = c;
        c = b;
        b = a;
        a = (temp1 + temp2) >>> 0;
      }

      H[0] = (H[0] + a) >>> 0;
      H[1] = (H[1] + b) >>> 0;
      H[2] = (H[2] + c) >>> 0;
      H[3] = (H[3] + d) >>> 0;
      H[4] = (H[4] + e) >>> 0;
      H[5] = (H[5] + f) >>> 0;
      H[6] = (H[6] + g) >>> 0;
      H[7] = (H[7] + h) >>> 0;
    }

    const result = [];

    for (let i = 0; i < H.length; i++) {
      result.push(
        (H[i] >>> 24) & 0xff,
        (H[i] >>> 16) & 0xff,
        (H[i] >>> 8) & 0xff,
        H[i] & 0xff
      );
    }

    return result;
  }

  function bytesToHex(bytes) {
    return bytes
      .map(b => b.toString(16).padStart(2, "0"))
      .join("");
  }

  const blockSize = 64;

  let keyBytes = utf8Bytes(key);
  const messageBytes = utf8Bytes(message);

  // المفاتيح الأطول من حجم كتلة SHA-256 يتم تجزئتها أولاً.
  if (keyBytes.length > blockSize) {
    keyBytes = sha256(keyBytes);
  }

  // تعبئة المفتاح إلى 64 بايت.
  while (keyBytes.length < blockSize) {
    keyBytes.push(0);
  }

  const innerPad = new Array(blockSize);
  const outerPad = new Array(blockSize);

  for (let i = 0; i < blockSize; i++) {
    innerPad[i] = keyBytes[i] ^ 0x36;
    outerPad[i] = keyBytes[i] ^ 0x5c;
  }

  const innerHash = sha256(
    innerPad.concat(messageBytes)
  );

  const hmac = sha256(
    outerPad.concat(innerHash)
  );

  return bytesToHex(hmac);
}

const payload = JSON.stringify({
    "the-json-payload": "goes-here"
});

const signature = "sha256=" + hmacSha256( payload, $vars.secret);

return {
    "payload": payload,
    "signature": signature
};

يأتي بعد ذلك خطوة طلب HTTP مع ترويسة بمفتاح X-Discourse-Workflow-Signature وقيمة {{ $("Code").item.json.signature }}، ونوع المحتوى JSON، وجسم الطلب {{ $("Code").item.json.payload }}.

النتيجة هي طلب HTTP مثل:

User-Agent: Faraday v2.14.4
Content-Length: 41
Accept: */*
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Content-Type: application/json
X-Discourse-Workflow-Signature: sha256=9ad2820b545013bdd18d4e7e6139624b91f576498e528201fa7759ec58dd598c

{"the-json-payload": "goes-here"}

X-Discourse-Workflow-Secret هو توقيع HMAC-SHA256 باستخدام قيمة الحجة secret… تماماً مثل Webhook موقّع! :slight_smile:

قد لا يكون الأمر جميلاً، لكنه يبدو أنه يعمل.

تحديث: أدرجت بادئة sha256= قبل التوقيع لمطابقة نمط توقيعات Webhook.

يبدو الأمر موثوقًا جدًا! شكرًا لمشاركتنا!

للتوضيح، قمت باختبار HMAC-SHA256 الأصلي مقابل Web Crypto API للتأكد من أنهما يعطيان نفس النتائج للرسائل والمفاتيح العشوائية، وتطابقت التواقيع. لا يمكنني ضمان عدم وجود حالات خاصة يختلف فيها سلوكهما، لكنني راضٍ بما يكفي لاستخدامه في بيئة الإنتاج. :slightly_smiling_face: