Recompense pentru voturi

Recompensele pentru voturi folosesc evenimentele Webhook server.vote și project.vote: handlerul primește evenimentul, obține datele votului prin API și acordă recompensa o singură dată.

Configurați mai întâi Webhook-ul proiectului și verificarea signature. Datele votului sunt cerute prin GET /votes/:vote_id.

Cum funcționează scenariul

  1. Primiți evenimentul Webhook și verificați signature. Dacă is_test este true, returnați 204 fără să cereți votul și fără să acordați recompensa.
  2. Asigurați-vă că event_type este server.vote sau project.vote.
  3. Folosiți event_id ca ID al votului.
  4. Obțineți datele votului prin GET /votes/:vote_id și găsiți jucătorul în sistemul dvs.
  5. Într-o singură tranzacție, aplicați protecția împotriva procesării repetate după event_type + event_id și acordați recompensa doar pentru un eveniment nou.
  6. Dacă recompensa nu poate fi acordată în siguranță, returnați un răspuns cu eroare. După remedierea cauzei, trimiteți din nou livrarea din interfață.

Exemplu de recompensă

Să presupunem că jucătorul PlayerName a votat pentru serverul cu ID 1, iar sistemul dvs. trebuie să îi adauge 100 monede.

  1. GAMEMONITORING trimite un Webhook cu event_type: server.vote și event_id: 9824cabb-2203-437e-9b6c-aba43dde3e4b.
  2. Handlerul verifică signature. Dacă semnătura este invalidă, returnează 401 și se oprește.
  3. Handlerul cere GET /votes/9824cabb-2203-437e-9b6c-aba43dde3e4b, primește nickname, server și utilizator, apoi găsește contul local.
  4. Într-o tranzacție, handlerul salvează event_type + event_id pentru protecția împotriva procesării repetate.
  5. Pentru un eveniment nou, handlerul adaugă 100 monede în aceeași tranzacție.
  6. La o livrare repetată, handlerul găsește evenimentul deja salvat, nu acordă recompensa din nou și returnează 204.

Același scenariu este potrivit și pentru obiecte, roluri, timp VIP, coduri promoționale sau sarcini într-o coadă internă.

Evenimentul de vot

Pentru un vot de server, GAMEMONITORING trimite server.vote, iar pentru un vot de proiect — project.vote. Body-ul evenimentului conține doar date de livrare: event_type, event_id, is_test și signature. Datele complete ale votului trebuie cerute separat.

Exemplu de eveniment
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "server.vote",
  "is_test": false,
  "signature": "ae83b8aba88a3a9ab3b97b1f6d65664da5628a9cb64d56d5132807bca5472e4f"
}

În acest eveniment, event_id este ID-ul votului. Nu folosiți body-ul Webhook ca sursă pentru nickname, server sau utilizator: aceste date vin din API.

Obținerea datelor votului

Folosiți event_id ca vote_id și cereți datele votului prin GET /votes/:vote_id:

Cerere pentru datele votului
curl -sS "https://api.gamemonitoring.ro/votes/9824cabb-2203-437e-9b6c-aba43dde3e4b"

Pentru acordarea recompensei, de obicei aveți nevoie de response.nickname, response.server și datele publice response.user. Dacă recompensa depinde de un anumit server, verificați întotdeauna response.server.id.

Cum se folosesc câmpurile: response.nickname ajută la găsirea contului jucătorului în baza dvs., response.server.id alege regula de recompensă pentru server, iar response.user.id poate fi salvat în logul recompenselor ca ID al utilizatorului GAMEMONITORING care a votat.

Dacă API-ul este temporar indisponibil sau returnează un răspuns neașteptat, nu acordați recompensa fără verificare. Returnați un cod de eroare, remediați cauza și trimiteți din nou livrarea din interfață.

Pasul 3. Handler pentru recompensa după vot

Exemplul continuă handlerul de bază: verifică semnătura, obține datele votului, protejează evenimentul de procesare repetată și acordă recompensa într-o singură tranzacție. Înlocuiți numele tabelului de utilizatori, câmpul de balanță și regula de căutare a jucătorului cu structura sistemului dvs.

Înainte de rularea exemplului, configurați Webhook-ul proiectului, verificați GET /votes/:vote_id și înlocuiți actualizările SQL pentru utilizator cu modelul dvs. de conturi.

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

// Add the GAMEMONITORING API URL and reward settings for vote events.
$apiUrl = 'https://api.gamemonitoring.ro';
$rewardAmount = '1.00';

// 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;
}

// This reward handler processes server and project vote events.
if (!in_array($eventType, ['server.vote', 'project.vote'], true)) {
    http_response_code(204);
    exit;
}

// At this point the webhook is trusted. Load vote data before opening a database transaction.
$pdo = null;

try {
    // Load full vote data by event_id. Nickname, entity, and user data are not
    // in the webhook body. Return 500 if the API cannot confirm the vote.
    $voteUrl = $apiUrl . '/votes/' . rawurlencode($eventId);
    $voteContext = stream_context_create(['http' => ['timeout' => 5]]);
    $voteBody = @file_get_contents($voteUrl, false, $voteContext);

    if ($voteBody === false) {
        throw new RuntimeException('Vote API request failed');
    }

    $voteResponse = json_decode($voteBody, true) ?: [];
    $vote = $voteResponse['response'] ?? null;

    // Do not issue a reward when the vote response is missing a concrete nickname.
    if (!is_array($vote) || !isset($vote['nickname']) || !is_string($vote['nickname'])) {
        throw new RuntimeException('Vote API response does not include nickname');
    }

    // Verify that the API entity matches the event before changing the account.
    $expectedEntityType = $eventType === 'project.vote' ? 'project' : 'server';
    if (($vote['entity_type'] ?? '') !== $expectedEntityType) {
        throw new RuntimeException('Vote entity type does not match event type');
    }

    // Use vote nickname to update the local account. The entity id is available in
    // vote.entity_id and in either vote.server.id or vote.project.id.
    $nickname = trim($vote['nickname']);

    if ($nickname === '') {
        throw new RuntimeException('Vote nickname is empty');
    }

    // 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.
    $balance = $pdo->prepare('UPDATE users SET balance = balance + ? WHERE nickname = ?');
    $balance->execute([$rewardAmount, $nickname]);

    // 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);
}