diff --git a/src/main/java/moe/nekojimi/chords/CommandOptions.java b/src/main/java/moe/nekojimi/chords/CommandOptions.java new file mode 100644 index 0000000..27c1848 --- /dev/null +++ b/src/main/java/moe/nekojimi/chords/CommandOptions.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 jimj316 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package moe.nekojimi.chords; + +import com.beust.jcommander.Parameter; + +/** + * + * @author jimj316 + */ +public class CommandOptions +{ + + @Parameter(description = "The API token for Discord.", required = true) + private String token; + + @Parameter(names = "-local-addr", description = "The local address to bind to.") + private String localAddress; + + public String getToken() + { + return token; + } + + public String getLocalAddress() + { + return localAddress; + } + +} diff --git a/src/main/java/moe/nekojimi/chords/LocalBindSocketFactory.java b/src/main/java/moe/nekojimi/chords/LocalBindSocketFactory.java new file mode 100644 index 0000000..09e1d06 --- /dev/null +++ b/src/main/java/moe/nekojimi/chords/LocalBindSocketFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 jimj316 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package moe.nekojimi.chords; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import javax.net.SocketFactory; + +/** + * + * @author jimj316 + */ +public class LocalBindSocketFactory extends SocketFactory +{ + + private InetAddress localAddress; + + public InetAddress getLocalAddress() + { + return localAddress; + } + + public void setLocalAddress(InetAddress localAddress) + { + this.localAddress = localAddress; + } + + @Override + public Socket createSocket(String remoteAddr, int remotePort) throws IOException, UnknownHostException + { + return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress)); + } + + @Override + public Socket createSocket(String remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException, UnknownHostException + { + return createSocket(InetAddress.getByName(remoteAddr), remotePort, localAddress, localPort); + } + + @Override + public Socket createSocket(InetAddress remoteAddr, int remotePort) throws IOException + { + return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress)); + } + + @Override + public Socket createSocket(InetAddress remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException + { + return new Socket(remoteAddr, remotePort, localAddress, localPort); + } + + private static int findFreePort(InetAddress localAddr) + { + int port = 0; + // For ServerSocket port number 0 means that the port number is automatically allocated. + try (ServerSocket socket = new ServerSocket(0, 0, localAddr)) + { + // Disable timeout and reuse address after closing the socket. + socket.setReuseAddress(true); + port = socket.getLocalPort(); + } catch (IOException ignored) + { + } + if (port > 0) + { + return port; + } + throw new RuntimeException("Could not find a free port"); + } + +} diff --git a/src/main/java/moe/nekojimi/chords/Main.java b/src/main/java/moe/nekojimi/chords/Main.java index c4aa11b..5bc3107 100644 --- a/src/main/java/moe/nekojimi/chords/Main.java +++ b/src/main/java/moe/nekojimi/chords/Main.java @@ -7,11 +7,13 @@ package moe.nekojimi.chords; import com.amihaiemil.eoyaml.Yaml; import com.amihaiemil.eoyaml.YamlMapping; +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.neovisionaries.ws.client.WebSocketFactory; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.*; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -20,6 +22,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; import javax.security.auth.login.LoginException; import moe.nekojimi.chords.commands.*; import moe.nekojimi.musicsearcher.providers.MetaSearcher; @@ -42,6 +46,8 @@ import net.dv8tion.jda.api.utils.cache.CacheFlag; public final class Main extends ListenerAdapter { + private static final CommandOptions options = new CommandOptions(); + private final File dataDirectory; private final File playlistsDirectory; @@ -66,6 +72,13 @@ public final class Main extends ListenerAdapter */ 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 @@ -91,49 +104,21 @@ public final class Main extends ListenerAdapter builder.addEventListeners(listener); builder.setAutoReconnect(true); + if (options.getLocalAddress() != null) + { + final WebSocketFactory webSocketFactory = new WebSocketFactory(); + final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory(); + localBindSocketFactory.setLocalAddress(InetAddress.getByName(options.getLocalAddress())); + webSocketFactory.setSocketFactory(localBindSocketFactory); + builder.setWebsocketFactory(webSocketFactory); + } + JDA jda = builder.build(); listener.setJda(jda); } - private final Consumer nowPlayingConsumer = (Song song) -> - { - if (song != null) - jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, song.toString())); - else - jda.getPresence().setActivity(null); - }; + private final Consumer nowPlayingConsumer = new NowPlayingConsumer(); - private final BiConsumer downloaderMessageHandler = (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 final BiConsumer downloaderMessageHandler = new DownloaderMessageHandler(); public Main() throws IOException { @@ -171,7 +156,6 @@ public final class Main extends ListenerAdapter log("INFO", "Started OK!"); } - private void addCommand(Command command) { commands.put(command.getKeyword(), command); @@ -407,6 +391,7 @@ public final class Main extends ListenerAdapter { return currentVoiceChannel; } + public QueueManager getQueueManager() { return queueManager; @@ -417,5 +402,62 @@ public final class Main extends ListenerAdapter return trackNumber; } + 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); + } + } }