de.btobastian.javacord.ImplDiscordAPI.java Source code

Java tutorial

Introduction

Here is the source code for de.btobastian.javacord.ImplDiscordAPI.java

Source

/*
 * Copyright (C) 2017 Bastian Oppermann
 * 
 * This file is part of Javacord.
 * 
 * Javacord is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser general Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 * 
 * Javacord is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
 */
package de.btobastian.javacord;

import com.google.common.io.BaseEncoding;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import de.btobastian.javacord.entities.*;
import de.btobastian.javacord.entities.impl.ImplInvite;
import de.btobastian.javacord.entities.impl.ImplServer;
import de.btobastian.javacord.entities.impl.ImplUser;
import de.btobastian.javacord.entities.message.Message;
import de.btobastian.javacord.entities.message.MessageHistory;
import de.btobastian.javacord.entities.message.impl.ImplMessageHistory;
import de.btobastian.javacord.entities.permissions.Permissions;
import de.btobastian.javacord.entities.permissions.PermissionsBuilder;
import de.btobastian.javacord.entities.permissions.impl.ImplPermissionsBuilder;
import de.btobastian.javacord.entities.permissions.impl.ImplRole;
import de.btobastian.javacord.exceptions.BadResponseException;
import de.btobastian.javacord.exceptions.NotSupportedForBotsException;
import de.btobastian.javacord.exceptions.PermissionsException;
import de.btobastian.javacord.exceptions.RateLimitedException;
import de.btobastian.javacord.listener.Listener;
import de.btobastian.javacord.listener.server.ServerJoinListener;
import de.btobastian.javacord.listener.user.UserChangeNameListener;
import de.btobastian.javacord.utils.DiscordWebsocketAdapter;
import de.btobastian.javacord.utils.LoggerUtil;
import de.btobastian.javacord.utils.ThreadPool;
import de.btobastian.javacord.utils.ratelimits.RateLimitManager;
import de.btobastian.javacord.utils.ratelimits.RateLimitType;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * The implementation of {@link DiscordAPI}.
 */
public class ImplDiscordAPI implements DiscordAPI {

    /**
     * The logger of this class.
     */
    private static final Logger logger = LoggerUtil.getLogger(ImplDiscordAPI.class);

    private final ThreadPool pool;

    private String email = null;
    private String password = null;
    private String token = null;
    private String game = null;
    private String streamingUrl = null;
    private boolean idle = false;

    private boolean autoReconnect = true;

    private boolean waitForServersOnStartup = true;

    private User you = null;

    private volatile int messageCacheSize = 200;

    private DiscordWebsocketAdapter socketAdapter = null;

    private RateLimitManager rateLimitManager = new RateLimitManager();

    private final ConcurrentHashMap<String, Server> servers = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();

    private final ArrayList<Message> messages = new ArrayList<>();

    private final ConcurrentHashMap<Class<?>, List<Listener>> listeners = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, SettableFuture<Server>> waitingForListener = new ConcurrentHashMap<>();

    // let the garbage collector delete old histories
    private final Set<MessageHistory> messageHistories = Collections
            .newSetFromMap(new WeakHashMap<MessageHistory, Boolean>());

    private final Object listenerLock = new Object();
    private final ServerJoinListener listener = new ServerJoinListener() {
        @Override
        public void onServerJoin(DiscordAPI api, Server server) {
            synchronized (listenerLock) { // be sure to read the guild id before trying to check for waiting listeners
                SettableFuture<Server> future = waitingForListener.get(server.getId());
                if (future != null) {
                    logger.debug("Joined or created server {}. We were waiting for this server!", server);
                    waitingForListener.remove(server.getId());
                    future.set(server);
                }
            }
        }
    };

    // a set with all unavailable servers
    private final Set<String> unavailableServers = new HashSet<>();

    /**
     * Creates a new instance of this class.
     *
     * @param pool The used pool of the library.
     */
    public ImplDiscordAPI(ThreadPool pool) {
        this.pool = pool;
    }

    @Override
    public void connect(FutureCallback<DiscordAPI> callback) {
        final DiscordAPI api = this;
        Futures.addCallback(pool.getListeningExecutorService().submit(new Callable<DiscordAPI>() {
            @Override
            public DiscordAPI call() throws Exception {
                connectBlocking();
                return api;
            }
        }), callback);
    }

    @Override
    public void connectBlocking() {
        if (token == null || !checkTokenBlocking(token)) {
            if (email == null || password == null) {
                throw new IllegalArgumentException(
                        "No valid token provided AND missing email or password. Connecting not possible!");
            }
            token = requestTokenBlocking();
        }
        String gateway = requestGatewayBlocking();
        socketAdapter = new DiscordWebsocketAdapter(this, gateway);
        try {
            if (!socketAdapter.isReady().get()) {
                throw new IllegalStateException("Socket closed before ready packet was received!");
            }
        } catch (InterruptedException | ExecutionException e) {
            logger.warn("Something went wrong while connecting. Please contact the developer!", e);
            throw new IllegalStateException("Could not figure out if ready or not. Please contact the developer!");
        }
    }

    @Override
    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public void setGame(String game) {
        setGame(game, null);
    }

    @Override
    public void setGame(String game, String streamingUrl) {
        this.game = game;
        this.streamingUrl = streamingUrl;
        try {
            if (socketAdapter != null && socketAdapter.isReady().isDone() && socketAdapter.isReady().get()) {
                socketAdapter.updateStatus();
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getGame() {
        return game;
    }

    @Override
    public String getStreamingUrl() {
        return streamingUrl;
    }

    @Override
    public Server getServerById(String id) {
        return servers.get(id);
    }

    @Override
    public Collection<Server> getServers() {
        return Collections.unmodifiableCollection(servers.values());
    }

    @Override
    public Collection<Channel> getChannels() {
        Collection<Channel> channels = new ArrayList<>();
        for (Server server : getServers()) {
            channels.addAll(server.getChannels());
        }
        return Collections.unmodifiableCollection(channels);
    }

    @Override
    public Channel getChannelById(String id) {
        Iterator<Server> serverIterator = getServers().iterator();
        while (serverIterator.hasNext()) {
            Channel channel = serverIterator.next().getChannelById(id);
            if (channel != null) {
                return channel;
            }
        }
        return null;
    }

    @Override
    public Collection<VoiceChannel> getVoiceChannels() {
        Collection<VoiceChannel> channels = new ArrayList<>();
        for (Server server : getServers()) {
            channels.addAll(server.getVoiceChannels());
        }
        return Collections.unmodifiableCollection(channels);
    }

    @Override
    public VoiceChannel getVoiceChannelById(String id) {
        Iterator<Server> serverIterator = getServers().iterator();
        while (serverIterator.hasNext()) {
            VoiceChannel channel = serverIterator.next().getVoiceChannelById(id);
            if (channel != null) {
                return channel;
            }
        }
        return null;
    }

    @Override
    public Future<User> getUserById(final String id) {
        User user = users.get(id);
        if (user != null) {
            return Futures.immediateFuture(user);
        }
        return getThreadPool().getListeningExecutorService().submit(new Callable<User>() {
            @Override
            public User call() throws Exception {
                logger.debug("Trying request/find user with id {} who isn't cached", id);
                User user = null;
                Iterator<Server> serverIterator = getServers().iterator();
                while (serverIterator.hasNext()) {
                    Server server = serverIterator.next();
                    HttpResponse<JsonNode> response = Unirest
                            .get("https://discordapp.com/api/guilds/" + server.getId() + "/members/" + id)
                            .header("authorization", token).asJson();
                    // user does not exist
                    if (response.getStatus() < 200 || response.getStatus() > 299) {
                        continue;
                    }
                    user = getOrCreateUser(response.getBody().getObject().getJSONObject("user"));
                    // add user to server
                    ((ImplServer) server).addMember(user);
                    // assign user roles
                    if (response.getBody().getObject().has("roles")) {
                        JSONArray roleIds = response.getBody().getObject().getJSONArray("roles");
                        for (int i = 0; i < roleIds.length(); i++) {
                            // add user to the role
                            ((ImplRole) server.getRoleById(roleIds.getString(i))).addUserNoUpdate(user);
                        }
                    }
                }
                if (user != null) {
                    logger.debug("Found user {} with id {}", user, id);
                } else {
                    logger.debug("No user with id {} was found", id);
                }
                return user;
            }
        });
    }

    @Override
    public User getCachedUserById(String id) {
        return users.get(id);
    }

    @Override
    public Collection<User> getUsers() {
        return Collections.unmodifiableCollection(users.values());
    }

    @Override
    public void registerListener(Listener listener) {
        for (Class<?> implementedInterface : TypeToken.of(listener.getClass()).getTypes().interfaces().rawTypes()) {
            if (Listener.class.isAssignableFrom(implementedInterface)) {
                List<Listener> listenersList = listeners.get(implementedInterface);
                if (listenersList == null) {
                    listenersList = new ArrayList<>();
                    listeners.put(implementedInterface, listenersList);
                }
                synchronized (listenersList) {
                    listenersList.add(listener);
                }
            }
        }
    }

    @Override
    public Message getMessageById(String id) {
        synchronized (messages) {
            for (Message message : messages) {
                if (message.getId().equals(id)) {
                    return message;
                }
            }
        }
        synchronized (messageHistories) {
            for (MessageHistory history : messageHistories) {
                history.getMessageById(id);
            }
        }
        return null;
    }

    @Override
    public ThreadPool getThreadPool() {
        return pool;
    }

    @Override
    public void setIdle(boolean idle) {
        this.idle = idle;
        try {
            if (socketAdapter != null && socketAdapter.isReady().isDone() && socketAdapter.isReady().get()) {
                socketAdapter.updateStatus();
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean isIdle() {
        return idle;
    }

    @Override
    public String getToken() {
        return token;
    }

    @Override
    public void setToken(String token, boolean bot) {
        this.token = bot ? "Bot " + token : token;
    }

    @Override
    public boolean checkTokenBlocking(String token) {
        try {
            // only the last 0-9 digits of the token should be visible.
            // We don't want someone being able to login to an account by reading the logs.
            logger.debug("Checking token {}", token.replaceAll(".{10}", "**********"));
            HttpResponse<JsonNode> response = Unirest.get("https://discordapp.com/api/users/@me/guilds")
                    .header("authorization", token).asJson();
            if (response.getStatus() < 200 || response.getStatus() > 299) {
                logger.debug("Checked token {} (valid: {})", token.replaceAll(".{10}", "**********"), false);
                return false;
            }
            logger.debug("Checked token {} (valid: {})", token.replaceAll(".{10}", "**********"), true);
            return true;
        } catch (UnirestException e) {
            return false;
        }
    }

    @Override
    public Future<Server> acceptInvite(String inviteCode) {
        return acceptInvite(inviteCode, null);
    }

    @Override
    public Future<Server> acceptInvite(final String inviteCode, FutureCallback<Server> callback) {
        if (getYourself().isBot()) {
            throw new NotSupportedForBotsException();
        }
        ListenableFuture<Server> future = getThreadPool().getListeningExecutorService()
                .submit(new Callable<Server>() {
                    @Override
                    public Server call() throws Exception {
                        logger.debug("Trying to accept invite (code: {})", inviteCode);
                        final SettableFuture<Server> settableFuture;
                        synchronized (listenerLock) {
                            HttpResponse<JsonNode> response = Unirest
                                    .post("https://discordapp.com/api/invite/" + inviteCode)
                                    .header("authorization", token).asJson();
                            checkResponse(response);
                            String guildId = response.getBody().getObject().getJSONObject("guild").getString("id");
                            if (getServerById(guildId) != null) {
                                throw new IllegalStateException("Already member of this server!");
                            }
                            logger.info(
                                    "Accepted invite and waiting for listener to be called (code: {}, server id: {})",
                                    inviteCode, guildId);
                            settableFuture = SettableFuture.create();
                            waitingForListener.put(guildId, settableFuture);
                        }
                        return settableFuture.get();
                    }
                });
        if (callback != null) {
            Futures.addCallback(future, callback);
        }
        return future;
    }

    @Override
    public Future<Server> createServer(String name) {
        return createServer(name, Region.US_WEST, null, null);
    }

    @Override
    public Future<Server> createServer(String name, FutureCallback<Server> callback) {
        return createServer(name, Region.US_WEST, null, callback);
    }

    @Override
    public Future<Server> createServer(String name, Region region) {
        return createServer(name, region, null, null);
    }

    @Override
    public Future<Server> createServer(String name, Region region, FutureCallback<Server> callback) {
        return createServer(name, region, null, callback);
    }

    @Override
    public Future<Server> createServer(String name, BufferedImage icon) {
        return createServer(name, Region.US_WEST, icon, null);
    }

    @Override
    public Future<Server> createServer(String name, BufferedImage icon, FutureCallback<Server> callback) {
        return createServer(name, Region.US_WEST, icon, callback);
    }

    @Override
    public Future<Server> createServer(String name, Region region, BufferedImage icon) {
        return createServer(name, region, icon, null);
    }

    @Override
    public Future<Server> createServer(final String name, final Region region, final BufferedImage icon,
            FutureCallback<Server> callback) {
        ListenableFuture<Server> future = getThreadPool().getListeningExecutorService()
                .submit(new Callable<Server>() {
                    @Override
                    public Server call() throws Exception {
                        logger.debug("Trying to create server (name: {}, region: {}, icon: {}", name,
                                region == null ? "null" : region.getKey(), icon != null);
                        if (name == null || name.length() < 2 || name.length() > 100) {
                            throw new IllegalArgumentException("Name must be 2-100 characters long!");
                        }
                        JSONObject params = new JSONObject();
                        if (icon != null) {
                            if (icon.getHeight() != 128 || icon.getWidth() != 128) {
                                throw new IllegalArgumentException("Icon must be 128*128px!");
                            }
                            ByteArrayOutputStream os = new ByteArrayOutputStream();
                            ImageIO.write(icon, "jpg", os);
                            params.put("icon",
                                    "data:image/jpg;base64," + BaseEncoding.base64().encode(os.toByteArray()));
                        }
                        params.put("name", name);
                        params.put("region", region == null ? Region.US_WEST.getKey() : region.getKey());
                        final SettableFuture<Server> settableFuture;
                        synchronized (listenerLock) {
                            HttpResponse<JsonNode> response = Unirest.post("https://discordapp.com/api/guilds")
                                    .header("authorization", token).header("Content-Type", "application/json")
                                    .body(params.toString()).asJson();
                            checkResponse(response);
                            String guildId = response.getBody().getObject().getString("id");
                            logger.info(
                                    "Created server and waiting for listener to be called "
                                            + "(name: {}, region: {}, icon: {}, server id: {})",
                                    name, region == null ? "null" : region.getKey(), icon != null, guildId);
                            settableFuture = SettableFuture.create();
                            waitingForListener.put(guildId, settableFuture);
                        }
                        return settableFuture.get();
                    }
                });
        if (callback != null) {
            Futures.addCallback(future, callback);
        }
        return future;
    }

    @Override
    public User getYourself() {
        return you;
    }

    @Override
    public Future<Void> updateUsername(String newUsername) {
        return updateProfile(newUsername, null, null, null);
    }

    @Override
    public Future<Void> updateEmail(String newEmail) {
        return updateProfile(null, newEmail, null, null);
    }

    @Override
    public Future<Void> updatePassword(String newPassword) {
        return updateProfile(null, null, newPassword, null);
    }

    @Override
    public Future<Void> updateAvatar(BufferedImage newAvatar) {
        return updateProfile(null, null, null, newAvatar);
    }

    @Override
    public Future<Void> updateProfile(final String newUsername, String newEmail, final String newPassword,
            final BufferedImage newAvatar) {
        logger.debug("Trying to update profile (username: {}, email: {}, password: {}, change avatar: {}",
                newUsername, email, newPassword == null ? "null" : newPassword.replaceAll(".", "*"),
                newAvatar != null);
        String avatarString = getYourself().getAvatarId();
        if (newAvatar != null) {
            try {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                ImageIO.write(newAvatar, "jpg", os);
                avatarString = "data:image/jpg;base64," + BaseEncoding.base64().encode(os.toByteArray());
            } catch (IOException ignored) {
            }
        }
        final JSONObject params = new JSONObject()
                .put("username", newUsername == null ? getYourself().getName() : newUsername)
                .put("avatar", avatarString == null ? JSONObject.NULL : avatarString);
        if (email != null && password != null) { // do not exist in bot accounts
            params.put("email", newEmail == null ? email : newEmail).put("password", password);
        }
        if (newPassword != null) {
            params.put("new_password", newPassword);
        }
        return getThreadPool().getExecutorService().submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                HttpResponse<JsonNode> response = Unirest.patch("https://discordapp.com/api/users/@me")
                        .header("authorization", token).header("Content-Type", "application/json")
                        .body(params.toString()).asJson();
                checkResponse(response);
                logger.info("Updated profile (username: {}, email: {}, password: {}, change avatar: {}",
                        newUsername, email, newPassword == null ? "null" : newPassword.replaceAll(".", "*"),
                        newAvatar != null);
                ((ImplUser) getYourself()).setAvatarId(response.getBody().getObject().getString("avatar"));
                if (response.getBody().getObject().has("email")) {
                    setEmail(response.getBody().getObject().getString("email"));
                }
                setToken(response.getBody().getObject().getString("token"), token.startsWith("Bot "));
                final String oldName = getYourself().getName();
                ((ImplUser) getYourself()).setName(response.getBody().getObject().getString("username"));
                if (newPassword != null) {
                    password = newPassword;
                }

                if (!getYourself().getName().equals(oldName)) {
                    getThreadPool().getSingleThreadExecutorService("listeners").submit(new Runnable() {
                        @Override
                        public void run() {
                            List<UserChangeNameListener> listeners = getListeners(UserChangeNameListener.class);
                            synchronized (listeners) {
                                for (UserChangeNameListener listener : listeners) {
                                    listener.onUserChangeName(ImplDiscordAPI.this, getYourself(), oldName);
                                }
                            }
                        }
                    });
                }
                return null;
            }
        });
    }

    @Override
    public Future<Invite> parseInvite(String invite) {
        return parseInvite(invite, null);
    }

    @Override
    public Future<Invite> parseInvite(final String invite, FutureCallback<Invite> callback) {
        final String inviteCode = invite.replace("https://discord.gg/", "").replace("http://discord.gg/", "");
        ListenableFuture<Invite> future = getThreadPool().getListeningExecutorService()
                .submit(new Callable<Invite>() {
                    @Override
                    public Invite call() throws Exception {
                        logger.debug("Trying to parse invite {} (parsed code: {})", invite, inviteCode);
                        HttpResponse<JsonNode> response = Unirest
                                .get("https://discordapp.com/api/invite/" + inviteCode)
                                .header("authorization", token).asJson();
                        checkResponse(response);
                        logger.debug("Parsed invite {} (parsed code: {})", invite, inviteCode);
                        return new ImplInvite(ImplDiscordAPI.this, response.getBody().getObject());
                    }
                });
        if (callback != null) {
            Futures.addCallback(future, callback);
        }
        return future;
    }

    @Override
    public Future<Void> deleteInvite(final String inviteCode) {
        return getThreadPool().getExecutorService().submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                logger.debug("Trying to delete invite {}", inviteCode);
                HttpResponse<JsonNode> response = Unirest.delete("https://discordapp.com/api/invite/" + inviteCode)
                        .header("authorization", token).asJson();
                checkResponse(response);
                logger.info("Deleted invite {}", inviteCode);
                return null;
            }
        });
    }

    @Override
    public void setMessageCacheSize(int size) {
        this.messageCacheSize = size < 0 ? 0 : size;
        synchronized (messages) {
            while (messages.size() > messageCacheSize) {
                messages.remove(0);
            }
        }
    }

    @Override
    public int getMessageCacheSize() {
        return messageCacheSize;
    }

    @Override
    public PermissionsBuilder getPermissionsBuilder() {
        return new ImplPermissionsBuilder();
    }

    @Override
    public PermissionsBuilder getPermissionsBuilder(Permissions permissions) {
        return new ImplPermissionsBuilder(permissions);
    }

    @Override
    public void setAutoReconnect(boolean autoReconnect) {
        this.autoReconnect = autoReconnect;
    }

    @Override
    public boolean isAutoReconnectEnabled() {
        return autoReconnect;
    }

    @Override
    public RateLimitManager getRateLimitManager() {
        return rateLimitManager;
    }

    @Override
    public void setWaitForServersOnStartup(boolean wait) {
        this.waitForServersOnStartup = wait;
    }

    @Override
    public boolean isWaitingForServersOnStartup() {
        return waitForServersOnStartup;
    }

    @Override
    public void disconnect() {
        if (socketAdapter != null) {
            socketAdapter.getWebSocket().sendClose(1000);
        }
    }

