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, ]); // Check ffmpeg $ffmpegPath = shell_exec("command -v ffmpeg"); if (!$ffmpegPath) { echo "Warning: ffmpeg not found in PATH. Voice functionality might fail.\n"; logToDb("Warning: ffmpeg not found in PATH.", 'warning'); } else { echo "ffmpeg found at: " . trim($ffmpegPath) . "\n"; } $voiceClient = null; $queue = []; $currentTrack = null; $isJoining = false; 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) { 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(); // Schedule Sahur and Alarms $discord->getLoop()->addPeriodicTimer(60, function () use ($discord) { $now = date('H:i'); $db = db(); // Refresh settings $settings = $db->query("SELECT setting_key, setting_value FROM bot_settings")->fetchAll(PDO::FETCH_KEY_PAIR); $sahurTime = $settings['sahur_time'] ?? '03:00'; $vcId = $settings['voice_channel_id'] ?? '1457687430189682781'; if ($now === $sahurTime) { echo "Sahur time ($now)! Playing audio...\n"; logToDb("Sahur time triggered. Playing audio."); playSahur($discord, $vcId, true); } // Check individual alarms $stmt = $db->prepare("SELECT * FROM bot_alarms WHERE alarm_time LIKE ? AND is_active = 1"); $stmt->execute([$now . '%']); $alarms = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($alarms as $alarm) { echo "Alarm triggered for user {$alarm['user_id']} at {$alarm['alarm_time']}\n"; logToDb("Alarm triggered for user {$alarm['user_id']} at {$alarm['alarm_time']}"); playAlarm($discord, $alarm); } }); // Register Slash Commands $discord->getLoop()->addTimer(2, function() use ($discord) { echo "Registering slash commands...\n"; registerCommands($discord); }); }); function registerCommands(Discord $discord) { $commands = [ CommandBuilder::new() ->setName('join') ->setDescription('Join your current voice channel'), CommandBuilder::new() ->setName('out') ->setDescription('Leave the voice channel'), CommandBuilder::new() ->setName('status') ->setDescription('Check bot status'), CommandBuilder::new() ->setName('play') ->setDescription('Play music from URL') ->addOption((new Option($discord)) ->setName('url') ->setDescription('YouTube/SoundCloud URL') ->setType(Option::STRING) ->setRequired(true)), CommandBuilder::new() ->setName('stop') ->setDescription('Stop music'), CommandBuilder::new() ->setName('settime') ->setDescription('Set your personal alarm time (HH:MM)') ->addOption((new Option($discord)) ->setName('time') ->setDescription('Time in HH:MM format') ->setType(Option::STRING) ->setRequired(true)), CommandBuilder::new() ->setName('setalarm') ->setDescription('Set your personal alarm audio link') ->addOption((new Option($discord)) ->setName('link') ->setDescription('YouTube or TikTok link for the alarm') ->setType(Option::STRING) ->setRequired(true)), CommandBuilder::new() ->setName('help') ->setDescription('Show help information'), ]; foreach ($commands as $command) { $discord->application->commands->save( $discord->application->commands->create($command->toArray()) ); } } function joinVoiceChannel(Discord $discord, $channelId, $interaction = null) { global $voiceClient, $isJoining; if ($isJoining) return; $existingVc = $discord->getVoiceClient($discord->guilds->first()->id ?? ''); if ($existingVc) { $voiceClient = $existingVc; return; } $channel = $discord->getChannel($channelId); 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) { $isJoining = true; $discord->joinVoiceChannel($channel)->then(function (VoiceClient $vc) use (&$voiceClient, &$isJoining) { $voiceClient = $vc; $isJoining = false; echo "Joined voice channel.\n"; }, function ($e) use (&$isJoining) { $isJoining = false; echo "Join failed: " . $e->getMessage() . "\n"; }); } } function playSahur(Discord $discord, $vcId, $retry = true) { global $voiceClient; $existingVc = $discord->getVoiceClient($discord->guilds->first()->id ?? ''); if ($existingVc) $voiceClient = $existingVc; if (!$voiceClient) { joinVoiceChannel($discord, $vcId); if ($retry) { $discord->getLoop()->addTimer(5, function() use ($discord, $vcId) { playSahur($discord, $vcId, false); }); } return; } $db = db(); $source = $db->query("SELECT setting_value FROM bot_settings WHERE setting_key = 'sahur_source'")->fetchColumn() ?: 'sahur.mp3'; if (filter_var($source, FILTER_VALIDATE_URL)) { $cmd = "yt-dlp -g -f bestaudio \"$source\""; exec($cmd, $output, $res); if ($res === 0 && !empty($output[0])) $voiceClient->playRawStream($output[0]); } else if (file_exists($source)) { $voiceClient->playFile($source); } } function playAlarm(Discord $discord, $alarm) { $channelId = $alarm['channel_id']; $url = $alarm['audio_url']; $vc = $discord->getVoiceClient($alarm['guild_id']); if ($vc && $vc->channel->id === $channelId) { streamAudio($vc, $url); } else { if ($vc) $vc->close(); $channel = $discord->getChannel($channelId); if ($channel) { $discord->joinVoiceChannel($channel)->then(function (VoiceClient $newVc) use ($url) { streamAudio($newVc, $url); }); } } } function streamAudio($vc, $url) { $cmd = "yt-dlp -g -f bestaudio \"$url\""; exec($cmd, $output, $res); if ($res === 0 && !empty($output[0])) { $vc->playRawStream($output[0]); } } $discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use (&$voiceClient, $vcId) { if ($interaction->type !== 2) return; $command = $interaction->data->name; $db = db(); switch ($command) { case 'help': $interaction->respondWithMessage(MessageBuilder::new()->setContent( "**Bot Commands:**\n" . "`/join` - Join your voice channel\n" . "`/play [url]` - Play music\n" . "`/stop` - Stop music\n" . "`/settime [HH:MM]` - Set alarm time\n" . "`/setalarm [link]` - Set alarm music\n" . "`/status` - Check bot status" )); break; case 'join': $interaction->acknowledge(); $userChannel = $interaction->member->getVoiceChannel(); if ($userChannel) { joinVoiceChannel($discord, $userChannel->id); $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Joined " . $userChannel->name)); } else { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Join a voice channel first!")); } break; case 'play': $url = $interaction->data->options['url']->value; handlePlay($interaction, $discord, $url); break; case 'stop': if ($voiceClient) $voiceClient->stop(); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Stopped.")); break; case 'settime': $time = $interaction->data->options['time']->value; if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) { $interaction->respondWithMessage(MessageBuilder::new()->setContent("Format: HH:MM")); break; } $userId = (string)$interaction->member->id; $guildId = (string)$interaction->guild_id; $channelId = (string)($interaction->member->getVoiceChannel()->id ?? $vcId); $stmt = $db->prepare("INSERT INTO bot_alarms (user_id, guild_id, channel_id, alarm_time, audio_url) VALUES (?, ?, ?, ?, 'sahur.mp3') ON DUPLICATE KEY UPDATE alarm_time = ?, guild_id = ?, channel_id = ?"); $stmt->execute([$userId, $guildId, $channelId, $time, $time, $guildId, $channelId]); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Alarm time set to $time.")); break; case 'setalarm': $link = $interaction->data->options['link']->value; $userId = (string)$interaction->member->id; $guildId = (string)$interaction->guild_id; $channelId = (string)($interaction->member->getVoiceChannel()->id ?? $vcId); $stmt = $db->prepare("INSERT INTO bot_alarms (user_id, guild_id, channel_id, alarm_time, audio_url) VALUES (?, ?, ?, '03:00', ?) ON DUPLICATE KEY UPDATE audio_url = ?, guild_id = ?, channel_id = ?"); $stmt->execute([$userId, $guildId, $channelId, $link, $link, $guildId, $channelId]); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Alarm music set.")); break; case 'status': $interaction->respondWithMessage(MessageBuilder::new()->setContent("Bot is online. Voice: " . ($voiceClient ? "Connected" : "Disconnected"))); break; case 'out': if ($voiceClient) { $voiceClient->close(); $voiceClient = null; $interaction->respondWithMessage(MessageBuilder::new()->setContent("Left voice channel.")); } else { $interaction->respondWithMessage(MessageBuilder::new()->setContent("Not in a channel.")); } break; } }); function handlePlay(Interaction $interaction, Discord $discord, string $url) { global $voiceClient; $interaction->acknowledge(); $guildId = $interaction->guild_id; $vc = $discord->getVoiceClient($guildId); if ($vc) $voiceClient = $vc; if (!$voiceClient) { $userChannel = $interaction->member->getVoiceChannel(); if ($userChannel) { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Joining " . $userChannel->name . "...")); $discord->joinVoiceChannel($userChannel)->then(function (VoiceClient $newVc) use ($interaction, $url) { global $voiceClient; $voiceClient = $newVc; processPlay($interaction, $newVc, $url); }); } else { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Join a VC first!")); } return; } processPlay($interaction, $voiceClient, $url); } function processPlay($interaction, $vc, $url) { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Loading: $url ...")); $cmd = "yt-dlp -g -f bestaudio \"$url\""; exec($cmd, $output, $res); if ($res === 0 && !empty($output[0])) { $vc->playRawStream($output[0]); $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Now playing: $url")); } else { $interaction->updateOriginalResponse(MessageBuilder::new()->setContent("Error fetching audio.")); } } $discord->run();