1028 lines
42 KiB
PHP
1028 lines
42 KiB
PHP
<?php
|
||
/**
|
||
* REST-Routen unter /wp-json/aeromc/v1/... für das Freundesystem.
|
||
*
|
||
* Anmeldung wie bei Minecraft-Servern: Der Launcher holt sich eine Aufgabe
|
||
* (/auth/challenge), meldet sie mit seinem Minecraft-Zugang bei Mojang an
|
||
* (sessionserver .../join), und das Plugin fragt bei Mojang nach, ob genau
|
||
* dieser Spieler das getan hat (hasJoined). Erst dann gibt es ein Sitzungs-
|
||
* Token (Kopfzeile X-AeroMC-Token), das jede Anfrage "als dieser Spieler"
|
||
* mitbringen muss - damit kann sich niemand mehr mit einer fremden UUID
|
||
* ausgeben. Übergangsmodus siehe AEROMC_FRIENDS_REQUIRE_AUTH.
|
||
*
|
||
* Eingaben werden 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);
|
||
|
||
// Sitzungen verfallen, wenn sie so lange nicht benutzt wurden.
|
||
define('AEROMC_FRIENDS_SESSION_DAYS', 30);
|
||
// So lange ist ein Teilen-Code gültig.
|
||
define('AEROMC_FRIENDS_SHARE_DAYS', 30);
|
||
// Höchstens so viele geteilte Instanzen pro Spieler und Tag.
|
||
define('AEROMC_FRIENDS_SHARES_PER_DAY', 30);
|
||
// Größe einer geteilten Mod-Liste (JSON) in Bytes.
|
||
define('AEROMC_FRIENDS_SHARE_MAX_BYTES', 262144);
|
||
// Kurznachrichten: Länge, Aufbewahrung, höchstens so viele pro Minute.
|
||
define('AEROMC_FRIENDS_MESSAGE_MAX_CHARS', 200);
|
||
define('AEROMC_FRIENDS_MESSAGE_DAYS', 7);
|
||
define('AEROMC_FRIENDS_MESSAGES_PER_MINUTE', 10);
|
||
// Bilder in Nachrichten (geteilte Screenshots): Höchstgröße und Ordner unter wp-content/uploads
|
||
define('AEROMC_FRIENDS_IMAGE_MAX_BYTES', 2500000);
|
||
define('AEROMC_FRIENDS_IMAGE_DIR', 'aeromc-bilder');
|
||
|
||
/** Speichert ein Bild (JPEG/PNG, base64) unter zufälligem Namen; gibt die URL zurück oder WP_Error. */
|
||
function aeromc_friends_save_image($base64) {
|
||
$daten = base64_decode(preg_replace('/^data:image\/\w+;base64,/', '', (string) $base64), true);
|
||
if ($daten === false || strlen($daten) < 100) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültiges Bild.', ['status' => 400]);
|
||
}
|
||
if (strlen($daten) > AEROMC_FRIENDS_IMAGE_MAX_BYTES) {
|
||
return new WP_Error('aeromc_too_big', 'Das Bild ist zu groß.', ['status' => 413]);
|
||
}
|
||
// nur echte JPEG- oder PNG-Dateien annehmen (Dateikopf prüfen, nicht der Angabe trauen)
|
||
if (strncmp($daten, "\xFF\xD8\xFF", 3) === 0) {
|
||
$endung = 'jpg';
|
||
} elseif (strncmp($daten, "\x89PNG\r\n\x1A\n", 8) === 0) {
|
||
$endung = 'png';
|
||
} else {
|
||
return new WP_Error('aeromc_invalid', 'Nur JPEG- oder PNG-Bilder.', ['status' => 400]);
|
||
}
|
||
$uploads = wp_upload_dir();
|
||
$ordner = trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_IMAGE_DIR;
|
||
if (!wp_mkdir_p($ordner)) {
|
||
return new WP_Error('aeromc_error', 'Bild konnte nicht gespeichert werden.', ['status' => 500]);
|
||
}
|
||
// Verzeichnisliste verhindern
|
||
if (!file_exists($ordner . '/index.html')) {
|
||
file_put_contents($ordner . '/index.html', '');
|
||
}
|
||
$name = bin2hex(random_bytes(16)) . '.' . $endung;
|
||
if (file_put_contents($ordner . '/' . $name, $daten) === false) {
|
||
return new WP_Error('aeromc_error', 'Bild konnte nicht gespeichert werden.', ['status' => 500]);
|
||
}
|
||
return trailingslashit($uploads['baseurl']) . AEROMC_FRIENDS_IMAGE_DIR . '/' . $name;
|
||
}
|
||
|
||
/** Bilddatei zu einer gespeicherten URL löschen (nur aus unserem Ordner). */
|
||
function aeromc_friends_delete_image($url) {
|
||
if (!$url) {
|
||
return;
|
||
}
|
||
$name = basename((string) $url);
|
||
if (!preg_match('/^[a-f0-9]{32}\.(jpg|png)$/', $name)) {
|
||
return;
|
||
}
|
||
$uploads = wp_upload_dir();
|
||
$datei = trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_IMAGE_DIR . '/' . $name;
|
||
if (file_exists($datei)) {
|
||
@unlink($datei);
|
||
}
|
||
}
|
||
// Fehlerberichte: Größe des mitgeschickten Berichts, höchstens so viele pro Stunde und Absender.
|
||
define('AEROMC_FRIENDS_REPORT_MAX_BYTES', 200000);
|
||
define('AEROMC_FRIENDS_REPORTS_PER_HOUR', 5);
|
||
|
||
function aeromc_friends_messages_table() {
|
||
global $wpdb;
|
||
return $wpdb->prefix . AEROMC_FRIENDS_MESSAGES_TABLE;
|
||
}
|
||
|
||
function aeromc_friends_reports_table() {
|
||
global $wpdb;
|
||
return $wpdb->prefix . AEROMC_FRIENDS_REPORTS_TABLE;
|
||
}
|
||
|
||
function aeromc_friends_sessions_table() {
|
||
global $wpdb;
|
||
return $wpdb->prefix . AEROMC_FRIENDS_SESSIONS_TABLE;
|
||
}
|
||
|
||
function aeromc_friends_shares_table() {
|
||
global $wpdb;
|
||
return $wpdb->prefix . AEROMC_FRIENDS_SHARES_TABLE;
|
||
}
|
||
|
||
/**
|
||
* Prüft, ob die Anfrage wirklich von diesem Spieler kommt.
|
||
* Gibt die Sitzung (Objekt mit username) zurück, null im Übergangsmodus
|
||
* ohne Token, oder WP_Error (401), wenn die Anmeldung fehlt oder ungültig ist.
|
||
*/
|
||
function aeromc_friends_check_auth($request, $uuid) {
|
||
global $wpdb;
|
||
$table = aeromc_friends_sessions_table();
|
||
$token = (string) $request->get_header('x-aeromc-token');
|
||
if ($token !== '') {
|
||
$hash = hash('sha256', $token);
|
||
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SESSION_DAYS * 86400);
|
||
$sitzung = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT uuid, username, last_used FROM $table WHERE token_hash = %s AND last_used >= %s", $hash, $grenze
|
||
));
|
||
if (!$sitzung || $sitzung->uuid !== $uuid) {
|
||
return new WP_Error('aeromc_auth', 'Anmeldung ungültig oder abgelaufen.', ['status' => 401]);
|
||
}
|
||
// "zuletzt benutzt" höchstens stündlich schreiben
|
||
if (time() - strtotime($sitzung->last_used) > 3600) {
|
||
$wpdb->update($table, ['last_used' => current_time('mysql', true)], ['token_hash' => $hash]);
|
||
}
|
||
return $sitzung;
|
||
}
|
||
// ohne Token: nur im Übergangsmodus und nur, solange dieser Spieler sich
|
||
// noch nie angemeldet hat - sonst könnte man sich weiter als er ausgeben
|
||
$schon_angemeldet = $wpdb->get_var($wpdb->prepare("SELECT 1 FROM $table WHERE uuid = %s LIMIT 1", $uuid));
|
||
if (AEROMC_FRIENDS_REQUIRE_AUTH || $schon_angemeldet) {
|
||
return new WP_Error('aeromc_auth', 'Anmeldung erforderlich.', ['status' => 401]);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
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', '/auth/challenge', [
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_auth_challenge',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/auth/verify', [
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_auth_verify',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/share', [
|
||
[
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_share_create',
|
||
'permission_callback' => '__return_true',
|
||
],
|
||
[
|
||
'methods' => 'GET',
|
||
'callback' => 'aeromc_friends_route_share_get',
|
||
'permission_callback' => '__return_true',
|
||
],
|
||
]);
|
||
register_rest_route('aeromc/v1', '/friends/message', [
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_message_send',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/friends/messages', [
|
||
'methods' => 'GET',
|
||
'callback' => 'aeromc_friends_route_messages',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/friends/messages/unread', [
|
||
'methods' => 'GET',
|
||
'callback' => 'aeromc_friends_route_messages_unread',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/friends/messages/read', [
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_messages_read',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('aeromc/v1', '/report', [
|
||
'methods' => 'POST',
|
||
'callback' => 'aeromc_friends_route_report',
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
if ($sitzung) {
|
||
$username = $sitzung->username; // von Mojang bestätigt
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$table = aeromc_friends_invites_table();
|
||
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE id = %d AND to_uuid = %s", $id, $uuid));
|
||
return ['ok' => true];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Anmeldung über Mojangs Sitzungsprüfung
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** POST /auth/challenge {uuid, username} - Aufgabe für die Anmeldung bei Mojang */
|
||
function aeromc_friends_route_auth_challenge($request) {
|
||
$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]);
|
||
}
|
||
$server_id = bin2hex(random_bytes(20));
|
||
set_transient('aeromc_ch_' . $uuid, ['sid' => $server_id, 'name' => $username], 120);
|
||
return ['serverId' => $server_id];
|
||
}
|
||
|
||
/** POST /auth/verify {uuid} - fragt bei Mojang nach und gibt ein Sitzungs-Token aus */
|
||
function aeromc_friends_route_auth_verify($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]);
|
||
}
|
||
$aufgabe = get_transient('aeromc_ch_' . $uuid);
|
||
delete_transient('aeromc_ch_' . $uuid); // nur einmal verwendbar
|
||
if (!is_array($aufgabe) || empty($aufgabe['sid'])) {
|
||
return new WP_Error('aeromc_auth', 'Keine oder abgelaufene Anmelde-Aufgabe.', ['status' => 403]);
|
||
}
|
||
$url = 'https://sessionserver.mojang.com/session/minecraft/hasJoined?username='
|
||
. rawurlencode($aufgabe['name']) . '&serverId=' . rawurlencode($aufgabe['sid']);
|
||
$antwort = wp_remote_get($url, ['timeout' => 10]);
|
||
if (is_wp_error($antwort) || wp_remote_retrieve_response_code($antwort) !== 200) {
|
||
return new WP_Error('aeromc_auth', 'Mojang hat die Anmeldung nicht bestätigt.', ['status' => 403]);
|
||
}
|
||
$profil = json_decode(wp_remote_retrieve_body($antwort), true);
|
||
if (!is_array($profil) || aeromc_friends_normalize_uuid($profil['id'] ?? '') !== $uuid
|
||
|| !aeromc_friends_valid_username($profil['name'] ?? '')) {
|
||
return new WP_Error('aeromc_auth', 'Mojang hat einen anderen Spieler gemeldet.', ['status' => 403]);
|
||
}
|
||
|
||
$table = aeromc_friends_sessions_table();
|
||
// abgelaufene Sitzungen (aller Spieler) wegräumen
|
||
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SESSION_DAYS * 86400);
|
||
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE last_used < %s", $grenze));
|
||
|
||
$token = bin2hex(random_bytes(32));
|
||
$jetzt = current_time('mysql', true);
|
||
$wpdb->insert($table, [
|
||
'token_hash' => hash('sha256', $token),
|
||
'uuid' => $uuid,
|
||
'username' => $profil['name'],
|
||
'created_at' => $jetzt,
|
||
'last_used' => $jetzt,
|
||
]);
|
||
return ['token' => $token, 'username' => $profil['name']];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Instanzen per Code teilen
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Code ohne verwechselbare Zeichen (kein 0/O, 1/I/L), Form XXXX-XXXX. */
|
||
function aeromc_friends_new_share_code() {
|
||
$zeichen = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
||
$code = '';
|
||
for ($i = 0; $i < 8; $i++) {
|
||
$code .= $zeichen[random_int(0, strlen($zeichen) - 1)];
|
||
if ($i === 3) {
|
||
$code .= '-';
|
||
}
|
||
}
|
||
return $code;
|
||
}
|
||
|
||
/** Eingegebenen Code vereinheitlichen ("ab12 cd34" -> "AB12-CD34"), false wenn ungültig. */
|
||
function aeromc_friends_normalize_share_code($code) {
|
||
$roh = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', (string) $code));
|
||
if (strlen($roh) !== 8) {
|
||
return false;
|
||
}
|
||
return substr($roh, 0, 4) . '-' . substr($roh, 4);
|
||
}
|
||
|
||
/** POST /share {uuid, manifest} - Mod-Liste einer Instanz hinterlegen, Code zurück */
|
||
function aeromc_friends_route_share_create($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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$manifest = $request->get_param('manifest');
|
||
if (!is_array($manifest) || empty($manifest['minecraft']) || !isset($manifest['inhalte'])) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültige Instanz-Beschreibung.', ['status' => 400]);
|
||
}
|
||
$json = wp_json_encode($manifest);
|
||
if (!$json || strlen($json) > AEROMC_FRIENDS_SHARE_MAX_BYTES) {
|
||
return new WP_Error('aeromc_too_big', 'Die Instanz ist zu groß zum Teilen.', ['status' => 413]);
|
||
}
|
||
$table = aeromc_friends_shares_table();
|
||
|
||
$seit = gmdate('Y-m-d H:i:s', time() - 86400);
|
||
$heute = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM $table WHERE uuid = %s AND created_at >= %s", $uuid, $seit
|
||
));
|
||
if ($heute >= AEROMC_FRIENDS_SHARES_PER_DAY) {
|
||
return new WP_Error('aeromc_too_fast', 'Heute wurden schon sehr viele Instanzen geteilt – bitte morgen wieder.', ['status' => 429]);
|
||
}
|
||
|
||
// abgelaufene Codes (aller Spieler) wegräumen
|
||
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SHARE_DAYS * 86400);
|
||
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||
|
||
for ($versuch = 0; $versuch < 5; $versuch++) {
|
||
$code = aeromc_friends_new_share_code();
|
||
$ok = $wpdb->insert($table, [
|
||
'code' => $code,
|
||
'uuid' => $uuid,
|
||
'manifest' => $json,
|
||
'created_at' => current_time('mysql', true),
|
||
]);
|
||
if ($ok) {
|
||
return ['code' => $code, 'days' => AEROMC_FRIENDS_SHARE_DAYS];
|
||
}
|
||
}
|
||
return new WP_Error('aeromc_error', 'Es konnte kein Code erzeugt werden.', ['status' => 500]);
|
||
}
|
||
|
||
/** GET /share?code= - geteilte Instanz abrufen (ohne Anmeldung: wer den Code hat, darf sie sehen) */
|
||
function aeromc_friends_route_share_get($request) {
|
||
global $wpdb;
|
||
$code = aeromc_friends_normalize_share_code($request->get_param('code'));
|
||
if (!$code) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültiger Code.', ['status' => 400]);
|
||
}
|
||
$table = aeromc_friends_shares_table();
|
||
$presence_table = aeromc_friends_presence_table();
|
||
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_SHARE_DAYS * 86400);
|
||
$row = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT s.code, s.uuid, s.manifest, s.created_at, p.username
|
||
FROM $table s LEFT JOIN $presence_table p ON p.uuid = s.uuid
|
||
WHERE s.code = %s AND s.created_at >= %s",
|
||
$code, $grenze
|
||
));
|
||
if (!$row) {
|
||
return new WP_Error('aeromc_not_found', 'Diesen Code gibt es nicht oder er ist abgelaufen.', ['status' => 404]);
|
||
}
|
||
return [
|
||
'code' => $row->code,
|
||
'fromUuid' => $row->uuid,
|
||
'fromUsername' => $row->username,
|
||
'createdAt' => $row->created_at,
|
||
'manifest' => json_decode($row->manifest, true),
|
||
];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Kurznachrichten zwischen Freunden
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** POST /friends/message {uuid, targetUuid, text} */
|
||
function aeromc_friends_route_message_send($request) {
|
||
global $wpdb;
|
||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||
$target = aeromc_friends_normalize_uuid($request->get_param('targetUuid'));
|
||
if (!$uuid || !$target) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/targetUuid.', ['status' => 400]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$text = trim(sanitize_text_field((string) $request->get_param('text')));
|
||
$bild = $request->get_param('image');
|
||
if ($text === '' && !$bild) {
|
||
return new WP_Error('aeromc_invalid', 'Leere Nachricht.', ['status' => 400]);
|
||
}
|
||
if (mb_strlen($text) > AEROMC_FRIENDS_MESSAGE_MAX_CHARS) {
|
||
$text = mb_substr($text, 0, AEROMC_FRIENDS_MESSAGE_MAX_CHARS);
|
||
}
|
||
if (!aeromc_friends_are_friends($uuid, $target)) {
|
||
return new WP_Error('aeromc_not_friends', 'Ihr seid nicht befreundet.', ['status' => 403]);
|
||
}
|
||
$table = aeromc_friends_messages_table();
|
||
|
||
$seit = gmdate('Y-m-d H:i:s', time() - 60);
|
||
$letzte_minute = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM $table WHERE from_uuid = %s AND created_at >= %s", $uuid, $seit
|
||
));
|
||
if ($letzte_minute >= AEROMC_FRIENDS_MESSAGES_PER_MINUTE) {
|
||
return new WP_Error('aeromc_too_fast', 'Zu viele Nachrichten – bitte kurz warten.', ['status' => 429]);
|
||
}
|
||
|
||
// alte Nachrichten (aller Spieler) samt ihrer Bilder wegräumen
|
||
$grenze = gmdate('Y-m-d H:i:s', time() - AEROMC_FRIENDS_MESSAGE_DAYS * 86400);
|
||
$alte_bilder = $wpdb->get_col($wpdb->prepare(
|
||
"SELECT image_url FROM $table WHERE created_at < %s AND image_url IS NOT NULL", $grenze
|
||
));
|
||
foreach ($alte_bilder as $alt) {
|
||
aeromc_friends_delete_image($alt);
|
||
}
|
||
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $grenze));
|
||
|
||
$bild_url = null;
|
||
if ($bild) {
|
||
$bild_url = aeromc_friends_save_image($bild);
|
||
if (is_wp_error($bild_url)) {
|
||
return $bild_url;
|
||
}
|
||
}
|
||
|
||
$wpdb->insert($table, [
|
||
'from_uuid' => $uuid,
|
||
'to_uuid' => $target,
|
||
'text' => $text,
|
||
'image_url' => $bild_url,
|
||
'created_at' => current_time('mysql', true),
|
||
]);
|
||
return ['ok' => true, 'id' => (int) $wpdb->insert_id];
|
||
}
|
||
|
||
/** GET /friends/messages?uuid=&with= - Unterhaltung mit einem Freund (älteste zuerst, höchstens 50) */
|
||
function aeromc_friends_route_messages($request) {
|
||
global $wpdb;
|
||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||
$other = aeromc_friends_normalize_uuid($request->get_param('with'));
|
||
if (!$uuid || !$other) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/with.', ['status' => 400]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$table = aeromc_friends_messages_table();
|
||
$rows = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT id, from_uuid, text, image_url, created_at, read_at FROM $table
|
||
WHERE (from_uuid = %s AND to_uuid = %s) OR (from_uuid = %s AND to_uuid = %s)
|
||
ORDER BY id DESC LIMIT 50",
|
||
$uuid, $other, $other, $uuid
|
||
));
|
||
return array_reverse(array_map(function ($r) {
|
||
return [
|
||
'id' => (int) $r->id,
|
||
'fromUuid' => $r->from_uuid,
|
||
'text' => $r->text,
|
||
'imageUrl' => $r->image_url,
|
||
'createdAt' => $r->created_at,
|
||
'read' => $r->read_at !== null,
|
||
];
|
||
}, $rows));
|
||
}
|
||
|
||
/** GET /friends/messages/unread?uuid= - ungelesene Nachrichten an mich */
|
||
function aeromc_friends_route_messages_unread($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]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$table = aeromc_friends_messages_table();
|
||
$presence_table = aeromc_friends_presence_table();
|
||
$rows = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT m.id, m.from_uuid, p.username AS from_username, m.text, m.image_url, m.created_at
|
||
FROM $table m LEFT JOIN $presence_table p ON p.uuid = m.from_uuid
|
||
WHERE m.to_uuid = %s AND m.read_at IS NULL
|
||
ORDER BY m.id ASC LIMIT 100",
|
||
$uuid
|
||
));
|
||
return array_map(function ($r) {
|
||
return [
|
||
'id' => (int) $r->id,
|
||
'fromUuid' => $r->from_uuid,
|
||
'fromUsername' => $r->from_username,
|
||
'text' => $r->text,
|
||
'imageUrl' => $r->image_url,
|
||
'createdAt' => $r->created_at,
|
||
];
|
||
}, $rows);
|
||
}
|
||
|
||
/** POST /friends/messages/read {uuid, fromUuid} - Nachrichten eines Freundes als gelesen markieren */
|
||
function aeromc_friends_route_messages_read($request) {
|
||
global $wpdb;
|
||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||
$from = aeromc_friends_normalize_uuid($request->get_param('fromUuid'));
|
||
if (!$uuid || !$from) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültige uuid/fromUuid.', ['status' => 400]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$table = aeromc_friends_messages_table();
|
||
$wpdb->query($wpdb->prepare(
|
||
"UPDATE $table SET read_at = %s WHERE to_uuid = %s AND from_uuid = %s AND read_at IS NULL",
|
||
current_time('mysql', true), $uuid, $from
|
||
));
|
||
return ['ok' => true];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Fehlerberichte aus dem Absturz-Assistenten
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Kurzer Text: Steuerzeichen raus, auf Länge kürzen. */
|
||
function aeromc_friends_kurz($wert, $max) {
|
||
$text = trim(preg_replace('/[\x00-\x1F\x7F]+/u', ' ', (string) $wert));
|
||
return mb_substr($text, 0, $max);
|
||
}
|
||
|
||
/**
|
||
* POST /report {launcherVersion, mcVersion, loader, loaderVersion, system, funde[], mods[], bericht, kommentar, uuid?}
|
||
* Ohne Anmeldung möglich (der Nutzer kann anonym senden); mit uuid wird die
|
||
* Anmeldung geprüft, damit niemand Berichte unter fremdem Namen einreicht.
|
||
*/
|
||
function aeromc_friends_route_report($request) {
|
||
global $wpdb;
|
||
// Schutz gegen Fluten: pro Absender-Adresse begrenzt
|
||
$ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
|
||
$schluessel = 'aeromc_rep_' . md5($ip);
|
||
$anzahl = (int) get_transient($schluessel);
|
||
if ($anzahl >= AEROMC_FRIENDS_REPORTS_PER_HOUR) {
|
||
return new WP_Error('aeromc_too_fast', 'Zu viele Berichte in kurzer Zeit – bitte später noch einmal.', ['status' => 429]);
|
||
}
|
||
|
||
$uuid = null;
|
||
$username = null;
|
||
if ($request->get_param('uuid')) {
|
||
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||
if (!$uuid) {
|
||
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||
}
|
||
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||
if (is_wp_error($sitzung)) {
|
||
return $sitzung;
|
||
}
|
||
$username = $sitzung ? $sitzung->username : aeromc_friends_kurz($request->get_param('username'), 32);
|
||
}
|
||
|
||
$bericht = (string) $request->get_param('bericht');
|
||
if (strlen($bericht) > AEROMC_FRIENDS_REPORT_MAX_BYTES) {
|
||
$bericht = substr($bericht, -AEROMC_FRIENDS_REPORT_MAX_BYTES);
|
||
}
|
||
$funde = $request->get_param('funde');
|
||
$mods = $request->get_param('mods');
|
||
|
||
$wpdb->insert(aeromc_friends_reports_table(), [
|
||
'created_at' => current_time('mysql', true),
|
||
'uuid' => $uuid,
|
||
'username' => $username ?: null,
|
||
'launcher_version' => aeromc_friends_kurz($request->get_param('launcherVersion'), 32),
|
||
'mc_version' => aeromc_friends_kurz($request->get_param('mcVersion'), 32),
|
||
'loader' => aeromc_friends_kurz($request->get_param('loader'), 32),
|
||
'loader_version' => aeromc_friends_kurz($request->get_param('loaderVersion'), 64),
|
||
'system' => aeromc_friends_kurz($request->get_param('system'), 128),
|
||
'funde' => wp_json_encode(is_array($funde) ? array_slice($funde, 0, 20) : []),
|
||
'mods' => wp_json_encode(is_array($mods) ? array_slice($mods, 0, 500) : []),
|
||
'bericht' => $bericht,
|
||
'kommentar' => mb_substr(sanitize_textarea_field((string) $request->get_param('kommentar')), 0, 2000),
|
||
'status' => 'neu',
|
||
]);
|
||
set_transient($schluessel, $anzahl + 1, HOUR_IN_SECONDS);
|
||
return ['ok' => true, 'id' => (int) $wpdb->insert_id];
|
||
}
|