Reward & Track every vote
{{ voteUrl }}?id=PlayerX&...
The webhook script would receive all variables passed to voting page
(the QUERY_STRING).
The important thing is for your script to be able to identify the voter and award him based on these
variables (eg. PlayerX
identifies `PlayerX` in your DB).
In some cases you might want to use a custom webhook URL for some specific votes.
In such a case, you can extend your link with the callback variable "_
":
{{ voteUrl }}?_
=strtr(base64_encode($customURL),'+/','-_')&id=PlayerX&...
The callback variable "_
" can be any custom URL,
base64
encoded, safely to be used as a URL
variable.
<?php
$customURL = 'https://vote.example.com/vote_webhook.php';
$callback = strtr(base64_encode($customURL), '+/', '-_');
echo '{{voteUrl}}?_='.$callback.'&id='.$playerID;
?>
All other variables would be sent to $customURL
as POST fields.
Each webhook request contains a security token in the X-JWT
header.
This is a JSON Web Token which encodes additional vote details,
securely signed with your Webhook Secret.
Thus you can make sure the request came from our server and avoid awarding users for fake votes.
The Webhook Secret is generated for your site under your account.
The webhook script would receive the folowing vote details as part of JWT's payload:
Field name | Example value | Description |
---|---|---|
jti |
"5d14ac20e4fd9455f1cf8e5b" | A unique vote ID. Use this to keep track of which vote has been rewarded. |
ip |
"3.136.233.4" | The voters' IP address. |
ref |
"https://example.com/vote/" | Referer with which the voter landed on voting page |
aud |
"example.com" | Hostname of the site the vote is for. |
sub |
"5d04f82b515e8b6b43f83c76" | Your site's ID on RSPS.Page. |
iss |
"https://rsps.page/" | Issuer. |
at |
1740096123456 | Unix timestamp of the vote (ms). |
iat |
1740096124 | Unix timestamp of the webhook request (sec). |
exp |
1740096244 | Expiration timestamp (sec). After this time the JWT is no longer valid. |
val |
1 | Vote value. |
qs |
{ "id": "PlayerX" } |
All variables passed to vote link
(the QUERY_STRING).
This value duplicates the request data, and it is signed as part of the JWT. |
<?php
/**
* Example PHP >= 5.4 WebHook Script
* https://example.com/vote_webhook.php
*
* RSPS.Page does not take responsibility or liability for the (miss)use of this php snippet.
*/
// Your site's webhook secret (see https://rsps.page/account/site/)
$myWebhookSecret = 'my-webhook-secret';
// Your Database Connection Details
$host = 'db.example.com';
$db_name = 'example_com';
$db_user = 'example_com';
$db_password = '**************';
// Get the exact request time, default to current system time
$request_time = getenv('REQUEST_TIME_FLOAT') ?: getenv('REQUEST_TIME') ?: microtime(true);
// Decode the JWT token provided through X-JWT request header
list($invalidReason, $vote) = jwt_decode(getenv('HTTP_X_JWT'), $myWebhookSecret, 120, $request_time);
// Validate the request using the JWT
if ($invalidReason) {
// Request not accepted
return rsps_webhook_response(406, $invalidReason);
}
// Unique vote ID - could be used to track reward for this specific vote
$voteId = $vote['jti'];
if (!$voteId) {
// Request not accepted
return rsps_webhook_response(406, $invalidReason);
}
// All variables passed to the voting page are received as payload ($_POST or $_GET)
$query = rsps_webhook_get_payload() ?: $vote['qs'];
// Player ID - fetch it from the variable you chose in the voting page's URL:
// https://rsps.page/vote/example.com?id=PlayerX -> $query['id'] === 'PlayerX'
$userId = $query['id'] ?: $query['userId'] ?: $query['playerId'] ?: null;
// IP address of the voter
$voterIp = $vote['ip'];
// Coordinates of the site this vote is for
$hostname = $vote['aud'];
$rspsSiteId = $vote['sub'];
// Referer with which the voter landed of voting page
$referer = $vote['ref'];
// Vote event timestamp in seconds (when the player clicked on vote button)
$votedAt = $vote['at'] / 1e3;
// The timestamp of webhook request (JWT creation) - should be close to $request_time
$webhookTs = $vote['iat'];
// Vote value, should be greated than 1
$value = intval($vote['val']);
// Make sure the user was not yet rewarded on your system
// For example by checking your reward logs
if ($userId && $value > 0 && $voteId) {
// Connect to the DB
$connection = mysqli_connect($host, $db_user, $db_password);
mysqli_select_db($connection, $db_name);
// Make userId safe to use in query
$userId = mysqli_real_escape_string($connection, $userId);
// Check to see whether this vote has been rewarded or not
$result = mysqli_query(
$connection,
'SELECT COUNT(*) FROM `reward_log` ' .
// track user rewards by userId & last reward time
' WHERE `userId`="' . $userId . '" AND `reward_datetime`>=FROM_UNIXTIME(' . $votedAt . ')' .
// or by checking individual votes
' OR `voteId`="' . $voteId . '" AND `votedAt`=' . $votedAt . ';'
);
if (!$result) {
return rsps_webhook_response(500, 'sql-error');
}
$rewarded = mysqli_num_rows($result);
// Already rewarded for this vote?
if ($rewarded) {
// Request not accepted
return rsps_webhook_response(406, 'rewarded');
}
// Grant reward and set the reward date/time log
if (!$rewarded) {
// Grant the reward, for example points
$ok = mysqli_query(
$connection,
'UPDATE `users` SET `points` = `points` + 1 WHERE `userId`="' . $userId . '";'
);
if (!$ok) {
return rsps_webhook_response(500, 'sql-error');
}
// Insert log
$ok = mysqli_query(
$connection,
'INSERT INTO `reward_log` (`voteId`, `votedAt`, `userId`, `reward_datetime`)' .
' VALUES (' . implode(', ', [
"'$voteId'", $votedAt,
"'$userId'", 'FROM_UNIXTIME(' . $request_time . ')'
]) . ');'
);
if (!$ok) {
return rsps_webhook_response(500, 'sql-error');
}
}
// Close connection
mysqli_close($connection);
}
// Request accepted
rsps_webhook_response(200);
/**
* Send response to webhook caller.
* @param int $code - HTTP response status code.
* 200 - Ok, 406 - Not accepted
* @param string $reason - If an error, send some error message to caller
*
* Note: This function invokes die!
*/
function rsps_webhook_response($code, $reason = null)
{
http_response_code($code);
if ($code != 200) {
$reason and header('x-reason: ' . urlencode($reason));
die(intval($code));
}
die(0);
}
/**
* Get WebHook request payload
*
* @return array $payload
*/
function rsps_webhook_get_payload()
{
$payload = $_POST ?: $_GET;
if (empty($payload)) {
$request_method = strtoupper(getenv('HTTP_X_HTTP_METHOD_OVERRIDE') ?: getenv('REQUEST_METHOD'));
switch ($request_method) {
case 'POST':
case 'PUT':
case 'PATCH':
// Only POST, PUT and PATCH requests should have a body.
if ($contentType = getenv('HTTP_CONTENT_TYPE')) {
// Extract content-type without the charset
$contentType = explode(';', $contentType);
$contentType = trim(reset($contentType));
// In the case the request body did not get decoded, for whatever reason, try to decode it here.
switch ($contentType) {
case 'application/json':
// PHP doesn't decode JSON request body
$tmp = @file_get_contents('php://input');
$payload = json_decode($tmp, true);
break;
case 'application/x-www-form-urlencoded':
// If there is no decoded $_POST super-global, decode the request body here
// (eg. PUT or PATCH request)
$tmp = @file_get_contents('php://input');
parse_str($tmp, $payload);
break;
}
}
break;
case 'GET':
case 'HEAD':
// GET and HEAD requests don't have a body, thus look for payload in the query string
$tmp = explode('?', getenv('REQUEST_URI'), 2);
if (count($tmp) === 2) {
parse_str($tmp[1], $payload);
}
break;
}
}
return $payload;
}
/**
* Decode a JWT (JSON Web Token) and validate it.
*
* See https://jwt.io/ for more details
*
* RFC 7519 https://tools.ietf.org/html/rfc7519
*
* @param string $jwt - A JWT string
* @param string $secret - The WebHook Secret
* @param int $defExpiresIn - Default expiration time, for when there is no 'exp' field in the JWT
* @param int $requestTime - Unix timestamp of the request. Defaults to current time.
* @return array [ $invalidReason, $payload, $header, $signature ]
*/
function jwt_decode($jwt, $secret = null, $defExpiresIn = 120, $requestTime = null)
{
if (empty($jwt)) {
return ['jwt', null];
}
$invalidReason = null; // In the case the JWT is invalid
list($header, $payload, $signature) = explode('.', $jwt);
$token = "$header.$payload";
$header = base64_decode(strtr($header, '-_', '+/'));
$payload = base64_decode(strtr($payload, '-_', '+/'));
$signature = base64_decode(strtr($signature, '-_', '+/'));
$header = json_decode($header);
if ($header->typ != 'JWT') {
// Wrong JWT type
$invalidReason = 'typ';
} elseif ($secret) {
static $algorithms = [
'HS256' => ['hash_hmac', 'SHA256'],
'HS384' => ['hash_hmac', 'SHA384'],
'HS512' => ['hash_hmac', 'SHA512'],
'none' => [],
// 'RS256' => ['openssl', 'SHA256'],
// 'RS384' => ['openssl', 'SHA384'],
// 'RS512' => ['openssl', 'SHA512'],
];
if (!isset($algorithms[$header->alg])) {
// Unsupported signing algorithm
$invalidReason = 'alg';
} else {
$alg = $algorithms[$header->alg];
if (!$alg || hash_equals($signature, $alg[0]($alg[1], $token, $secret, true))) {
// Invalid signature ('none' is always invalid)
$invalidReason = 'sig';
}
}
}
$payload = json_decode($payload);
if (!$invalidReason) {
isset($requestTime) or $requestTime = time();
if (isset($payload->exp) && $requestTime > $payload->exp) {
// JWT expired
$invalidReason = 'exp';
} elseif (isset($payload->iat) && $defExpiresIn && $requestTime > $payload->iat + $defExpiresIn) {
// JWT expired by default expiration time, based on .iat
$invalidReason = 'iat';
}
}
return [
$invalidReason,
$payload,
$header,
$signature,
];
}
if (!function_exists('hash_equals')) {
// Polyfill for PHP < 5.6
function hash_equals($known_string, $user_string)
{
return hash_hmac('SHA256', $known_string, __FUNCTION__, true) === hash_hmac('SHA256', $user_string, __FUNCTION__, true);
}
}