Webhook

Webhook trimite un eveniment GAMEMONITORING către sistemul dvs. după o acțiune pe platformă. Este o cerere obișnuită POST cu body JSON, astfel încât site-ul, panoul sau serviciul de joc să poată reacționa automat.

Dacă setați recompense pentru voturi, conectați mai întâi Webhook-ul după acest ghid, apoi folosiți fluxul dedicat: Recompense pentru voturi.

Conectare

Această configurare leagă un proiect GAMEMONITORING de handlerul dvs. și oferă tokenul de semnare pentru verificarea cererilor primite.

  1. Deschideți Proiectele mele, creați un proiect sau alegeți unul existent, apoi mergeți la setările Webhook.
  2. Creați un endpoint HTTPS public care acceptă POST cu Content-Type: application/json și nu redirecționează cererile.
  3. În setările Webhook, introduceți URL-ul complet al handlerului, de exemplu https://panel.example.com/gamemonitoring-webhook, și salvați-l.
  4. Copiați tokenul de semnare din același bloc și introduceți-l în scriptul handlerului.
  5. În handler, verificați signature, procesați event_type necesar și returnați un răspuns 2xx reușit doar după procesarea evenimentului.

Pentru testare locală, puteți rula handlerul pe computerul dvs. și îl puteți expune printr-un URL HTTPS public. Folosiți ngrok sau un alt serviciu de tunelare, apoi introduceți URL-ul public generat în setările Webhook.

După configurare, trimiteți un Webhook de test din interfață și verificați statusul livrării. Pentru URL-ul din exemplu, serverul trebuie să accepte POST pe /gamemonitoring-webhook.

Dacă testul eșuează, începeți cu răspunsul handlerului: 401 înseamnă eroare de semnătură, 403 sau o pagină HTML de verificare indică de obicei WAF ori bot protection, iar timeout-ul înseamnă că URL-ul nu este accesibil din internet sau răspunde prea lent.

Cerințe pentru handler

  • URL-ul trebuie să fie accesibil din internet. Adresele locale, rețelele private și URL-urile cu login sau parolă nu sunt potrivite.
  • HTTPS este recomandat pentru producție. HTTP este acceptat, dar protejează mai slab datele în tranzit.
  • Handlerul trebuie să accepte metoda POST și body JSON fără redirectări.
  • Returnați 2xx doar după ce sistemul dvs. a procesat evenimentul. De obicei 204 No Content este suficient.
  • Dacă evenimentul nu poate fi procesat în siguranță, returnați un cod de eroare. 3xx, 4xx, 5xx, timeout-ul și eroarea de conexiune sunt tratate ca livrare eșuată.
  • Dacă folosiți firewall, bot protection sau allowlist, adăugați IP-urile GAMEMONITORING în excepții.
  • Nu returnați în răspuns tokenuri, stack trace, erori SQL sau alte detalii interne. Răspunsul handlerului este afișat în interfață, deci textul erorii trebuie să fie sigur și clar.

Datele evenimentului

Fiecare Webhook vine cu body JSON și câmpuri de bază:

  • event_type — ce eveniment trebuie procesat.
  • event_id — ID-ul unic al evenimentului. Folosiți-l împreună cu event_type pentru idempotency și protecție împotriva livrării repetate.
  • is_test — indică o livrare de test din interfață.
  • signature — semnătura body-ului evenimentului.
Exemplu de eveniment Webhook
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "example.event",
  "is_test": false,
  "signature": "0ac4c97a5d934599dbd78985c4bcbb6926e77b4809d2be56333b1b25f638f064"
}

Cum citiți exemplul: event_type arată ce eveniment trebuie procesat; event_id este necesar pentru idempotency înainte de schimbări de stare; is_test: true înseamnă o verificare tehnică a livrării; signature nu este dată de business și se folosește doar pentru autentificarea cererii.

Logica de procesare depinde de event_type. Pentru server.vote și project.vote, folosiți fluxul separat: Recompense pentru voturi.

Dacă is_test este true, verificați semnătura și returnați 2xx, dar nu modificați balanța, nu acordați obiecte și nu porniți operații de producție.

Exemplu de răspuns al handlerului

Finalizați fiecare eveniment primit cu un singur rezultat clar:

  • 204 No Content — semnătura este corectă, iar evenimentul a fost procesat sau omis în siguranță. Returnați același răspuns pentru o livrare de test și pentru un eveniment deja procesat.
  • 400 Bad Request — lipsesc câmpuri obligatorii. Aceasta înseamnă o eroare în handler sau un body de cerere neașteptat; nu porniți logica de business.
  • 401 Unauthorized — semnătura este invalidă. Nu faceți cereri API, nu modificați baza de date și nu acordați recompense.
  • 500 Internal Server Error — baza, coada sau un sistem intern este temporar indisponibil. Livrarea rămâne eșuată și poate fi retrimisă după remedierea cauzei.

Exemplu: dacă handlerul a primit evenimentul, a verificat semnătura, a salvat event_type + event_id și a procesat evenimentul, poate returna 204. Dacă baza este indisponibilă și evenimentul nu poate fi salvat, este mai bine să returnați 500, ca livrarea să nu fie marcată prea devreme ca reușită.

Verificarea semnăturii

Semnătura se află în câmpul signature. Verificați-o înainte de orice logică de business, cereri API și modificări în baza de date.

