38431-vm/bot.php
2026-02-14 18:26:42 +00:00

361 lines
13 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/db/config.php';
use Discord\Discord;
use Discord\Voice\VoiceClient;
use Discord\Parts\Channel\Channel;
use Discord\Parts\User\Member;
use Discord\WebSockets\Event;
use Discord\WebSockets\Intents;
use Discord\Builders\CommandBuilder;
use Discord\Builders\MessageBuilder;
use Discord\Parts\Interactions\Interaction;
use Discord\Parts\Interactions\Command\Option;
$db = db();
$settings = $db->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();