350 lines
13 KiB
JavaScript
350 lines
13 KiB
JavaScript
const ffmpeg = require('ffmpeg-static');
|
||
const path = require('path');
|
||
process.env.FFMPEG_PATH = ffmpeg;
|
||
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 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, {
|
||
emitNewSongOnly: true,
|
||
plugins: [
|
||
new SoundCloudPlugin(),
|
||
new SpotifyPlugin(),
|
||
],
|
||
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 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,
|
||
});
|
||
|
||
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 {
|
||
let url = query;
|
||
let title = query;
|
||
const validation = await play.validate(query);
|
||
|
||
logToFile(`Validation for "${query}": ${validation}`);
|
||
|
||
if (!validation || validation === 'search') {
|
||
logToFile(`Searching via play-dl for: ${query}`);
|
||
const searchResults = await play.search(query, { limit: 1 });
|
||
|
||
if (!searchResults || searchResults.length === 0) {
|
||
return interaction.editReply('Lagu tidak ditemukan!');
|
||
}
|
||
|
||
url = searchResults[0].url;
|
||
title = searchResults[0].title;
|
||
} else {
|
||
// It's a URL, attempt to get title
|
||
try {
|
||
const info = await play.video_info(url);
|
||
title = info.video_details.title;
|
||
url = info.video_details.url;
|
||
} catch (err) {
|
||
logToFile(`Info fetch error: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
logToFile(`Playing: ${url}`);
|
||
await distube.play(voiceChannel, url, {
|
||
member: interaction.member,
|
||
textChannel: interaction.channel,
|
||
metadata: { interaction, songTitle: title }
|
||
});
|
||
|
||
await interaction.editReply(`Memainkan: **${title}**... 🎶`);
|
||
} 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 {
|
||
let url = query;
|
||
let title = query;
|
||
const validation = await play.validate(query);
|
||
|
||
if (!validation || validation === 'search') {
|
||
const searchResults = await play.search(query, { limit: 1 });
|
||
if (!searchResults || searchResults.length === 0) return message.reply('Lagu tidak ditemukan!');
|
||
url = searchResults[0].url;
|
||
title = searchResults[0].title;
|
||
} else {
|
||
try {
|
||
const info = await play.video_info(url);
|
||
title = info.video_details.title;
|
||
url = info.video_details.url;
|
||
} catch (err) {
|
||
logToFile(`Prefix info fetch error: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
message.reply(`🔍 Memainkan: **${title}**...`);
|
||
|
||
await distube.play(voiceChannel, url, {
|
||
member: message.member,
|
||
textChannel: message.channel,
|
||
message
|
||
});
|
||
} 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);
|