437 lines
18 KiB
PHP
437 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* REST-Routen unter /wp-json/aeromc/v1/... für das Freundesystem.
|
|
*
|
|
* Bewusst OHNE echte Authentifizierung (Absprache mit dem Nutzer): der
|
|
* Launcher meldet sich einfach mit der Minecraft-UUID des aktiven Kontos an.
|
|
* Wer eine fremde UUID kennt, könnte sich theoretisch als dieser Spieler
|
|
* ausgeben - für den privaten Rahmen dieses Projekts akzeptiert. Eingaben
|
|
* werden trotzdem serverseitig geprüft (Format von UUID/Name) und jede
|
|
* Datenbankabfrage läuft ausschließlich über $wpdb->prepare, um SQL-Injection
|
|
* auszuschließen.
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
// Konten gelten als "online", solange ihr letzter Herzschlag (der Launcher
|
|
// sendet alle 60s einen) nicht älter als das hier ist.
|
|
define('AEROMC_FRIENDS_ONLINE_SECONDS', 180);
|
|
// Häufigere Herzschläge als das werden ignoriert (Schutz gegen Missbrauch/
|
|
// versehentliche Anfrageflut) - der Launcher selbst sendet ohnehin nur alle 60s.
|
|
// Ausnahme: hat sich der Server geändert, wird sofort gespeichert ("Beitreten").
|
|
define('AEROMC_FRIENDS_HEARTBEAT_MIN_GAP', 20);
|
|
// So lange ist eine Einladung "Komm auf meinen Server" gültig.
|
|
define('AEROMC_FRIENDS_INVITE_SECONDS', 600);
|
|
// Frühestens nach so vielen Sekunden darf derselbe Freund erneut eingeladen werden.
|
|
define('AEROMC_FRIENDS_INVITE_MIN_GAP', 30);
|
|
|
|
function aeromc_friends_invites_table() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . AEROMC_FRIENDS_INVITES_TABLE;
|
|
}
|
|
|
|
/** Server-Adresse (Host oder IP, optional :Port) - nur harmlose Zeichen. */
|
|
function aeromc_friends_valid_server($server) {
|
|
return is_string($server) && preg_match('/^[A-Za-z0-9.\-_:\[\]]{1,128}$/', $server);
|
|
}
|
|
|
|
/** Minecraft-Version wie "1.21.1", "24w14a" oder "1.20.1-pre2". */
|
|
function aeromc_friends_clean_version($version) {
|
|
$version = (string) $version;
|
|
return preg_match('/^[A-Za-z0-9.\-_ ]{1,32}$/', $version) ? $version : '';
|
|
}
|
|
|
|
/** Sind die beiden befreundet (Anfrage angenommen)? */
|
|
function aeromc_friends_are_friends($a, $b) {
|
|
global $wpdb;
|
|
$friends_table = aeromc_friends_friends_table();
|
|
return (bool) $wpdb->get_var($wpdb->prepare(
|
|
"SELECT id FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
|
$a, $b, $b, $a
|
|
));
|
|
}
|
|
|
|
function aeromc_friends_presence_table() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . AEROMC_FRIENDS_PRESENCE_TABLE;
|
|
}
|
|
|
|
function aeromc_friends_friends_table() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . AEROMC_FRIENDS_FRIENDS_TABLE;
|
|
}
|
|
|
|
/** Minecraft-UUIDs kommen mal mit, mal ohne Bindestriche an - einheitlich
|
|
* ohne Bindestriche, klein geschrieben speichern/vergleichen. Gibt false
|
|
* zurück, wenn es keine gültige UUID (32 Hex-Zeichen) ist. */
|
|
function aeromc_friends_normalize_uuid($uuid) {
|
|
$stripped = strtolower(str_replace('-', '', (string) $uuid));
|
|
if (!preg_match('/^[a-f0-9]{32}$/', $stripped)) {
|
|
return false;
|
|
}
|
|
return $stripped;
|
|
}
|
|
|
|
/** Minecraft-Namensregeln: 1-16 Zeichen, Buchstaben/Ziffern/Unterstrich. */
|
|
function aeromc_friends_valid_username($name) {
|
|
return is_string($name) && preg_match('/^[A-Za-z0-9_]{1,16}$/', $name);
|
|
}
|
|
|
|
add_action('rest_api_init', function () {
|
|
register_rest_route('aeromc/v1', '/presence', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_presence',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends', [
|
|
'methods' => 'GET',
|
|
'callback' => 'aeromc_friends_route_list',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/requests', [
|
|
'methods' => 'GET',
|
|
'callback' => 'aeromc_friends_route_requests',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/sent', [
|
|
'methods' => 'GET',
|
|
'callback' => 'aeromc_friends_route_sent',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/request', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_request',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/respond', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_respond',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/cancel', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_cancel',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/remove', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_remove',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/invite', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_invite',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/invites', [
|
|
'methods' => 'GET',
|
|
'callback' => 'aeromc_friends_route_invites',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('aeromc/v1', '/friends/invites/dismiss', [
|
|
'methods' => 'POST',
|
|
'callback' => 'aeromc_friends_route_invite_dismiss',
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
});
|
|
|
|
/** POST /presence {uuid, username, instance, server, version} */
|
|
function aeromc_friends_route_presence($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$username = $request->get_param('username');
|
|
if (!$uuid || !aeromc_friends_valid_username($username)) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/username.', ['status' => 400]);
|
|
}
|
|
$instance = sanitize_text_field((string) $request->get_param('instance'));
|
|
$server = sanitize_text_field((string) $request->get_param('server'));
|
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
|
$table = aeromc_friends_presence_table();
|
|
|
|
$letzte = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT last_seen, server_label FROM $table WHERE uuid = %s", $uuid
|
|
));
|
|
$server_gleich = $letzte && (string) $letzte->server_label === (string) $server;
|
|
if ($letzte && $server_gleich && (time() - strtotime($letzte->last_seen)) < AEROMC_FRIENDS_HEARTBEAT_MIN_GAP) {
|
|
return ['ok' => true]; // zu häufig - stillschweigend ignoriert
|
|
}
|
|
|
|
// current_time('mysql', true) statt current_time('mysql'): das zweite
|
|
// Argument erzwingt GMT/UTC. Ohne das liefert current_time() die an die
|
|
// WordPress-Zeitzone angepasste ORTSZEIT (Einstellungen > Allgemein) -
|
|
// das online/offline-Fenster unten vergleicht last_seen aber per
|
|
// strtotime()/time() als UTC, wodurch ein Freund bei abweichender
|
|
// Zeitzone praktisch nie als offline erkannt worden wäre.
|
|
$jetzt_utc = current_time('mysql', true);
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO $table (uuid, username, instance_name, server_label, mc_version, last_seen)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE username = %s, instance_name = %s, server_label = %s, mc_version = %s, last_seen = %s",
|
|
$uuid, $username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc,
|
|
$username, $instance ?: null, $server ?: null, $version ?: null, $jetzt_utc
|
|
));
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** GET /friends?uuid= - akzeptierte Freunde inkl. Online-Status */
|
|
function aeromc_friends_route_list($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
if (!$uuid) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
$presence_table = aeromc_friends_presence_table();
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT IF(f.uuid_a = %s, f.uuid_b, f.uuid_a) AS other_uuid
|
|
FROM $friends_table f
|
|
WHERE (f.uuid_a = %s OR f.uuid_b = %s) AND f.status = 'accepted'",
|
|
$uuid, $uuid, $uuid
|
|
));
|
|
|
|
$ergebnis = [];
|
|
foreach ($rows as $row) {
|
|
$p = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT username, instance_name, server_label, mc_version, last_seen FROM $presence_table WHERE uuid = %s",
|
|
$row->other_uuid
|
|
));
|
|
$online = $p && (time() - strtotime($p->last_seen)) < AEROMC_FRIENDS_ONLINE_SECONDS;
|
|
$ergebnis[] = [
|
|
'uuid' => $row->other_uuid,
|
|
'username' => $p ? $p->username : null,
|
|
'online' => (bool) $online,
|
|
'instance' => $online ? $p->instance_name : null,
|
|
'server' => $online ? $p->server_label : null,
|
|
'version' => $online ? $p->mc_version : null,
|
|
];
|
|
}
|
|
return $ergebnis;
|
|
}
|
|
|
|
/** GET /friends/requests?uuid= - offene, an mich gerichtete Anfragen */
|
|
function aeromc_friends_route_requests($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
if (!$uuid) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
$presence_table = aeromc_friends_presence_table();
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT f.uuid_a AS from_uuid, p.username AS from_username
|
|
FROM $friends_table f
|
|
LEFT JOIN $presence_table p ON p.uuid = f.uuid_a
|
|
WHERE f.uuid_b = %s AND f.status = 'pending'",
|
|
$uuid
|
|
));
|
|
return array_map(function ($r) {
|
|
return ['uuid' => $r->from_uuid, 'username' => $r->from_username];
|
|
}, $rows);
|
|
}
|
|
|
|
/** GET /friends/sent?uuid= - offene, von mir SELBST gesendete Anfragen */
|
|
function aeromc_friends_route_sent($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
if (!$uuid) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
$presence_table = aeromc_friends_presence_table();
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT f.uuid_b AS to_uuid, p.username AS to_username
|
|
FROM $friends_table f
|
|
LEFT JOIN $presence_table p ON p.uuid = f.uuid_b
|
|
WHERE f.uuid_a = %s AND f.status = 'pending'",
|
|
$uuid
|
|
));
|
|
return array_map(function ($r) {
|
|
return ['uuid' => $r->to_uuid, 'username' => $r->to_username];
|
|
}, $rows);
|
|
}
|
|
|
|
/** POST /friends/request {uuid, username, targetUsername} */
|
|
function aeromc_friends_route_request($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$targetUsername = $request->get_param('targetUsername');
|
|
if (!$uuid || !aeromc_friends_valid_username($targetUsername)) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUsername.', ['status' => 400]);
|
|
}
|
|
$presence_table = aeromc_friends_presence_table();
|
|
$friends_table = aeromc_friends_friends_table();
|
|
|
|
$ziel = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT uuid FROM $presence_table WHERE username = %s", $targetUsername
|
|
));
|
|
if (!$ziel) {
|
|
return new WP_Error('aeromc_not_found', 'Dieser Name wurde noch nie im Launcher gesehen.', ['status' => 404]);
|
|
}
|
|
$ziel_uuid = $ziel->uuid;
|
|
if ($ziel_uuid === $uuid) {
|
|
return new WP_Error('aeromc_self', 'Du kannst dich nicht selbst hinzufügen.', ['status' => 400]);
|
|
}
|
|
|
|
$vorhanden = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT id FROM $friends_table WHERE (uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)",
|
|
$uuid, $ziel_uuid, $ziel_uuid, $uuid
|
|
));
|
|
if ($vorhanden) {
|
|
return new WP_Error('aeromc_exists', 'Es gibt schon eine Anfrage oder Freundschaft.', ['status' => 409]);
|
|
}
|
|
|
|
$wpdb->insert($friends_table, [
|
|
'uuid_a' => $uuid,
|
|
'uuid_b' => $ziel_uuid,
|
|
'status' => 'pending',
|
|
'created_at' => current_time('mysql'),
|
|
]);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** POST /friends/respond {uuid, otherUuid, accept} */
|
|
function aeromc_friends_route_respond($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
|
$accept = (bool) $request->get_param('accept');
|
|
if (!$uuid || !$other) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
|
|
if ($accept) {
|
|
$wpdb->query($wpdb->prepare(
|
|
"UPDATE $friends_table SET status = 'accepted' WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
|
$other, $uuid
|
|
));
|
|
} else {
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
|
$other, $uuid
|
|
));
|
|
}
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** POST /friends/cancel {uuid, otherUuid} - eine SELBST gesendete Anfrage zurückziehen */
|
|
function aeromc_friends_route_cancel($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
|
if (!$uuid || !$other) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM $friends_table WHERE uuid_a = %s AND uuid_b = %s AND status = 'pending'",
|
|
$uuid, $other
|
|
));
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** POST /friends/remove {uuid, otherUuid} */
|
|
function aeromc_friends_route_remove($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$other = aeromc_friends_normalize_uuid($request->get_param('otherUuid'));
|
|
if (!$uuid || !$other) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/otherUuid.', ['status' => 400]);
|
|
}
|
|
$friends_table = aeromc_friends_friends_table();
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM $friends_table WHERE ((uuid_a = %s AND uuid_b = %s) OR (uuid_a = %s AND uuid_b = %s)) AND status = 'accepted'",
|
|
$uuid, $other, $other, $uuid
|
|
));
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** POST /friends/invite {uuid, targetUuid, server, version} - "Komm auf meinen Server" */
|
|
function aeromc_friends_route_invite($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$target = aeromc_friends_normalize_uuid($request->get_param('targetUuid'));
|
|
$server = (string) $request->get_param('server');
|
|
$version = aeromc_friends_clean_version($request->get_param('version'));
|
|
if (!$uuid || !$target || !aeromc_friends_valid_server($server)) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUuid/server.', ['status' => 400]);
|
|
}
|
|
if (!aeromc_friends_are_friends($uuid, $target)) {
|
|
return new WP_Error('aeromc_not_friends', 'Ihr seid nicht befreundet.', ['status' => 403]);
|
|
}
|
|
$table = aeromc_friends_invites_table();
|
|
|
|
$letzte = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT created_at FROM $table WHERE from_uuid = %s AND to_uuid = %s ORDER BY id DESC LIMIT 1",
|
|
$uuid, $target
|
|
));
|
|
if ($letzte && (time() - strtotime($letzte)) < AEROMC_FRIENDS_INVITE_MIN_GAP) {
|
|
return new WP_Error('aeromc_too_fast', 'Du hast diesen Freund gerade erst eingeladen.', ['status' => 429]);
|
|
}
|
|
|
|
// ältere Einladung an denselben Freund ersetzen
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM $table WHERE from_uuid = %s AND to_uuid = %s", $uuid, $target
|
|
));
|
|
$wpdb->insert($table, [
|
|
'from_uuid' => $uuid,
|
|
'to_uuid' => $target,
|
|
'server' => $server,
|
|
'mc_version' => $version ?: null,
|
|
'created_at' => current_time('mysql', true),
|
|
]);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
/** GET /friends/invites?uuid= - an mich gerichtete, noch gültige Einladungen */
|
|
function aeromc_friends_route_invites($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
if (!$uuid) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
|
}
|
|
$table = aeromc_friends_invites_table();
|
|
$presence_table = aeromc_friends_presence_table();
|
|
|
|
// abgelaufene Einladungen (aller Spieler) wegräumen
|
|
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_INVITE_SECONDS);
|
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT i.id, i.from_uuid, p.username AS from_username, i.server, i.mc_version, i.created_at
|
|
FROM $table i
|
|
LEFT JOIN $presence_table p ON p.uuid = i.from_uuid
|
|
WHERE i.to_uuid = %s
|
|
ORDER BY i.id DESC",
|
|
$uuid
|
|
));
|
|
return array_map(function ($r) {
|
|
return [
|
|
'id' => (int) $r->id,
|
|
'fromUuid' => $r->from_uuid,
|
|
'fromUsername' => $r->from_username,
|
|
'server' => $r->server,
|
|
'version' => $r->mc_version,
|
|
'createdAt' => $r->created_at,
|
|
];
|
|
}, $rows);
|
|
}
|
|
|
|
/** POST /friends/invites/dismiss {uuid, id} - Einladung erledigt/abgelehnt */
|
|
function aeromc_friends_route_invite_dismiss($request) {
|
|
global $wpdb;
|
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
|
$id = (int) $request->get_param('id');
|
|
if (!$uuid || $id <= 0) {
|
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid/id.', ['status' => 400]);
|
|
}
|
|
$table = aeromc_friends_invites_table();
|
|
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE id = %d AND to_uuid = %s", $id, $uuid));
|
|
return ['ok' => true];
|
|
}
|