diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..654cd05
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,101 @@
+body {
+ background-color: #fafafa;
+ color: #1a1a1a;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.container {
+ max-width: 900px;
+}
+
+.card {
+ background-color: #ffffff;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+ box-shadow: none;
+ margin-bottom: 2rem;
+}
+
+.card-header {
+ background-color: #ffffff;
+ border-bottom: 1px solid #e0e0e0;
+ padding: 1rem 1.5rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+ color: #666;
+}
+
+.card-body {
+ padding: 1.5rem;
+}
+
+.btn-primary {
+ background-color: #000;
+ border-color: #000;
+ border-radius: 4px;
+ padding: 0.5rem 1.25rem;
+ font-weight: 600;
+ font-size: 0.8rem;
+}
+
+.btn-primary:hover {
+ background-color: #333;
+ border-color: #333;
+}
+
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.status-online {
+ background-color: #e6f4ea;
+ color: #1e8e3e;
+}
+
+.status-offline {
+ background-color: #fce8e6;
+ color: #d93025;
+}
+
+pre {
+ background-color: #f1f1f1;
+ padding: 1rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ color: #333;
+}
+
+.form-control {
+ border-radius: 4px;
+ border: 1px solid #e0e0e0;
+ padding: 0.6rem;
+}
+
+.form-control:focus {
+ border-color: #000;
+ box-shadow: none;
+}
+
+h1, h2, h3 {
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.nav-link {
+ color: #666;
+ font-weight: 500;
+}
+
+.nav-link.active {
+ color: #000 !important;
+ font-weight: 700;
+}
diff --git a/bot.log b/bot.log
new file mode 100644
index 0000000..06854a6
--- /dev/null
+++ b/bot.log
@@ -0,0 +1,17 @@
+Logged in as AsepXiaoQin#6954!
+Bot Status: Online
+(node:17079) DeprecationWarning: The ready event has been renamed to clientReady to distinguish it from the gateway READY event and will only emit under that name in v15. Please use clientReady instead.
+(Use `node --trace-deprecation ...` to show where the warning was created)
+/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:529
+ throw new Error(`No compatible encryption modes. Available include: ${options.join(", ")}`);
+ ^
+
+Error: No compatible encryption modes. Available include: aead_aes256_gcm_rtpsize, aead_xchacha20_poly1305_rtpsize
+ at chooseEncryptionMode (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:529:11)
+ at /home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:721:21
+Emitted 'error' event on VoiceConnection instance at:
+ at VoiceConnection.onNetworkingError (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1914:10)
+ at Networking.emit (node:events:518:28)
+ at /home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:729:32
+
+Node.js v22.18.0
diff --git a/bot_control.php b/bot_control.php
new file mode 100644
index 0000000..02a3811
--- /dev/null
+++ b/bot_control.php
@@ -0,0 +1,28 @@
+ bot.log 2>&1 &");
+ sleep(1); // Give it a second to start
+ }
+ } elseif ($_GET['action'] === 'stop') {
+ // Find PID and kill
+ $output = [];
+ exec("ps aux | grep 'node index.js' | grep -v grep | awk '{print $2}'", $output);
+ foreach ($output as $pid) {
+ exec("kill $pid");
+ }
+ sleep(1);
+ }
+ header("Location: index.php");
+ exit;
+}
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..8da172b
--- /dev/null
+++ b/config.json
@@ -0,0 +1,4 @@
+{
+ "DISCORD_TOKEN": "MTQ3MTkwOTE5Mzg4Njg1OTI5NA.GN4TSo.GJ2qwdMUfFHHvJb0EUzSplAAdeHCCXwJy2K128",
+ "DISCORD_CLIENT_ID": "1471909193886859294"
+}
\ No newline at end of file
diff --git a/db/migrations/001_create_settings.sql b/db/migrations/001_create_settings.sql
new file mode 100644
index 0000000..add2428
--- /dev/null
+++ b/db/migrations/001_create_settings.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS settings (
+ setting_key VARCHAR(50) PRIMARY KEY,
+ setting_value TEXT
+);
+
+INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('discord_token', ''), ('application_id', '');
diff --git a/deploy.js b/deploy.js
new file mode 100644
index 0000000..6615425
--- /dev/null
+++ b/deploy.js
@@ -0,0 +1,52 @@
+const { REST, Routes, SlashCommandBuilder } = require('discord.js');
+const fs = require('fs');
+const path = require('path');
+
+// Load config
+let TOKEN, CLIENT_ID;
+try {
+ const configPath = path.join(__dirname, 'config.json');
+ if (fs.existsSync(configPath)) {
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
+ TOKEN = config.DISCORD_TOKEN;
+ CLIENT_ID = config.DISCORD_CLIENT_ID;
+ } else {
+ TOKEN = process.env.DISCORD_TOKEN;
+ CLIENT_ID = process.env.DISCORD_CLIENT_ID;
+ }
+} catch (err) {
+ console.error('ERROR: config.json is invalid or error reading it.');
+ process.exit(1);
+}
+
+if (!TOKEN || !CLIENT_ID) {
+ console.error('ERROR: DISCORD_TOKEN or DISCORD_CLIENT_ID is missing in config.json.');
+ process.exit(1);
+}
+
+const commands = [
+ new SlashCommandBuilder()
+ .setName('join')
+ .setDescription('Masuk ke Voice Channel kamu'),
+ new SlashCommandBuilder()
+ .setName('sahur')
+ .setDescription('Masuk ke Voice Channel dan putar alarm sahur'),
+].map(command => command.toJSON());
+
+const rest = new REST({ version: '10' }).setToken(TOKEN);
+
+(async () => {
+ try {
+ console.log(`Started refreshing ${commands.length} application (/) commands.`);
+
+ const data = await rest.put(
+ Routes.applicationCommands(CLIENT_ID),
+ { body: commands },
+ );
+
+ console.log(`Successfully reloaded ${data.length} application (/) commands.`);
+ } catch (error) {
+ console.error(error);
+ process.exit(1);
+ }
+})();
diff --git a/deploy_commands.php b/deploy_commands.php
new file mode 100644
index 0000000..adf4aa7
--- /dev/null
+++ b/deploy_commands.php
@@ -0,0 +1,17 @@
+&1", $output, $return_var);
+
+$log = implode("\n", $output);
+file_put_contents('bot.log', "[Deploy] " . date('Y-m-d H:i:s') . "\n" . $log . "\n\n", FILE_APPEND);
+
+if ($return_var === 0) {
+ header("Location: index.php?deploy_success=1");
+} else {
+ header("Location: index.php?deploy_error=1");
+}
+exit;
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..5852831
--- /dev/null
+++ b/index.js
@@ -0,0 +1,118 @@
+const fs = require('fs');
+const path = require('path');
+process.env.FFMPEG_PATH = require('ffmpeg-static');
+
+const {
+ Client,
+ GatewayIntentBits
+} = require('discord.js');
+const {
+ joinVoiceChannel,
+ createAudioPlayer,
+ createAudioResource,
+ AudioPlayerStatus,
+ VoiceConnectionStatus,
+ getVoiceConnection
+} = require('@discordjs/voice');
+
+// --- CONFIGURATION ---
+let TOKEN, CLIENT_ID;
+try {
+ const config = require('./config.json');
+ TOKEN = config.DISCORD_TOKEN;
+ CLIENT_ID = config.DISCORD_CLIENT_ID;
+} catch (err) {
+ console.warn('Warning: config.json not found. Using environment variables if available.');
+ TOKEN = process.env.DISCORD_TOKEN;
+ CLIENT_ID = process.env.DISCORD_CLIENT_ID;
+}
+
+if (!TOKEN) {
+ console.error('ERROR: DISCORD_TOKEN is missing. Please set it in the dashboard.');
+}
+
+const client = new Client({
+ intents: [
+ GatewayIntentBits.Guilds,
+ GatewayIntentBits.GuildVoiceStates,
+ ]
+});
+
+// Helper function to join voice
+function connectToVoice(interaction) {
+ const member = interaction.member;
+ const voiceChannel = member.voice.channel;
+
+ if (!voiceChannel) {
+ throw new Error('Kamu harus berada di Voice Channel untuk menggunakan perintah ini!');
+ }
+
+ return joinVoiceChannel({
+ channelId: voiceChannel.id,
+ guildId: interaction.guildId,
+ adapterCreator: interaction.guild.voiceAdapterCreator,
+ });
+}
+
+client.on('ready', () => {
+ console.log(`Logged in as ${client.user.tag}!`);
+ console.log('Bot Status: Online');
+});
+
+client.on('interactionCreate', async interaction => {
+ if (!interaction.isChatInputCommand()) return;
+
+ const { commandName } = interaction;
+
+ if (commandName === 'join') {
+ await interaction.deferReply();
+ try {
+ connectToVoice(interaction);
+ await interaction.editReply('Berhasil masuk ke Voice Channel!');
+ } catch (error) {
+ await interaction.editReply({ content: error.message, ephemeral: true });
+ }
+ }
+
+ if (commandName === 'sahur') {
+ await interaction.deferReply();
+
+ try {
+ let connection = getVoiceConnection(interaction.guild.id);
+
+ if (!connection) {
+ connection = connectToVoice(interaction);
+ }
+
+ const player = createAudioPlayer();
+ const resource = createAudioResource(path.join(process.cwd(), 'sahur.mp3'));
+
+ connection.subscribe(player);
+ player.play(resource);
+
+ console.log('Bot Status: Playing');
+ await interaction.editReply('🔊 Sedang memutar alarm sahur!');
+
+ player.on(AudioPlayerStatus.Idle, () => {
+ console.log('Bot Status: Finished playing (Idle)');
+ });
+
+ player.on('error', e => {
+ console.error('ERROR_AUDIO:', e.message);
+ interaction.followUp({ content: 'Terjadi kesalahan saat memutar audio.', ephemeral: true });
+ });
+
+ } catch (error) {
+ console.error('ERROR:', error);
+ await interaction.editReply(error.message || 'Gagal memutar audio.');
+ }
+ }
+});
+
+if (TOKEN) {
+ client.login(TOKEN).catch(err => {
+ console.error('Login failed:', err.message);
+ });
+} else {
+ console.log('Bot Status: Waiting for token setup...');
+}
diff --git a/index.php b/index.php
index 7205f3d..120face 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,177 @@
query("SELECT * FROM settings");
+ while ($row = $stmt->fetch()) {
+ $settings[$row['setting_key']] = $row['setting_value'];
+ }
+} catch (Exception $e) {
+ // Table might not exist yet or other error
+}
+
+$discordToken = $settings['discord_token'] ?? '';
+$applicationId = $settings['application_id'] ?? '';
+
+// Helper function to check if bot is running (simplified)
+function isBotRunning(): bool {
+ $output = [];
+ exec("ps aux | grep 'node index.js' | grep -v grep", $output);
+ return !empty($output);
+}
+
+// Project info
+$projectName = $_SERVER['PROJECT_NAME'] ?? 'Sahur Alarm Bot';
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Discord voice bot for sahur alarms.';
-$phpVersion = PHP_VERSION;
-$now = date('Y-m-d H:i:s');
?>
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ = htmlspecialchars($projectName) ?> - Control Panel
+
+
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+
+
+
+
+
+
+
+
+
Welcome to your Sahur Bot Panel
+
= htmlspecialchars($projectDescription) ?>
+
+
Bot Source Code (index.js)
+
The bot is built using Node.js and discord.js v14. It handles the /join and /sahur commands, joins your voice channel, and plays the alarm.
+
+
+
Quick Start:
+
+ - Masukkan Application ID dan Bot Token di panel kanan, lalu klik Save.
+ - Klik Deploy Slash Commands agar perintah
/join dan /sahur muncul di Discord.
+ - Klik Start Bot untuk mengaktifkan bot.
+
+
+
+
+
+
+
+
+
+
+
+
+
Upload your sahur.mp3 file here. Ensure it is named exactly sahur.mp3.
+
+
+
+
+ 🔊
+ sahur.mp3 (= round(filesize('sahur.mp3') / 1024 / 1024, 2) ?> MB)
+
+
+
+ ⚠️ sahur.mp3 not found.
+
+
+
+
+
+
+
+
+
To link your bot, you need to provide credentials from the Discord Developer Portal.
+
+
+
Settings saved!
+
+
+
Slash commands deployed!
+
+
+
Error deploying commands. Check logs.
+
+
+
Error saving settings.
+
+
+
+
+
+
+
Click this if slash commands (/sahur) are not showing up in Discord.
+
Deploy Slash Commands
+
+
Config is saved to the database and config.json.
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..76df96a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "sahur-alarm-bot",
+ "version": "1.0.0",
+ "description": "Discord Sahur Alarm Bot",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "dependencies": {
+ "discord.js": "^14.14.1",
+ "@discordjs/voice": "^0.16.1",
+ "@discordjs/opus": "^0.9.0",
+ "ffmpeg-static": "^5.2.0",
+ "opusscript": "^0.0.8"
+ }
+}
diff --git a/sahur.mp3 b/sahur.mp3
new file mode 100644
index 0000000..13d50ac
Binary files /dev/null and b/sahur.mp3 differ
diff --git a/save_settings.php b/save_settings.php
new file mode 100644
index 0000000..c81db50
--- /dev/null
+++ b/save_settings.php
@@ -0,0 +1,29 @@
+prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'discord_token'");
+ $stmt->execute([$token]);
+
+ $stmt = db()->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'application_id'");
+ $stmt->execute([$app_id]);
+
+ // Generate config.json for the Node.js bot
+ $config = [
+ 'DISCORD_TOKEN' => $token,
+ 'DISCORD_CLIENT_ID' => $app_id
+ ];
+ file_put_contents(__DIR__ . '/config.json', json_encode($config, JSON_PRETTY_PRINT));
+
+ header('Location: index.php?success=1');
+ exit;
+ } catch (Exception $e) {
+ header('Location: index.php?error=' . urlencode($e->getMessage()));
+ exit;
+ }
+}
diff --git a/upload_audio.php b/upload_audio.php
new file mode 100644
index 0000000..37cded1
--- /dev/null
+++ b/upload_audio.php
@@ -0,0 +1,24 @@
+