Belohnungen für Stimmen

Belohnungen für Stimmen nutzen das Webhook-Ereignis server.vote: Der Handler empfängt das Ereignis, ruft Stimmdaten über die API ab, findet den Spieler in Ihrem System und gibt die Belohnung nur einmal aus.

Richten Sie zuerst den Projekt-Webhook und die Prüfung von signature ein. Stimmdaten werden über GET /votes/:vote_id abgefragt.

So funktioniert der Ablauf

  1. Nehmen Sie das Webhook-Ereignis an und prüfen Sie signature. Wenn is_test true ist, geben Sie 204 zurück, ohne die Stimme abzurufen und ohne eine Belohnung auszugeben.
  2. Stellen Sie sicher, dass event_type server.vote ist.
  3. Verwenden Sie event_id als ID der Stimme.
  4. Rufen Sie Stimmdaten über GET /votes/:vote_id ab und finden Sie den Spieler in Ihrem System.
  5. Wenden Sie in einer Transaktion den Schutz vor erneuter Verarbeitung über event_type + event_id an und geben Sie die Belohnung nur für ein neues Ereignis aus.
  6. Wenn die Belohnung nicht sicher ausgegeben werden kann, geben Sie eine Fehlerantwort zurück. Nach Behebung der Ursache wiederholen Sie die Zustellung aus der Oberfläche.

Belohnungsbeispiel

Angenommen, Spieler PlayerName hat für den Server mit ID 1 gestimmt, und Ihr System soll ihm 100 Münzen gutschreiben.

  1. GAMEMONITORING sendet einen Webhook mit event_type: server.vote und event_id: 9824cabb-2203-437e-9b6c-aba43dde3e4b.
  2. Der Handler prüft signature. Bei falscher Signatur gibt er 401 zurück und stoppt.
  3. Der Handler ruft GET /votes/9824cabb-2203-437e-9b6c-aba43dde3e4b auf, erhält Nickname, Server und Benutzer und findet das lokale Konto.
  4. In einer Transaktion speichert der Handler event_type + event_id für den Schutz vor erneuter Verarbeitung.
  5. Für ein neues Ereignis schreibt der Handler 100 Münzen in derselben Transaktion gut.
  6. Bei erneuter Zustellung findet der Handler das bereits gespeicherte Ereignis, gibt die Belohnung nicht erneut aus und gibt 204 zurück.

Der gleiche Ablauf eignet sich auch für Gegenstände, Rollen, VIP-Zeit, Promo-Codes oder Aufgaben in einer internen Queue.

Stimmenereignis

Wenn ein Server eine Stimme erhält, sendet GAMEMONITORING das Ereignis server.vote. Der Ereignis-Body enthält nur Zustelldaten: event_type, event_id, is_test und signature. Vollständige Stimmdaten müssen separat abgefragt werden.

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

In diesem Ereignis ist event_id die ID der Stimme. Verwenden Sie den Webhook-Body nicht als Quelle für Nickname, Server oder Benutzer: Diese Daten kommen aus der API.

Stimmdaten abrufen

Verwenden Sie event_id als vote_id und rufen Sie die Stimmdaten über GET /votes/:vote_id ab:

Anfrage der Stimmdaten
curl -sS "https://api.gamemonitoring.de/votes/9824cabb-2203-437e-9b6c-aba43dde3e4b"

Für die Belohnung benötigen Sie normalerweise response.nickname, response.server und öffentliche Daten aus response.user. Wenn die Belohnung von einem bestimmten Server abhängt, prüfen Sie immer response.server.id.

So verwenden Sie die Felder: response.nickname hilft, das Spielerkonto in Ihrer Datenbank zu finden, response.server.id wählt die Belohnungsregel für den Server aus, und response.user.id kann im Belohnungsprotokoll als GAMEMONITORING-Benutzer-ID gespeichert werden.

Wenn die API vorübergehend nicht verfügbar ist oder eine unerwartete Antwort liefert, geben Sie keine Belohnung ohne Prüfung aus. Geben Sie einen Fehlercode zurück, beheben Sie die Ursache und wiederholen Sie die Zustellung aus der Oberfläche.

Schritt 3. Handler für Stimm-Belohnungen

Das Beispiel setzt den Basis-Handler fort: Es prüft die Signatur, ruft Stimmdaten ab, schützt das Ereignis vor erneuter Verarbeitung und schreibt die Belohnung in einer Transaktion gut. Ersetzen Sie den Namen der Benutzertabelle, das Kontostandfeld und die Regel zur Spielersuche durch die Struktur Ihres Systems.

Vor dem Start des Beispiels richten Sie den Projekt-Webhook ein, prüfen GET /votes/:vote_id und ersetzen die SQL-Updates für Benutzer durch Ihr Kontomodell.

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 the server.vote event.
$apiUrl = 'https://api.gamemonitoring.de';
$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 only server.vote events.
if ($eventType !== 'server.vote') {
    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, server, 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');
    }

    // Use vote nickname to update the local account. If rewards depend on a server,
    // validate (string) ($vote['server']['id'] ?? '') before changing the account.
    $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);
}