import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } from 'discord.js'; let botClient = null; let triggerRemoteActionFn = null; let tvSessionsStore = null; let featuredChannelsList = []; let allChannelsList = []; // Helper to create the Remote Control Embed function createRemoteEmbed(voiceChannelId, channel) { return new EmbedBuilder() .setTitle('šŸ“ŗ WISE TV - REMOTE CONTROL') .setDescription( `Sedang Menonton di Voice Channel: <#${voiceChannelId}>\n\n` + `**Saluran Aktif:** \`${channel.name}\`\n` + `**Kategori:** \`${channel.category}\`\n\n` + `*Gunakan tombol dan menu pilihan di bawah untuk mengontrol TV secara real-time untuk semua orang di channel!*` ) .setThumbnail(channel.logo || 'https://images.unsplash.com/photo-1598257006458-087169a1f08d?w=128&h=128&fit=crop') .setColor('#5865F2') .setFooter({ text: 'Wise TV • Piala Dunia Edition ⚽', iconURL: 'https://upload.wikimedia.org/wikipedia/commons/e/ea/Fifa_world_cup_logo.png' }) .setTimestamp(); } // Helper to create Remote Control Components (Buttons & Dropdown) function createRemoteComponents(voiceChannelId, currentChannelId) { // Action Row 1: Buttons const buttonsRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`remote_prev_${voiceChannelId}`) .setLabel('ā—€ļø Sebelumnya') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId(`remote_reload_${voiceChannelId}`) .setLabel('šŸ”„ Reload') .setStyle(ButtonStyle.Secondary), new ButtonBuilder() .setCustomId(`remote_next_${voiceChannelId}`) .setLabel('ā–¶ļø Selanjutnya') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId(`remote_stop_${voiceChannelId}`) .setLabel('āŒ Matikan') .setStyle(ButtonStyle.Danger) ); // Action Row 2: Select Menu (featured channels) // Limit to 25 items (Discord select menu limit) const selectMenu = new StringSelectMenuBuilder() .setCustomId(`remote_select_${voiceChannelId}`) .setPlaceholder('šŸ“ŗ Pilih Saluran TV...'); const options = featuredChannelsList.slice(0, 25).map(c => ({ label: c.name, value: c.id, description: `Kategori: ${c.category}`, emoji: c.category === 'Sport' ? '⚽' : (c.category === 'Lokal' ? 'šŸ‡®šŸ‡©' : 'šŸŒŽ'), default: c.id === currentChannelId })); selectMenu.addOptions(options); const selectRow = new ActionRowBuilder().addComponents(selectMenu); return [buttonsRow, selectRow]; } // Function to register slash commands async function registerCommands(token, clientId, guildId) { const commands = [ new SlashCommandBuilder() .setName('nonton') .setDescription('Mulai menonton TV di Voice Channel aktif Anda.'), new SlashCommandBuilder() .setName('worldcup') .setDescription('Mulai menonton saluran Sport / Piala Dunia di Voice Channel aktif Anda.'), new SlashCommandBuilder() .setName('help') .setDescription('Melihat daftar command dan cara menggunakan Wise TV.') ].map(command => command.toJSON()); const rest = new REST({ version: '10' }).setToken(token); try { if (guildId) { console.log(`Registering slash commands to Guild ID: ${guildId}`); await rest.put( Routes.applicationGuildCommands(clientId, guildId), { body: commands } ); } else { console.log('Registering slash commands globally...'); await rest.put( Routes.applicationCommands(clientId), { body: commands } ); } console.log('Successfully registered Slash Commands!'); } catch (error) { console.error('Error registering slash commands:', error); } } export function initBot(triggerRemoteAction, tvSessions, featuredChannels, allChannels) { triggerRemoteActionFn = triggerRemoteAction; tvSessionsStore = tvSessions; featuredChannelsList = featuredChannels; allChannelsList = allChannels; const token = process.env.DISCORD_TOKEN; const clientId = process.env.DISCORD_CLIENT_ID; const guildId = process.env.DISCORD_GUILD_ID; if (!token || !clientId) { console.error('ERROR: Cannot start bot. Check if DISCORD_TOKEN and DISCORD_CLIENT_ID are set in .env'); return; } const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] }); botClient = client; client.once('ready', () => { console.log(`Bot logged in as ${client.user.tag}`); registerCommands(token, clientId, guildId); }); // Handle Slash Commands client.on('interactionCreate', async interaction => { if (!interaction.isChatInputCommand()) return; const { commandName } = interaction; if (commandName === 'nonton' || commandName === 'worldcup') { const voiceChannel = interaction.member.voice.channel; if (!voiceChannel) { return interaction.reply({ content: 'āŒ **Kamu harus berada di Voice Channel terlebih dahulu untuk menonton TV!** Please join a voice channel and try again.', ephemeral: true }); } // Check if there is a default channel for worldcup, or fallback to featured let defaultChannel = featuredChannelsList[0]; // TVRI Nasional if (commandName === 'worldcup') { const sportChannel = featuredChannelsList.find(c => c.category === 'Sport'); if (sportChannel) defaultChannel = sportChannel; } try { // 1. Send LAUNCH_ACTIVITY (type 12) callback to Discord // This instructs Discord Client to open the embedded app in the voice channel console.log(`Launching Activity in voice channel ${voiceChannel.id} for user ${interaction.user.tag}`); await interaction.client.rest.post( Routes.interactionCallback(interaction.id, interaction.token), { body: { type: 12, // LAUNCH_ACTIVITY } } ); // Save session state tvSessionsStore.set(voiceChannel.id, defaultChannel); // 2. Post the Remote Control in the text channel await interaction.channel.send({ embeds: [createRemoteEmbed(voiceChannel.id, defaultChannel)], components: createRemoteComponents(voiceChannel.id, defaultChannel.id) }); } catch (error) { console.error('Failed to launch activity:', error); // If launch activity callback failed (e.g. app hasn't enabled Activities), send a fallback web link const webUrl = `http://localhost:3000/?voiceChannelId=${voiceChannel.id}`; await interaction.reply({ content: `āš ļø **Gagal membuka Activity otomatis.** Pastikan fitur Activity sudah diaktifkan di Developer Portal Anda.\n\nSebagai alternatif, Anda bisa menonton secara manual lewat browser di link berikut:\nšŸ”— **[Buka Wise TV Player](${webUrl})**`, ephemeral: true }); } } if (commandName === 'help') { const helpEmbed = new EmbedBuilder() .setTitle('šŸ“ŗ Bantuan Wise TV') .setDescription( 'Wise TV adalah Discord Activity & Bot untuk menonton TV lokal, internasional, dan olahraga bersama di server Anda!\n\n' + '**Daftar Command:**\n' + '• `/nonton` - Membuka TV Player (Activity) di Voice Channel Anda dan memposting remote di chat.\n' + '• `/worldcup` - Membuka TV Player langsung pada saluran olahraga/bola.\n' + '• `/help` - Membuka halaman bantuan ini.' ) .addFields( { name: 'šŸŽ® Cara Menonton', value: '1. Masuk ke salah satu **Voice Channel**.\n2. Ketik `/nonton` di text channel.\n3. Panel video akan terbuka di Voice Channel Anda, dan remote control akan muncul di text channel.' }, { name: 'šŸŽ›ļø Cara Memakai Remote', value: 'Gunakan tombol di bawah embed remote untuk ganti channel sebelumnya/selanjutnya atau memuat ulang siaran. Semua orang yang menonton di voice channel tersebut akan tersinkronisasi otomatis.' } ) .setColor('#5865F2'); await interaction.reply({ embeds: [helpEmbed] }); } }); // Handle Button & Dropdown Interactions (Remote Control) client.on('interactionCreate', async interaction => { if (!interaction.isButton() && !interaction.isStringSelectMenu()) return; const customId = interaction.customId; if (!customId.startsWith('remote_')) return; const parts = customId.split('_'); // [remote, action, voiceChannelId] const action = parts[1]; const voiceChannelId = parts[2]; const currentChannel = tvSessionsStore.get(voiceChannelId) || featuredChannelsList[0]; let targetChannel = currentChannel; if (action === 'select' && interaction.isStringSelectMenu()) { const selectedId = interaction.values[0]; const found = featuredChannelsList.find(c => c.id === selectedId) || allChannelsList.find(c => c.id === selectedId); if (found) targetChannel = found; } else if (action === 'next') { const currentIndex = featuredChannelsList.findIndex(c => c.id === currentChannel.id); if (currentIndex !== -1) { const nextIndex = (currentIndex + 1) % featuredChannelsList.length; targetChannel = featuredChannelsList[nextIndex]; } } else if (action === 'prev') { const currentIndex = featuredChannelsList.findIndex(c => c.id === currentChannel.id); if (currentIndex !== -1) { const prevIndex = (currentIndex - 1 + featuredChannelsList.length) % featuredChannelsList.length; targetChannel = featuredChannelsList[prevIndex]; } } else if (action === 'reload') { // Trigger reload event to SSE clients triggerRemoteActionFn(voiceChannelId, { action: 'reload' }); return interaction.reply({ content: 'šŸ”„ Mengirim sinyal reload ke pemutar TV...', ephemeral: true }); } else if (action === 'stop') { // Trigger stop event to SSE clients triggerRemoteActionFn(voiceChannelId, { action: 'stop' }); // Update remote embed const stoppedEmbed = new EmbedBuilder() .setTitle('šŸ“ŗ WISE TV - NONAKTIF') .setDescription(`Sesi TV di Voice Channel <#${voiceChannelId}> telah dihentikan oleh <@${interaction.user.id}>.`) .setColor('#ED4245') .setTimestamp(); await interaction.update({ embeds: [stoppedEmbed], components: [] }); return; } // Update state and trigger SSE event tvSessionsStore.set(voiceChannelId, targetChannel); triggerRemoteActionFn(voiceChannelId, { action: 'change-channel', channel: targetChannel }); // Update the message components and embed to show new active channel await interaction.update({ embeds: [createRemoteEmbed(voiceChannelId, targetChannel)], components: createRemoteComponents(voiceChannelId, targetChannel.id) }); }); client.login(token); }