require('dotenv').config(); const { Client, GatewayIntentBits, Events } = require('discord.js'); const { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, NoSubscriberBehavior, VoiceConnectionStatus, generateDependencyReport, entersState, StreamType } = require('@discordjs/voice'); const { CronJob } = require('cron'); const path = require('path'); const play = require('play-dl'); const fs = require('fs'); const libsodium = require('libsodium-wrappers'); const ffmpegPath = require('ffmpeg-static'); const { spawn } = require('child_process'); async function startBot() { logToFile('Dependency Report:\n' + generateDependencyReport()); await libsodium.ready; logToFile('Encryption library (libsodium) is ready.'); client.login(process.env.DISCORD_TOKEN); } function logToFile(message) { const logMessage = `[${new Date().toISOString()}] ${message}\n`; fs.appendFileSync(path.join(__dirname, 'bot.log'), logMessage); console.log(message); } process.on('unhandledRejection', error => { logToFile(`Unhandled Rejection: ${error.message}\n${error.stack}`); }); process.on('uncaughtException', error => { logToFile(`Uncaught Exception: ${error.message}\n${error.stack}`); }); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] }); const VC_ID = process.env.VC_ID; const AUDIO_PATH = path.join(__dirname, 'assets/audio/sahur.mp3'); // Global state for music queues const queues = new Map(); client.once(Events.ClientReady, async c => { logToFile(`Ready! Logged in as ${c.user.tag}`); // Sahur Alarm (Jakarta Time) const sahurTime = process.env.SAHUR_TIME || '30 03 * * *'; let cronTime = sahurTime; if (sahurTime.includes(':') && !sahurTime.includes('*')) { const [hour, minute] = sahurTime.split(':'); cronTime = `0 ${minute} ${hour} * * *`; } 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) { playLocal(channel, AUDIO_PATH); } }, null, true, 'Asia/Jakarta'); logToFile(`Alarm scheduled at ${cronTime}`); } catch (e) { logToFile(`Failed to schedule alarm: ${e.message}`); } // Auto-join on start if VC_ID is provided if (VC_ID) { const guild = client.guilds.cache.first(); if (guild) { const channel = guild.channels.cache.get(VC_ID); if (channel) { logToFile(`Auto-joining VC: ${channel.name}`); const connection = joinVoiceChannel({ channelId: channel.id, guildId: guild.id, adapterCreator: guild.voiceAdapterCreator, }); setupConnectionListeners(connection); } } } }); function setupConnectionListeners(connection) { connection.on(VoiceConnectionStatus.Disconnected, async () => { logToFile('Disconnected from VC. Attempting to reconnect...'); try { await Promise.race([ entersState(connection, VoiceConnectionStatus.Signalling, 5_000), entersState(connection, VoiceConnectionStatus.Connecting, 5_000), ]); // Reconnected! } catch (error) { logToFile(`Reconnection failed: ${error.message}`); connection.destroy(); } }); } // Handle Slash Commands client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; logToFile(`Interaction received: ${interaction.commandName} from ${interaction.user.tag}`); const { commandName } = interaction; const voiceChannel = interaction.member?.voice.channel; try { if (commandName === 'join') { if (!voiceChannel) return interaction.reply({ content: 'Kamu harus di voice channel!', ephemeral: true }); const connection = joinVoiceChannel({ channelId: voiceChannel.id, guildId: interaction.guild.id, adapterCreator: interaction.guild.voiceAdapterCreator, }); setupConnectionListeners(connection); await interaction.reply('Siap! Sudah join voice channel. 🎧 Bot akan standby di sini.'); } if (commandName === 'testsahur') { if (!voiceChannel) return interaction.reply({ content: 'Kamu harus di voice channel!', ephemeral: true }); await interaction.reply('Memainkan suara sahur... 📢'); playLocal(voiceChannel, AUDIO_PATH); } if (commandName === 'play') { if (!voiceChannel) return interaction.reply({ content: 'Kamu harus di voice channel!', ephemeral: true }); const query = interaction.options.getString('query'); await interaction.deferReply(); let serverQueue = queues.get(interaction.guild.id); if (!serverQueue) { serverQueue = { songs: [], connection: null, player: createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }), textChannel: interaction.channel }; queues.set(interaction.guild.id, serverQueue); } let songInfo; // Validate and search try { if (query.includes('spotify.com')) { if (play.sp_validate(query)) { const sp_data = await play.spotify(query); if (sp_data.type === 'track') { const search = await play.search(`${sp_data.name} ${sp_data.artists[0].name}`, { limit: 1 }); songInfo = { title: sp_data.name, url: search[0].url }; } else { return interaction.editReply('Hanya mendukung link track Spotify untuk saat ini.'); } } } else if (query.includes('soundcloud.com')) { const so_data = await play.soundcloud(query); songInfo = { title: so_data.name, url: so_data.url }; } else { const yt_info = await play.search(query, { limit: 1 }); if (yt_info.length === 0) return interaction.editReply('Lagu tidak ditemukan!'); songInfo = { title: yt_info[0].title, url: yt_info[0].url }; } } catch (err) { logToFile(`Search error: ${err.message}`); return interaction.editReply('Gagal mencari lagu. Coba judul yang berbeda.'); } serverQueue.songs.push(songInfo); if (serverQueue.songs.length === 1) { playSong(interaction.guild.id, voiceChannel); await interaction.editReply(`🎵 Sekarang memutar: **${songInfo.title}**`); } else { await interaction.editReply(`✅ **${songInfo.title}** ditambahkan ke antrean.`); } } if (commandName === 'pause') { const serverQueue = queues.get(interaction.guild.id); if (serverQueue) { serverQueue.player.pause(); return interaction.reply('Musik dipause. ⏸️'); } interaction.reply('Tidak ada musik yang sedang diputar.'); } if (commandName === 'resume') { const serverQueue = queues.get(interaction.guild.id); if (serverQueue) { serverQueue.player.unpause(); return interaction.reply('Musik dilanjutkan. ▶️'); } interaction.reply('Tidak ada musik yang sedang diputar.'); } if (commandName === 'skip') { const serverQueue = queues.get(interaction.guild.id); if (serverQueue) { serverQueue.player.stop(); return interaction.reply('Lagu dilewati! ⏭️'); } interaction.reply('Tidak ada musik yang sedang diputar.'); } if (commandName === 'stop') { const serverQueue = queues.get(interaction.guild.id); if (serverQueue) { serverQueue.songs = []; serverQueue.player.stop(); return interaction.reply('Musik dihentikan dan antrean dihapus. Bot tetap di channel.'); } interaction.reply('Bot tidak sedang memutar musik.'); } } catch (err) { logToFile(`Error handling command ${commandName}: ${err.message}`); if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'Terjadi kesalahan saat menjalankan command.', ephemeral: true }); } else if (interaction.deferred) { await interaction.editReply('Terjadi kesalahan saat menjalankan command.'); } } }); async function playLocal(channel, filePath) { logToFile(`Playing local file using ffmpeg-static: ${filePath}`); if (!fs.existsSync(filePath)) { logToFile(`Error: File not found at ${filePath}`); return; } const connection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, adapterCreator: channel.guild.voiceAdapterCreator, }); const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play, }, }); try { const ffmpegProcess = spawn(ffmpegPath, [ '-i', filePath, '-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1' ]); const resource = createAudioResource(ffmpegProcess.stdout, { inputType: StreamType.Raw }); player.play(resource); connection.subscribe(player); logToFile(`Successfully started playing with ffmpeg-static: ${filePath}`); ffmpegProcess.on('error', error => { logToFile(`FFmpeg process error: ${error.message}`); }); } catch (error) { logToFile(`Error creating audio resource with ffmpeg: ${error.message}`); } player.on('error', error => { logToFile(`Audio player error: ${error.message}`); console.error('Audio Player Error Detail:', error); }); player.on(AudioPlayerStatus.Playing, () => { logToFile('Audio player started playing!'); }); player.once(AudioPlayerStatus.Idle, () => { logToFile('Local audio finished. Staying in channel.'); }); } async function playSong(guildId, channel) { const serverQueue = queues.get(guildId); if (!serverQueue || serverQueue.songs.length === 0) { return; } const song = serverQueue.songs[0]; try { logToFile(`Fetching stream for: ${song.title}`); const stream = await play.stream(song.url); logToFile(`Stream fetched successfully for ${song.title}`); const resource = createAudioResource(stream.stream, { inputType: StreamType.Arbitrary }); if (!serverQueue.connection) { logToFile(`Creating new connection for guild: ${guildId}`); serverQueue.connection = joinVoiceChannel({ channelId: channel.id, guildId: guildId, adapterCreator: channel.guild.voiceAdapterCreator, }); setupConnectionListeners(serverQueue.connection); serverQueue.connection.subscribe(serverQueue.player); } serverQueue.player.play(resource); logToFile(`Now playing: ${song.title}`); serverQueue.player.once(AudioPlayerStatus.Idle, () => { logToFile(`Finished playing: ${song.title}`); serverQueue.songs.shift(); if (serverQueue.songs.length > 0) { playSong(guildId, channel); serverQueue.textChannel.send(`🎵 Sekarang memutar: **${serverQueue.songs[0].title}**`); } else { logToFile('Queue empty. Staying in channel.'); } }); serverQueue.player.on('error', error => { logToFile(`Error in player while playing ${song.title}: ${error.message}`); console.error('Player Error Detail:', error); serverQueue.songs.shift(); playSong(guildId, channel); }); } catch (e) { logToFile(`Error playing song ${song.title}: ${e.message}`); console.error('PlaySong Error Detail:', e); serverQueue.songs.shift(); playSong(guildId, channel); } } startBot();