Peut-on inclure les noms d'utilisateur dans la charge utile du webhook user_badge ?

Y a-t-il une possibilité que les webhooks user_badge incluent username en plus de user_id ? Actuellement, la charge utile pour un événement user_badge ressemble à ceci :

{
  "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
  }
}

Obtenir le nom d’utilisateur pour un user_id (et granted_by_id s’il n’est pas égal à -1) nécessite un appel supplémentaire à /admin/users/{user_id}.json, ce qui non seulement requiert des requêtes supplémentaires, mais nécessite également des privilèges d’administrateur, si je ne m’abuse.

Y a-t-il un moyen d’obtenir le username pour un user_id donné en utilisant une clé API sans avoir besoin de privilèges d’administrateur ?

Je crée un service externe que j’espérais déclencher lorsqu’un ou plusieurs badges spécifiques sont attribués ; cependant, il a besoin du nom d’utilisateur et les webhooks de badges ne renvoient que des identifiants d’utilisateur. :confused:

Pour information, ma solution de contournement pour les webhooks non standard ou lorsque j’ai besoin de données supplémentaires consiste à le faire via Workflows et d’envoyer ceci à n8n.

Pour information, récupérer le nom d’utilisateur à partir de l’identifiant n’est qu’à un appel d’API. À moins que vous n’attribuiez manuellement des milliers de badges instantanément, vous devriez être dans les limites par défaut de l’API.

Merci ! Je vais jeter un œil à Workflows.

C’est une excellente nouvelle ! Pourriez-vous s’il vous plaît fournir des détails sur cet appel API qui n’est pas /admin/users/{user_id}.json (lequel nécessite des privilèges d’administrateur) ? Je n’ai pas réussi à trouver un point d’accès permettant de le faire avec une clé API sans privilèges d’administrateur.

Quel est le problème avec une clé API d’admin ? C’est censé être un tâche en arrière-plan, non ?

Le principe du moindre privilège. Ce service externe sera exécuté sur un autre serveur et n’a besoin que d’un accès en lecture à Discourse pour effectuer son travail. Moins il y a de clés maîtresses qui circulent dans l’infrastructure, mieux c’est.

Cela soulève le point intéressant de savoir pourquoi il n’existe pas de point d’accès public permettant de récupérer un utilisateur par son identifiant, en ne montrant que les champs publics, lorsque login_required n’est pas sélectionné. Cela serait utile, non ? Je me demande pourquoi cela n’a pas été mis en place. Ou peut-être que seuls les utilisateurs connectés peuvent bénéficier de ce privilège, mais dans tous les cas, il devrait exister un moyen d’y accéder sans recourir au point d’accès administrateur…

@burke Une solution de contournement plus fastidieuse consisterait à consulter /groups/trust_level_0/members.json (ou un groupe plus spécifique) et à trouver l’utilisateur par son identifiant, car cette page contient également le nom d’utilisateur.

Cela s’explique par le fait que, pour tous les usages et dans toutes les situations, les identifiants d’utilisateur ne sont qu’une convention interne dans Discourse, et que nous sommes censés nous fier aux noms d’utilisateur, qui sont bien plus visibles et utilisés de manière générale.

Ce qui nous ramène à la demande initiale. Le webhook doit respecter ce concept et envoyer le nom d’utilisateur, et non l’identifiant utilisateur.

D’accord ! Cela semble être la meilleure solution. Ajouter username et granted_by_username et mettre en dépréciation user_id et granted_by_id pourrait permettre une période de transition sans rupture.

Actuellement, si un badge est modifié par le système (c’est-à-dire automatiquement), le webhook renvoie un granted_by_id de -1. Quel serait l’équivalent pour granted_by_username ?

  • "" (chaîne vide)
  • "<system>" (une valeur magique qui ne pourrait pas être un nom d’utilisateur)
  • null
  • Rien – c’est-à-dire, omettre granted_by_username de la charge utile s’il s’agit d’un événement déclenché par le système

Je suppose que null ou rien serait le plus logique.

-1 est en réalité un identifiant d’utilisateur valide qui possède un nom d’utilisateur (généralement system) associé, tout comme tout autre utilisateur. Il n’y a aucune raison de le traiter différemment.

