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… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
- + + +
+
+
+
+
System Overview
+
+

Welcome to your Sahur Bot Panel

+

+
+
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: +
    +
  1. Masukkan Application ID dan Bot Token di panel kanan, lalu klik Save.
  2. +
  3. Klik Deploy Slash Commands agar perintah /join dan /sahur muncul di Discord.
  4. +
  5. Klik Start Bot untuk mengaktifkan bot.
  6. +
+
+
+
+ +
+
+ Logs & Activity + +
+
+
+
+
+
+ +
+
+
Audio Management
+
+

Upload your sahur.mp3 file here. Ensure it is named exactly sahur.mp3.

+
+
+ +
+ +
+ + +
+ 🔊 + sahur.mp3 ( MB) +
+ +
+ ⚠️ sahur.mp3 not found. +
+ +
+
+ +
+
Bot Configuration
+
+

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 @@ +