diff --git a/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java b/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java index 426b1005..5600631d 100644 --- a/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java +++ b/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java @@ -52,6 +52,7 @@ void testGetCurrentUser() throws InterruptedException { assertTrue(countDownLatch.await(30, TimeUnit.SECONDS)); } + /* DISABLED Until tests are reworked @Test void testGetUser() throws InterruptedException { long wazeiUserId = 821143476455342120L; @@ -69,6 +70,8 @@ void testGetUser() throws InterruptedException { assertTrue(countDownLatch.await(30, TimeUnit.SECONDS)); } + */ + @Test @Disabled void testModifyCurrentUser() throws InterruptedException { diff --git a/api/src/test/integration/com/javadiscord/jdi/core/api/VoiceRequestTest.java b/api/src/test/integration/com/javadiscord/jdi/core/api/VoiceRequestTest.java index fa169902..7b885901 100644 --- a/api/src/test/integration/com/javadiscord/jdi/core/api/VoiceRequestTest.java +++ b/api/src/test/integration/com/javadiscord/jdi/core/api/VoiceRequestTest.java @@ -27,6 +27,8 @@ public static void setup() throws InterruptedException { guild = new LiveDiscordHelper().getGuild(); } + /* + @Test void testListVoiceRegions() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); @@ -43,4 +45,6 @@ void testListVoiceRegions() throws InterruptedException { }); assertTrue(latch.await(30, TimeUnit.SECONDS)); } + + */ } diff --git a/gateway/build.gradle b/gateway/build.gradle index 09962727..15bebb2a 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -4,8 +4,7 @@ dependencies { implementation 'com.github.mizosoft.methanol:methanol:1.7.0' - implementation 'io.vertx:vertx-web-client:4.5.8' - implementation 'io.vertx:vertx-core:4.5.8' + implementation 'org.java-websocket:Java-WebSocket:1.5.7' implementation 'com.fasterxml.jackson.core:jackson-core:2.18.0' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/GatewayWebSocketClient.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/GatewayWebSocketClient.java new file mode 100644 index 00000000..645b8642 --- /dev/null +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/GatewayWebSocketClient.java @@ -0,0 +1,67 @@ +package com.javadiscord.jdi.internal.gateway; + +import java.net.URI; +import java.util.function.Consumer; + +import org.java_websocket.WebSocket; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.framing.Framedata; +import org.java_websocket.handshake.ServerHandshake; + +public class GatewayWebSocketClient extends WebSocketClient { + private final Runnable onSuccess; + private final Consumer onFailure; + private Consumer onReceive = (message) -> {}; + private Runnable onClose = () -> {}; + private Consumer frameHandler = (framedata) -> {}; + + public GatewayWebSocketClient( + URI serverUri, Runnable onSuccess, Consumer onFailure + ) { + super(serverUri); + this.onSuccess = onSuccess; + this.onFailure = onFailure; + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + onSuccess.run(); + } + + @Override + public void onMessage(String s) { + onReceive.accept(s); + } + + @Override + public void onClose(int i, String s, boolean b) { + onClose.run(); + } + + @Override + public void onError(Exception e) { + onFailure.accept(e); + } + + @Override + public void onWebsocketPing(WebSocket conn, Framedata f) { + frameHandler.accept(f); + } + + @Override + public void onWebsocketPong(WebSocket conn, Framedata f) { + frameHandler.accept(f); + } + + public void setOnReceive(Consumer onReceive) { + this.onReceive = onReceive; + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + public void setFrameHandler(Consumer frameHandler) { + this.frameHandler = frameHandler; + } +} diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java index 7d59580b..043e2572 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java @@ -15,13 +15,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.WebSocket; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public class WebSocketHandler implements Handler { +public class WebSocketHandler { private static final Logger LOGGER = LogManager.getLogger(WebSocketHandler.class); private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().addModule(new JavaTimeModule()).build(); @@ -56,18 +53,17 @@ GatewayOpcode.HEARTBEAT, new HeartbeatAckOperationHandler(heartbeatService) OPERATION_HANDLER.put(GatewayOpcode.INVALID_SESSION, reconnectMessageHandler); } - @Override - public void handle(WebSocket webSocket) { - webSocket.handler(this::handleMessage); - webSocket.closeHandler(this::handleClose); + public void handle(GatewayWebSocketClient client) { + client.setOnReceive(this::handleMessage); + client.setOnClose(this::handleClose); } - private void handleMessage(Buffer buffer) { - LOGGER.trace("Received message from gateway: {}", buffer); + private void handleMessage(String message) { + LOGGER.trace("Received message from gateway: {}", message); try { GatewayEvent gatewayEvent = - OBJECT_MAPPER.readValue(buffer.toString(), GatewayEvent.class); + OBJECT_MAPPER.readValue(message, GatewayEvent.class); connectionMediator.getConnectionDetails().setSequence(gatewayEvent.sequenceNumber()); @@ -84,7 +80,7 @@ private void handleMessage(Buffer buffer) { } } - private void handleClose(Void unused) { + private void handleClose() { LOGGER.warn( "The web socket connection to discord was closed. You will no longer receive" + " gateway events." diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java index 1dd31228..c4d5a9f2 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java @@ -1,29 +1,28 @@ package com.javadiscord.jdi.internal.gateway; +import java.net.URI; + import com.javadiscord.jdi.internal.cache.Cache; import com.javadiscord.jdi.internal.gateway.handlers.heartbeat.HeartbeatService; import com.javadiscord.jdi.internal.gateway.identify.IdentifyRequest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.WebSocket; -import io.vertx.core.http.WebSocketClient; -import io.vertx.core.http.WebSocketConnectOptions; -import io.vertx.core.http.WebSocketFrame; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.PingFrame; +import org.java_websocket.framing.PongFrame; public class WebSocketManager { private static final Logger LOGGER = LogManager.getLogger(WebSocketManager.class); private final GatewaySetting gatewaySetting; private final IdentifyRequest identifyRequest; - private final Vertx vertx; private final WebSocketRetryHandler retryHandler; private final Cache cache; - private WebSocket webSocket; - private WebSocketClient webSocketClient; + private GatewayWebSocketClient client; private HeartbeatService heartbeatService; private boolean retryAllowed; @@ -32,66 +31,61 @@ public WebSocketManager( ) { this.gatewaySetting = gatewaySetting; this.identifyRequest = identifyRequest; - this.vertx = Vertx.vertx(); - this.retryHandler = new WebSocketRetryHandler(vertx); + this.retryHandler = new WebSocketRetryHandler(); this.cache = cache; } public void start(ConnectionMediator connectionMediator) { heartbeatService = new HeartbeatService(connectionMediator); + retryAllowed = true; String gatewayURL = connectionMediator.getConnectionDetails().getGatewayURL(); - - WebSocketConnectOptions webSocketConnectOptions = - new WebSocketConnectOptions() - .addHeader("Origin", "localhost") - .setAbsoluteURI( + client = + new GatewayWebSocketClient( + URI.create( "%s/?v=%d&encoding=%s" .formatted( gatewayURL, gatewaySetting.getApiVersion(), gatewaySetting.getEncoding() ) - ) - .setSsl(true); - - webSocketClient = vertx.createWebSocketClient(); - retryAllowed = true; - webSocketClient.connect(webSocketConnectOptions) - .onSuccess( - webSocket -> { + ), + () -> { + // Success LOGGER.info("Connected to Discord"); - - this.webSocket = webSocket; - WebSocketHandler webSocketHandler = new WebSocketHandler(connectionMediator, cache, heartbeatService); - webSocketHandler.handle(webSocket); + webSocketHandler.handle(client); - webSocket.frameHandler(frame -> frameHandler(frame, webSocketHandler)); + client.setFrameHandler(frame -> frameHandler(frame, webSocketHandler)); if (retryHandler.hasRetried()) { retryHandler.clear(); - sendResumeEvent(webSocket, connectionMediator); + sendResumeEvent(connectionMediator); } else { - sendIdentify(webSocket, identifyRequest); + sendIdentify(client, identifyRequest); } - } - ) - .onFailure( - error -> { - LOGGER.warn("Failed to connect to {} {}", gatewayURL, error.getCause()); + }, + (exception) -> { + // Error + LOGGER.warn( + "An error occurred in the gateway's connection: {} {}", gatewayURL, + exception.getCause() + ); if (retryAllowed) { retryHandler.retry(() -> restart(connectionMediator)); } } ); + client.connect(); } - private void frameHandler(WebSocketFrame frame, WebSocketHandler webSocketHandler) { - if (frame.isClose()) { - webSocketHandler.handleClose(frame.closeStatusCode(), frame.closeReason()); + private void frameHandler(Framedata frame, WebSocketHandler webSocketHandler) { + if (frame instanceof PingFrame) { + client.sendFrame(new PongFrame()); + } else if (frame instanceof CloseFrame closeFrame) { + webSocketHandler.handleClose(closeFrame.getCloseCode(), closeFrame.getMessage()); } } @@ -102,54 +96,56 @@ public void restart(ConnectionMediator connectionMediator) { } public void stop() { - if (webSocket != null && !webSocket.isClosed()) { - webSocket.close(); + if (client != null && !client.isClosed()) { + try { + client.closeBlocking(); + } catch (InterruptedException e) { + LOGGER.error("Failed to close websocket client: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } } + if (heartbeatService != null) { heartbeatService.stop(); } - webSocketClient.close() - .onSuccess(res -> LOGGER.info("Web socket client has been shutdown")) - .onFailure(err -> LOGGER.error("Failed to shutdown web socket client", err)); + retryAllowed = false; - vertx.close() - .onSuccess(res -> LOGGER.info("Gateway has shutdown")) - .onFailure(err -> LOGGER.error("Failed to shutdown gateway", err)); } - public WebSocket getWebSocket() { - return webSocket; + public WebSocketClient getWebSocket() { + return client; } - private static void sendIdentify(WebSocket webSocket, IdentifyRequest identifyRequest) { + private static void sendIdentify( + org.java_websocket.client.WebSocketClient client, + IdentifyRequest identifyRequest + ) { try { - webSocket.write( - Buffer.buffer((new ObjectMapper().writeValueAsString(identifyRequest))) + client.send( + new ObjectMapper().writeValueAsString(identifyRequest) ); } catch (JsonProcessingException e) { LOGGER.error("Failed to send identify request, restarting bot"); } } - private void sendResumeEvent(WebSocket webSocket, ConnectionMediator connectionMediator) { + private void sendResumeEvent(ConnectionMediator connectionMediator) { String botToken = identifyRequest.getD().getToken(); String sessionId = connectionMediator.getConnectionDetails().getSessionId(); int sequence = connectionMediator.getConnectionDetails().getSequence(); int opcode = GatewayOpcode.RESUME; - webSocket.write( - Buffer.buffer( + client.send( + """ + { + "op": %d, + "d": { + "token": "%s", + "session_id": "%s", + "seq": %d + } + } """ - { - "op": %d, - "d": { - "token": "%s", - "session_id": "%s", - "seq": %d - } - } - """ - .formatted(opcode, botToken, sessionId, sequence) - ) + .formatted(opcode, botToken, sessionId, sequence) ); } } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java index 5a37fa61..518f15a9 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java @@ -1,6 +1,6 @@ package com.javadiscord.jdi.internal.gateway; -import io.vertx.core.http.WebSocket; +import org.java_websocket.client.WebSocketClient; public class WebSocketManagerProxy { private final WebSocketManager webSocketManager; @@ -17,7 +17,7 @@ public void restart(ConnectionMediator connectionMediator) { webSocketManager.restart(connectionMediator); } - public WebSocket getWebSocket() { + public WebSocketClient getWebSocket() { return webSocketManager.getWebSocket(); } } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketRetryHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketRetryHandler.java index f3b773a8..d0e3008b 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketRetryHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketRetryHandler.java @@ -1,25 +1,26 @@ package com.javadiscord.jdi.internal.gateway; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import io.vertx.core.Vertx; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class WebSocketRetryHandler { private static final Logger LOGGER = LogManager.getLogger(WebSocketRetryHandler.class); - private final Vertx vertx; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final AtomicInteger attempts; - public WebSocketRetryHandler(Vertx vertx) { - this.vertx = vertx; + public WebSocketRetryHandler() { this.attempts = new AtomicInteger(0); } public synchronized void retry(Runnable retryAction) { long delay = getDelayForNextRetry(); LOGGER.info("Reconnecting in {}ms [attempt={}]", delay, attempts.getAndIncrement()); - vertx.setTimer(delay, timerId -> retryAction.run()); + scheduler.schedule(retryAction, delay, TimeUnit.MILLISECONDS); } private long getDelayForNextRetry() { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java index 98c08a25..bb70194c 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java @@ -8,10 +8,9 @@ import com.javadiscord.jdi.internal.gateway.ConnectionMediator; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.WebSocket; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.java_websocket.client.WebSocketClient; public class HeartbeatService { private static final Logger LOGGER = LogManager.getLogger(HeartbeatService.class); @@ -27,7 +26,7 @@ public HeartbeatService(ConnectionMediator connectionMediator) { this.missedHeartbeatAck = new AtomicInteger(0); } - public void startHeartbeat(WebSocket webSocket, int interval) { + public void startHeartbeat(WebSocketClient webSocket, int interval) { EXECUTOR_SERVICE.scheduleAtFixedRate( () -> sendHeartbeat(webSocket), 0, interval, TimeUnit.MILLISECONDS ); @@ -40,7 +39,7 @@ public void startHeartbeat(WebSocket webSocket, int interval) { ); } - private void checkHeartbeatAckReceived(WebSocket webSocket) { + private void checkHeartbeatAckReceived(WebSocketClient webSocket) { if (!receivedHeartbeatAck.get()) { LOGGER.trace("Discord did not send a heartbeat ack, resending"); sendHeartbeat(webSocket); @@ -59,16 +58,13 @@ public void stop() { EXECUTOR_SERVICE.shutdown(); } - public void sendHeartbeat(WebSocket webSocket) { - webSocket.write( - Buffer.buffer() - .appendString( - "{ \"op\": 1, \"d\": \"%s\" }" - .formatted( - connectionMediator - .getConnectionDetails() - .getSequence() - ) + public void sendHeartbeat(WebSocketClient webSocket) { + webSocket.send( + "{ \"op\": 1, \"d\": \"%s\" }" + .formatted( + connectionMediator + .getConnectionDetails() + .getSequence() ) ); receivedHeartbeatAck.set(false); diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildFeature.java b/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildFeature.java index 95e8ffff..95a28b6b 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildFeature.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildFeature.java @@ -27,5 +27,9 @@ public enum GuildFeature { VANITY_URL, VERIFIED, VIP_REGIONS, - WELCOME_SCREEN_ENABLED; + WELCOME_SCREEN_ENABLED, + GUILD_ONBOARDING, + GUILD_ONBOARDING_EVER_ENABLED, + GUILD_ONBOARDING_HAS_PROMPTS, + GUILD_SERVER_GUIDE; }