38465-vm/bot/index.js
2026-02-16 06:00:13 +00:00

355 lines
13 KiB
JavaScript

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();