const { Client, GatewayIntentBits, Events, EmbedBuilder, PermissionsBitField } = require('discord.js'); const { DisTube } = require('distube'); const { YtDlpPlugin } = require('@distube/yt-dlp'); const { SoundCloudPlugin } = require('@distube/soundcloud'); const { SpotifyPlugin } = require('@distube/spotify'); const { CronJob } = require('cron'); const path = require('path'); const fs = require('fs'); // Load environment variables require('dotenv').config({ path: path.join(__dirname, '.env') }); // Setup FFMPEG const ffmpegStatic = require('ffmpeg-static'); process.env.FFMPEG_PATH = ffmpegStatic; const ffmpegDir = path.dirname(ffmpegStatic); if (!process.env.PATH.includes(ffmpegDir)) { process.env.PATH = `${ffmpegDir}${path.delimiter}${process.env.PATH}`; } // Generate dependency report for debugging try { const { generateDependencyReport } = require('@discordjs/voice'); console.log("Dependency Report:\n" + generateDependencyReport()); } catch (e) {} const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] }); // Detect libsodium try { const sodium = require('libsodium-wrappers'); sodium.ready.then(() => { logToFile("Encryption library (libsodium) is ready."); }); } catch (e) { logToFile("Warning: libsodium-wrappers not found or failed to load."); } const distube = new DisTube(client, { plugins: [ new SoundCloudPlugin(), new SpotifyPlugin(), new YtDlpPlugin(), ], emitNewSongOnly: true, emitAddSongWhenCreatingQueue: false, }); const VC_ID = process.env.VC_ID; const AUDIO_PATH = path.join(__dirname, 'assets/audio/sahur.mp3'); const PREFIX = '!'; function logToFile(message) { const logMessage = `[${new Date().toISOString()}] ${message}\n`; try { fs.appendFileSync(path.join(__dirname, 'bot.log'), logMessage); } catch (err) { console.error('Failed to write to log file:', err); } console.log(message); } client.once(Events.ClientReady, () => { logToFile(`Bot berhasil login!`); logToFile(`Ready! Logged in as ${client.user.tag}`); logToFile(`FFMPEG Path: ${process.env.FFMPEG_PATH}`); // Sahur Alarm const rawSahurTime = process.env.SAHUR_TIME || '03:30'; let cronTime = '30 3 * * *'; if (rawSahurTime.includes(':')) { const [hour, minute] = rawSahurTime.split(':'); cronTime = `${parseInt(minute)} ${parseInt(hour)} * * *`; } else { cronTime = rawSahurTime; } try { new CronJob(cronTime, async () => { logToFile('Sahur alarm triggered!'); const guild = client.guilds.cache.first(); if (!guild) return; const channel = guild.channels.cache.get(VC_ID); if (channel && fs.existsSync(AUDIO_PATH)) { distube.voices.join(channel).then(voice => { const stream = fs.createReadStream(AUDIO_PATH); voice.play(stream); }).catch(err => { logToFile(`Alarm join error: ${err.message}`); }); } }, null, true, 'Asia/Jakarta'); logToFile(`Alarm scheduled at ${cronTime}`); } catch (e) { logToFile(`Failed to schedule alarm: ${e.message}`); } }); // Slash Command Handler client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; const { commandName } = interaction; const voiceChannel = interaction.member?.voice.channel; try { if (commandName === 'join') { if (!voiceChannel) return interaction.reply({ content: 'Anda harus berada di voice channel!', ephemeral: true }); await interaction.deferReply(); await distube.voices.join(voiceChannel); await interaction.editReply('Sudah join! 🎧'); } else if (commandName === 'testsahur') { if (!voiceChannel) return interaction.reply({ content: 'Anda harus berada di voice channel!', ephemeral: true }); if (!fs.existsSync(AUDIO_PATH)) return interaction.reply({ content: `File audio tidak ditemukan di ${AUDIO_PATH}`, ephemeral: true }); await interaction.deferReply(); logToFile(`Playing local file using stream: ${AUDIO_PATH}`); try { const voice = await distube.voices.join(voiceChannel); const stream = fs.createReadStream(AUDIO_PATH); voice.play(stream); await interaction.editReply('Memainkan suara sahur... πŸ“’'); } catch (e) { logToFile(`Error in testsahur: ${e.message}`); await interaction.editReply(`Gagal memutar audio: ${e.message}`); } } else if (commandName === 'play') { const query = interaction.options.getString('query'); if (!voiceChannel) return interaction.reply({ content: 'Anda harus berada di voice channel!', ephemeral: true }); await interaction.deferReply(); logToFile(`Fetching stream for: ${query}`); distube.play(voiceChannel, query, { member: interaction.member, textChannel: interaction.channel, interaction }).catch(e => { logToFile(`Play Error: ${e.message}`); interaction.editReply(`Terjadi kesalahan saat memutar: ${e.message}`); }); await interaction.editReply(`Mencari: **${query}**... πŸ”`); } else if (commandName === 'pause') { distube.pause(interaction.guildId); await interaction.reply('Musik dipause. ⏸️'); } else if (commandName === 'resume') { distube.resume(interaction.guildId); await interaction.reply('Musik dilanjutkan. ▢️'); } else if (commandName === 'skip') { await distube.skip(interaction.guildId); await interaction.reply('Lagu dilewati! ⏭️'); } else if (commandName === 'stop') { await distube.stop(interaction.guildId); await interaction.reply('Musik dihentikan dan antrean dihapus. πŸ‘‹'); } } catch (error) { logToFile(`Interaction Error (${commandName}): ${error.message}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: `Terjadi kesalahan: ${error.message}`, ephemeral: true }); } else { await interaction.editReply({ content: `Terjadi kesalahan: ${error.message}`, ephemeral: true }); } } }); // Prefix Command Handler (Legacy/Backup) client.on(Events.MessageCreate, async message => { if (message.author.bot || !message.content.startsWith(PREFIX)) return; const args = message.content.slice(PREFIX.length).trim().split(/ +/); const command = args.shift().toLowerCase(); const voiceChannel = message.member?.voice.channel; try { if (command === 'play') { const query = args.join(' '); if (!voiceChannel) return message.reply('Anda harus berada di voice channel!'); if (!query) return message.reply('Masukkan judul atau link lagu!'); distube.play(voiceChannel, query, { member: message.member, textChannel: message.channel, message }); } else if (['skip', 'stop', 'pause', 'resume'].includes(command)) { if (!voiceChannel) return message.reply('Anda harus berada di voice channel!'); const method = command === 'skip' ? 'skip' : (command === 'stop' ? 'stop' : (command === 'pause' ? 'pause' : 'resume')); await distube[method](message.guildId); message.reply(`${command} berhasil!`); } else if (command === 'testsahur') { if (!voiceChannel) return message.reply('Anda harus berada di voice channel!'); try { const voice = await distube.voices.join(voiceChannel); const stream = fs.createReadStream(AUDIO_PATH); voice.play(stream); message.reply('Mengetes suara sahur... πŸ“’'); } catch (e) { message.reply(`❌ Error: ${e.message}`); } } } catch (error) { logToFile(`Message Command Error: ${error.message}`); message.reply(`❌ Error: ${error.message}`); } }); // DisTube Events distube .on('playSong', (queue, song) => { queue.textChannel.send(`🎡 Sekarang memutar: **${song.name}** - \`${song.formatDuration}\`\nRequested by: ${song.user}`); }) .on('addSong', (queue, song) => { queue.textChannel.send(`βœ… **${song.name}** ditambahkan ke antrean oleh ${song.user}`); }) .on('error', (channel, e) => { logToFile(`DisTube Error: ${e.message}`); if (e.message.includes('Sign in to confirm you’re not a bot')) { if (channel) channel.send(`❌ YouTube memblokir akses bot. Silakan coba link dari SoundCloud atau Spotify.`); } else if (channel) { channel.send(`❌ Terjadi kesalahan: ${e.message.slice(0, 2000)}`); } }); // Handle uncaught errors to prevent crash process.on('unhandledRejection', error => { logToFile(`Unhandled Rejection: ${error.message}`); console.error(error); }); process.on('uncaughtException', error => { logToFile(`Uncaught Exception: ${error.message}`); console.error(error); }); // Login client.login(process.env.DISCORD_TOKEN);