/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package moe.nekojimi.chords; import com.amihaiemil.eoyaml.Yaml; import com.amihaiemil.eoyaml.YamlMapping; import com.beust.jcommander.JCommander; import com.neovisionaries.ws.client.WebSocketFactory; import java.io.File; import java.io.IOException; import java.net.*; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import javax.security.auth.login.LoginException; import moe.nekojimi.chords.commands.*; import moe.nekojimi.musicsearcher.providers.MetaSearcher; import moe.nekojimi.musicsearcher.providers.Searcher; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.managers.AudioManager; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.Compression; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.internal.utils.IOUtil; import okhttp3.OkHttpClient; /** * The Chords music Discord bot. * * @author Nekojimi */ public final class Chords extends ListenerAdapter { private static final CommandOptions options = new CommandOptions(); private static Settings settings; private final File dataDirectory; private final File playlistsDirectory; private MusicHandler musicHandler; private final Downloader downloader; private final Searcher searcher; private final QueueManager queueManager; private JDA jda; private final Map commands = new HashMap<>(); private final Command helpCommand; private PlayCommand playCommand; private AudioChannel currentVoiceChannel = null; private int trackNumber = 1; private final Map playlists = new HashMap<>(); /** * @param args the command line arguments */ public static void main(String[] args) throws LoginException, IOException { JCommander commander = JCommander.newBuilder() .args(args) .acceptUnknownOptions(true) .addObject(options) .build(); // We only need 2 gateway intents enabled for this example: EnumSet intents = EnumSet.of( // We need messages in guilds to accept commands from users GatewayIntent.GUILD_MESSAGES, // We need voice states to connect to the voice channel GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT ); settings = new Settings(new File(options.getSettingsPath())); JDABuilder builder = JDABuilder.createDefault(settings.getDiscordToken(), intents); // Disable parts of the cache builder.disableCache(CacheFlag.MEMBER_OVERRIDES); // Enable the bulk delete event builder.setBulkDeleteSplittingEnabled(false); // Disable compression (not recommended) builder.setCompression(Compression.NONE); // Set activity (like "playing Something") builder.setActivity(Activity.playing("music!")); final Chords listener = new Chords(); builder.addEventListeners(listener); builder.setAutoReconnect(true); if (settings.getLocalAddr() != null) { System.out.println("Using local address: " + settings.getLocalAddr()); // make local binding socket factory final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory(); localBindSocketFactory.setLocalAddress(InetAddress.getByName(settings.getLocalAddr())); // install local socket factory for HTTP OkHttpClient.Builder httpBuilder = IOUtil.newHttpClientBuilder(); httpBuilder.socketFactory(localBindSocketFactory); builder.setHttpClientBuilder(httpBuilder); // install local socket factory for websockets final WebSocketFactory webSocketFactory = new WebSocketFactory(); webSocketFactory.setSocketFactory(localBindSocketFactory); builder.setWebsocketFactory(webSocketFactory); } JDA jda = builder.build(); listener.setJda(jda); } private final Consumer nowPlayingConsumer = new NowPlayingConsumer(); private final BiConsumer downloaderMessageHandler = new DownloaderMessageHandler(); public Chords() throws IOException { log("INFO", "Starting up..."); // init dirs dataDirectory = new File(System.getProperty("user.dir")); playlistsDirectory = initDirectory(dataDirectory, "playlists"); // init downloader downloader = new Downloader(); downloader.setMessageHandler(downloaderMessageHandler); // init searcher searcher = MetaSearcher.loadYAML(new File("searchproviders.yml")); // init queue manager queueManager = new QueueManager(); // init commands addCommand(new JoinCommand(this)); addCommand(new LeaveCommand(this)); playCommand = new PlayCommand(this); addCommand(playCommand); addCommand(new QueueCommand(this)); addCommand(new RemoveCommand(this)); addCommand(new RestartCommand(this)); addCommand(new SkipCommand(this)); helpCommand = new HelpCommand(this); addCommand(helpCommand); // load playlists loadPlaylists(); log("INFO", "Started OK!"); } private void addCommand(Command command) { commands.put(command.getKeyword(), command); } public void setJda(JDA jda) { this.jda = jda; } @Override public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) { super.onGuildVoiceUpdate(event); if (this.currentVoiceChannel == null) return; final AudioChannelUnion channelLeft = event.getChannelLeft(); if (channelLeft.getMembers().isEmpty()) if (channelLeft == currentVoiceChannel) disconnect(); } @Override public void onMessageReceived(MessageReceivedEvent event) { super.onMessageReceived(event); //To change body of generated methods, choose Tools | Templates. Message message = event.getMessage(); User author = message.getAuthor(); String content = message.getContentRaw(); Guild guild = event.getGuild(); // Ignore message if it's not in a music channel if (!event.getChannel().getName().contains("music")) return; // Ignore message if bot if (author.isBot()) return; log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content); try { URL parseURL = new URL(content.trim()); playCommand.call(null); } catch (MalformedURLException ex) { // not a URL, then } try { String[] split = content.split("\\s+"); String cmd = split[0].toLowerCase(); if (!cmd.startsWith("!")) return; // doesn't start with prefix char cmd = cmd.substring(1); // strip prefix char // String arg = ""; List args = new ArrayList<>(); Collections.addAll(args, split); args.remove(0); Invocation invocation = new Invocation(event, args); invocation.setRequestMessage(message); if (commands.containsKey(cmd)) { Command command = commands.get(cmd); command.call(invocation); } else { helpCommand.call(invocation); } } catch (Exception ex) { event.getChannel().sendMessage("Error in command! " + ex.getMessage()).queue(); log("UERR", "Command error:" + ex.getMessage()); } } public Song queueDownload(SongRequest request) { Song song; if (request.getUrl() != null) { song = new Song(request.getUrl()); } else { // interpret search result throw new UnsupportedOperationException("Not supported yet."); } if (request.getInvocation().getRequestMessage() != null) { song.setRequestedBy(request.getInvocation().getRequestMessage().getAuthor().getName()); song.setRequestedIn(request.getInvocation().getRequestMessage().getChannel().getId()); } song.setNumber(trackNumber); trackNumber++; request.setSong(song); downloader.accept(new Downloader.DownloadTask(request, queueManager)); request.getInvocation().respond("Request pending..."); return song; } // public Song queueDownload(final URL url, GuildMessageReceivedEvent event) // { // Song song = new Song(url); // song.setRequestedBy(event.getAuthor().getName()); // song.setRequestedIn(event.getChannel().getId()); // song.setNumber(trackNumber); // trackNumber++; // downloader.accept(new Downloader.DownloadTask(song, musicHandler)); // return song; // } // // public Song queueDownload(Result res, GuildMessageReceivedEvent event) // { // Song song = queueDownload(res.getLink(), event); // song.setArtist(res.getArtist()); // song.setTitle(res.getTitle()); // song.setNumber(trackNumber); // return song; // } public void setStatus(Song nowPlaying) { jda.getPresence().setActivity(Activity.listening(nowPlaying.toString())); } public void clearStatus() { jda.getPresence().setActivity(null); } /** * Connect to requested channel and start echo handler * * @param channel * The channel to connect to */ public void connectTo(AudioChannel channel) { Guild guild = channel.getGuild(); // Get an audio manager for this guild, this will be created upon first use for each guild AudioManager audioManager = guild.getAudioManager(); musicHandler = new MusicHandler(); queueManager.setHandler(musicHandler); // Create our Send/Receive handler for the audio connection // EchoHandler handler = new EchoHandler(); // The order of the following instructions does not matter! // Set the sending handler to our echo system audioManager.setSendingHandler(musicHandler); // Set the receiving handler to the same echo system, otherwise we can't echo anything // audioManager.setReceivingHandler(handler); // Connect to the voice channel audioManager.openAudioConnection(channel); currentVoiceChannel = channel; musicHandler.setNowPlayingConsumer(nowPlayingConsumer); } public void disconnect() { if (currentVoiceChannel != null) { Guild guild = currentVoiceChannel.getGuild(); AudioManager audioManager = guild.getAudioManager(); audioManager.setSendingHandler(null); audioManager.closeAudioConnection(); musicHandler = null; currentVoiceChannel = null; } } public void log(String type, String message) { System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message); } public File initDirectory(File parent, String name) throws IOException { File ret = new File(parent, name); if (!ret.exists()) Files.createDirectories(ret.toPath()); if (!ret.canRead()) throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!"); if (!ret.canWrite()) throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!"); return ret; } private void loadPlaylists() { File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml")); for (File file : files) { try { YamlMapping map = Yaml.createYamlInput(file).readYamlMapping(); Playlist playlist = Playlist.fromYaml(map); playlists.put(playlist.getName(), playlist); } catch (IOException ex) { Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex); } } } public MusicHandler getMusicHandler() { return musicHandler; } public Downloader getDownloader() { return downloader; } public Searcher getSearcher() { return searcher; } public JDA getJda() { return jda; } public AudioChannel getCurrentVoiceChannel() { return currentVoiceChannel; } public QueueManager getQueueManager() { return queueManager; } public int getTrackNumber() { return trackNumber; } public static Settings getSettings() { return settings; } private class DownloaderMessageHandler implements BiConsumer { public DownloaderMessageHandler() { } @Override public void accept(SongRequest request, Exception ex) { Song song = request.getSong(); // TextChannel channel = jda.getTextChannelById(song.getRequestedIn()); // String bracketNo = "[" + song.getNumber() + "] "; if (ex == null) if (song.getLocation() != null) { request.getInvocation().respond("Finished downloading " + song + ", added to queue!"); log("DOWN", "Downloaded " + song); } else { Format format = song.getBestFormat(); String formatDetails = ""; if (format != null) { final int bitrate = format.getSampleRate() / 1000; final long size = format.getSize(); String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB"; String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k"; formatDetails = " (" + bitFmt + ", " + sizeFmt + ")"; } request.getInvocation().respond("Now downloading " + song + formatDetails + " ..."); log("DOWN", "Downloading " + song + "..."); } else { request.getInvocation().respond("Failed to download " + song + "! Reason: " + ex.getMessage()); log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage()); } } } private class NowPlayingConsumer implements Consumer { public NowPlayingConsumer() { } @Override public void accept(Song song) { if (song != null) jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, song.toString())); else jda.getPresence().setActivity(null); } } }