Java tutorial
/* * Copyright 2015-2017 Austin Keener & Michael Ritter & Florian Spie * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.dv8tion.jda.core.requests; import com.neovisionaries.ws.client.*; import gnu.trove.iterator.TLongObjectIterator; import gnu.trove.map.TLongObjectMap; import net.dv8tion.jda.client.entities.impl.JDAClientImpl; import net.dv8tion.jda.client.handle.*; import net.dv8tion.jda.core.AccountType; import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.Permission; import net.dv8tion.jda.core.WebSocketCode; import net.dv8tion.jda.core.audio.hooks.ConnectionListener; import net.dv8tion.jda.core.audio.hooks.ConnectionStatus; import net.dv8tion.jda.core.entities.Guild; import net.dv8tion.jda.core.entities.VoiceChannel; import net.dv8tion.jda.core.entities.impl.GuildImpl; import net.dv8tion.jda.core.entities.impl.JDAImpl; import net.dv8tion.jda.core.events.*; import net.dv8tion.jda.core.handle.*; import net.dv8tion.jda.core.managers.AudioManager; import net.dv8tion.jda.core.managers.impl.AudioManagerImpl; import net.dv8tion.jda.core.managers.impl.PresenceImpl; import net.dv8tion.jda.core.utils.MiscUtil; import net.dv8tion.jda.core.utils.SimpleLog; import net.dv8tion.jda.core.utils.tuple.MutablePair; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.time.OffsetDateTime; import java.util.*; import java.util.zip.DataFormatException; import java.util.zip.Inflater; public class WebSocketClient extends WebSocketAdapter implements WebSocketListener { public static final SimpleLog LOG = SimpleLog.getLog("JDASocket"); public static final int DISCORD_GATEWAY_VERSION = 6; public static final int IDENTIFY_DELAY = 5; protected final JDAImpl api; protected final JDA.ShardInfo shardInfo; protected final Map<String, SocketHandler> handlers = new HashMap<>(); protected final Set<String> cfRays = new HashSet<>(); protected final Set<String> traces = new HashSet<>(); protected WebSocket socket; protected String gatewayUrl = null; protected String sessionId = null; protected volatile Thread keepAliveThread; protected boolean connected; protected volatile boolean chunkingAndSyncing = false; protected boolean sentAuthInfo = false; protected boolean initiating; //cache all events? protected final List<JSONObject> cachedEvents = new LinkedList<>(); protected boolean shouldReconnect = true; protected int reconnectTimeoutS = 2; protected long heartbeatStartTime; //GuildId, <TimeOfNextAttempt, AudioConnection> protected final TLongObjectMap<MutablePair<Long, VoiceChannel>> queuedAudioConnections = MiscUtil.newLongMap(); protected final LinkedList<String> chunkSyncQueue = new LinkedList<>(); protected final LinkedList<String> ratelimitQueue = new LinkedList<>(); protected volatile Thread ratelimitThread = null; protected volatile long ratelimitResetTime; protected volatile int messagesSent; protected volatile boolean printedRateLimitMessage = false; protected boolean firstInit = true; protected boolean processingReady = true; protected boolean handleIdentifyRateLimit = false; public WebSocketClient(JDAImpl api) { this.api = api; this.shardInfo = api.getShardInfo(); this.shouldReconnect = api.isAutoReconnect(); setupHandlers(); setupSendingThread(); connect(); } public Set<String> getCfRays() { return cfRays; } public Set<String> getTraces() { return traces; } protected void updateTraces(JSONArray arr, String type, int opCode) { final String msg = String.format("Received a _trace for %s (OP: %d) with %s", type, opCode, arr); WebSocketClient.LOG.debug(msg); traces.clear(); for (Object o : arr) traces.add(String.valueOf(o)); } public void setAutoReconnect(boolean reconnect) { this.shouldReconnect = reconnect; } public boolean isConnected() { return connected; } public void ready() { if (initiating) { initiating = false; processingReady = false; if (firstInit) { firstInit = false; JDAImpl.LOG.info("Finished Loading!"); if (api.getGuilds().size() >= 2500) //Show large warning when connected to >2500 guilds { JDAImpl.LOG.warn(" __ __ _ ___ _ _ ___ _ _ ___ _ "); JDAImpl.LOG.warn(" \\ \\ / //_\\ | _ \\| \\| ||_ _|| \\| | / __|| |"); JDAImpl.LOG.warn(" \\ \\/\\/ // _ \\ | /| .` | | | | .` || (_ ||_|"); JDAImpl.LOG.warn(" \\_/\\_//_/ \\_\\|_|_\\|_|\\_||___||_|\\_| \\___|(_)"); JDAImpl.LOG.warn("You're running a session with over 2500 connected"); JDAImpl.LOG.warn("guilds. You should shard the connection in order"); JDAImpl.LOG.warn("to split the load or things like resuming"); JDAImpl.LOG.warn("connection might not work as expected."); JDAImpl.LOG.warn("For more info see https://git.io/vrFWP"); } api.getEventManager().handle(new ReadyEvent(api, api.getResponseTotal())); } else { updateAudioManagerReferences(); JDAImpl.LOG.info("Finished (Re)Loading!"); api.getEventManager().handle(new ReconnectedEvent(api, api.getResponseTotal())); } } else { JDAImpl.LOG.info("Successfully resumed Session!"); api.getEventManager().handle(new ResumedEvent(api, api.getResponseTotal())); } api.setStatus(JDA.Status.CONNECTED); LOG.debug("Resending " + cachedEvents.size() + " cached events..."); handle(cachedEvents); LOG.debug("Sending of cached events finished."); cachedEvents.clear(); } public boolean isReady() { return !initiating; } public void handle(List<JSONObject> events) { events.forEach(this::handleEvent); } public void send(String message) { ratelimitQueue.addLast(message); } public void chunkOrSyncRequest(JSONObject request) { chunkSyncQueue.addLast(request.toString()); } private boolean send(String message, boolean skipQueue) { if (!connected) return false; long now = System.currentTimeMillis(); if (this.ratelimitResetTime <= now) { this.messagesSent = 0; this.ratelimitResetTime = now + 60000;//60 seconds this.printedRateLimitMessage = false; } //Allows 115 messages to be sent before limiting. if (this.messagesSent <= 115 || (skipQueue && this.messagesSent <= 119)) //technically we could go to 120, but we aren't going to chance it { LOG.trace("<- " + message); socket.sendText(message); this.messagesSent++; return true; } else { if (!printedRateLimitMessage) { LOG.warn( "Hit the WebSocket RateLimit! If you see this message a lot then you might need to talk to DV8FromTheWorld."); printedRateLimitMessage = true; } return false; } } private void setupSendingThread() { ratelimitThread = new Thread(api.getIdentifierString() + " MainWS-Sending Thread") { @Override public void run() { boolean needRatelimit; boolean attemptedToSend; while (!this.isInterrupted()) { try { //Make sure that we don't send any packets before sending auth info. if (!sentAuthInfo) { Thread.sleep(500); continue; } attemptedToSend = false; needRatelimit = false; MutablePair<Long, VoiceChannel> audioRequest = getNextAudioConnectRequest(); String chunkOrSyncRequest = chunkSyncQueue.peekFirst(); if (chunkOrSyncRequest != null) { needRatelimit = !send(chunkOrSyncRequest, false); if (!needRatelimit) { chunkSyncQueue.removeFirst(); } attemptedToSend = true; } else if (audioRequest != null) { VoiceChannel channel = audioRequest.getRight(); AudioManager audioManager = channel.getGuild().getAudioManager(); JSONObject audioConnectPacket = new JSONObject().put("op", 4).put("d", new JSONObject().put("guild_id", channel.getGuild().getId()) .put("channel_id", channel.getId()) .put("self_mute", audioManager.isSelfMuted()) .put("self_deaf", audioManager.isSelfDeafened())); needRatelimit = !send(audioConnectPacket.toString(), false); if (!needRatelimit) { //If we didn't get RateLimited, Next allowed connect request will be 2 seconds from now audioRequest.setLeft(System.currentTimeMillis() + 2000); //If the connection is already established, then the packet just sent // was a move channel packet, thus, it won't trigger the removal from // queuedAudioConnections in VoiceServerUpdateHandler because we won't receive // that event just for a move, so we remove it here after successfully sending. if (audioManager.isConnected()) { queuedAudioConnections.remove(channel.getGuild().getIdLong()); } } attemptedToSend = true; } else { String message = ratelimitQueue.peekFirst(); if (message != null) { needRatelimit = !send(message, false); if (!needRatelimit) { ratelimitQueue.removeFirst(); } attemptedToSend = true; } } if (needRatelimit || !attemptedToSend) { Thread.sleep(1000); } } catch (InterruptedException ignored) { LOG.debug( "Main WS send thread interrupted. Most likely JDA is disconnecting the websocket."); break; } } } }; ratelimitThread.start(); } public void close() { socket.sendClose(1000); } public void close(int code) { socket.sendClose(code); } /* ### Start Internal methods ### */ protected void connect() { if (api.getStatus() != JDA.Status.ATTEMPTING_TO_RECONNECT) api.setStatus(JDA.Status.CONNECTING_TO_WEBSOCKET); initiating = true; try { if (gatewayUrl == null) { gatewayUrl = getGateway(); if (gatewayUrl == null) { throw new RuntimeException("Could not fetch WS-Gateway!"); } } socket = api.getWebSocketFactory().createSocket(gatewayUrl).addHeader("Accept-Encoding", "gzip") .addListener(this); socket.connect(); } catch (IOException | WebSocketException e) { //Completely fail here. We couldn't make the connection. throw new RuntimeException(e); } } protected String getGateway() { try { RestAction<String> gateway = new RestAction<String>(api, Route.Misc.GATEWAY.compile()) { @Override protected void handleResponse(Response response, Request<String> request) { try { if (response.isOk()) request.onSuccess(response.getObject().getString("url")); else request.onFailure(new Exception("Failed to get gateway url")); } catch (Exception e) { request.onFailure(e); } } }; return gateway.complete(false) + "?encoding=json&v=" + DISCORD_GATEWAY_VERSION; } catch (Exception ex) { return null; } } @Override public void onConnected(WebSocket websocket, Map<String, List<String>> headers) { api.setStatus(JDA.Status.LOADING_SUBSYSTEMS); LOG.info("Connected to WebSocket"); if (headers.containsKey("cf-ray")) { List<String> values = headers.get("cf-ray"); if (!values.isEmpty()) { String ray = values.get(0); cfRays.add(ray); LOG.debug("Received new CF-RAY: " + ray); } } connected = true; reconnectTimeoutS = 2; messagesSent = 0; ratelimitResetTime = System.currentTimeMillis() + 60000; if (sessionId == null) sendIdentify(); else sendResume(); } @Override public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) { sentAuthInfo = false; connected = false; api.setStatus(JDA.Status.DISCONNECTED); CloseCode closeCode = null; int rawCloseCode = 1000; if (keepAliveThread != null) { keepAliveThread.interrupt(); keepAliveThread = null; } if (serverCloseFrame != null) { rawCloseCode = serverCloseFrame.getCloseCode(); closeCode = CloseCode.from(rawCloseCode); if (closeCode == CloseCode.RATE_LIMITED) LOG.fatal( "WebSocket connection closed due to ratelimit! Sent more than 120 websocket messages in under 60 seconds!"); else if (closeCode != null) LOG.debug("WebSocket connection closed with code " + closeCode); else LOG.warn("WebSocket connection closed with unknown meaning for close-code " + rawCloseCode); } // null is considered -reconnectable- as we do not know the close-code meaning boolean closeCodeIsReconnect = closeCode == null || closeCode.isReconnect(); if (!shouldReconnect || !closeCodeIsReconnect) //we should not reconnect { if (ratelimitThread != null) ratelimitThread.interrupt(); if (!closeCodeIsReconnect) { //it is possible that a token can be invalidated due to too many reconnect attempts //or that a bot reached a new shard minimum and cannot connect with the current settings //if that is the case we have to drop our connection and inform the user with a fatal error message LOG.fatal("WebSocket connection was closed and cannot be recovered due to identification issues"); LOG.fatal(closeCode); } api.setStatus(JDA.Status.SHUTDOWN); api.getEventManager().handle(new ShutdownEvent(api, OffsetDateTime.now(), rawCloseCode)); } else { if (rawCloseCode == 1000) invalidate(); // 1000 means our session is dropped so we cannot resume api.getEventManager().handle(new DisconnectEvent(api, serverCloseFrame, clientCloseFrame, closedByServer, OffsetDateTime.now())); reconnect(); } } protected void reconnect() { if (!handleIdentifyRateLimit) LOG.warn("Got disconnected from WebSocket (Internet?!)... Attempting to reconnect in " + reconnectTimeoutS + "s"); while (shouldReconnect) { try { api.setStatus(JDA.Status.WAITING_TO_RECONNECT); if (handleIdentifyRateLimit) { handleIdentifyRateLimit = false; LOG.fatal("Encountered IDENTIFY (OP " + WebSocketCode.IDENTIFY + ") Rate Limit! " + "Waiting " + IDENTIFY_DELAY + " seconds before trying again!"); Thread.sleep(IDENTIFY_DELAY * 1000); } else { Thread.sleep(reconnectTimeoutS * 1000); } api.setStatus(JDA.Status.ATTEMPTING_TO_RECONNECT); } catch (InterruptedException ignored) { } LOG.warn("Attempting to reconnect!"); try { connect(); break; } catch (RuntimeException ex) { reconnectTimeoutS = Math.min(reconnectTimeoutS << 1, api.getMaxReconnectDelay()); LOG.warn("Reconnect failed! Next attempt in " + reconnectTimeoutS + "s"); } } } @Override public void onTextMessage(WebSocket websocket, String message) { JSONObject content = new JSONObject(message); int opCode = content.getInt("op"); if (!content.isNull("s")) { api.setResponseTotal(content.getInt("s")); } switch (opCode) { case WebSocketCode.DISPATCH: handleEvent(content); break; case WebSocketCode.HEARTBEAT: LOG.debug("Got Keep-Alive request (OP 1). Sending response..."); sendKeepAlive(); break; case WebSocketCode.RECONNECT: LOG.debug("Got Reconnect request (OP 7). Closing connection now..."); close(); break; case WebSocketCode.INVALIDATE_SESSION: LOG.debug("Got Invalidate request (OP 9). Invalidating..."); final boolean isResume = content.getBoolean("d"); // When d: true we can wait a bit and then try to resume again //sending 4000 to not drop session int closeCode = isResume ? 4000 : 1000; if (isResume) LOG.debug("Session can be recovered... Closing and sending new RESUME request"); else if (!handleIdentifyRateLimit) // this can also mean we got rate limited in IDENTIFY (no need to invalidate then) invalidate(); close(closeCode); break; case WebSocketCode.HELLO: LOG.debug("Got HELLO packet (OP 10). Initializing keep-alive."); final JSONObject data = content.getJSONObject("d"); setupKeepAlive(data.getLong("heartbeat_interval")); if (!data.isNull("_trace")) updateTraces(data.getJSONArray("_trace"), "HELLO", WebSocketCode.HELLO); break; case WebSocketCode.HEARTBEAT_ACK: LOG.trace("Got Heartbeat Ack (OP 11)."); api.setPing(System.currentTimeMillis() - heartbeatStartTime); break; default: LOG.debug("Got unknown op-code: " + opCode + " with content: " + message); } } protected void setupKeepAlive(long timeout) { keepAliveThread = new Thread(() -> { while (connected) { try { sendKeepAlive(); //Sleep for heartbeat interval Thread.sleep(timeout); } catch (InterruptedException ex) { //connection got cut... terminating keepAliveThread break; } } }); keepAliveThread.setName(api.getIdentifierString() + " MainWS-KeepAlive Thread"); keepAliveThread.setPriority(Thread.MAX_PRIORITY); keepAliveThread.setDaemon(true); keepAliveThread.start(); } protected void sendKeepAlive() { String keepAlivePacket = new JSONObject().put("op", WebSocketCode.HEARTBEAT) .put("d", api.getResponseTotal()).toString(); if (!send(keepAlivePacket, true)) ratelimitQueue.addLast(keepAlivePacket); heartbeatStartTime = System.currentTimeMillis(); } protected void sendIdentify() { LOG.debug("Sending Identify-packet..."); PresenceImpl presenceObj = (PresenceImpl) api.getPresence(); JSONObject connectionProperties = new JSONObject().put("$os", System.getProperty("os.name")) .put("$browser", "JDA").put("$device", "JDA").put("$referring_domain", "").put("$referrer", ""); JSONObject payload = new JSONObject().put("presence", presenceObj.getFullPresence()) .put("token", getToken()).put("properties", connectionProperties).put("v", DISCORD_GATEWAY_VERSION) .put("large_threshold", 250) //Used to make the READY event be given // as compressed binary data when over a certain size. TY @ShadowLordAlpha .put("compress", true); JSONObject identify = new JSONObject().put("op", WebSocketCode.IDENTIFY).put("d", payload); if (shardInfo != null) { payload.put("shard", new JSONArray().put(shardInfo.getShardId()).put(shardInfo.getShardTotal())); } send(identify.toString(), true); handleIdentifyRateLimit = true; sentAuthInfo = true; } protected void sendResume() { LOG.debug("Sending Resume-packet..."); JSONObject resume = new JSONObject().put("op", WebSocketCode.RESUME).put("d", new JSONObject() .put("session_id", sessionId).put("token", getToken()).put("seq", api.getResponseTotal())); send(resume.toString(), true); sentAuthInfo = true; } protected void invalidate() { sessionId = null; chunkingAndSyncing = false; sentAuthInfo = false; api.getTextChannelMap().clear(); api.getVoiceChannelMap().clear(); api.getGuildMap().clear(); api.getUserMap().clear(); api.getPrivateChannelMap().clear(); api.getFakeUserMap().clear(); api.getFakePrivateChannelMap().clear(); api.getEntityBuilder().clearCache(); api.getEventCache().clear(); api.getGuildLock().clear(); this.<ReadyHandler>getHandler("READY").clearCache(); this.<GuildMembersChunkHandler>getHandler("GUILD_MEMBERS_CHUNK").clearCache(); if (api.getAccountType() == AccountType.CLIENT) { JDAClientImpl client = (JDAClientImpl) api.asClient(); client.getRelationshipMap().clear(); client.getGroupMap().clear(); client.getCallUserMap().clear(); } } protected void updateAudioManagerReferences() { if (api.getAudioManagerMap().size() > 0) LOG.trace("Updating AudioManager references"); api.getAudioManagerMap().transformValues(mng -> { final long guildId = mng.getGuild().getIdLong(); ConnectionListener listener = mng.getConnectionListener(); GuildImpl guild = (GuildImpl) api.getGuildById(guildId); if (guild == null) { //We no longer have access to the guild that this audio manager was for. Set the value to null. queuedAudioConnections.remove(guildId); if (listener != null) listener.onStatusChange(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); return null; } else { AudioManagerImpl newMng = new AudioManagerImpl(guild); newMng.setSelfMuted(mng.isSelfMuted()); newMng.setSelfDeafened(mng.isSelfDeafened()); newMng.setQueueTimeout(mng.getConnectTimeout()); newMng.setSendingHandler(mng.getSendingHandler()); newMng.setReceivingHandler(mng.getReceiveHandler()); newMng.setConnectionListener(mng.getConnectionListener()); newMng.setAutoReconnect(mng.isAutoReconnect()); if (mng.isConnected() || mng.isAttemptingToConnect()) { String channelId = mng.isConnected() ? mng.getConnectedChannel().getId() : mng.getQueuedAudioConnection().getId(); VoiceChannel channel = api.getVoiceChannelById(channelId); if (channel != null) { if (mng.isConnected()) newMng.setConnectedChannel(channel); else newMng.setQueuedAudioConnection(channel); } else { //The voice channel is not cached. It was probably deleted. queuedAudioConnections.remove(guildId); if (listener != null) listener.onStatusChange(ConnectionStatus.DISCONNECTED_CHANNEL_DELETED); } } } return mng; }); api.getAudioManagerMap().valueCollection().removeIf(Objects::isNull); } protected String getToken() { if (api.getAccountType() == AccountType.BOT) return api.getToken().substring("Bot ".length()); return api.getToken(); } protected void handleEvent(JSONObject raw) { String type = raw.getString("t"); long responseTotal = api.getResponseTotal(); if (type.equals("GUILD_MEMBER_ADD")) ((GuildMembersChunkHandler) getHandler("GUILD_MEMBERS_CHUNK")) .modifyExpectedGuildMember(raw.getJSONObject("d").getLong("guild_id"), 1); if (type.equals("GUILD_MEMBER_REMOVE")) ((GuildMembersChunkHandler) getHandler("GUILD_MEMBERS_CHUNK")) .modifyExpectedGuildMember(raw.getJSONObject("d").getLong("guild_id"), -1); //If initiating, only allows READY, RESUMED, GUILD_MEMBERS_CHUNK, GUILD_SYNC, and GUILD_CREATE through. // If we are currently chunking, we don't allow GUILD_CREATE through anymore. if (initiating && !(type.equals("READY") || type.equals("GUILD_MEMBERS_CHUNK") || type.equals("RESUMED") || type.equals("GUILD_SYNC") || (!chunkingAndSyncing && type.equals("GUILD_CREATE")))) { //If we are currently GuildStreaming, and we get a GUILD_DELETE informing us that a Guild is unavailable // convert it to a GUILD_CREATE for handling. JSONObject content = raw.getJSONObject("d"); if (!chunkingAndSyncing && type.equals("GUILD_DELETE") && content.has("unavailable") && content.getBoolean("unavailable")) { type = "GUILD_CREATE"; raw.put("t", "GUILD_CREATE").put("jda-field", "This event was originally a GUILD_DELETE but was converted to GUILD_CREATE for WS init Guild streaming"); } else { LOG.debug("Caching " + type + " event during init!"); cachedEvents.add(raw); return; } } // // // Needs special handling due to content of "d" being an array // if(type.equals("PRESENCE_REPLACE")) // { // JSONArray presences = raw.getJSONArray("d"); // LOG.trace(String.format("%s -> %s", type, presences.toString())); // PresenceUpdateHandler handler = new PresenceUpdateHandler(api, responseTotal); // for (int i = 0; i < presences.length(); i++) // { // JSONObject presence = presences.getJSONObject(i); // handler.handle(presence); // } // return; // } JSONObject content = raw.getJSONObject("d"); LOG.trace(String.format("%s -> %s", type, content.toString())); try { switch (type) { //INIT types case "READY": //LOG.debug(String.format("%s -> %s", type, content.toString())); already logged on trace level processingReady = true; handleIdentifyRateLimit = false; sessionId = content.getString("session_id"); if (!content.isNull("_trace")) updateTraces(content.getJSONArray("_trace"), "READY", WebSocketCode.DISPATCH); handlers.get("READY").handle(responseTotal, raw); break; case "RESUMED": if (!processingReady) { initiating = false; ready(); } if (!content.isNull("_trace")) updateTraces(content.getJSONArray("_trace"), "RESUMED", WebSocketCode.DISPATCH); break; default: SocketHandler handler = handlers.get(type); if (handler != null) handler.handle(responseTotal, raw); else LOG.debug("Unrecognized event:\n" + raw); } } catch (JSONException ex) { LOG.warn("Got an unexpected Json-parse error. Please redirect following message to the devs:\n\t" + ex.getMessage() + "\n\t" + type + " -> " + content); LOG.log(ex); } catch (Exception ex) { LOG.log(ex); } } @Override public void onBinaryMessage(WebSocket websocket, byte[] binary) throws UnsupportedEncodingException, DataFormatException { //Thanks to ShadowLordAlpha for code and debugging. //Get the compressed message and inflate it StringBuilder builder = new StringBuilder(); Inflater decompresser = new Inflater(); decompresser.setInput(binary, 0, binary.length); byte[] result = new byte[128]; while (!decompresser.finished()) { int resultLength = decompresser.inflate(result); builder.append(new String(result, 0, resultLength, "UTF-8")); } decompresser.end(); // send the inflated message to the TextMessage method onTextMessage(websocket, builder.toString()); } @Override public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws Exception { handleCallbackError(websocket, cause); } @Override public void handleCallbackError(WebSocket websocket, Throwable cause) { api.getEventManager().handle(new ExceptionEvent(api, cause, false)); } @Override public void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws Exception { String identifier = api.getIdentifierString(); switch (threadType) { case CONNECT_THREAD: thread.setName(identifier + " MainWS-ConnectThread"); break; case FINISH_THREAD: thread.setName(identifier + " MainWS-FinishThread"); break; case READING_THREAD: thread.setName(identifier + " MainWS-ReadThread"); break; case WRITING_THREAD: thread.setName(identifier + " MainWS-WriteThread"); break; default: thread.setName(identifier + " MainWS-" + threadType); } } public void setChunkingAndSyncing(boolean active) { chunkingAndSyncing = active; } public void queueAudioConnect(VoiceChannel channel) { queuedAudioConnections.put(channel.getGuild().getIdLong(), new MutablePair<>(System.currentTimeMillis(), channel)); } public TLongObjectMap<MutablePair<Long, VoiceChannel>> getQueuedAudioConnectionMap() { return queuedAudioConnections; } protected MutablePair<Long, VoiceChannel> getNextAudioConnectRequest() { //Don't try to setup audio connections before JDA has finished loading. if (!isReady()) return null; synchronized (queuedAudioConnections) { long now = System.currentTimeMillis(); TLongObjectIterator<MutablePair<Long, VoiceChannel>> it = queuedAudioConnections.iterator(); while (it.hasNext()) { it.advance(); MutablePair<Long, VoiceChannel> audioRequest = it.value(); if (audioRequest.getLeft() < now) { VoiceChannel channel = audioRequest.getRight(); Guild guild = channel.getGuild(); ConnectionListener listener = guild.getAudioManager().getConnectionListener(); Guild connGuild = api.getGuildById(guild.getIdLong()); if (connGuild == null) { it.remove(); if (listener != null) listener.onStatusChange(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); continue; } VoiceChannel connChannel = connGuild.getVoiceChannelById(channel.getIdLong()); if (connChannel == null) { it.remove(); if (listener != null) listener.onStatusChange(ConnectionStatus.DISCONNECTED_CHANNEL_DELETED); continue; } if (!connGuild.getSelfMember().hasPermission(connChannel, Permission.VOICE_CONNECT)) { it.remove(); if (listener != null) listener.onStatusChange(ConnectionStatus.DISCONNECTED_LOST_PERMISSION); continue; } return audioRequest; } } } return null; } public Map<String, SocketHandler> getHandlers() { return handlers; } public <T> T getHandler(String type) { return (T) handlers.get(type); } private void setupHandlers() { handlers.put("CHANNEL_CREATE", new ChannelCreateHandler(api)); handlers.put("CHANNEL_DELETE", new ChannelDeleteHandler(api)); handlers.put("CHANNEL_UPDATE", new ChannelUpdateHandler(api)); handlers.put("GUILD_BAN_ADD", new GuildBanHandler(api, true)); handlers.put("GUILD_BAN_REMOVE", new GuildBanHandler(api, false)); handlers.put("GUILD_CREATE", new GuildCreateHandler(api)); handlers.put("GUILD_DELETE", new GuildDeleteHandler(api)); handlers.put("GUILD_EMOJIS_UPDATE", new GuildEmojisUpdateHandler(api)); handlers.put("GUILD_MEMBER_ADD", new GuildMemberAddHandler(api)); handlers.put("GUILD_MEMBER_REMOVE", new GuildMemberRemoveHandler(api)); handlers.put("GUILD_MEMBER_UPDATE", new GuildMemberUpdateHandler(api)); handlers.put("GUILD_MEMBERS_CHUNK", new GuildMembersChunkHandler(api)); handlers.put("GUILD_ROLE_CREATE", new GuildRoleCreateHandler(api)); handlers.put("GUILD_ROLE_DELETE", new GuildRoleDeleteHandler(api)); handlers.put("GUILD_ROLE_UPDATE", new GuildRoleUpdateHandler(api)); handlers.put("GUILD_SYNC", new GuildSyncHandler(api)); handlers.put("GUILD_UPDATE", new GuildUpdateHandler(api)); handlers.put("MESSAGE_CREATE", new MessageCreateHandler(api)); handlers.put("MESSAGE_DELETE", new MessageDeleteHandler(api)); handlers.put("MESSAGE_DELETE_BULK", new MessageBulkDeleteHandler(api)); handlers.put("MESSAGE_REACTION_ADD", new MessageReactionHandler(api, true)); handlers.put("MESSAGE_REACTION_REMOVE", new MessageReactionHandler(api, false)); handlers.put("MESSAGE_REACTION_REMOVE_ALL", new MessageReactionBulkRemoveHandler(api)); handlers.put("MESSAGE_UPDATE", new MessageUpdateHandler(api)); handlers.put("PRESENCE_UPDATE", new PresenceUpdateHandler(api)); handlers.put("READY", new ReadyHandler(api)); handlers.put("TYPING_START", new TypingStartHandler(api)); handlers.put("USER_UPDATE", new UserUpdateHandler(api)); handlers.put("VOICE_SERVER_UPDATE", new VoiceServerUpdateHandler(api)); handlers.put("VOICE_STATE_UPDATE", new VoiceStateUpdateHandler(api)); if (api.getAccountType() == AccountType.CLIENT) { handlers.put("CALL_CREATE", new CallCreateHandler(api)); handlers.put("CALL_DELETE", new CallDeleteHandler(api)); handlers.put("CALL_UPDATE", new CallUpdateHandler(api)); handlers.put("CHANNEL_RECIPIENT_ADD", new ChannelRecipientAddHandler(api)); handlers.put("CHANNEL_RECIPIENT_REMOVE", new ChannelRecipientRemoveHandler(api)); handlers.put("RELATIONSHIP_ADD", new RelationshipAddHandler(api)); handlers.put("RELATIONSHIP_REMOVE", new RelationshipRemoveHandler(api)); handlers.put("MESSAGE_ACK", new SocketHandler(api) { @Override protected Long handleInternally(JSONObject content) { return null; } }); } } }