    /**
     * Gets a list with all unavailable servers.
     *
     * @return A list with all unavailable servers.
     */
    public Set<String> getUnavailableServers() {
        return unavailableServers;
    }

    /**
     * Sets yourself.
     *
     * @param user You.
     */
    public void setYourself(User user) {
        this.you = user;
    }

    /**
     * Gets or creates a user based on the given data.
     *
     * @param data The JSONObject containing the data.
     * @return The user.
     */
    public User getOrCreateUser(JSONObject data) {
        String id = data.getString("id");
        User user = users.get(id);
        if (user == null) {
            if (!data.has("username")) {
                return null;
            }
            user = new ImplUser(data, this);
        }
        return user;
    }

    /**
     * Gets the map which contains all known servers.
     *
     * @return The map which contains all known servers.
     */
    public ConcurrentHashMap<String, Server> getServerMap() {
        return servers;
    }

    /**
     * Gets the map which contains all known users.
     *
     * @return The map which contains all known users.
     */
    public ConcurrentHashMap<String, User> getUserMap() {
        return users;
    }

    /**
     * Gets the used socket adapter.
     *
     * @return The socket adapter.
     */
    public DiscordWebsocketAdapter getSocketAdapter() {
        return socketAdapter;
    }

    /**
     * Requests a new token.
     *
     * @return The requested token.
     */
    public String requestTokenBlocking() {
        try {
            logger.debug("Trying to request token (email: {}, password: {})", email, password.replaceAll(".", "*"));
            HttpResponse<JsonNode> response = Unirest.post("https://discordapp.com/api/auth/login")
                    .header("User-Agent", Javacord.USER_AGENT).header("Content-Type", "application/json")
                    .body(new JSONObject().put("email", email).put("password", password).toString()).asJson();
            JSONObject jsonResponse = response.getBody().getObject();
            if (response.getStatus() == 400) {
                throw new IllegalArgumentException("400 Bad request! Maybe wrong email or password? StatusText: "
                        + response.getStatusText() + "; Body: " + response.getBody());
            }
            if (response.getStatus() < 200 || response.getStatus() > 299) {
                throw new IllegalStateException("Received http status code " + response.getStatus()
                        + " with message " + response.getStatusText() + " and body " + response.getBody());
            }
            if (jsonResponse.has("password") || jsonResponse.has("email")) {
                throw new IllegalArgumentException("Wrong email or password!");
            }
            String token = jsonResponse.getString("token");
            logger.debug("Requested token {} (email: {}, password: {})", token.replaceAll(".{10}", "**********"),
                    email, password.replaceAll(".", "*"));
            return token;
        } catch (UnirestException e) {
            logger.warn("Couldn't request token (email: {}, password: {}). Please contact the developer!", email,
                    password.replaceAll(".", "*"), e);
            return null;
        }
    }