Pentru verificare, luați toate câmpurile body-ului în afară de signature, sortați cheile alfabetic și construiți un șir key=value unit prin &. Valorile boolean se scriu ca true sau false.

Șir pentru semnătură
event_id=9824cabb-2203-437e-9b6c-aba43dde3e4b&event_type=example.event&is_test=false

Pentru exemplul de mai sus, șirul de semnare se construiește doar din event_id, event_type și is_test. Apoi calculați HMAC-SHA256 cu tokenul de semnare din setările Webhook și comparați rezultatul cu signature din cerere.

Semnăturile precalculate din exemple folosesc tokenul demonstrativ paste-webhook-token-here. În handlerul dvs., folosiți tokenul din setările Webhook.

În handlerul dvs.:

  • construiți șirul pentru semnătură din chei sortate;
  • calculați HMAC-SHA256 cu tokenul de semnare;
  • comparați rezultatul cu signature folosind o funcție cu timp constant de comparare: hash_equals în PHP, timingSafeEqual în Node.js sau compare_digest în Python;
  • returnați 401 dacă semnătura este invalidă.

Pasul 1. Handler de bază

Începeți cu un handler care poate primi orice Webhook: citește JSON, verifică signature, procesează livrarea de test, verifică câmpurile de bază și returnează 204. În acest pas, handlerul doar confirmă că livrarea este acceptată corect. Adăugați logica specifică evenimentului după ce acest flux de bază funcționează.

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Add event-specific logic here.
syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

// 204 tells GAMEMONITORING that the delivery was accepted successfully.
http_response_code(204);

Pasul 2. Adăugați deduplicarea

Webhook folosește modelul at-least-once: același eveniment poate ajunge de mai multe ori. Înainte ca handlerul să schimbe starea sistemului dvs., faceți operația idempotentă pe baza event_type + event_id.

Creați mai întâi un tabel care salvează perechea event_type și event_id cu cheie unică. Dacă înregistrarea există deja, evenimentul a fost procesat.

Tabel cu evenimente Webhook procesate
CREATE TABLE gamemonitoring_webhooks (
  event_type varchar(64) NOT NULL,
  event_id varchar(100) NOT NULL,
  created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (event_type, event_id)
);

Apoi extindeți handlerul de bază: după verificarea semnăturii și a câmpurilor de bază, salvați event_type + event_id și executați schimbările de stare în aceeași tranzacție.

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Deduplicate it before event-specific logic.
$pdo = null;

try {
    // Add your local database connection for deduplication and event-specific work.
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=game;charset=utf8mb4', 'game', 'password', [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);

    // Keep deduplication and the real state change in one transaction.
    // If any step fails, return 500 so the delivery can be retried.
    $pdo->beginTransaction();

    // Store the event once. This requires the table to have a unique key on
    // (event_type, event_id). Duplicate deliveries affect zero rows.
    $deduplicate = $pdo->prepare('INSERT IGNORE INTO gamemonitoring_webhooks (event_type, event_id) VALUES (?, ?)');
    $deduplicate->execute([$eventType, $eventId]);

    // The event was already processed earlier. Return success without changing
    // state again, because duplicate delivery is expected.
    if ($deduplicate->rowCount() === 0) {
        $pdo->commit();
        http_response_code(204);
        exit;
    }

    // Add event-specific database changes here. Keep them after the
    // deduplication insert and inside this same transaction.

    // Commit only after deduplication and event-specific work both succeed.
    $pdo->commit();

    // Log only newly processed real events after the transaction succeeds.
    syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

    http_response_code(204);
} catch (Throwable $error) {
    // Roll back partial database work so the event can be retried safely.
    if ($pdo instanceof PDO && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    // 500 keeps the delivery failed instead of marking unfinished work as done.
    http_response_code(500);
}

Nu folosiți nickname-ul, ID-ul utilizatorului sau ID-ul serverului ca cheie de deduplicare: un utilizator poate declanșa evenimente diferite sau poate repeta mai târziu o acțiune permisă. Cheia trebuie să fie event_type + event_id.

Exemplu: handlerul a schimbat starea sistemului dvs., dar conexiunea s-a întrerupt înainte ca GAMEMONITORING să primească 204. Mai târziu livrarea este retrimisă și același eveniment sosește din nou. Handlerul trebuie să găsească event_type + event_id salvate, să sară schimbarea de stare repetată și să returneze 204.

Dacă handlerul nu poate procesa temporar evenimentul, returnați un răspuns cu eroare. După remedierea cauzei, livrarea poate fi retrimisă din interfață, dacă retrimiterea este disponibilă pentru acel eveniment.

Teste și retrimitere

O livrare de test (is_test: true) verifică URL-ul, semnătura și răspunsul HTTP al handlerului. Handlerul trebuie să treacă prin același flux de procesare: citește JSON, verifică signature, recunoaște is_test și returnează un răspuns 2xx reușit.

Un eveniment de test nu trebuie să modifice balanța, inventarul, rolurile, abonamentele sau alte date de producție. Pentru test sunt suficiente un log tehnic și răspunsul 204.

Dacă livrarea eșuează, interfața arată statusul, codul HTTP și răspunsul handlerului. După remedierea cauzei, livrarea eșuată poate fi retrimisă dacă retrimiterea este disponibilă pentru acel eveniment.