Ici sur meta https://meta.discourse.org/u/system/summary

Ah. Merci de m’avoir appris cela. Donc, a priori, le champ granted_by_username serait simplement "system" pour les modifications automatisées.

@putty J’ai étudié les Workflows. Jusqu’à ce que (ou à moins que) les événements de badge incluent les noms d’utilisateur (et peut-être même indépendamment de cela, puisque je peux utiliser des filtres dans Workflows pour n’envoyer que les modifications dont mon service a besoin), cela semble être une excellente alternative ! Merci encore !

Avec les webhooks, il y a un X-Discourse-Event-Signature avec une signature HMAC-SHA256 de la charge utile utilisant un secret qui me permet de vérifier l’expéditeur. Y a-t-il une possibilité de signer les requêtes HTTP (POST) depuis Workflows ? Peut-être que je n’ai simplement pas encore trouvé. J’espérais qu’il y ait un réglage comme « Signer la requête à l’aide d’un secret » que je pourrais activer et pour lequel je pourrais fournir un secret. Le plus proche que j’ai vu est l’ajout d’un identifiant header_auth pour envoyer un secret dans un en-tête (par exemple, X-Discourse-Event-Secret: Discourse Rocks!). C’est mieux que rien, mais une signature utilisant un secret (plutôt que d’envoyer le secret lui-même) serait préférable.

Malheureusement, j’ai également utilisé l’envoi du secret via un en-tête. Peut-être que @j.jaffeux pourrait suggérer une meilleure alternative, que j’adopterai sans hésiter :sweat_smile:

J’ai créé une étape de code pour calculer une signature. Il existe une Web Crypto API qui peut calculer un HMAC-SHA256 beaucoup plus facilement, mais elle utilise une promesse, ce qui ne fonctionne pas à l’intérieur d’un eval. J’ai donc demandé à une IA de rédiger une méthode JavaScript native pour calculer un HMAC-SHA256, j’ai ajouté une variable avec la clé secret et le secret comme valeur (, et je suis arrivé à un script Code comme celui-ci :

// Available variables:
//   $input.item.json       - current item's JSON data
//   $input.all()           - array of all input items
//   $json                  - shortcut for $input.item.json
//   $("NodeName").item     - access the paired item from another node
//   $vars.KEY              - workflow variables
//   $site_settings.NAME    - site settings
//   $execution             - execution metadata (id, workflow_name, ...)
//   $current_user          - user running the workflow
//   $helpers.absoluteUrl(path) - convert a relative path to an absolute 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);

      // Handle surrogate pairs
      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 padding
    data.push(0x80);

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

    // Append 64-bit big-endian message length.
    // JS integers are safe here for normal workflow payload sizes.
    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);

  // Keys longer than the SHA-256 block size are first hashed.
  if (keyBytes.length > blockSize) {
    keyBytes = sha256(keyBytes);
  }

  // Pad key to 64 bytes.
  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
};

Cela est suivi d’une étape de requête HTTP avec un en-tête ayant la clé X-Discourse-Workflow-Signature et la valeur {{ $("Code").item.json.signature }}, un type de contenu JSON, et un corps de requête {{ $("Code").item.json.payload }}.

Le résultat est une requête HTTP comme celle-ci :

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 est une signature HMAC-SHA256 utilisant la valeur de l’argument secret… tout comme un webhook signé ! :slight_smile:

Ce n’est peut-être pas très élégant, mais cela semble fonctionner.

Mise à jour : J’ai préfixé la signature avec sha256= pour correspondre au style des signatures de webhook.

Ça a l’air vraiment sérieux ! Merci d’avoir partagé !

Pour information, j’ai testé cette implémentation native HMAC-SHA256 par rapport à la Web Crypto API pour confirmer qu’elles produisent les mêmes résultats pour des messages et des clés aléatoires, et les signatures correspondaient. Je ne peux pas garantir qu’il n’existe pas de cas particuliers où elles divergent, mais je suis suffisamment convaincu pour l’utiliser en production. :slightly_smiling_face: