From ef6d1164c63caf3d943cde9c7a7d9b6dc892081b Mon Sep 17 00:00:00 2001 From: Napster Date: Wed, 25 Apr 2018 17:11:27 +0200 Subject: [PATCH 01/27] Implement Magma --- LavalinkServer/build.gradle | 12 +++- .../main/java/lavalink/server/Launcher.java | 5 -- .../server/io/ConnectionManagerImpl.java | 23 ------ .../lavalink/server/io/CoreClientImpl.java | 61 ---------------- .../lavalink/server/io/SocketContext.java | 70 ++++-------------- .../java/lavalink/server/io/SocketServer.java | 71 +++++++++++++------ .../java/lavalink/server/player/Player.java | 2 +- .../server/util/DebugConnectionListener.java | 62 ---------------- .../server/util/SimpleLogToSLF4JAdapter.java | 68 ------------------ build.gradle | 7 +- 10 files changed, 74 insertions(+), 307 deletions(-) delete mode 100644 LavalinkServer/src/main/java/lavalink/server/io/ConnectionManagerImpl.java delete mode 100644 LavalinkServer/src/main/java/lavalink/server/io/CoreClientImpl.java delete mode 100644 LavalinkServer/src/main/java/lavalink/server/util/DebugConnectionListener.java delete mode 100644 LavalinkServer/src/main/java/lavalink/server/util/SimpleLogToSLF4JAdapter.java diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index 63dc02c8c..2f50ea416 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -35,17 +35,23 @@ test { } dependencies { + compile group: 'space.npstr', name: 'Magma', version: magmaVersion compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion - compile group: 'com.github.DV8FromTheWorld', name: 'JDA-Audio', version: jdaAudioVersion - compile group: 'com.github.FredBoat.jda-nas', name: 'jda-nas', version: jdaNasVersion + compile group: 'com.sedmelluq', name: 'jda-nas', version: jdaNasVersion + compile group: 'com.github.shredder121', name: 'jda-async-packetprovider', version: jappVersion + //required by japp + compile group: 'org.apache.commons', name: 'commons-lang3', version: commonsLangVersion compile group: 'org.java-websocket', name: 'Java-WebSocket', version: javaWebSocketVersion compile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion compile group: 'io.sentry', name: 'sentry-logback', version: sentryLogbackVersion compile group: 'com.github.oshi', name: 'oshi-core', version: oshiVersion compile group: 'org.json', name: 'json', version: jsonOrgVersion - compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion + compile(group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion) { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + } + compile group: 'org.springframework.boot', name: 'spring-boot-starter-undertow', version: springBootVersion compileOnly group: 'com.github.spotbugs', name: 'spotbugs-annotations', version: spotbugsAnnotationsVersion compile group: 'io.prometheus', name: 'simpleclient', version: prometheusVersion diff --git a/LavalinkServer/src/main/java/lavalink/server/Launcher.java b/LavalinkServer/src/main/java/lavalink/server/Launcher.java index f00ca914d..bb71f2c19 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Launcher.java +++ b/LavalinkServer/src/main/java/lavalink/server/Launcher.java @@ -26,8 +26,6 @@ import lavalink.server.info.AppInfo; import lavalink.server.info.GitRepoState; import lavalink.server.io.SocketServer; -import lavalink.server.util.SimpleLogToSLF4JAdapter; -import net.dv8tion.jda.utils.SimpleLog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; @@ -85,9 +83,6 @@ public Launcher(SocketServer socketServer) { log.warn("Interrupted while stopping socket server", e); } }, "shutdown hook")); - - SimpleLog.LEVEL = SimpleLog.Level.OFF; - SimpleLog.addListener(new SimpleLogToSLF4JAdapter()); } private static String getVersionInfo() { diff --git a/LavalinkServer/src/main/java/lavalink/server/io/ConnectionManagerImpl.java b/LavalinkServer/src/main/java/lavalink/server/io/ConnectionManagerImpl.java deleted file mode 100644 index 22c101672..000000000 --- a/LavalinkServer/src/main/java/lavalink/server/io/ConnectionManagerImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package lavalink.server.io; - -import net.dv8tion.jda.manager.ConnectionManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Created by Repulser - * https://github.com/Repulser - */ -public class ConnectionManagerImpl implements ConnectionManager { - private static final Logger log = LoggerFactory.getLogger(ConnectionManagerImpl.class); - - @Override - public void removeAudioConnection(String guildId) { - } - - @Override - public void queueAudioConnect(String guildId, String channelId) { - log.warn("queueAudioConnect was requested, this shouldn't happen, guildId:" + guildId + " channelId:" + channelId, new RuntimeException()); - } - -} diff --git a/LavalinkServer/src/main/java/lavalink/server/io/CoreClientImpl.java b/LavalinkServer/src/main/java/lavalink/server/io/CoreClientImpl.java deleted file mode 100644 index 1cb045e46..000000000 --- a/LavalinkServer/src/main/java/lavalink/server/io/CoreClientImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2017 Frederik Ar. Mikkelsen & NoobLance - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package lavalink.server.io; - -import net.dv8tion.jda.CoreClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CoreClientImpl implements CoreClient { - - private static final Logger log = LoggerFactory.getLogger(CoreClientImpl.class); - - @Override - public void sendWS(String message) { - } - - @Override - public boolean isConnected() { - log.warn("isConnected was requested, this shouldn't happen", new RuntimeException()); - return true; - } - - @Override - public boolean inGuild(String guildId) { - log.warn("inGuild was requested, this shouldn't happen, guildId:" + guildId, new RuntimeException()); - return true; - } - - @Override - public boolean voiceChannelExists(String guildId, String channelId) { - log.warn("voiceChannelExists was requested, this shouldn't happen, guildId:" + guildId + " channelId:" + channelId, new RuntimeException()); - return true; - } - - @Override - public boolean hasPermissionInChannel(String guildId, String channelId, long l) { - log.warn("hasPermissionInChannel was requested, this shouldn't happen, guildId:" + guildId + " channelId:" + channelId + " l:" + l, new RuntimeException()); - return true; - } - -} diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index da276ef14..64c54466c 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -22,22 +22,13 @@ package lavalink.server.io; -import com.github.shredder121.asyncaudio.jdaaudio.AsyncPacketProviderFactory; -import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import lavalink.server.config.AudioSendFactoryConfiguration; -import lavalink.server.config.ServerConfig; import lavalink.server.player.Player; -import lavalink.server.util.Util; -import net.dv8tion.jda.Core; -import net.dv8tion.jda.audio.factory.IAudioSendFactory; -import net.dv8tion.jda.manager.AudioManager; -import net.dv8tion.jda.manager.ConnectionManagerBuilder; import org.java_websocket.WebSocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import space.npstr.magma.MagmaApi; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -52,26 +43,20 @@ public class SocketContext { private static final Logger log = LoggerFactory.getLogger(SocketContext.class); private final AudioPlayerManager audioPlayerManager; - private final ServerConfig serverConfig; private final WebSocket socket; - private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; private String userId; - private int shardCount; - private final Map cores = new HashMap<>(); + private final MagmaApi magmaApi; + //guildId <-> Player private final Map players = new ConcurrentHashMap<>(); private ScheduledExecutorService statsExecutor; public final ScheduledExecutorService playerUpdateService; - private final ConcurrentHashMap sendFactories = new ConcurrentHashMap<>(); - SocketContext(Supplier audioPlayerManagerSupplier, ServerConfig serverConfig, WebSocket socket, - AudioSendFactoryConfiguration audioSendFactoryConfiguration, SocketServer socketServer, - String userId, int shardCount) { + SocketContext(Supplier audioPlayerManagerSupplier, WebSocket socket, SocketServer socketServer, String userId, + MagmaApi magmaApi) { this.audioPlayerManager = audioPlayerManagerSupplier.get(); - this.serverConfig = serverConfig; this.socket = socket; - this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; this.userId = userId; - this.shardCount = shardCount; + this.magmaApi = magmaApi; statsExecutor = Executors.newSingleThreadScheduledExecutor(); statsExecutor.scheduleAtFixedRate(new StatsTask(this, socketServer), 0, 1, TimeUnit.MINUTES); @@ -84,15 +69,8 @@ public class SocketContext { }); } - Core getCore(int shardId) { - return cores.computeIfAbsent(shardId, - __ -> { - if (audioSendFactoryConfiguration.isNasSupported()) - return new Core(userId, new CoreClientImpl(), core -> new ConnectionManagerImpl(), getAudioSendFactory(shardId)); - else - return new Core(userId, new CoreClientImpl(), (ConnectionManagerBuilder) core -> new ConnectionManagerImpl()); - } - ); + public String getUserId() { + return userId; } Player getPlayer(String guildId) { @@ -101,10 +79,6 @@ Player getPlayer(String guildId) { ); } - int getShardCount() { - return shardCount; - } - public WebSocket getSocket() { return socket; } @@ -122,38 +96,18 @@ List getPlayingPlayers() { } void shutdown() { - log.info("Shutting down " + cores.size() + " cores and " + getPlayingPlayers().size() + " playing players."); + log.info("Shutting down " + getPlayingPlayers().size() + " playing players."); statsExecutor.shutdown(); audioPlayerManager.shutdown(); playerUpdateService.shutdown(); - players.keySet().forEach(s -> { - Core core = cores.get(Util.getShardFromSnowflake(s, shardCount)); - if (core != null) { - AudioManager audioManager = core.getAudioManager(s); - if (audioManager != null) { - audioManager.closeAudioConnection(); - } - } + players.keySet().forEach(guildId -> { + magmaApi.removeSendHandler(userId, guildId); + magmaApi.closeConnection(userId, guildId); }); players.values().forEach(Player::stop); } - private IAudioSendFactory getAudioSendFactory(int shardId) { - return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.getAudioSendFactoryCount(), - integer -> { - Integer customBuffer = serverConfig.getBufferDurationMs(); - NativeAudioSendFactory nativeAudioSendFactory; - if (customBuffer != null) { - nativeAudioSendFactory = new NativeAudioSendFactory(customBuffer); - } else { - nativeAudioSendFactory = new NativeAudioSendFactory(); - } - - return AsyncPacketProviderFactory.adapt(nativeAudioSendFactory); - }); - } - public AudioPlayerManager getAudioPlayerManager() { return audioPlayerManager; } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index 8cd7578b3..a21be1cbd 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -22,6 +22,8 @@ package lavalink.server.io; +import com.github.shredder121.asyncaudio.jda.AsyncPacketProviderFactory; +import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.TrackMarker; @@ -31,8 +33,7 @@ import lavalink.server.player.Player; import lavalink.server.player.TrackEndMarkerHandler; import lavalink.server.util.Util; -import net.dv8tion.jda.Core; -import net.dv8tion.jda.manager.AudioManager; +import net.dv8tion.jda.core.audio.factory.IAudioSendFactory; import org.java_websocket.WebSocket; import org.java_websocket.drafts.Draft; import org.java_websocket.exceptions.InvalidDataException; @@ -43,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import space.npstr.magma.MagmaApi; import javax.annotation.PostConstruct; import java.io.IOException; @@ -50,6 +52,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import static lavalink.server.io.WSCodes.AUTHORIZATION_REJECTED; @@ -60,10 +63,14 @@ public class SocketServer extends WebSocketServer { private static final Logger log = LoggerFactory.getLogger(SocketServer.class); + private final MagmaApi magmaApi = MagmaApi.of(this::getAudioSendFactory); + // userId <-> shardCount + private final Map shardCounts = new ConcurrentHashMap<>(); private final Map contextMap = new HashMap<>(); private final ServerConfig serverConfig; private final Supplier audioPlayerManagerSupplier; private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; + private final ConcurrentHashMap sendFactories = new ConcurrentHashMap<>(); public SocketServer(WebsocketConfig websocketConfig, ServerConfig serverConfig, Supplier audioPlayerManagerSupplier, AudioSendFactoryConfiguration audioSendFactoryConfiguration) { @@ -93,10 +100,11 @@ public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) { int shardCount = Integer.parseInt(clientHandshake.getFieldValue("Num-Shards")); String userId = clientHandshake.getFieldValue("User-Id"); + shardCounts.put(userId, shardCount); + if (clientHandshake.getFieldValue("Authorization").equals(serverConfig.getPassword())) { log.info("Connection opened from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); - contextMap.put(webSocket, new SocketContext(audioPlayerManagerSupplier, serverConfig, webSocket, - audioSendFactoryConfiguration, this, userId, shardCount)); + contextMap.put(webSocket, new SocketContext(audioPlayerManagerSupplier, webSocket, this, userId, magmaApi)); } else { log.error("Authentication failed from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); webSocket.close(AUTHORIZATION_REJECTED, "Authorization rejected"); @@ -146,12 +154,16 @@ public void onMessage(WebSocket webSocket, String s) { switch (json.getString("op")) { /* JDAA ops */ case "voiceUpdate": - Core core = contextMap.get(webSocket).getCore(getShardId(webSocket, json)); - core.provideVoiceServerUpdate( - json.getString("sessionId"), - json.getJSONObject("event") - ); - core.getAudioManager(json.getJSONObject("event").getString("guild_id")).setAutoReconnect(false); + String sessionId = json.getString("sessionId"); + String guildId = json.getString("guildId"); + + JSONObject event = json.getJSONObject("event"); + String endpoint = event.getString("endpoint"); + String token = event.getString("token"); + //todo endpoint empty check? + + SocketContext sktContext = contextMap.get(webSocket); + magmaApi.provideVoiceServerUpdate(sktContext.getUserId(), sessionId, guildId, endpoint, token); break; /* Player ops */ @@ -176,8 +188,11 @@ public void onMessage(WebSocket webSocket, String s) { SocketContext context = contextMap.get(webSocket); - context.getCore(getShardId(webSocket, json)).getAudioManager(json.getString("guildId")) - .setSendingHandler(context.getPlayer(json.getString("guildId"))); + magmaApi.setSendHandler( + context.getUserId(), + json.getString("guildId"), + context.getPlayer(json.getString("guildId"))); + sendPlayerUpdate(webSocket, player); } catch (IOException e) { throw new RuntimeException(e); @@ -202,13 +217,11 @@ public void onMessage(WebSocket webSocket, String s) { player4.setVolume(json.getInt("volume")); break; case "destroy": - Player player5 = contextMap.get(webSocket).getPlayers().remove(json.getString("guildId")); + SocketContext socketContext = contextMap.get(webSocket); + Player player5 = socketContext.getPlayers().remove(json.getString("guildId")); if (player5 != null) player5.stop(); - AudioManager audioManager = contextMap.get(webSocket) - .getCore(getShardId(webSocket, json)) - .getAudioManager(json.getString("guildId")); - audioManager.setSendingHandler(null); - audioManager.closeAudioConnection(); + magmaApi.removeSendHandler(socketContext.getUserId(), json.getString("guildId")); + magmaApi.closeConnection(socketContext.getUserId(), json.getString("guildId")); break; default: log.warn("Unexpected operation: " + json.getString("op")); @@ -235,13 +248,25 @@ public static void sendPlayerUpdate(WebSocket webSocket, Player player) { webSocket.send(json.toString()); } - //Shorthand method - private int getShardId(WebSocket webSocket, JSONObject json) { - return Util.getShardFromSnowflake(json.getString("guildId"), contextMap.get(webSocket).getShardCount()); - } - Collection getContexts() { return contextMap.values(); } + private IAudioSendFactory getAudioSendFactory(String userId, String guildId) { + int shardCount = shardCounts.getOrDefault(userId, 1); + int shardId = Util.getShardFromSnowflake(guildId, shardCount); + + return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.getAudioSendFactoryCount(), + integer -> { + Integer customBuffer = serverConfig.getBufferDurationMs(); + NativeAudioSendFactory nativeAudioSendFactory; + if (customBuffer != null) { + nativeAudioSendFactory = new NativeAudioSendFactory(customBuffer); + } else { + nativeAudioSendFactory = new NativeAudioSendFactory(); + } + + return AsyncPacketProviderFactory.adapt(nativeAudioSendFactory); + }); + } } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index 67af5313c..e6fd9476a 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -30,7 +30,7 @@ import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; import lavalink.server.io.SocketContext; import lavalink.server.io.SocketServer; -import net.dv8tion.jda.audio.AudioSendHandler; +import net.dv8tion.jda.core.audio.AudioSendHandler; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/LavalinkServer/src/main/java/lavalink/server/util/DebugConnectionListener.java b/LavalinkServer/src/main/java/lavalink/server/util/DebugConnectionListener.java deleted file mode 100644 index 2b5b10959..000000000 --- a/LavalinkServer/src/main/java/lavalink/server/util/DebugConnectionListener.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2017 Frederik Ar. Mikkelsen & NoobLance - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package lavalink.server.util; - - -import net.dv8tion.jda.audio.hooks.ConnectionListener; -import net.dv8tion.jda.audio.hooks.ConnectionStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 65,42% copy pasta from the DebugConnectionListener in FredBoat - */ -public class DebugConnectionListener implements ConnectionListener { - - private static final Logger log = LoggerFactory.getLogger(DebugConnectionListener.class); - - private ConnectionStatus oldStatus = null; - private final long guildId; - - public DebugConnectionListener(long guildId) { - this.guildId = guildId; - } - - @Override - public void onPing(long l) { - - } - - @Override - public void onStatusChange(ConnectionStatus connectionStatus) { - log.debug("Status change for audio connection in guild {} : {} => {}", - guildId, oldStatus, connectionStatus); - - oldStatus = connectionStatus; - } - - @Override - public void onUserSpeaking(String s, boolean b) { - - } -} diff --git a/LavalinkServer/src/main/java/lavalink/server/util/SimpleLogToSLF4JAdapter.java b/LavalinkServer/src/main/java/lavalink/server/util/SimpleLogToSLF4JAdapter.java deleted file mode 100644 index 8ddc27f34..000000000 --- a/LavalinkServer/src/main/java/lavalink/server/util/SimpleLogToSLF4JAdapter.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2017 Frederik Ar. Mikkelsen & NoobLance - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package lavalink.server.util; - -import net.dv8tion.jda.utils.SimpleLog; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author sedmelluq - */ -public class SimpleLogToSLF4JAdapter implements SimpleLog.LogListener { - private static final Logger log = LoggerFactory.getLogger("JDAA"); - - @Override - public void onLog(SimpleLog simpleLog, SimpleLog.Level logLevel, Object message) { - if (message == null) { - message = "null"; - } - - switch (logLevel) { - case TRACE: - if (log.isTraceEnabled()) { - log.trace(message.toString()); - } - break; - case DEBUG: - if (log.isDebugEnabled()) { - log.debug(message.toString()); - } - break; - case INFO: - log.info(message.toString()); - break; - case WARNING: - log.warn(message.toString()); - break; - case FATAL: - log.error(message.toString()); - break; - } - } - - @Override - public void onError(SimpleLog simpleLog, Throwable err) { - log.error("An exception occurred", err); - } -} diff --git a/build.gradle b/build.gradle index 3c0db4e06..854c213a0 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ allprojects { mavenCentral() // main maven repo jcenter() // JDA and some other stuff mavenLocal() // useful for developing - maven { url "https://jitpack.io" } // build projects directly from github + maven { url "https://jitpack.io" } // build projects directly from github } group = 'lavalink' @@ -47,8 +47,8 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - jdaAudioVersion = '4d7abb48aec49f0a996ba0d87df34fdc67f71275' - jdaNasVersion = '1.0.6.2-JDA-Audio' + magmaVersion = '0.1.1' + jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' @@ -61,6 +61,7 @@ subprojects { jsonOrgVersion = '20180130' spotbugsAnnotationsVersion = '3.1.3' prometheusVersion = '0.4.0' + commonsLangVersion = '3.7' junitJupiterVersion = '5.2.0' junitPlatformVersion = '1.2.0' From 107d327e03a89ea53c19d5c7c0c6c2ff2de5fed2 Mon Sep 17 00:00:00 2001 From: Napster Date: Tue, 8 May 2018 03:12:21 +0200 Subject: [PATCH 02/27] Bump to Magma v0.2.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 854c213a0..8755f80e9 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.1.1' + magmaVersion = '0.2.0' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From bd0e2cae31b2578809b64379480abc876877b160 Mon Sep 17 00:00:00 2001 From: Napster Date: Sun, 13 May 2018 19:09:22 +0200 Subject: [PATCH 03/27] Bump to Magma v0.2.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8755f80e9..ba3d446b4 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.2.0' + magmaVersion = '0.2.1' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From 3a69948db6f9faf9a9e89a6ae0b7df283a475bf8 Mon Sep 17 00:00:00 2001 From: Napster Date: Wed, 23 May 2018 21:42:23 +0200 Subject: [PATCH 04/27] Bump to Magma v0.3.1 --- .../lavalink/server/io/SocketContext.java | 10 ++++- .../java/lavalink/server/io/SocketServer.java | 38 ++++++++++++++----- build.gradle | 2 +- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index 64c54466c..82c180b91 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -28,6 +28,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import space.npstr.magma.MagmaApi; +import space.npstr.magma.MagmaMember; +import space.npstr.magma.Member; import java.util.LinkedList; import java.util.List; @@ -101,8 +103,12 @@ void shutdown() { audioPlayerManager.shutdown(); playerUpdateService.shutdown(); players.keySet().forEach(guildId -> { - magmaApi.removeSendHandler(userId, guildId); - magmaApi.closeConnection(userId, guildId); + Member member = MagmaMember.builder() + .userId(userId) + .guildId(guildId) + .build(); + magmaApi.removeSendHandler(member); + magmaApi.closeConnection(member); }); players.values().forEach(Player::stop); diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index a21be1cbd..d61ae7240 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -45,6 +45,10 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import space.npstr.magma.MagmaApi; +import space.npstr.magma.MagmaMember; +import space.npstr.magma.MagmaServerUpdate; +import space.npstr.magma.Member; +import space.npstr.magma.ServerUpdate; import javax.annotation.PostConstruct; import java.io.IOException; @@ -163,7 +167,16 @@ public void onMessage(WebSocket webSocket, String s) { //todo endpoint empty check? SocketContext sktContext = contextMap.get(webSocket); - magmaApi.provideVoiceServerUpdate(sktContext.getUserId(), sessionId, guildId, endpoint, token); + Member member = MagmaMember.builder() + .userId(sktContext.getUserId()) + .guildId(guildId) + .build(); + ServerUpdate serverUpdate = MagmaServerUpdate.builder() + .sessionId(sessionId) + .endpoint(endpoint) + .token(token) + .build(); + magmaApi.provideVoiceServerUpdate(member, serverUpdate); break; /* Player ops */ @@ -188,10 +201,11 @@ public void onMessage(WebSocket webSocket, String s) { SocketContext context = contextMap.get(webSocket); - magmaApi.setSendHandler( - context.getUserId(), - json.getString("guildId"), - context.getPlayer(json.getString("guildId"))); + Member m = MagmaMember.builder() + .userId(context.getUserId()) + .guildId(json.getString("guildId")) + .build(); + magmaApi.setSendHandler(m, context.getPlayer(json.getString("guildId"))); sendPlayerUpdate(webSocket, player); } catch (IOException e) { @@ -220,8 +234,12 @@ public void onMessage(WebSocket webSocket, String s) { SocketContext socketContext = contextMap.get(webSocket); Player player5 = socketContext.getPlayers().remove(json.getString("guildId")); if (player5 != null) player5.stop(); - magmaApi.removeSendHandler(socketContext.getUserId(), json.getString("guildId")); - magmaApi.closeConnection(socketContext.getUserId(), json.getString("guildId")); + Member mem = MagmaMember.builder() + .userId(socketContext.getUserId()) + .guildId(json.getString("guildId")) + .build(); + magmaApi.removeSendHandler(mem); + magmaApi.closeConnection(mem); break; default: log.warn("Unexpected operation: " + json.getString("op")); @@ -252,9 +270,9 @@ Collection getContexts() { return contextMap.values(); } - private IAudioSendFactory getAudioSendFactory(String userId, String guildId) { - int shardCount = shardCounts.getOrDefault(userId, 1); - int shardId = Util.getShardFromSnowflake(guildId, shardCount); + private IAudioSendFactory getAudioSendFactory(Member member) { + int shardCount = shardCounts.getOrDefault(member.getUserId(), 1); + int shardId = Util.getShardFromSnowflake(member.getGuildId(), shardCount); return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.getAudioSendFactoryCount(), integer -> { diff --git a/build.gradle b/build.gradle index ba3d446b4..02249cb2f 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.2.1' + magmaVersion = '0.3.1' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From ab58c19279acc68169f255a1579a3202a5ef7b8e Mon Sep 17 00:00:00 2001 From: Napster Date: Wed, 23 May 2018 21:44:28 +0200 Subject: [PATCH 05/27] Handle voice server update without endpoint --- .../src/main/java/lavalink/server/io/SocketServer.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index d61ae7240..c076b4837 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -162,9 +162,13 @@ public void onMessage(WebSocket webSocket, String s) { String guildId = json.getString("guildId"); JSONObject event = json.getJSONObject("event"); - String endpoint = event.getString("endpoint"); + String endpoint = event.optString("endpoint"); String token = event.getString("token"); - //todo endpoint empty check? + + //discord sometimes send a partial server update missing the endpoint, which can be ignored. + if (endpoint == null || endpoint.isEmpty()) { + return; + } SocketContext sktContext = contextMap.get(webSocket); Member member = MagmaMember.builder() From 21c47ca3356cb31b94b0500c36128f33042fb685 Mon Sep 17 00:00:00 2001 From: Napster Date: Wed, 20 Jun 2018 02:01:49 +0200 Subject: [PATCH 06/27] Bump to Magma v0.3.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 02249cb2f..61c42d835 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.3.1' + magmaVersion = '0.3.2' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From 888f011deac07129f68b4fd3b833ee03bcce05b2 Mon Sep 17 00:00:00 2001 From: Napster Date: Sat, 23 Jun 2018 17:55:23 +0200 Subject: [PATCH 07/27] Bump to Magma v0.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 61c42d835..05821a37c 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.3.2' + magmaVersion = '0.4.0' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From cdc012735ffd574557c3ce73bf1d634c2e36bbdc Mon Sep 17 00:00:00 2001 From: Napster Date: Tue, 17 Jul 2018 19:11:55 +0200 Subject: [PATCH 08/27] Bump Magma to 0.4.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 05821a37c..65e980592 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.4.0' + magmaVersion = '0.4.4' jdaNasVersion = '1.0.6' jappVersion = '1.2' jdaVersion = '3.6.0_367' From 97f462a0bdcfeff1663edf798389509503254891 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Tue, 7 Aug 2018 18:57:49 +0200 Subject: [PATCH 09/27] Replace Java_Websocket with spring-websocket --- LavalinkServer/build.gradle | 2 +- .../main/java/lavalink/server/Launcher.java | 14 -- .../server/config/WebsocketConfig.java | 42 +++--- .../server/io/HandshakeInterceptorImpl.java | 53 +++++++ .../lavalink/server/io/SocketContext.java | 8 +- .../java/lavalink/server/io/SocketServer.java | 137 +++++------------- .../java/lavalink/server/io/StatsTask.java | 9 +- .../lavalink/server/player/EventEmitter.java | 21 ++- .../java/lavalink/server/player/Player.java | 7 +- build.gradle | 2 +- 10 files changed, 145 insertions(+), 150 deletions(-) create mode 100644 LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index 1f0d7f7ce..dc1b296c6 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -43,7 +43,7 @@ dependencies { compile group: 'com.github.shredder121', name: 'jda-async-packetprovider', version: jappVersion //required by japp compile group: 'org.apache.commons', name: 'commons-lang3', version: commonsLangVersion - compile group: 'org.java-websocket', name: 'Java-WebSocket', version: javaWebSocketVersion + compile group: 'org.springframework', name: 'spring-websocket', version: springWebSocketVersion compile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion compile group: 'io.sentry', name: 'sentry-logback', version: sentryLogbackVersion compile group: 'com.github.oshi', name: 'oshi-core', version: oshiVersion diff --git a/LavalinkServer/src/main/java/lavalink/server/Launcher.java b/LavalinkServer/src/main/java/lavalink/server/Launcher.java index bb71f2c19..5b4ba9a6d 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Launcher.java +++ b/LavalinkServer/src/main/java/lavalink/server/Launcher.java @@ -25,7 +25,6 @@ import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary; import lavalink.server.info.AppInfo; import lavalink.server.info.GitRepoState; -import lavalink.server.io.SocketServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; @@ -33,14 +32,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; -import org.springframework.context.annotation.ComponentScan; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @SpringBootApplication -@ComponentScan public class Launcher { private static final Logger log = LoggerFactory.getLogger(Launcher.class); @@ -74,17 +71,6 @@ public static void main(String[] args) { sa.run(args); } - public Launcher(SocketServer socketServer) { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - log.info("Shutdown hook triggered"); - try { - socketServer.stop(30); - } catch (InterruptedException e) { - log.warn("Interrupted while stopping socket server", e); - } - }, "shutdown hook")); - } - private static String getVersionInfo() { AppInfo appInfo = new AppInfo(); GitRepoState gitRepoState = new GitRepoState(); diff --git a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java index 838619006..105de19fd 100644 --- a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java +++ b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java @@ -1,31 +1,29 @@ package lavalink.server.config; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; +import lavalink.server.io.HandshakeInterceptorImpl; +import lavalink.server.io.SocketServer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -/** - * Created by napster on 05.03.18. - */ -@ConfigurationProperties(prefix = "lavalink.server.ws") -@Component -public class WebsocketConfig { +@Configuration +@EnableWebSocket +public class WebsocketConfig implements WebSocketConfigurer { - private int port = 80; - private String host = "0.0.0.0"; + private final SocketServer server; + private final HandshakeInterceptorImpl handshakeInterceptor; - public int getPort() { - return port; + @Autowired + public WebsocketConfig(SocketServer server, HandshakeInterceptorImpl handshakeInterceptor) { + this.server = server; + this.handshakeInterceptor = handshakeInterceptor; } - public void setPort(int port) { - this.port = port; - } - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(server, "/") + .addInterceptors(handshakeInterceptor); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java new file mode 100644 index 000000000..93f512c4b --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java @@ -0,0 +1,53 @@ +package lavalink.server.io; + +import lavalink.server.config.ServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; +import java.util.Objects; + +@Controller +public class HandshakeInterceptorImpl implements HandshakeInterceptor { + + private static final Logger log = LoggerFactory.getLogger(HandshakeInterceptorImpl.class); + private final ServerConfig serverConfig; + + @Autowired + public HandshakeInterceptorImpl(ServerConfig serverConfig) { + this.serverConfig = serverConfig; + } + + /** + * Checks credentials and sets the Lavalink version header + * + * @return true if authenticated + */ + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Map attributes) { + response.getHeaders().add("Lavalink-Major-Version", "3"); + + String password = request.getHeaders().getFirst("Authorization"); + boolean matches = Objects.equals(password, serverConfig.getPassword()); + + if (!matches) { + log.info("Incoming connection from " + request.getRemoteAddress()); + } else { + log.error("Authentication failed from " + request.getRemoteAddress()); + } + + return matches; + } + + // No action required + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Exception exception) {} +} diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index 82c180b91..a8b9d4a17 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -24,9 +24,9 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import lavalink.server.player.Player; -import org.java_websocket.WebSocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.socket.WebSocketSession; import space.npstr.magma.MagmaApi; import space.npstr.magma.MagmaMember; import space.npstr.magma.Member; @@ -45,7 +45,7 @@ public class SocketContext { private static final Logger log = LoggerFactory.getLogger(SocketContext.class); private final AudioPlayerManager audioPlayerManager; - private final WebSocket socket; + private final WebSocketSession socket; private String userId; private final MagmaApi magmaApi; //guildId <-> Player @@ -53,7 +53,7 @@ public class SocketContext { private ScheduledExecutorService statsExecutor; public final ScheduledExecutorService playerUpdateService; - SocketContext(Supplier audioPlayerManagerSupplier, WebSocket socket, SocketServer socketServer, String userId, + SocketContext(Supplier audioPlayerManagerSupplier, WebSocketSession socket, SocketServer socketServer, String userId, MagmaApi magmaApi) { this.audioPlayerManager = audioPlayerManagerSupplier.get(); this.socket = socket; @@ -81,7 +81,7 @@ Player getPlayer(String guildId) { ); } - public WebSocket getSocket() { + public WebSocketSession getSession() { return socket; } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index c076b4837..096a84327 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -29,129 +29,76 @@ import com.sedmelluq.discord.lavaplayer.track.TrackMarker; import lavalink.server.config.AudioSendFactoryConfiguration; import lavalink.server.config.ServerConfig; -import lavalink.server.config.WebsocketConfig; import lavalink.server.player.Player; import lavalink.server.player.TrackEndMarkerHandler; import lavalink.server.util.Util; import net.dv8tion.jda.core.audio.factory.IAudioSendFactory; -import org.java_websocket.WebSocket; -import org.java_websocket.drafts.Draft; -import org.java_websocket.exceptions.InvalidDataException; -import org.java_websocket.handshake.ClientHandshake; -import org.java_websocket.handshake.ServerHandshakeBuilder; -import org.java_websocket.server.WebSocketServer; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import space.npstr.magma.MagmaApi; -import space.npstr.magma.MagmaMember; -import space.npstr.magma.MagmaServerUpdate; -import space.npstr.magma.Member; -import space.npstr.magma.ServerUpdate; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import space.npstr.magma.*; -import javax.annotation.PostConstruct; import java.io.IOException; -import java.net.InetSocketAddress; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; -import static lavalink.server.io.WSCodes.AUTHORIZATION_REJECTED; -import static lavalink.server.io.WSCodes.INTERNAL_ERROR; - -@Component -public class SocketServer extends WebSocketServer { +@Service +public class SocketServer extends TextWebSocketHandler { private static final Logger log = LoggerFactory.getLogger(SocketServer.class); private final MagmaApi magmaApi = MagmaApi.of(this::getAudioSendFactory); // userId <-> shardCount private final Map shardCounts = new ConcurrentHashMap<>(); - private final Map contextMap = new HashMap<>(); + private final Map contextMap = new HashMap<>(); private final ServerConfig serverConfig; private final Supplier audioPlayerManagerSupplier; private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; private final ConcurrentHashMap sendFactories = new ConcurrentHashMap<>(); - public SocketServer(WebsocketConfig websocketConfig, ServerConfig serverConfig, Supplier audioPlayerManagerSupplier, + public SocketServer(ServerConfig serverConfig, Supplier audioPlayerManagerSupplier, AudioSendFactoryConfiguration audioSendFactoryConfiguration) { - super(new InetSocketAddress(websocketConfig.getHost(), websocketConfig.getPort())); - this.setReuseAddr(true); this.serverConfig = serverConfig; this.audioPlayerManagerSupplier = audioPlayerManagerSupplier; this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; } + @SuppressWarnings("ConstantConditions") @Override - @PostConstruct - public void start() { - super.start(); - } - - @Override - public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, ClientHandshake request) throws InvalidDataException { - ServerHandshakeBuilder builder = super.onWebsocketHandshakeReceivedAsServer(conn, draft, request); - builder.put("Lavalink-Major-Version", "3"); - return builder; - } - - @Override - public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) { - try { - int shardCount = Integer.parseInt(clientHandshake.getFieldValue("Num-Shards")); - String userId = clientHandshake.getFieldValue("User-Id"); - - shardCounts.put(userId, shardCount); - - if (clientHandshake.getFieldValue("Authorization").equals(serverConfig.getPassword())) { - log.info("Connection opened from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); - contextMap.put(webSocket, new SocketContext(audioPlayerManagerSupplier, webSocket, this, userId, magmaApi)); - } else { - log.error("Authentication failed from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); - webSocket.close(AUTHORIZATION_REJECTED, "Authorization rejected"); - } - } catch (Exception e) { - log.error("Error when opening websocket", e); - webSocket.close(INTERNAL_ERROR, e.getMessage()); - } - } + public void afterConnectionEstablished(WebSocketSession session) { + int shardCount = Integer.parseInt(session.getHandshakeHeaders().getFirst("Num-Shards")); + String userId = session.getHandshakeHeaders().getFirst("User-Id"); - @Override - public void onCloseInitiated(WebSocket webSocket, int code, String reason) { - close(webSocket, code, reason); - } + shardCounts.put(userId, shardCount); - @Override - public void onClosing(WebSocket webSocket, int code, String reason, boolean remote) { - close(webSocket, code, reason); + contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId, magmaApi)); } @Override - public void onClose(WebSocket webSocket, int code, String reason, boolean remote) { - close(webSocket, code, reason); - } - - // WebSocketServer has a very questionable attitude towards communicating close events, so we override ALL the closing methods - private void close(WebSocket webSocket, int code, String reason) { - SocketContext context = contextMap.remove(webSocket); + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + SocketContext context = contextMap.remove(session.getId()); if (context != null) { - log.info("Connection closed from {} with protocol {} with reason {} with code {}", - webSocket.getRemoteSocketAddress().toString(), webSocket.getDraft(), reason, code); + log.info("Connection closed from {} -- {}", session.getRemoteAddress(), status); context.shutdown(); } } @Override - public void onMessage(WebSocket webSocket, String s) { - JSONObject json = new JSONObject(s); + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { + JSONObject json = new JSONObject(message.getPayload()); - log.info(s); + log.info(message.getPayload()); - if (webSocket.isClosing()) { - log.error("Ignoring closing websocket: " + webSocket.getRemoteSocketAddress().toString()); + if (!session.isOpen()) { + log.error("Ignoring closing websocket: " + session.getRemoteAddress()); return; } @@ -170,7 +117,7 @@ public void onMessage(WebSocket webSocket, String s) { return; } - SocketContext sktContext = contextMap.get(webSocket); + SocketContext sktContext = contextMap.get(session.getId()); Member member = MagmaMember.builder() .userId(sktContext.getUserId()) .guildId(guildId) @@ -186,7 +133,7 @@ public void onMessage(WebSocket webSocket, String s) { /* Player ops */ case "play": try { - SocketContext ctx = contextMap.get(webSocket); + SocketContext ctx = contextMap.get(session.getId()); Player player = ctx.getPlayer(json.getString("guildId")); AudioTrack track = Util.toAudioTrack(ctx.getAudioPlayerManager(), json.getString("track")); if (json.has("startTime")) { @@ -203,7 +150,7 @@ public void onMessage(WebSocket webSocket, String s) { player.play(track); - SocketContext context = contextMap.get(webSocket); + SocketContext context = contextMap.get(session.getId()); Member m = MagmaMember.builder() .userId(context.getUserId()) @@ -211,31 +158,31 @@ public void onMessage(WebSocket webSocket, String s) { .build(); magmaApi.setSendHandler(m, context.getPlayer(json.getString("guildId"))); - sendPlayerUpdate(webSocket, player); + sendPlayerUpdate(session, player); } catch (IOException e) { throw new RuntimeException(e); } break; case "stop": - Player player = contextMap.get(webSocket).getPlayer(json.getString("guildId")); + Player player = contextMap.get(session.getId()).getPlayer(json.getString("guildId")); player.stop(); break; case "pause": - Player player2 = contextMap.get(webSocket).getPlayer(json.getString("guildId")); + Player player2 = contextMap.get(session.getId()).getPlayer(json.getString("guildId")); player2.setPause(json.getBoolean("pause")); - sendPlayerUpdate(webSocket, player2); + sendPlayerUpdate(session, player2); break; case "seek": - Player player3 = contextMap.get(webSocket).getPlayer(json.getString("guildId")); + Player player3 = contextMap.get(session.getId()).getPlayer(json.getString("guildId")); player3.seekTo(json.getLong("position")); - sendPlayerUpdate(webSocket, player3); + sendPlayerUpdate(session, player3); break; case "volume": - Player player4 = contextMap.get(webSocket).getPlayer(json.getString("guildId")); + Player player4 = contextMap.get(session.getId()).getPlayer(json.getString("guildId")); player4.setVolume(json.getInt("volume")); break; case "destroy": - SocketContext socketContext = contextMap.get(webSocket); + SocketContext socketContext = contextMap.get(session.getId()); Player player5 = socketContext.getPlayers().remove(json.getString("guildId")); if (player5 != null) player5.stop(); Member mem = MagmaMember.builder() @@ -251,23 +198,13 @@ public void onMessage(WebSocket webSocket, String s) { } } - @Override - public void onError(WebSocket webSocket, Exception e) { - log.error("Caught exception in websocket", e); - } - - @Override - public void onStart() { - log.info("Started WS server with port " + getPort()); - } - - public static void sendPlayerUpdate(WebSocket webSocket, Player player) { + public static void sendPlayerUpdate(WebSocketSession session, Player player) throws IOException { JSONObject json = new JSONObject(); json.put("op", "playerUpdate"); json.put("guildId", player.getGuildId()); json.put("state", player.getState()); - webSocket.send(json.toString()); + session.sendMessage(new TextMessage(json.toString())); } Collection getContexts() { diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java index 3f97f3cec..9e280b868 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java @@ -28,11 +28,14 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.socket.TextMessage; import oshi.SystemInfo; import oshi.hardware.HardwareAbstractionLayer; import oshi.software.os.OSProcess; import oshi.software.os.OperatingSystem; +import java.io.IOException; + public class StatsTask implements Runnable { private static final Logger log = LoggerFactory.getLogger(StatsTask.class); @@ -56,7 +59,7 @@ public void run() { } } - public void sendStats() { + private void sendStats() throws IOException { JSONObject out = new JSONObject(); final int[] playersTotal = {0}; @@ -116,14 +119,14 @@ public void sendStats() { out.put("frameStats", frames); } - context.getSocket().send(out.toString()); + context.getSession().sendMessage(new TextMessage(out.toString())); } private double uptime = 0; private double cpuTime = 0; private double getProcessRecentCpuUsage() { - double output = 0d; + double output; HardwareAbstractionLayer hal = si.getHardware(); OperatingSystem os = si.getOperatingSystem(); OSProcess p = os.getProcess(os.getProcessId()); diff --git a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java index 336bf59c0..b0528ac32 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java @@ -33,6 +33,7 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.socket.TextMessage; import java.io.IOException; @@ -61,7 +62,11 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason out.put("reason", endReason.toString()); - linkPlayer.getSocket().getSocket().send(out.toString()); + try { + linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); + } catch (IOException e) { + throw new RuntimeException(e); + } } // These exceptions are already logged by Lavaplayer @@ -79,7 +84,11 @@ public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyExcep out.put("error", exception.getMessage()); - linkPlayer.getSocket().getSocket().send(out.toString()); + try { + linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Override @@ -98,8 +107,12 @@ public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) out.put("thresholdMs", thresholdMs); - linkPlayer.getSocket().getSocket().send(out.toString()); - SocketServer.sendPlayerUpdate(linkPlayer.getSocket().getSocket(), linkPlayer); + try { + linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); + SocketServer.sendPlayerUpdate(linkPlayer.getSocket().getSession(), linkPlayer); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index e6fd9476a..f2c3045e1 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -136,7 +137,11 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason public void onTrackStart(AudioPlayer player, AudioTrack track) { if (myFuture == null || myFuture.isCancelled()) { myFuture = socketContext.playerUpdateService.scheduleAtFixedRate(() -> { - SocketServer.sendPlayerUpdate(socketContext.getSocket(), this); + try { + SocketServer.sendPlayerUpdate(socketContext.getSession(), this); + } catch (IOException e) { + log.error("Failed to send player update after track start", e); + } }, 0, 5, TimeUnit.SECONDS); } } diff --git a/build.gradle b/build.gradle index d30dce972..f473c49f4 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ subprojects { jappVersion = '1.2' springBootVersion = "${springBootVersion}" - javaWebSocketVersion = '1.3.8' + springWebSocketVersion = '5.0.8.RELEASE' logbackVersion = '1.2.3' sentryLogbackVersion = '1.7.0' oshiVersion = '3.5.0' From 68be2ecbe23a29991b7fc321aed185b4bafc193b Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Tue, 7 Aug 2018 19:07:57 +0200 Subject: [PATCH 10/27] Update config example --- LavalinkServer/application.yml.example | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/LavalinkServer/application.yml.example b/LavalinkServer/application.yml.example index f475eeba7..f7152c23d 100644 --- a/LavalinkServer/application.yml.example +++ b/LavalinkServer/application.yml.example @@ -1,4 +1,4 @@ -server: # REST server +server: # REST and WS server port: 2333 address: 0.0.0.0 spring: @@ -7,9 +7,6 @@ spring: lavalink: server: password: "youshallnotpass" - ws: - port: 80 - host: 0.0.0.0 sources: youtube: true bandcamp: true From 87cab71092e48df0e254d280d17951aaa14fbad1 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 18:38:29 +0200 Subject: [PATCH 11/27] Fix auth logic error --- .../main/java/lavalink/server/io/HandshakeInterceptorImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java index 93f512c4b..aa0b80341 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.java @@ -37,7 +37,7 @@ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse res String password = request.getHeaders().getFirst("Authorization"); boolean matches = Objects.equals(password, serverConfig.getPassword()); - if (!matches) { + if (matches) { log.info("Incoming connection from " + request.getRemoteAddress()); } else { log.error("Authentication failed from " + request.getRemoteAddress()); From 98abc658097731e4db6e2b980c7ed94bc93e8cb2 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 18:40:23 +0200 Subject: [PATCH 12/27] Log when we establish connection --- .../src/main/java/lavalink/server/io/SocketServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index 096a84327..b1b612da7 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -80,6 +80,7 @@ public void afterConnectionEstablished(WebSocketSession session) { shardCounts.put(userId, shardCount); contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId, magmaApi)); + log.info("Connection successfully established from " + session.getRemoteAddress()); } @Override From c516504e8b620974e0d56b3233cb2fc3d575704a Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 18:46:19 +0200 Subject: [PATCH 13/27] Fix NumberFormatException --- .../src/main/java/lavalink/server/info/AppInfo.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java index 0143e13de..1d61c257a 100644 --- a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java +++ b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java @@ -36,7 +36,11 @@ public AppInfo() { this.groupId = prop.getProperty("groupId"); this.artifactId = prop.getProperty("artifactId"); this.buildNumber = prop.getProperty("buildNumber"); - this.buildTime = Long.parseLong(prop.getProperty("buildTime")); + long bTime = -1L; + try { + bTime = Long.parseLong(prop.getProperty("buildTime")); + } catch (NumberFormatException ignored) { } + this.buildTime = bTime; } public String getVersion() { From 0156a5a06ad5f3fb3acdfba47986551ad68b801b Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 18:56:32 +0200 Subject: [PATCH 14/27] Fix "null" git date crash --- .../src/main/java/lavalink/server/info/GitRepoState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java index 0c1dcdf01..5753e4d9f 100644 --- a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java +++ b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java @@ -50,7 +50,7 @@ public GitRepoState() { this.commitMessageFull = String.valueOf(properties.getOrDefault("git.commit.message.full", "")); this.commitMessageShort = String.valueOf(properties.getOrDefault("git.commit.message.short", "")); final String time = String.valueOf(properties.get("git.commit.time")); - if (time == null) { + if (time == null || time.equals("null")) { this.commitTime = 0; } else { // https://github.com/n0mer/gradle-git-properties/issues/71 From f3693013736a0b62848e636189ad4a5ad299c153 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 21:08:33 +0200 Subject: [PATCH 15/27] Handle errors in WS handling --- .../src/main/java/lavalink/server/io/SocketServer.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index b1b612da7..bb03ff68e 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -93,7 +93,15 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) } @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + try { + handleTextMessageSafe(session, message); + } catch (Exception e) { + log.error("Exception while handling websocket message", e); + } + } + + private void handleTextMessageSafe(WebSocketSession session, TextMessage message) throws IOException { JSONObject json = new JSONObject(message.getPayload()); log.info(message.getPayload()); From 063a9b3bb1c1ffc3fa8d462865fd973f48dc6436 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Wed, 8 Aug 2018 23:10:29 +0200 Subject: [PATCH 16/27] Fix websocket message sending --- .../java/lavalink/server/io/SocketServer.java | 6 ++-- .../java/lavalink/server/io/StatsTask.java | 4 +-- .../lavalink/server/player/EventEmitter.java | 22 +++---------- .../java/lavalink/server/player/Player.java | 7 +--- .../main/java/lavalink/server/util/Ws.java | 33 +++++++++++++++++++ 5 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 LavalinkServer/src/main/java/lavalink/server/util/Ws.java diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index bb03ff68e..900de47e1 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -32,6 +32,7 @@ import lavalink.server.player.Player; import lavalink.server.player.TrackEndMarkerHandler; import lavalink.server.util.Util; +import lavalink.server.util.Ws; import net.dv8tion.jda.core.audio.factory.IAudioSendFactory; import org.json.JSONObject; import org.slf4j.Logger; @@ -80,6 +81,7 @@ public void afterConnectionEstablished(WebSocketSession session) { shardCounts.put(userId, shardCount); contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId, magmaApi)); + session.setTextMessageSizeLimit(512); log.info("Connection successfully established from " + session.getRemoteAddress()); } @@ -207,13 +209,13 @@ private void handleTextMessageSafe(WebSocketSession session, TextMessage message } } - public static void sendPlayerUpdate(WebSocketSession session, Player player) throws IOException { + public static void sendPlayerUpdate(WebSocketSession session, Player player) { JSONObject json = new JSONObject(); json.put("op", "playerUpdate"); json.put("guildId", player.getGuildId()); json.put("state", player.getState()); - session.sendMessage(new TextMessage(json.toString())); + Ws.send(session, json); } Collection getContexts() { diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java index 9e280b868..d29f489f4 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java @@ -25,10 +25,10 @@ import lavalink.server.Launcher; import lavalink.server.player.AudioLossCounter; import lavalink.server.player.Player; +import lavalink.server.util.Ws; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.socket.TextMessage; import oshi.SystemInfo; import oshi.hardware.HardwareAbstractionLayer; import oshi.software.os.OSProcess; @@ -119,7 +119,7 @@ private void sendStats() throws IOException { out.put("frameStats", frames); } - context.getSession().sendMessage(new TextMessage(out.toString())); + Ws.send(context.getSession(), out); } private double uptime = 0; diff --git a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java index b0528ac32..174422a2d 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java @@ -30,10 +30,10 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import lavalink.server.io.SocketServer; import lavalink.server.util.Util; +import lavalink.server.util.Ws; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.socket.TextMessage; import java.io.IOException; @@ -62,11 +62,7 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason out.put("reason", endReason.toString()); - try { - linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); - } catch (IOException e) { - throw new RuntimeException(e); - } + Ws.send(linkPlayer.getSocket().getSession(), out); } // These exceptions are already logged by Lavaplayer @@ -84,11 +80,7 @@ public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyExcep out.put("error", exception.getMessage()); - try { - linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); - } catch (IOException e) { - throw new RuntimeException(e); - } + Ws.send(linkPlayer.getSocket().getSession(), out); } @Override @@ -107,12 +99,8 @@ public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) out.put("thresholdMs", thresholdMs); - try { - linkPlayer.getSocket().getSession().sendMessage(new TextMessage(out.toString())); - SocketServer.sendPlayerUpdate(linkPlayer.getSocket().getSession(), linkPlayer); - } catch (IOException e) { - throw new RuntimeException(e); - } + Ws.send(linkPlayer.getSocket().getSession(), out); + SocketServer.sendPlayerUpdate(linkPlayer.getSocket().getSession(), linkPlayer); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index f2c3045e1..3555559f6 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -35,7 +35,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -137,11 +136,7 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason public void onTrackStart(AudioPlayer player, AudioTrack track) { if (myFuture == null || myFuture.isCancelled()) { myFuture = socketContext.playerUpdateService.scheduleAtFixedRate(() -> { - try { - SocketServer.sendPlayerUpdate(socketContext.getSession(), this); - } catch (IOException e) { - log.error("Failed to send player update after track start", e); - } + SocketServer.sendPlayerUpdate(socketContext.getSession(), this); }, 0, 5, TimeUnit.SECONDS); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/util/Ws.java b/LavalinkServer/src/main/java/lavalink/server/util/Ws.java new file mode 100644 index 000000000..46d12b34a --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/util/Ws.java @@ -0,0 +1,33 @@ +package lavalink.server.util; + +import io.undertow.websockets.core.WebSocketCallback; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSockets; +import io.undertow.websockets.jsr.UndertowSession; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.standard.StandardWebSocketSession; + +public class Ws { + + private static final Logger log = LoggerFactory.getLogger(Ws.class); + + public static void send(WebSocketSession session, JSONObject json) { + UndertowSession undertowSession = (UndertowSession) ((StandardWebSocketSession) session).getNativeSession(); + WebSockets.sendText(json.toString(), undertowSession.getWebSocketChannel(), + new WebSocketCallback<>() { + @Override + public void complete(WebSocketChannel channel, Void context) { + log.trace("Sent {}", json); + } + + @Override + public void onError(WebSocketChannel channel, Void context, Throwable throwable) { + log.error("Error", throwable); + } + }); + } + +} From 1c8e98ecd579270473d4b649ab28565e35304700 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Sat, 18 Aug 2018 13:07:51 +0200 Subject: [PATCH 17/27] Remove text message size limit --- .../src/main/java/lavalink/server/io/SocketServer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index 900de47e1..43b7ca52b 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -81,7 +81,6 @@ public void afterConnectionEstablished(WebSocketSession session) { shardCounts.put(userId, shardCount); contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId, magmaApi)); - session.setTextMessageSizeLimit(512); log.info("Connection successfully established from " + session.getRemoteAddress()); } @@ -103,7 +102,7 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) } } - private void handleTextMessageSafe(WebSocketSession session, TextMessage message) throws IOException { + private void handleTextMessageSafe(WebSocketSession session, TextMessage message) { JSONObject json = new JSONObject(message.getPayload()); log.info(message.getPayload()); From 642138ef26d69d7783dff5b4b1f82a9f3b679046 Mon Sep 17 00:00:00 2001 From: Shikhir Arora Date: Wed, 5 Sep 2018 00:46:17 -0400 Subject: [PATCH 18/27] Update dependencies (experimental/magma) --- build.gradle | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index f473c49f4..6955d3e08 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - springBootVersion = '2.0.2.RELEASE' - gradleGitVersion = '1.5.1' + springBootVersion = '2.0.4.RELEASE' + gradleGitVersion = '1.5.2' sonarqubeVersion = '2.6.2' } repositories { @@ -47,19 +47,19 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.4.4' + magmaVersion = '0.5.0' jdaNasVersion = '1.0.6' - jappVersion = '1.2' + jappVersion = '1.3' springBootVersion = "${springBootVersion}" springWebSocketVersion = '5.0.8.RELEASE' - logbackVersion = '1.2.3' - sentryLogbackVersion = '1.7.0' - oshiVersion = '3.5.0' - jsonOrgVersion = '20180130' - spotbugsAnnotationsVersion = '3.1.3' - prometheusVersion = '0.4.0' - commonsLangVersion = '3.7' + logbackVersion = '1.3.0-alpha4' + sentryLogbackVersion = '1.7.7' + oshiVersion = '3.8.1' + jsonOrgVersion = '20180813' + spotbugsAnnotationsVersion = '3.1.6' + prometheusVersion = '0.5.0' + commonsLangVersion = '3.8' //@formatter:on } From def53e57e86cee3c4b8f69b12a17e86934eb7e55 Mon Sep 17 00:00:00 2001 From: Shikhir Arora Date: Wed, 5 Sep 2018 04:54:34 -0400 Subject: [PATCH 19/27] Pin logback at 1.2.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6955d3e08..a0d5f05f1 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ subprojects { springBootVersion = "${springBootVersion}" springWebSocketVersion = '5.0.8.RELEASE' - logbackVersion = '1.3.0-alpha4' + logbackVersion = '1.2.3' sentryLogbackVersion = '1.7.7' oshiVersion = '3.8.1' jsonOrgVersion = '20180813' From ab3593ce28139fd6e94ad31167a1ed793ff9de46 Mon Sep 17 00:00:00 2001 From: Frederik Mikkelsen Date: Wed, 12 Sep 2018 11:33:30 +0200 Subject: [PATCH 20/27] Add WebSocketClosedEvent --- LavalinkServer/build.gradle | 2 +- .../lavalink/server/io/SocketContext.java | 34 +++++++++++++++++-- .../java/lavalink/server/io/SocketServer.java | 13 ++++--- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index dc1b296c6..b6348c7c7 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -36,7 +36,7 @@ test { } dependencies { - compile group: 'space.npstr', name: 'Magma', version: magmaVersion + compile 'com.github.Frederikam2:magma:bee4ebfbeb' //compile group: 'space.npstr', name: 'Magma', version: magmaVersion compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion compile group: 'com.sedmelluq', name: 'jda-nas', version: jdaNasVersion diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index a8b9d4a17..6345ce034 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -24,13 +24,19 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import lavalink.server.player.Player; +import lavalink.server.util.Util; +import lavalink.server.util.Ws; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.socket.WebSocketSession; import space.npstr.magma.MagmaApi; import space.npstr.magma.MagmaMember; import space.npstr.magma.Member; +import space.npstr.magma.events.api.MagmaEvent; +import space.npstr.magma.events.api.WebSocketClosedEvent; +import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -38,6 +44,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Supplier; public class SocketContext { @@ -53,12 +60,13 @@ public class SocketContext { private ScheduledExecutorService statsExecutor; public final ScheduledExecutorService playerUpdateService; - SocketContext(Supplier audioPlayerManagerSupplier, WebSocketSession socket, SocketServer socketServer, String userId, - MagmaApi magmaApi) { + SocketContext(Supplier audioPlayerManagerSupplier, WebSocketSession socket, + SocketServer socketServer, String userId) { this.audioPlayerManager = audioPlayerManagerSupplier.get(); this.socket = socket; this.userId = userId; - this.magmaApi = magmaApi; + this.magmaApi = MagmaApi.of(socketServer::getAudioSendFactory); + magmaApi.getEventStream().subscribe(this::handleMagmaEvent); statsExecutor = Executors.newSingleThreadScheduledExecutor(); statsExecutor.scheduleAtFixedRate(new StatsTask(this, socketServer), 0, 1, TimeUnit.MINUTES); @@ -97,6 +105,25 @@ List getPlayingPlayers() { return newList; } + MagmaApi getMagma() { + return magmaApi; + } + + private void handleMagmaEvent(MagmaEvent magmaEvent) { + if (magmaEvent instanceof WebSocketClosedEvent) { + WebSocketClosedEvent event = (WebSocketClosedEvent) magmaEvent; + JSONObject out = new JSONObject(); + out.put("op", "event"); + out.put("type", "WebSocketClosedEvent"); + out.put("guildId", event.getMember().getGuildId()); + out.put("reason", event.getReason()); + out.put("code", event.getCloseCode()); + out.put("byRemote", event.isByRemote()); + + Ws.send(socket, out); + } + } + void shutdown() { log.info("Shutting down " + getPlayingPlayers().size() + " playing players."); statsExecutor.shutdown(); @@ -112,6 +139,7 @@ void shutdown() { }); players.values().forEach(Player::stop); + magmaApi.shutdown(); } public AudioPlayerManager getAudioPlayerManager() { diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index 43b7ca52b..460657232 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -56,7 +56,6 @@ public class SocketServer extends TextWebSocketHandler { private static final Logger log = LoggerFactory.getLogger(SocketServer.class); - private final MagmaApi magmaApi = MagmaApi.of(this::getAudioSendFactory); // userId <-> shardCount private final Map shardCounts = new ConcurrentHashMap<>(); private final Map contextMap = new HashMap<>(); @@ -80,7 +79,7 @@ public void afterConnectionEstablished(WebSocketSession session) { shardCounts.put(userId, shardCount); - contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId, magmaApi)); + contextMap.put(session.getId(), new SocketContext(audioPlayerManagerSupplier, session, this, userId)); log.info("Connection successfully established from " + session.getRemoteAddress()); } @@ -137,7 +136,7 @@ private void handleTextMessageSafe(WebSocketSession session, TextMessage message .endpoint(endpoint) .token(token) .build(); - magmaApi.provideVoiceServerUpdate(member, serverUpdate); + sktContext.getMagma().provideVoiceServerUpdate(member, serverUpdate); break; /* Player ops */ @@ -166,7 +165,7 @@ private void handleTextMessageSafe(WebSocketSession session, TextMessage message .userId(context.getUserId()) .guildId(json.getString("guildId")) .build(); - magmaApi.setSendHandler(m, context.getPlayer(json.getString("guildId"))); + context.getMagma().setSendHandler(m, context.getPlayer(json.getString("guildId"))); sendPlayerUpdate(session, player); } catch (IOException e) { @@ -199,8 +198,8 @@ private void handleTextMessageSafe(WebSocketSession session, TextMessage message .userId(socketContext.getUserId()) .guildId(json.getString("guildId")) .build(); - magmaApi.removeSendHandler(mem); - magmaApi.closeConnection(mem); + socketContext.getMagma().removeSendHandler(mem); + socketContext.getMagma().closeConnection(mem); break; default: log.warn("Unexpected operation: " + json.getString("op")); @@ -221,7 +220,7 @@ Collection getContexts() { return contextMap.values(); } - private IAudioSendFactory getAudioSendFactory(Member member) { + IAudioSendFactory getAudioSendFactory(Member member) { int shardCount = shardCounts.getOrDefault(member.getUserId(), 1); int shardId = Util.getShardFromSnowflake(member.getGuildId(), shardCount); From 4b813447ddc177559d414786e41435ef8c6e5d80 Mon Sep 17 00:00:00 2001 From: Frederik Mikkelsen Date: Wed, 12 Sep 2018 11:33:37 +0200 Subject: [PATCH 21/27] Document WebSocketClosedEvent --- IMPLEMENTATION.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index c0a85cee1..674c496a6 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -173,7 +173,7 @@ Server emitted an event. See the client implementation below. ```json { "op": "event", - ... + "type": "..." } ``` @@ -185,7 +185,7 @@ Server emitted an event. See the client implementation below. * 2. TrackExceptionEvent * 3. TrackStuckEvent *

- * The remaining are caused by the client + * The remaining lavaplayer events are caused by client actions, and are therefore not forwarded via WS. */ private void handleEvent(JSONObject json) throws IOException { LavalinkPlayer player = (LavalinkPlayer) lavalink.getPlayer(json.getString("guildId")); @@ -221,6 +221,22 @@ private void handleEvent(JSONObject json) throws IOException { See also: [AudioTrackEndReason.java](https://github.com/sedmelluq/lavaplayer/blob/master/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java) +Additionally there is also the `WebSocketClosedEvent`, which signals when an audio web socket (to Discord) is closed. +This can happen for various reasons (normal and abnormal), e.g when using an expired voice server update. +4xxx codes are usually bad. +See the [Discord docs](https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes). + +```json +{ + "op": "event", + "type": "WebSocketClosedEvent", + "guildId": "...", + "code": 4006, + "reason": "Your session is no longer valid.", + "byRemote": true +} +``` + ### REST API The REST api is used to resolve audio tracks for use with the `play` op. ``` From b22c6d88cc18dc6368a7987dff15943ba033650d Mon Sep 17 00:00:00 2001 From: Frederik Mikkelsen Date: Thu, 13 Sep 2018 16:23:57 +0200 Subject: [PATCH 22/27] Throw exception when attempting to seek null track --- .../src/main/java/lavalink/server/player/Player.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index 3555559f6..3c7201745 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -75,7 +75,11 @@ public String getGuildId() { } public void seekTo(long position) { - player.getPlayingTrack().setPosition(position); + AudioTrack track = player.getPlayingTrack(); + + if (track == null) throw new RuntimeException("Can't seek when not playing anything"); + + track.setPosition(position); } public void setVolume(int volume) { From faafcd00651afc4416324bad8381d5ea0bfdff4d Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Sat, 29 Sep 2018 18:09:40 +0200 Subject: [PATCH 23/27] Update magma --- LavalinkServer/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index b6348c7c7..726366194 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -36,7 +36,7 @@ test { } dependencies { - compile 'com.github.Frederikam2:magma:bee4ebfbeb' //compile group: 'space.npstr', name: 'Magma', version: magmaVersion + compile 'com.github.Frederikam2:magma:2a1ccc3c78' //compile group: 'space.npstr', name: 'Magma', version: magmaVersion compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion compile group: 'com.sedmelluq', name: 'jda-nas', version: jdaNasVersion From b0d0dcb480a8ca55a0fa50fa7a2c5d23d7784641 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Sat, 29 Sep 2018 18:16:15 +0200 Subject: [PATCH 24/27] Rename missing Magma class names --- .../src/main/java/lavalink/server/io/SocketContext.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index 6345ce034..bfa2a3b43 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -24,7 +24,6 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import lavalink.server.player.Player; -import lavalink.server.util.Util; import lavalink.server.util.Ws; import org.json.JSONObject; import org.slf4j.Logger; @@ -34,9 +33,8 @@ import space.npstr.magma.MagmaMember; import space.npstr.magma.Member; import space.npstr.magma.events.api.MagmaEvent; -import space.npstr.magma.events.api.WebSocketClosedEvent; +import space.npstr.magma.events.api.WebSocketClosed; -import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -44,7 +42,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import java.util.function.Supplier; public class SocketContext { @@ -110,8 +107,8 @@ MagmaApi getMagma() { } private void handleMagmaEvent(MagmaEvent magmaEvent) { - if (magmaEvent instanceof WebSocketClosedEvent) { - WebSocketClosedEvent event = (WebSocketClosedEvent) magmaEvent; + if (magmaEvent instanceof WebSocketClosed) { + WebSocketClosed event = (WebSocketClosed) magmaEvent; JSONObject out = new JSONObject(); out.put("op", "event"); out.put("type", "WebSocketClosedEvent"); From d1d8c27b24a1710a3490c3b2d98291992fac51c1 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Mon, 1 Oct 2018 01:14:29 +0200 Subject: [PATCH 25/27] Update Magma to 0.6.1 --- LavalinkServer/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index 726366194..dc1b296c6 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -36,7 +36,7 @@ test { } dependencies { - compile 'com.github.Frederikam2:magma:2a1ccc3c78' //compile group: 'space.npstr', name: 'Magma', version: magmaVersion + compile group: 'space.npstr', name: 'Magma', version: magmaVersion compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion compile group: 'com.sedmelluq', name: 'jda-nas', version: jdaNasVersion diff --git a/build.gradle b/build.gradle index a0d5f05f1..a373d7fba 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ subprojects { //@formatter:off lavaplayerVersion = '1.3.7' - magmaVersion = '0.5.0' + magmaVersion = '0.6.1' jdaNasVersion = '1.0.6' jappVersion = '1.3' From 313bb707f02ea008d379221ac3d0f54278bec766 Mon Sep 17 00:00:00 2001 From: Frederik Mikkelsen Date: Wed, 3 Oct 2018 14:53:43 +0200 Subject: [PATCH 26/27] No more "Channel is closed" when emitting events (to a closed WS) --- .../src/main/java/lavalink/server/player/EventEmitter.java | 6 +++--- LavalinkServer/src/main/java/lavalink/server/util/Ws.java | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java index 174422a2d..6ac253202 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java @@ -62,7 +62,7 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason out.put("reason", endReason.toString()); - Ws.send(linkPlayer.getSocket().getSession(), out); + Ws.sendIfOpen(linkPlayer.getSocket().getSession(), out); } // These exceptions are already logged by Lavaplayer @@ -80,7 +80,7 @@ public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyExcep out.put("error", exception.getMessage()); - Ws.send(linkPlayer.getSocket().getSession(), out); + Ws.sendIfOpen(linkPlayer.getSocket().getSession(), out); } @Override @@ -99,7 +99,7 @@ public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) out.put("thresholdMs", thresholdMs); - Ws.send(linkPlayer.getSocket().getSession(), out); + Ws.sendIfOpen(linkPlayer.getSocket().getSession(), out); SocketServer.sendPlayerUpdate(linkPlayer.getSocket().getSession(), linkPlayer); } diff --git a/LavalinkServer/src/main/java/lavalink/server/util/Ws.java b/LavalinkServer/src/main/java/lavalink/server/util/Ws.java index 46d12b34a..bede83749 100644 --- a/LavalinkServer/src/main/java/lavalink/server/util/Ws.java +++ b/LavalinkServer/src/main/java/lavalink/server/util/Ws.java @@ -14,6 +14,13 @@ public class Ws { private static final Logger log = LoggerFactory.getLogger(Ws.class); + /** + * Like #send(), but without logging an exception if the socket is closed. + */ + public static void sendIfOpen(WebSocketSession session, JSONObject json) { + if (session.isOpen()) send(session, json); + } + public static void send(WebSocketSession session, JSONObject json) { UndertowSession undertowSession = (UndertowSession) ((StandardWebSocketSession) session).getNativeSession(); WebSockets.sendText(json.toString(), undertowSession.getWebSocketChannel(), From 1b1eb724a280a506afc385a0eaa3c41f0c702660 Mon Sep 17 00:00:00 2001 From: "Frederik Ar. Mikkelsen" Date: Mon, 8 Oct 2018 01:15:42 +0200 Subject: [PATCH 27/27] Update gradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a373d7fba..223af44e8 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { } task wrapper(type: Wrapper) { - gradleVersion = '4.7' + gradleVersion = '4.10.2' //noinspection UnnecessaryQualifiedReference distributionType = Wrapper.DistributionType.ALL } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c8b802289c3a438766657a5e98f20eff03..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49462 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

Gauk6VzRKaGELdj)1bl0ug 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 B@>Bo- diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6a30918e..d76b502e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists