Nagrody za głosy

Nagrody za głosy używają zdarzeń Webhook server.vote i project.vote: handler odbiera zdarzenie, pobiera dane głosu przez API i wydaje nagrodę tylko jeden raz.

Najpierw skonfiguruj Webhook projektu i weryfikację signature. Dane głosu są pobierane przez GET /votes/:vote_id.

Jak działa scenariusz

  1. Przyjmij zdarzenie Webhook i sprawdź signature. Jeśli is_test ma wartość true, zwróć 204 bez pobierania głosu i bez wydawania nagrody.
  2. Upewnij się, że event_type to server.vote lub project.vote.
  3. Użyj event_id jako ID głosu.
  4. Pobierz dane głosu przez GET /votes/:vote_id i znajdź gracza w swoim systemie.
  5. W jednej transakcji zastosuj ochronę przed ponownym przetworzeniem po event_type + event_id i wydaj nagrodę tylko dla nowego zdarzenia.
  6. Jeśli nagrody nie da się bezpiecznie wydać, zwróć odpowiedź z błędem. Po usunięciu przyczyny ponów dostawę z interfejsu.

Przykład nagrody

Załóżmy, że gracz PlayerName zagłosował na serwer o ID 1, a Twój system ma dodać mu 100 monet.

  1. GAMEMONITORING wysyła Webhook z event_type: server.vote i event_id: 9824cabb-2203-437e-9b6c-aba43dde3e4b.
  2. Handler sprawdza signature. Jeśli podpis jest błędny, zwraca 401 i kończy.
  3. Handler wywołuje GET /votes/9824cabb-2203-437e-9b6c-aba43dde3e4b, otrzymuje nick, serwer oraz użytkownika i znajduje lokalne konto.
  4. W transakcji handler zapisuje event_type + event_id dla ochrony przed ponownym przetworzeniem.
  5. Dla nowego zdarzenia handler dodaje 100 monet w tej samej transakcji.
  6. Przy ponownej dostawie handler znajduje już zapisane zdarzenie, nie wydaje nagrody drugi raz i zwraca 204.

Ten sam scenariusz pasuje też do przedmiotów, ról, czasu VIP, kodów promocyjnych lub zadań w wewnętrznej kolejce.

Zdarzenie głosu

Dla głosu na serwer GAMEMONITORING wysyła server.vote, a dla głosu na projekt — project.vote. Body zdarzenia zawiera tylko dane dostawy: event_type, event_id, is_test i signature. Pełne dane głosu trzeba pobrać osobno.

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

event_id w tym zdarzeniu jest ID głosu. Nie używaj body Webhook jako źródła nicku, serwera lub użytkownika: te dane pochodzą z API.

Pobieranie danych głosu

Użyj event_id jako vote_id i pobierz dane głosu przez GET /votes/:vote_id:

Żądanie danych głosu
curl -sS "https://api.gamemonitoring.pl/votes/9824cabb-2203-437e-9b6c-aba43dde3e4b"

Do wydania nagrody zwykle potrzebujesz response.nickname, response.server i publicznych danych response.user. Jeśli nagroda zależy od konkretnego serwera, zawsze sprawdzaj response.server.id.

Jak używać pól: response.nickname pomaga znaleźć konto gracza w Twojej bazie, response.server.id wybiera regułę nagrody dla serwera, a response.user.id można zapisać w logu nagród jako ID użytkownika GAMEMONITORING, który oddał głos.

Jeśli API jest chwilowo niedostępne albo zwraca nieoczekiwaną odpowiedź, nie wydawaj nagrody bez weryfikacji. Zwróć kod błędu, napraw przyczynę i ponów dostawę z interfejsu.

Krok 3. Handler nagrody za głos

Przykład kontynuuje podstawowy handler: sprawdza podpis, pobiera dane głosu, chroni zdarzenie przed ponownym przetworzeniem i dodaje nagrodę w jednej transakcji. Nazwę tabeli użytkowników, pole salda i regułę wyszukiwania gracza zastąp strukturą swojego systemu.

Przed uruchomieniem przykładu skonfiguruj Webhook projektu, sprawdź GET /votes/:vote_id i zastąp SQL aktualizacji użytkownika swoim modelem kont.

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.pl';
$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);
}