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