Files
WP-Business-Forum/includes/class-forum-mc-bridge.php
2026-03-30 20:41:51 +02:00

480 lines
21 KiB
PHP

<?php
/**
* WBF_MC_Bridge — Minecraft ↔ Forum Verknüpfung & Ingame-Benachrichtigungen
*
* Dieses Modul verbindet das WP Business Forum mit dem BungeeCord StatusAPI Plugin.
*
* Features:
* - Account-Verknüpfung: Forum-User ↔ MC-UUID (über Token-System)
* - Push-Benachrichtigungen: Neue Antwort/Erwähnung/PN → Ingame-Nachricht
* - REST API Endpoints für die BungeeCord-Seite
*
* Einbindung in wp-business-forum.php:
* require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
*
* Konfiguration in WBF-Einstellungen (Admin → Forum → Einstellungen):
* mc_bridge_enabled = true/false
* mc_bridge_api_url = http://server-ip:9191 (StatusAPI URL)
* mc_bridge_api_secret = Shared Secret für API-Authentifizierung
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WBF_MC_Bridge {
/** Meta-Keys in forum_user_meta */
const META_MC_UUID = 'mc_uuid';
const META_MC_NAME = 'mc_name';
const META_LINK_TOKEN = 'mc_link_token';
const META_LINK_EXPIRY = 'mc_link_token_expires';
/**
* Hooks registrieren — wird beim Plugin-Laden aufgerufen.
*/
public static function init() {
// Hook: Wird in der modifizierten WBF_DB::create_notification() gefeuert
add_action( 'wbf_notification_created', [ __CLASS__, 'on_notification' ], 10, 4 );
// REST API Endpoints für BungeeCord
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
// AJAX: Token generieren (für eingeloggte Forum-User)
add_action( 'wp_ajax_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
// AJAX: Verknüpfung lösen
add_action( 'wp_ajax_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
// AJAX: Link-Status prüfen (Polling im Profil nach Token-Generierung)
add_action( 'wp_ajax_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
}
// ══════════════════════════════════════════════════════════════════════════
// ── Einstellungen ─────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Prüft ob die MC-Bridge aktiviert ist.
*/
public static function is_enabled() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return ! empty( $s['mc_bridge_enabled'] );
}
/**
* Gibt die StatusAPI-URL zurück (z.B. http://192.168.1.100:9191).
*/
private static function get_api_url() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return rtrim( $s['mc_bridge_api_url'] ?? '', '/' );
}
/**
* Gibt das Shared Secret zurück.
*/
private static function get_api_secret() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return $s['mc_bridge_api_secret'] ?? '';
}
// ══════════════════════════════════════════════════════════════════════════
// ── Notification Hook ─────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Wird aufgerufen wenn eine Forum-Notification erstellt wird.
* Prüft ob der Empfänger eine MC-UUID hat und pusht die Nachricht an BungeeCord.
*
* @param int $user_id Forum-User ID des Empfängers
* @param string $type Typ: 'reply', 'mention', 'message'
* @param int $object_id Thread-ID (bei reply/mention) oder Message-ID (bei message)
* @param int $actor_id Forum-User ID des Auslösers
*/
public static function on_notification( $user_id, $type, $object_id, $actor_id ) {
if ( ! self::is_enabled() ) return;
$api_url = self::get_api_url();
if ( empty( $api_url ) ) return;
// MC-UUID des Empfängers prüfen
$mc_uuid = WBF_DB::get_user_meta_single( $user_id, self::META_MC_UUID );
if ( empty( $mc_uuid ) ) return;
// Actor-Info laden
$actor = WBF_DB::get_user( (int) $actor_id );
$actor_name = $actor ? $actor->display_name : 'Unbekannt';
// Kontext-Daten sammeln
$title = '';
$url = '';
$forum_url = wbf_get_forum_url();
switch ( $type ) {
case 'reply':
case 'mention':
$thread = WBF_DB::get_thread( (int) $object_id );
if ( $thread ) {
$title = $thread->title;
$url = $forum_url . '?forum_thread=' . (int) $thread->id;
}
break;
case 'message':
$title = 'Neue Privatnachricht';
$url = $forum_url . '?forum_dm=1';
break;
}
// Push an BungeeCord senden
self::push_to_bungee( $mc_uuid, $type, $title, $actor_name, $url, $user_id );
}
/**
* Sendet die Benachrichtigung per HTTP POST an den BungeeCord StatusAPI Server.
*/
private static function push_to_bungee( $mc_uuid, $type, $title, $author, $url, $wp_user_id ) {
$api_url = self::get_api_url();
$secret = self::get_api_secret();
$payload = wp_json_encode( [
'player_uuid' => $mc_uuid,
'type' => $type,
'title' => $title,
'author' => $author,
'url' => $url,
'wp_user_id' => (int) $wp_user_id,
] );
$args = [
'method' => 'POST',
'timeout' => 5,
'blocking' => false, // Non-blocking — Seite wartet nicht auf Antwort
'headers' => [
'Content-Type' => 'application/json; charset=UTF-8',
'X-Api-Key' => $secret,
],
'body' => $payload,
'sslverify' => false, // Lokales Netzwerk braucht kein SSL
];
wp_remote_post( $api_url . '/forum/notify', $args );
}
// ══════════════════════════════════════════════════════════════════════════
// ── REST API (für BungeeCord → WordPress) ─────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
public static function register_rest_routes() {
// POST /wp-json/mc-bridge/v1/verify-link
// BungeeCord schickt Token + MC-UUID → WP verifiziert und speichert
register_rest_route( 'mc-bridge/v1', '/verify-link', [
'methods' => 'POST',
'callback' => [ __CLASS__, 'rest_verify_link' ],
'permission_callback' => '__return_true',
] );
// POST /wp-json/mc-bridge/v1/unlink
// BungeeCord kann Verknüpfung auch von der MC-Seite lösen
register_rest_route( 'mc-bridge/v1', '/unlink', [
'methods' => 'POST',
'callback' => [ __CLASS__, 'rest_unlink' ],
'permission_callback' => '__return_true',
] );
// GET /wp-json/mc-bridge/v1/status
// Verbindungstest
register_rest_route( 'mc-bridge/v1', '/status', [
'methods' => 'GET',
'callback' => [ __CLASS__, 'rest_status' ],
'permission_callback' => '__return_true',
] );
}
/**
* REST: Verknüpfung bestätigen.
* BungeeCord sendet: { "token": "...", "mc_uuid": "...", "mc_name": "..." }
*/
public static function rest_verify_link( $request ) {
// Rate Limiting: max 10 Versuche pro IP pro Minute
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$limit_key = 'wbf_mc_link_' . md5($ip);
$attempts = (int) get_transient($limit_key);
if ($attempts >= 10) {
return new WP_REST_Response([
'success' => false,
'error' => 'rate_limited',
'message' => 'Zu viele Versuche. Bitte warte eine Minute.'
], 429);
}
set_transient($limit_key, $attempts + 1, 60);
// API-Secret prüfen
$secret = self::get_api_secret();
if ( ! empty( $secret ) ) {
$provided = $request->get_header( 'X-Api-Key' );
if ( $provided !== $secret ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
}
}
$token = sanitize_text_field( $request->get_param( 'token' ) );
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
$mc_name = sanitize_text_field( $request->get_param( 'mc_name' ) );
if ( empty( $token ) || empty( $mc_uuid ) ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_fields' ], 400 );
}
// Token in forum_user_meta suchen
global $wpdb;
$meta_row = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_LINK_TOKEN, $token
) );
if ( ! $meta_row ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'invalid_token' ], 404 );
}
$forum_user_id = (int) $meta_row->user_id;
// Ablauf prüfen
$expiry_meta = WBF_DB::get_user_meta( $forum_user_id );
$expiry = $expiry_meta[ self::META_LINK_EXPIRY ] ?? '0';
if ( (int) $expiry < time() ) {
// Token abgelaufen — aufräumen
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
return new WP_REST_Response( [ 'success' => false, 'error' => 'token_expired' ], 410 );
}
// Prüfen ob diese MC-UUID bereits mit einem anderen Account verknüpft ist
$existing = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_MC_UUID, $mc_uuid
) );
if ( $existing && (int) $existing->user_id !== $forum_user_id ) {
return new WP_REST_Response( [
'success' => false,
'error' => 'uuid_already_linked',
'message' => 'Diese Minecraft-UUID ist bereits mit einem anderen Forum-Account verknüpft.',
], 409 );
}
// Verknüpfung speichern
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, $mc_uuid );
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, $mc_name );
// Token aufräumen
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
// Forum-User-Info für BungeeCord zurückgeben
$forum_user = WBF_DB::get_user( $forum_user_id );
return new WP_REST_Response( [
'success' => true,
'forum_user_id' => $forum_user_id,
'display_name' => $forum_user ? $forum_user->display_name : '',
'username' => $forum_user ? $forum_user->username : '',
], 200 );
}
/**
* REST: Verknüpfung lösen (von BungeeCord-Seite).
*/
public static function rest_unlink( $request ) {
$secret = self::get_api_secret();
if ( ! empty( $secret ) ) {
$provided = $request->get_header( 'X-Api-Key' );
if ( $provided !== $secret ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
}
}
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
if ( empty( $mc_uuid ) ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_mc_uuid' ], 400 );
}
global $wpdb;
$meta_row = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_MC_UUID, $mc_uuid
) );
if ( ! $meta_row ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'not_linked' ], 404 );
}
$forum_user_id = (int) $meta_row->user_id;
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, '' );
return new WP_REST_Response( [ 'success' => true ], 200 );
}
/**
* REST: Status-Endpoint für Verbindungstest.
*/
public static function rest_status( $request ) {
return new WP_REST_Response( [
'success' => true,
'enabled' => self::is_enabled(),
'version' => defined( 'WBF_VERSION' ) ? WBF_VERSION : '?',
'plugin' => 'WP Business Forum',
], 200 );
}
// ══════════════════════════════════════════════════════════════════════════
// ── AJAX: Token generieren ────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Generiert einen 8-stelligen Verknüpfungs-Token (15 Minuten gültig).
* Der User gibt diesen Token dann ingame mit /forumlink <token> ein.
*/
public static function ajax_generate_token() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
}
// Prüfen ob bereits verknüpft
$existing_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
if ( ! empty( $existing_uuid ) ) {
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
wp_send_json_error( [
'message' => 'Dein Account ist bereits mit ' . esc_html( $mc_name ?: $existing_uuid ) . ' verknüpft.',
'linked' => true,
'mc_name' => $mc_name,
'mc_uuid' => $existing_uuid,
] );
}
// Token generieren: 8 Zeichen, alphanumerisch, uppercase
$token = strtoupper( substr( bin2hex( random_bytes( 5 ) ), 0, 8 ) );
$expiry = time() + ( 15 * 60 ); // 15 Minuten
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, $token );
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, (string) $expiry );
wp_send_json_success( [
'token' => $token,
'expires_in' => 15, // Minuten
'command' => '/forumlink ' . $token,
] );
}
/**
* AJAX: Verknüpfung lösen (von der Forum-Seite).
*/
public static function ajax_unlink() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
}
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
// Meta löschen
WBF_DB::set_user_meta( $user->id, self::META_MC_UUID, '' );
WBF_DB::set_user_meta( $user->id, self::META_MC_NAME, '' );
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, '' );
// Optional: BungeeCord informieren
if ( ! empty( $mc_uuid ) && self::is_enabled() ) {
$api_url = self::get_api_url();
$secret = self::get_api_secret();
if ( ! empty( $api_url ) ) {
wp_remote_post( $api_url . '/forum/unlink', [
'method' => 'POST',
'timeout' => 3,
'blocking' => false,
'headers' => [
'Content-Type' => 'application/json',
'X-Api-Key' => $secret,
],
'body' => wp_json_encode( [ 'mc_uuid' => $mc_uuid ] ),
'sslverify' => false,
] );
}
}
wp_send_json_success( [ 'message' => 'Minecraft-Verknüpfung wurde aufgehoben.' ] );
}
/**
* AJAX: Verknüpfungs-Status prüfen.
* Wird vom Frontend-Polling alle 5 Sekunden nach Token-Generierung aufgerufen.
* Gibt zurück ob der User bereits verknüpft ist (BungeeCord hat verify-link gesendet).
*/
public static function ajax_link_status() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.', 'linked' => false ] );
return;
}
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
if ( ! empty( $mc_uuid ) ) {
wp_send_json_success( [
'linked' => true,
'mc_uuid' => $mc_uuid,
'mc_name' => $mc_name,
] );
} else {
wp_send_json_success( [ 'linked' => false ] );
}
}
// ══════════════════════════════════════════════════════════════════════════
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Gibt die MC-UUID eines Forum-Users zurück (oder leer).
*/
public static function get_mc_uuid( $forum_user_id ) {
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_UUID );
}
/**
* Gibt den MC-Namen eines Forum-Users zurück (oder leer).
*/
public static function get_mc_name( $forum_user_id ) {
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_NAME );
}
/**
* Prüft ob ein Forum-User mit MC verknüpft ist.
*/
public static function is_linked( $forum_user_id ) {
$uuid = self::get_mc_uuid( $forum_user_id );
return ! empty( $uuid );
}
/**
* HTML-Badge für Profilansicht: Zeigt MC-Verknüpfung an.
*/
public static function profile_badge( $forum_user_id ) {
if ( ! self::is_enabled() ) return '';
$mc_name = self::get_mc_name( $forum_user_id );
if ( empty( $mc_name ) ) return '';
$name_esc = esc_html( $mc_name );
$head_url = 'https://mc-heads.net/avatar/' . urlencode( $mc_name ) . '/24';
return "<span class=\"wbf-mc-badge\" title=\"Minecraft: {$name_esc}\">"
. "<img src=\"{$head_url}\" alt=\"\" width=\"16\" height=\"16\" style=\"border-radius:2px;vertical-align:middle;margin-right:4px\">"
. "<span style=\"color:#55ff55\">{$name_esc}</span>"
. "</span>";
}
}
// Initialisierung
add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );