361 lines
13 KiB
PHP
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();
|