process.env.FFMPEG_PATH = require('ffmpeg-static'); const ffmpeg = require('ffmpeg-static'); const path = require('path'); if (ffmpeg) { const ffmpegDir = path.dirname(ffmpeg); if (!process.env.PATH.includes(ffmpegDir)) { process.env.PATH = `${ffmpegDir}${path.delimiter}${process.env.PATH}`; } } const { Client, GatewayIntentBits, Events, EmbedBuilder, PermissionsBitField } = require('discord.js'); const { DisTube } = require('distube'); const { SoundCloudPlugin } = require('@distube/soundcloud'); const { SpotifyPlugin } = require('@distube/spotify'); const { YtDlpPlugin } = require('@distube/yt-dlp'); const play = require('play-dl'); const { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, StreamType } = require('@discordjs/voice'); const { CronJob } = require('cron'); const fs = require('fs'); // Load environment variables require('dotenv').config({ path: path.join(__dirname, '.env') }); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers // Added to fetch members for alarm ] }); // 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, { ffmpegPath: require('ffmpeg-static'), plugins: [ new SpotifyPlugin(), new SoundCloudPlugin(), new YtDlpPlugin({ update: true }) ] }); 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); } async function playLocalIndependent(channel) { if (!fs.existsSync(AUDIO_PATH)) { logToFile(`Audio file not found: ${AUDIO_PATH}`); return; } try { // Leave DisTube first to avoid collision await distube.voices.leave(channel.guild.id); const connection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, adapterCreator: channel.guild.voiceAdapterCreator, selfDeaf: false, }); const player = createAudioPlayer(); const resource = createAudioResource(AUDIO_PATH, { inputType: StreamType.Arbitrary }); player.play(resource); connection.subscribe(player); player.on(AudioPlayerStatus.Idle, () => { connection.destroy(); }); player.on('error', error => { logToFile(`Audio Player Error: ${error.message}`); connection.destroy(); }); } catch (err) { logToFile(`Independent play error: ${err.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)) { try { await playLocalIndependent(channel); logToFile('Alarm playing via independent @discordjs/voice.'); } catch (err) { logToFile(`Alarm play 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, { selfDeaf: false, selfMute: false }); 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 independently: ${AUDIO_PATH}`); try { await playLocalIndependent(voiceChannel); 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') { await interaction.deferReply(); const query = interaction.options.getString('query'); const voiceChannel = interaction.member?.voice.channel; if (!voiceChannel) return interaction.editReply({ content: 'Anda harus berada di voice channel!' }); try { await distube.play(voiceChannel, query, { textChannel: interaction.channel, member: interaction.member }); await interaction.editReply(`Mencari dan memutar: **${query}**... 🎢`); } catch (e) { logToFile(`Play Error: ${e.message}`); if (interaction.deferred) { await interaction.editReply(`Terjadi kesalahan: ${e.message}`); } } } 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!'); try { await distube.play(voiceChannel, query, { member: message.member, textChannel: message.channel, message }); message.reply(`πŸ” Mencari dan memutar: **${query}**...`); } catch (e) { logToFile(`Message Play Error: ${e.message}`); message.reply(`❌ Error: ${e.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 { await playLocalIndependent(voiceChannel); 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 || e}`); if (e.message && 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 || e).toString().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);