    /**
     * Requests the gateway.
     *
     * @return The requested gateway.
     */
    public String requestGatewayBlocking() {
        try {
            logger.debug("Requesting gateway (token: {})", token.replaceAll(".{10}", "**********"));
            HttpResponse<JsonNode> response = Unirest.get("https://discordapp.com/api/gateway")
                    .header("authorization", token).asJson();
            if (response.getStatus() == 401) {
                throw new IllegalStateException("Cannot request gateway! Invalid token?");
            }
            if (response.getStatus() < 200 || response.getStatus() > 299) {
                throw new IllegalStateException("Received http status code " + response.getStatus()
                        + " with message " + response.getStatusText() + " and body " + response.getBody());
            }
            String gateway = response.getBody().getObject().getString("url");
            logger.debug("Requested gateway {} (token: {})", gateway, token.replaceAll(".{10}", "**********"));
            return gateway;
        } catch (UnirestException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Gets a list with all registers listeners of the given class.
     *
     * @param <T> The type of the listener.
     * @param listenerClass The type of the listener.
     * @return A list with all registers listeners of the given type.
     */
    public <T extends Listener> List<T> getListeners(Class<T> listenerClass) {
        List<T> listenersList = (List<T>) listeners.get(listenerClass);
        return listenersList == null ? new ArrayList<T>() : listenersList;
    }

    /**
     * Gets a list with all registers listeners of the given generic type.
     * This method hasn't the extra listenerClass-parameter of {@link #getListeners(Class)} but is a little bot slower.
     *
     * @param <T> The type of the listener.
     * @return A list with all registers listeners of the given type.
     */
    public <T extends Listener> List<T> getListeners() {
        for (List<Listener> list : listeners.values()) {
            try {
                return (List<T>) list;
            } catch (ClassCastException ignored) {
            }
        }
        return new ArrayList<>();
    }

    /**
     * Adds a message to the message cache.
     *
     * @param message The message to add.
     */
    public void addMessage(Message message) {
        synchronized (messages) {
            if (messages.size() > messageCacheSize) { // only cache the last 200 messages
                messages.remove(0);
            }
            messages.add(message);
        }
    }

    /**
     * Removes a message from the cache.
     *
     * @param message The message to remove.
     */
    public void removeMessage(Message message) {
        synchronized (messages) {
            messages.remove(message);
        }
        synchronized (messageHistories) {
            for (MessageHistory history : messageHistories) {
                ((ImplMessageHistory) history).removeMessage(message.getId());
            }
        }
    }

    /**
     * Adds a history to the history list.
     *
     * @param history The history to add.
     */
    public void addHistory(MessageHistory history) {
        synchronized (messageHistories) {
            messageHistories.add(history);
        }
    }

    /**
     * Checks the response.
     *
     * @param response The response to check.
     * @throws Exception If the response has problems (status code not between 200 and 300).
     */
    public void checkResponse(HttpResponse<JsonNode> response) throws Exception {
        String message = "";
        if (response.getBody() != null && !response.getBody().isArray()
                && response.getBody().getObject().has("message")) {
            message = " " + response.getBody().getObject().getString("message");
        }
        if (response.getStatus() == 403) {
            throw new PermissionsException("Missing permissions!" + message);
        }
        if (response.getStatus() == 429) {
            // Handled in #checkRateLimit
            return;
        }
        if (response.getStatus() < 200 || response.getStatus() > 299) {
            throw new BadResponseException(
                    "Received http status code " + response.getStatus() + " with message "
                            + response.getStatusText() + " and body " + response.getBody(),
                    response.getStatus(), response.getStatusText(), response);
        }
    }

    /**
     * Checks if there current action if rate limited. The check should be performed before AND after making a request.
     *
     * @param response The response to check. Can be <code>null</code>.
     * @param type The type of the rate limit.
     * @param server The server of the rate limit.
     * @param channel The channel of the rate limit.
     *
     * @throws RateLimitedException if there's a rate limit.
     */
    public void checkRateLimit(HttpResponse<JsonNode> response, RateLimitType type, Server server, Channel channel)
            throws RateLimitedException {
        if (rateLimitManager.isRateLimited(type, server, channel) && type != RateLimitType.UNKNOWN) {
            long retryAfter = rateLimitManager.getRateLimit(type, server, channel);
            throw new RateLimitedException("We are rate limited for " + retryAfter + " ms!", retryAfter, type,
                    server, channel, rateLimitManager);
        }
        // {"global":false,"retry_after":104,"message":"You are being rate limited."}
        if (response != null && response.getStatus() == 429) {
            long retryAfter = response.getBody().getObject().getLong("retry_after");
            rateLimitManager.addRateLimit(type, server, channel, retryAfter);
            throw new RateLimitedException(
                    "We are rate limited for " + retryAfter + " ms (type: " + type.name() + ")!", retryAfter, type,
                    server, channel, rateLimitManager);
        }
    }

    /**
     * Gets a set with all message histories.
     *
     * @return A set with all message histories.
     */
    public Set<MessageHistory> getMessageHistories() {
        return messageHistories;
    }

    /**
     * Gets the internal used server join listener (for server creations and invite accepts).
     *
     * @return The internal used server join listener.
     */
    public ServerJoinListener getInternalServerJoinListener() {
        return listener;
    }

    /**
     * Sets the socket adapter.
     *
     * @param socketAdapter The socket adapter to set.
     */
    public void setSocketAdapter(DiscordWebsocketAdapter socketAdapter) {
        this.socketAdapter = socketAdapter;
    }

}