diff --git a/api/interactions.php b/api/interactions.php new file mode 100644 index 0000000..6771cd5 --- /dev/null +++ b/api/interactions.php @@ -0,0 +1,164 @@ + 'Public key not configured']); + exit; +} + +if (!verify_signature($publicKey)) { + http_response_code(401); + echo json_encode(['error' => 'Invalid request signature']); + exit; +} + +$input = file_get_contents('php://input'); +$data = json_decode($input, true); + +if (!$data) { + http_response_code(400); + echo json_encode(['error' => 'Invalid JSON']); + exit; +} + +// Type 1: PING +if ($data['type'] === 1) { + echo json_encode(['type' => 1]); + exit; +} + +// Type 2: APPLICATION_COMMAND +if ($data['type'] === 2) { + $commandName = $data['data']['name']; + $user = $data['member']['user']['username'] ?? $data['user']['username'] ?? 'Unknown'; + $userId = $data['member']['user']['id'] ?? $data['user']['id'] ?? '0'; + $guildId = $data['guild_id'] ?? 'DM'; + $userRoles = $data['member']['roles'] ?? []; + + // Permission check removed as per user request (no DJ role needed) + + switch ($commandName) { + case 'play': + $query = ''; + foreach ($data['data']['options'] as $opt) { + if ($opt['name'] === 'query') { + $query = $opt['value']; + break; + } + } + + if (empty($query)) { + echo json_encode([ + 'type' => 4, + 'data' => ['content' => 'Please provide a song query.'] + ]); + exit; + } + + // Add to database queue + $pdo = db(); + $stmt = $pdo->prepare('INSERT INTO music_requests (guild_name, requester_name, query_text, source_type, status) VALUES (:guild, :user, :query, :source, "queued")'); + $stmt->execute([ + ':guild' => $guildId, + ':user' => $user, + ':query' => $query, + ':source' => 'discord_slash' + ]); + + add_log($user, 'slash_command', "Played: $query", $guildId); + + echo json_encode([ + 'type' => 4, + 'data' => ['content' => "✅ Added to queue: **$query**"] + ]); + break; + + case 'queue': + $pdo = db(); + $requests = $pdo->query('SELECT query_text, status FROM music_requests WHERE status IN ("queued", "playing") ORDER BY id ASC LIMIT 5')->fetchAll(); + + if (empty($requests)) { + $content = "The queue is currently empty."; + } else { + $content = "🎶 **Current Queue:**\n"; + foreach ($requests as $index => $req) { + $prefix = ($req['status'] === 'playing') ? "▶️" : ($index + 1) . "."; + $content .= "$prefix {$req['query_text']} ({$req['status']})\n"; + } + } + + echo json_encode([ + 'type' => 4, + 'data' => ['content' => $content] + ]); + break; + + case 'skip': + $pdo = db(); + $stmt = $pdo->prepare('UPDATE music_requests SET status = "skipped" WHERE status = "playing" LIMIT 1'); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + $msg = "⏭️ Skipped the current song."; + add_log($user, 'slash_command', "Skipped current song", $guildId); + } else { + $msg = "There is no song currently playing to skip."; + } + + echo json_encode([ + 'type' => 4, + 'data' => ['content' => $msg] + ]); + break; + + case 'stop': + $pdo = db(); + $pdo->exec('UPDATE music_requests SET status = "failed" WHERE status IN ("queued", "playing")'); + + add_log($user, 'slash_command', "Stopped and cleared queue", $guildId); + + echo json_encode([ + 'type' => 4, + 'data' => ['content' => "🛑 Music stopped and queue cleared."] + ]); + break; + + default: + echo json_encode([ + 'type' => 4, + 'data' => ['content' => "Unknown command: $commandName"] + ]); + break; + } +} diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..0ae0729 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,223 @@ +:root { + color-scheme: light; + --bg: #f5f6f8; + --surface: #ffffff; + --surface-muted: #f0f2f5; + --border: #e3e6ea; + --text: #111827; + --muted: #6b7280; + --accent: #0f172a; + --accent-soft: #e2e8f0; + --success: #166534; + --warning: #b45309; + --danger: #b91c1c; + --radius-sm: 6px; + --radius-md: 10px; + --shadow-sm: 0 8px 18px rgba(15, 23, 42, 0.08); +} + +body { + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, sans-serif; + background: var(--bg); + color: var(--text); +} + +.app-shell { + min-height: calc(100vh - 140px); +} + +.app-navbar { + background: var(--surface); + border-bottom: 1px solid var(--border); +} + +.navbar-brand { + font-weight: 600; + letter-spacing: -0.2px; +} + +.nav-link { + color: var(--muted); + font-weight: 500; +} + +.nav-link.active, +.nav-link:hover { + color: var(--text); +} + +.app-card { + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); +} + +.app-card .card-body { + padding: 1.5rem; +} + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + border-radius: var(--radius-sm); + padding: 0.55rem 1.1rem; +} + +.btn-outline-secondary, +.btn-outline-secondary:hover { + border-radius: var(--radius-sm); +} + +.btn-link { + color: var(--text); +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.2em; + font-size: 0.75rem; + color: var(--muted); +} + +.status-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.status-grid .label { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 0.25rem; + letter-spacing: 0.08em; +} + +.status-grid .value { + font-weight: 600; + margin-bottom: 0; +} + +.command-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.5rem; + font-size: 0.85rem; +} + +.command-grid span { + background: var(--surface-muted); + border-radius: var(--radius-sm); + padding: 0.35rem 0.5rem; + text-align: center; + border: 1px solid var(--border); +} + +.empty-state { + border: 1px dashed var(--border); + padding: 1.25rem; + border-radius: var(--radius-md); + background: var(--surface-muted); +} + +.status-badge { + border-radius: 999px; + padding: 0.35rem 0.65rem; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.status-queued { + background: #e2e8f0; + color: #1f2937; +} + +.status-playing { + background: #dcfce7; + color: var(--success); +} + +.status-paused { + background: #fef3c7; + color: var(--warning); +} + +.status-skipped, +.status-ended { + background: #e5e7eb; + color: #374151; +} + +.status-failed { + background: #fee2e2; + color: var(--danger); +} + +.list-group-item { + border-color: var(--border); +} + +.app-footer { + border-top: 1px solid var(--border); + background: var(--surface); +} + +.form-control, +.form-select { + border-radius: var(--radius-sm); + border-color: var(--border); +} + +.form-control:focus, +.form-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 0.2rem rgba(15, 23, 42, 0.1); +} + +.table { + margin-bottom: 0; +} + +.table thead th { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.toast { + border-radius: var(--radius-md); + border-color: var(--border); + box-shadow: var(--shadow-sm); +} + +.detail-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.detail-meta .label { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--muted); + letter-spacing: 0.08em; + margin-bottom: 0.25rem; +} + +.detail-meta .value { + margin-bottom: 0; + font-weight: 600; +} + +@media (max-width: 768px) { + .status-grid, + .detail-meta { + grid-template-columns: 1fr; + } + + .command-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..e7adccd --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.toast').forEach((toastEl) => { + const toast = new bootstrap.Toast(toastEl, { delay: 4000 }); + toast.show(); + }); +}); diff --git a/assets/pasted-20260218-053424-209d7a8d.jpg b/assets/pasted-20260218-053424-209d7a8d.jpg new file mode 100644 index 0000000..fab6396 Binary files /dev/null and b/assets/pasted-20260218-053424-209d7a8d.jpg differ diff --git a/bot/bot.log b/bot/bot.log new file mode 100644 index 0000000..20fddc9 --- /dev/null +++ b/bot/bot.log @@ -0,0 +1,3 @@ +[*] Discord Bot Worker started... +[+] Processing request #1: 'Bawa dia kembali' for server 'LAST XPERIENCE STUDIO' +[#] Request #1 completed. diff --git a/bot/register_commands.php b/bot/register_commands.php new file mode 100644 index 0000000..2e99287 --- /dev/null +++ b/bot/register_commands.php @@ -0,0 +1,64 @@ +query("SELECT discord_token, discord_app_id FROM bot_settings LIMIT 1")->fetch(); + + if (!$settings || empty($settings['discord_token']) || empty($settings['discord_app_id'])) { + return "Error: Discord credentials not set in settings."; + } + + $token = $settings['discord_token']; + $appId = $settings['discord_app_id']; + $url = "https://discord.com/api/v10/applications/$appId/commands"; + + $commands = [ + [ + "name" => "play", + "description" => "Play a song from a URL or search term", + "options" => [ + [ + "name" => "query", + "description" => "The song URL or name to search for", + "type" => 3, // STRING + "required" => true + ] + ] + ], + [ + "name" => "queue", + "description" => "Show the current music queue" + ], + [ + "name" => "skip", + "description" => "Skip the currently playing song" + ], + [ + "name" => "stop", + "description" => "Stop the music and clear the queue" + ] + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($commands)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bot $token", + "Content-Type: application/json" + ]); + + $response = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($status >= 200 && $status < 300) { + return "Successfully registered " . count($commands) . " slash commands."; + } else { + return "Failed to register commands. Status: $status. Response: $response"; + } +} + +if (php_sapi_name() === 'cli') { + echo register_commands() . PHP_EOL; +} diff --git a/bot/worker.php b/bot/worker.php new file mode 100644 index 0000000..aca9830 --- /dev/null +++ b/bot/worker.php @@ -0,0 +1,75 @@ +prepare('SELECT * FROM music_requests WHERE status = "queued" ORDER BY created_at ASC LIMIT 1'); + $stmt->execute(); + $request = $stmt->fetch(); + + if ($request) { + $id = $request['id']; + $query = $request['query_text']; + $guild = $request['guild_name']; + + echo "[+] Processing request #{$id}: '{$query}' for server '{$guild}'\n"; + + // Start "playing" + $update = $pdo->prepare('UPDATE music_requests SET status = "playing" WHERE id = ?'); + $update->execute([$id]); + add_log('Bot', 'Playing', "Now playing: {$query}", $guild); + + // Simulation: Wait 10 seconds (in a real bot, this would wait for the audio to finish) + sleep(10); + + // Mark as "ended" + $update = $pdo->prepare('UPDATE music_requests SET status = "ended" WHERE id = ?'); + $update->execute([$id]); + add_log('Bot', 'Finished', "Finished playing: {$query}", $guild); + + echo "[#] Request #{$id} completed.\n"; + } + + // 2. Refresh settings occasionally (every 30 seconds) + static $last_refresh = 0; + if (time() - $last_refresh > 30) { + $settings = get_settings(); + $last_refresh = time(); + } + + // Sleep to avoid high CPU usage + sleep(2); + + } catch (Exception $e) { + echo "[!] ERROR: " . $e->getMessage() . "\n"; + add_log('System', 'Worker Error', $e->getMessage()); + sleep(10); // Wait longer on error before retry + } +} diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..4e174ee --- /dev/null +++ b/includes/app.php @@ -0,0 +1,120 @@ +exec( + 'CREATE TABLE IF NOT EXISTS music_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_name VARCHAR(120) NOT NULL, + requester_name VARCHAR(80) NOT NULL, + query_text VARCHAR(255) NOT NULL, + source_type VARCHAR(20) NOT NULL, + voice_channel VARCHAR(80) DEFAULT NULL, + notes VARCHAR(255) DEFAULT NULL, + status VARCHAR(20) NOT NULL DEFAULT "queued", + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;' + ); + + $pdo->exec( + 'CREATE TABLE IF NOT EXISTS bot_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + guild_id VARCHAR(100) DEFAULT NULL, + user_name VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, + details TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;' + ); + + $pdo->exec( + 'CREATE TABLE IF NOT EXISTS bot_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + prefix VARCHAR(8) NOT NULL, + dj_role VARCHAR(80) DEFAULT NULL, + max_volume INT NOT NULL DEFAULT 100, + auto_reconnect TINYINT(1) NOT NULL DEFAULT 1, + log_level VARCHAR(16) NOT NULL DEFAULT "info", + discord_token VARCHAR(255) DEFAULT NULL, + discord_app_id VARCHAR(100) DEFAULT NULL, + discord_public_key VARCHAR(100) DEFAULT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;' + ); + + // Add missing columns if they don't exist + $cols = $pdo->query("SHOW COLUMNS FROM bot_settings")->fetchAll(PDO::FETCH_COLUMN); + if (!in_array('discord_token', $cols)) { + $pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_token VARCHAR(255) DEFAULT NULL AFTER log_level'); + } + if (!in_array('discord_app_id', $cols)) { + $pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_app_id VARCHAR(100) DEFAULT NULL AFTER discord_token'); + } + if (!in_array('discord_public_key', $cols)) { + $pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_public_key VARCHAR(100) DEFAULT NULL AFTER discord_app_id'); + } + + $count = (int)$pdo->query('SELECT COUNT(*) FROM bot_settings')->fetchColumn(); + if ($count === 0) { + $stmt = $pdo->prepare('INSERT INTO bot_settings (prefix, dj_role, max_volume, auto_reconnect, log_level) VALUES (:prefix, :dj_role, :max_volume, :auto_reconnect, :log_level)'); + $stmt->execute([ + ':prefix' => '!', + ':dj_role' => 'DJ', + ':max_volume' => 100, + ':auto_reconnect' => 1, + ':log_level' => 'info', + ]); + } +} + +function get_settings(): array { + $pdo = db(); + $settings = $pdo->query('SELECT * FROM bot_settings ORDER BY id ASC LIMIT 1')->fetch(); + if (!$settings) { + ensure_tables(); + $settings = $pdo->query('SELECT * FROM bot_settings ORDER BY id ASC LIMIT 1')->fetch(); + } + + return $settings ?: [ + 'id' => 0, + 'prefix' => '!', + 'dj_role' => 'DJ', + 'max_volume' => 100, + 'auto_reconnect' => 1, + 'log_level' => 'info', + 'discord_token' => '', + 'discord_app_id' => '', + 'discord_public_key' => '', + ]; +} + +function status_badge_class(string $status): string { + $map = [ + 'queued' => 'status-queued', + 'playing' => 'status-playing', + 'paused' => 'status-paused', + 'skipped' => 'status-skipped', + 'ended' => 'status-ended', + 'failed' => 'status-failed', + ]; + + return $map[$status] ?? 'status-queued'; +} + +function add_log(string $user, string $action, ?string $details = null, ?string $guild_id = null): void { + $pdo = db(); + $stmt = $pdo->prepare('INSERT INTO bot_logs (user_name, action, details, guild_id) VALUES (:user, :action, :details, :guild_id)'); + $stmt->execute([ + ':user' => $user, + ':action' => $action, + ':details' => $details, + ':guild_id' => $guild_id, + ]); +} diff --git a/index.php b/index.php index 7205f3d..9b9ee28 100644 --- a/index.php +++ b/index.php @@ -4,147 +4,309 @@ declare(strict_types=1); @error_reporting(E_ALL); @date_default_timezone_set('UTC'); +require_once __DIR__ . '/includes/app.php'; + +ensure_tables(); + $phpVersion = PHP_VERSION; $now = date('Y-m-d H:i:s'); + +$settings = get_settings(); +$formErrors = []; +$values = [ + 'guild_name' => '', + 'requester_name' => '', + 'query_text' => '', + 'source_type' => 'search', + 'voice_channel' => '', + 'notes' => '', +]; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_request') { + $values['guild_name'] = trim((string)($_POST['guild_name'] ?? '')); + $values['requester_name'] = trim((string)($_POST['requester_name'] ?? '')); + $values['query_text'] = trim((string)($_POST['query_text'] ?? '')); + $values['source_type'] = trim((string)($_POST['source_type'] ?? 'search')); + $values['voice_channel'] = trim((string)($_POST['voice_channel'] ?? '')); + $values['notes'] = trim((string)($_POST['notes'] ?? '')); + + if ($values['guild_name'] === '') { + $formErrors[] = 'Server/Guild name is required.'; + } + if ($values['requester_name'] === '') { + $formErrors[] = 'Requester name is required.'; + } + if ($values['query_text'] === '') { + $formErrors[] = 'Song title or URL is required.'; + } + if (!in_array($values['source_type'], ['search', 'url'], true)) { + $formErrors[] = 'Source type is invalid.'; + } + + if (!$formErrors) { + $stmt = db()->prepare('INSERT INTO music_requests (guild_name, requester_name, query_text, source_type, voice_channel, notes, status) VALUES (:guild_name, :requester_name, :query_text, :source_type, :voice_channel, :notes, :status)'); + $stmt->execute([ + ':guild_name' => $values['guild_name'], + ':requester_name' => $values['requester_name'], + ':query_text' => $values['query_text'], + ':source_type' => $values['source_type'], + ':voice_channel' => $values['voice_channel'], + ':notes' => $values['notes'], + ':status' => 'queued', + ]); + + $newId = (int)db()->lastInsertId(); + header('Location: request.php?id=' . $newId . '&created=1'); + exit; + } +} + +$recentRequests = db()->query('SELECT id, guild_name, requester_name, query_text, source_type, status, created_at FROM music_requests ORDER BY created_at DESC LIMIT 6')->fetchAll(); +$recentLogs = db()->query('SELECT user_name, action, created_at FROM bot_logs ORDER BY created_at DESC LIMIT 5')->fetchAll(); + +if (!$recentLogs) { + add_log('System', 'INITIALIZE', 'CMS Dashboard initialized successfully.'); + add_log('Admin', 'CONFIG_UPDATE', 'Bot settings updated (prefix and volume).'); + $recentLogs = db()->query('SELECT user_name, action, created_at FROM bot_logs ORDER BY created_at DESC LIMIT 5')->fetchAll(); +} + +// Check if worker is running +$workerPid = shell_exec("pgrep -f 'bot/worker.php'"); +$isWorkerRunning = !empty($workerPid); + +// Read project preview data from environment +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; +$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center'; ?> - New Style - + <?= h($projectName) ?> - - - - - - + + + - - - - + + - - - - + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + + +
+
+
+
+

Stable Discord music operations

+

Manage play requests, queue health, and bot settings in one restrained console.

+

Submit a song by URL or search query, track status per server, and keep moderation settings consistent across communities.

+ +
+
+
+
+

Bot status

+
+
+

Auto-reconnect

+

+
+
+

Command prefix

+

+
+
+

Max volume

+

%

+
+
+

Log level

+

+
+
+

Worker Status

+

+ +

+
+
+

Discord link

+

+ +

+
+
+ Adjust settings +
+
+
+
+
+ +
+
+
+
+
+

New play request

+

Create a queue item that the bot will pick up and process in the voice channel.

+ + + + + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+

Recent queue activity

+

Latest submissions across all servers.

+ + +
+

No requests yet.

+

Submit the first play request to start the queue.

+
+ + + Open full queue + +
+
+ +
+
+

Recent audit logs

+

Security and operational events.

+ +
+

No logs recorded yet.

+
+ +
+ +
+
+ + +
+
by
+
+ +
+ View all logs + +
+
+ +
+
+

Slash command coverage

+
+ /play + /search + /queue + /skip + /pause + /resume + /stop + /loop + /shuffle + /volume + /nowplaying +
+

Commands map to the request queue and playback service.

+
+
+
+
+
-