query("SELECT setting_key, setting_value FROM bot_settings")->fetchAll(PDO::FETCH_KEY_PAIR); $token = $settings['bot_token'] ?? ''; $vcId = $settings['voice_channel_id'] ?? '1457687430189682781'; $sahurTime = $settings['sahur_time'] ?? '03:00'; $sahurSource = $settings['sahur_source'] ?? 'sahur.mp3'; if (empty($token)) { echo "Error: Bot token is missing in settings.\n"; exit(1); } $discord = new Discord([ 'token' => $token, 'intents' => Intents::getDefaultIntents() | Intents::GUILD_VOICE_STATES | Intents::MESSAGE_CONTENT, ]); $voiceClient = null; $queue = []; $currentTrack = null; function logToDb($message, $level = 'info') { $db = db(); $stmt = $db->prepare("INSERT INTO bot_logs (message, log_level) VALUES (?, ?)"); $stmt->execute([$message, $level]); } $discord->on('ready', function (Discord $discord) use (&$voiceClient, $vcId, $sahurTime) { echo "Bot is ready!", PHP_EOL; logToDb("Bot is online and ready."); // Update status in DB $db = db(); $db->prepare("UPDATE bot_settings SET setting_value = 'online' WHERE setting_key = 'bot_status'")->execute(); // Auto-join VC joinVoiceChannel($discord, $vcId); // Schedule Sahur $discord->getLoop()->addPeriodicTimer(60, function () use ($discord, $sahurTime, $vcId) { $now = date('H:i'); if ($now === $sahurTime) { echo "Sahur time! Playing audio...\n"; logToDb("Sahur time triggered. Playing audio."); playSahur($discord, $vcId); } }); // Register Slash Commands registerCommands($discord); }); function joinVoiceChannel(Discord $discord, $channelId, $interaction = null) { global $voiceClient; if ($voiceClient) { $msg = "I am already in a voice channel: " . ($voiceClient->getChannel()->name ?? 'Unknown'); if ($interaction) { $interaction->respondWithMessage(MessageBuilder::new()->setContent($msg)); } return; } $channel = $discord->getChannel($channelId); // If channel not in cache, try to find it in guilds if (!$channel) { foreach ($discord->guilds as $guild) { $channel = $guild->channels->get('id', $channelId); if ($channel) break; } } if ($channel instanceof Channel && $channel->type === Channel::TYPE_VOICE) { if ($interaction) { $interaction->respondWithMessage(MessageBuilder::new()->setContent("Connecting to voice channel: " . $channel->name . "...")); } $discord->joinVoiceChannel($channel)->then(function (VoiceClient $vc) use (&$voiceClient, $discord, $channelId, $channel, $interaction) { $voiceClient = $vc; echo "Joined voice channel: " . $channel->name, PHP_EOL; logToDb("Joined voice channel: " . $channel->name); if ($interaction) { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Successfully joined voice channel: " . $channel->name)); } $vc->on('error', function ($e) use ($discord, $channelId) { echo "Voice Error: " . $e->getMessage(), PHP_EOL; logToDb("Voice error: " . $e->getMessage(), 'error'); }); }, function ($e) use ($interaction) { $errorMsg = "Could not join voice channel: " . $e->getMessage(); echo $errorMsg, PHP_EOL; logToDb($errorMsg, 'error'); if ($interaction) { try { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent($errorMsg)); } catch (\Exception $ex) { // Fallback if update fails } } }); } else { $errorMsg = "Voice channel not found or invalid ID: " . $channelId; if ($interaction) { $interaction->respondWithMessage(MessageBuilder::new()->setContent($errorMsg)); } echo $errorMsg, PHP_EOL; } } function playSahur(Discord $discord, $vcId) { global $voiceClient, $sahurSource; if (!$voiceClient) { joinVoiceChannel($discord, $vcId); // Wait a bit for connection $discord->getLoop()->addTimer(3, function() use ($discord, $vcId) { playSahur($discord, $vcId); }); return; } if (filter_var($sahurSource, FILTER_VALIDATE_URL)) { echo "Playing sahur from URL: $sahurSource\n"; $cmd = "yt-dlp -g -f bestaudio \"$sahurSource\""; exec($cmd, $output, $resultCode); if ($resultCode === 0 && !empty($output[0])) { $voiceClient->playRawStream($output[0]); } } else { if (!file_exists($sahurSource)) { $error = "Error: Audio file '$sahurSource' not found. Please upload it to the bot's root directory."; echo $error, PHP_EOL; logToDb($error, 'error'); return; } echo "Playing sahur from file: $sahurSource\n"; $voiceClient->playFile($sahurSource)->done(function() { echo "Finished playing sahur audio.\n"; }); } } function registerCommands(Discord $discord) { $commands = [ CommandBuilder::new() ->setName('join') ->setDescription('Join the default voice channel'), CommandBuilder::new() ->setName('out') ->setDescription('Leave the voice channel'), CommandBuilder::new() ->setName('status') ->setDescription('Check bot status'), CommandBuilder::new() ->setName('testsahur') ->setDescription('Test play sahur audio now'), CommandBuilder::new() ->setName('play') ->setDescription('Play music from URL') ->addOption((new Option($discord)) ->setName('url') ->setDescription('The URL of the song (YouTube, SoundCloud, etc.)') ->setType(Option::STRING) ->setRequired(true)), CommandBuilder::new() ->setName('skip') ->setDescription('Skip current song'), CommandBuilder::new() ->setName('stop') ->setDescription('Stop music and clear queue'), ]; foreach ($commands as $command) { $discord->application->commands->save( $discord->application->commands->create($command->toArray()) ); } } $discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use (&$voiceClient, $vcId) { $command = $interaction->data->name; switch ($command) { case 'join': $userChannel = $interaction->member->getVoiceChannel(); if ($userChannel) { joinVoiceChannel($discord, $userChannel->id, $interaction); } else { joinVoiceChannel($discord, $vcId, $interaction); } break; case 'out': if ($voiceClient) { $channelName = $voiceClient->getChannel()->name ?? 'channel'; $voiceClient->close(); $voiceClient = null; $interaction->respondWithMessage(MessageBuilder::new()->setContent("Left voice channel: $channelName.")); } else { $interaction->respondWithMessage(MessageBuilder::new()->setContent("I'm not in a voice channel.")); } break; case 'status': $vcStatus = $voiceClient ? "Connected to: " . ($voiceClient->getChannel()->name ?? 'Unknown') : "Not connected to any voice channel."; $status = "Bot is online.\n$vcStatus\nTarget default VC: $vcId"; $interaction->respondWithMessage(MessageBuilder::new()->setContent($status)); break; case 'testsahur': playSahur($discord, $vcId); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Testing sahur audio...")); break; case 'play': $url = $interaction->data->options['url']->value; handlePlay($interaction, $discord, $url); break; case 'skip': if ($voiceClient) { $voiceClient->stop(); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Skipped.")); } break; case 'stop': global $queue; $queue = []; if ($voiceClient) { $voiceClient->stop(); } $interaction->respondWithMessage(MessageBuilder::new()->setContent("Stopped and cleared queue.")); break; } }); function handlePlay(Interaction $interaction, Discord $discord, string $url) { global $voiceClient, $queue, $vcId; if (!$voiceClient) { $interaction->respondWithMessage(MessageBuilder::new()->setContent("I need to be in a voice channel first. use `/join`")); return; } $interaction->acknowledge(); // Simple play logic using yt-dlp $cmd = "yt-dlp -g -f bestaudio \"$url\""; exec($cmd, $output, $resultCode); if ($resultCode === 0 && !empty($output[0])) { $audioUrl = $output[0]; $voiceClient->playRawStream($audioUrl)->done(function() use ($interaction) { // Logic for next in queue could go here }); $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Now playing: $url")); } else { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Error: Could not fetch audio for that URL.")); } } $discord->run();