332 lines
12 KiB
JavaScript
332 lines
12 KiB
JavaScript
const { Client, GatewayIntentBits, Events, EmbedBuilder, PermissionsBitField } = require('discord.js');
|
||
const {
|
||
joinVoiceChannel,
|
||
createAudioPlayer,
|
||
createAudioResource,
|
||
AudioPlayerStatus,
|
||
StreamType,
|
||
VoiceConnectionStatus
|
||
} = require('@discordjs/voice');
|
||
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);
|
||
}
|
||
|
||
async function playLocalFile(voiceChannel) {
|
||
const connection = joinVoiceChannel({
|
||
channelId: voiceChannel.id,
|
||
guildId: voiceChannel.guild.id,
|
||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||
selfDeaf: false,
|
||
selfMute: false,
|
||
});
|
||
|
||
const player = createAudioPlayer();
|
||
|
||
player.on('error', error => {
|
||
logToFile(`Audio Player Error: ${error.message}`);
|
||
});
|
||
|
||
const resource = createAudioResource(AUDIO_PATH, {
|
||
inputType: StreamType.Arbitrary,
|
||
inlineVolume: true,
|
||
});
|
||
|
||
resource.volume.setVolume(1.0);
|
||
player.play(resource);
|
||
connection.subscribe(player);
|
||
|
||
player.on(AudioPlayerStatus.Idle, () => {
|
||
// connection.destroy();
|
||
});
|
||
|
||
return connection;
|
||
}
|
||
|
||
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 {
|
||
const connection = joinVoiceChannel({
|
||
channelId: channel.id,
|
||
guildId: guild.id,
|
||
adapterCreator: guild.voiceAdapterCreator,
|
||
selfDeaf: false,
|
||
selfMute: false,
|
||
});
|
||
|
||
const player = createAudioPlayer();
|
||
const resource = createAudioResource(AUDIO_PATH, {
|
||
inputType: StreamType.Arbitrary,
|
||
inlineVolume: true,
|
||
});
|
||
|
||
resource.volume.setVolume(1.0);
|
||
player.play(resource);
|
||
connection.subscribe(player);
|
||
|
||
logToFile('Alarm playing with custom audio resource.');
|
||
} 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();
|
||
joinVoiceChannel({
|
||
channelId: voiceChannel.id,
|
||
guildId: voiceChannel.guild.id,
|
||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||
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 using custom resource: ${AUDIO_PATH}`);
|
||
try {
|
||
await playLocalFile(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') {
|
||
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}`);
|
||
|
||
// Ensure connection with specific settings
|
||
joinVoiceChannel({
|
||
channelId: voiceChannel.id,
|
||
guildId: voiceChannel.guild.id,
|
||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||
selfDeaf: false,
|
||
selfMute: false,
|
||
});
|
||
|
||
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!');
|
||
|
||
joinVoiceChannel({
|
||
channelId: voiceChannel.id,
|
||
guildId: voiceChannel.guild.id,
|
||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||
selfDeaf: false,
|
||
selfMute: false,
|
||
});
|
||
|
||
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 {
|
||
await playLocalFile(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}`);
|
||
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);
|