oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/state/DefaultStateController.java
public class DefaultStateController implements StateController
{
    private final static String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private final HashMap<String,String> states = new HashMap<>();
    
    @Override
    public String generateNewState(String redirectUri)
    {
        String state = randomState();
        states.put(state, redirectUri);
        return state;
    }

    @Override
    public String consumeState(String state)
    {
        return states.remove(state);
    }
    
    private static String randomState()
    {
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < 10; i++)
            sb.append(CHARACTERS.charAt((int)(Math.random()*CHARACTERS.length())));
        return sb.toString();
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/session/SessionData.java
public class SessionData
{
    private final String identifier, accessToken, refreshToken, tokenType;
    private final OffsetDateTime expiration;
    private final Scope[] scopes;

    public SessionData(String identifier, String accessToken, String refreshToken, String tokenType, OffsetDateTime expiration, Scope[] scopes)
    {
        this.identifier = identifier;
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.tokenType = tokenType;
        this.expiration = expiration;
        this.scopes = scopes;
    }

    /**
     * Gets the session identifier.
     *
     * @return The session identifier.
     */
    public String getIdentifier()
    {
        return identifier;
    }

    /**
     * Gets the session access token.
     *
     * @return The session access token.
     */
    public String getAccessToken()
    {
        return accessToken;
    }

    /**
     * Gets the session refresh token.
     *
     * @return The session refresh token.
     */
    public String getRefreshToken()
    {
        return refreshToken;
    }

    /**
     * Gets the session token type.
     *
     * @return The session token type.
     */
    public String getTokenType()
    {
        return tokenType;
    }

    /**
     * Gets the session expiration time.
     *
     * @return The session expiration time.
     */
    public OffsetDateTime getExpiration()
    {
        return expiration;
    }

    /**
     * Gets the session {@link com.jagrosh.jdautilities.oauth2.Scope Scopes}.
     *
     * @return The session Scopes.
     */
    public Scope[] getScopes()
    {
        return scopes;
    }

    @Override
    public boolean equals(Object obj)
    {
        if(!(obj instanceof SessionData))
            return false;

        SessionData data = ((SessionData) obj);

        return getIdentifier().equals(data.getIdentifier()) && getTokenType().equals(data.getTokenType());
    }

    @Override
    public String toString()
    {
        return String.format("SessionData(identifier: %s, access-token: %s, refresh-token: %s, type: %s, expires: %s)",
            getIdentifier(), getAccessToken(), getRefreshToken(), getTokenType(),
            getExpiration().format(DateTimeFormatter.RFC_1123_DATE_TIME));
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/session/DefaultSessionController.java
public class DefaultSessionController implements SessionController<DefaultSession>
{
    private final HashMap<String, DefaultSession> sessions = new HashMap<>();
    
    @Override
    public DefaultSession getSession(String identifier)
    {
        return sessions.get(identifier);
    }

    @Override
    public DefaultSession createSession(SessionData data)
    {
        DefaultSession created = new DefaultSession(data);
        sessions.put(data.getIdentifier(), created);
        return created;
    }

    public class DefaultSession implements Session
    {
        private final String accessToken, refreshToken, tokenType;
        private final OffsetDateTime expiration;
        private final Scope[] scopes;
        
        private DefaultSession(String accessToken, String refreshToken, String tokenType, OffsetDateTime expiration, Scope[] scopes)
        {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
            this.tokenType = tokenType;
            this.expiration = expiration;
            this.scopes = scopes;
        }

        private DefaultSession(SessionData data)
        {
            this(data.getAccessToken(), data.getRefreshToken(), data.getTokenType(), data.getExpiration(), data.getScopes());
        }
        
        @Override
        public String getAccessToken()
        {
            return accessToken;
        }

        @Override
        public String getRefreshToken()
        {
            return refreshToken;
        }

        @Override
        public Scope[] getScopes()
        {
            return scopes;
        }

        @Override
        public String getTokenType()
        {
            return tokenType;
        }

        @Override
        public OffsetDateTime getExpiration()
        {
            return expiration;
        }
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/requests/OAuth2URL.java
public enum OAuth2URL
{
    AUTHORIZE("/oauth2/authorize",
        "client_id=%d",
        "redirect_uri=%s",
        "response_type=code",
        "scope=%s",
        "state=%s"),
    TOKEN("/oauth2/token",
        "client_id=%d",
        "redirect_uri=%s",
        "grant_type=authorization_code",
        "code=%s",
        "client_secret=%s",
        "scope=%s"),
    CURRENT_USER("/users/@me"),
    CURRENT_USER_GUILDS("/users/@me/guilds");

    public static final String BASE_API_URL = String.format("https://discord.com/api/v%d", OAuth2Client.DISCORD_REST_VERSION);

    private final String route;
    private final String formattableRoute;
    private final boolean hasQueryParams;
    private final String queryParams;

    OAuth2URL(String route, String... queryParams)
    {
        this.route = route;
        this.hasQueryParams = queryParams.length > 0;

        if(hasQueryParams)
        {
            StringBuilder b = new StringBuilder();

            for(int i = 0; i < queryParams.length; i++)
            {
                b.append(i == 0? '?' : '&');
                b.append(queryParams[i]);
            }

            this.formattableRoute = route + b.toString();
            this.queryParams = b.toString();
        }
        else
        {
            this.formattableRoute = route;
            this.queryParams = "";
        }
    }

    public String getRoute()
    {
        return route;
    }

    public boolean hasQueryParams()
    {
        return hasQueryParams;
    }

    public String compileQueryParams(Object... values) {
        return String.format(queryParams, values).replaceFirst("\\?", "");
    }

    public String getRouteWithBaseUrl()
    {
        return BASE_API_URL + route;
    }

    public String compile(Object... values)
    {
        return BASE_API_URL + (hasQueryParams? String.format(formattableRoute, values) : formattableRoute);
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/session/Session.java
public interface Session
{
    /**
     * Gets the session's access token.
     *
     * @return The session's access token.
     */
    String getAccessToken();

    /**
     * Gets the session's refresh token.
     *
     * @return The session's refresh token.
     */
    String getRefreshToken();

    /**
     * Gets the session's {@link com.jagrosh.jdautilities.oauth2.Scope Scopes}.
     *
     * @return The session's Scopes.
     */
    Scope[] getScopes();

    /**
     * Gets the session's token type.
     *
     * @return The session's token type.
     */
    String getTokenType();

    /**
     * Gets the session's expiration time.
     *
     * @return The session's expiration time.
     */
    OffsetDateTime getExpiration();
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/exceptions/InvalidStateException.java
public class InvalidStateException extends Exception
{
    public InvalidStateException(String message)
    {
        super(message);
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/exceptions/MissingScopeException.java
public class MissingScopeException extends RuntimeException
{
    private static final String FORMAT = "Cannot %s without '%s' scope!";

    public MissingScopeException(String action, Scope missing)
    {
        super(String.format(FORMAT, action, missing.getText()));
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/entities/OAuth2Guild.java
public interface OAuth2Guild extends ISnowflake
{
    /**
     * Gets the underlying {@link com.jagrosh.jdautilities.oauth2.OAuth2Client OAuth2Client}
     * that created this OAuth2Guild.
     *
     * @return The OAuth2Client that created this OAuth2Guild.
     */
    OAuth2Client getClient();

    /**
     * Gets the Guild's name.
     *
     * @return The Guild's name.
     */
    String getName();

    /**
     * Gets the Guild's icon ID, or {@code null} if the Guild does not have an icon.
     *
     * @return The Guild's icon ID.
     */
    String getIconId();

    /**
     * Gets the Guild's icon URL, or {@code null} if the Guild does not have an icon.
     *
     * @return The Guild's icon URL.
     */
    String getIconUrl();

    /**
     * Gets the Session User's raw permission value for the Guild.
     *
     * @return The Session User's raw permission value for the Guild.
     */
    int getPermissionsRaw();

    /**
     * Gets the Session User's {@link net.dv8tion.jda.api.Permission Permissions} for the Guild.
     *
     * @return The Session User's Permissions for the Guild.
     */
    EnumSet<Permission> getPermissions();

    /**
     * Whether or not the Session User is the owner of the Guild.
     *
     * @return {@code true} if the Session User is the owner of
     *         the Guild, {@code false} otherwise.
     */
    boolean isOwner();

    /**
     * Whether or not the Session User has all of the specified
     * {@link net.dv8tion.jda.api.Permission Permissions} in the Guild.
     *
     * @param  perms
     *         The Permissions to check for.
     *
     * @return {@code true} if and only if the Session User has all of the
     *         specified Permissions, {@code false} otherwise.
     */
    boolean hasPermission(Permission... perms);
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/entities/OAuth2User.java
public interface OAuth2User extends ISnowflake, IMentionable
{
    /**
     * Gets the underlying {@link com.jagrosh.jdautilities.oauth2.OAuth2Client OAuth2Client}
     * that created this OAuth2User.
     *
     * @return The OAuth2Client that created this OAuth2User.
     */
    OAuth2Client getClient();

    /**
     * Gets the originating {@link com.jagrosh.jdautilities.oauth2.session.Session}
     * that is responsible for this OAuth2User.
     *
     * @return The Session responsible for this OAuth2User.
     */
    Session getSession();

    /**
     * Gets the user's Snowflake ID as a String.
     *
     * @return The user's Snowflake ID as a String.
     */
    String getId();

    /**
     * Gets the user's Snowflake ID as a {@code long}.
     *
     * @return The user's Snowflake ID as a {@code long}.
     */
    long getIdLong();

    /**
     * Gets the user's account name.
     *
     * @return The user's account name.
     */
    String getName();

    /**
     * Gets the user's email address that is associated with their Discord account.
     *
     * <p>Note that if this user is acquired without the '{@link com.jagrosh.jdautilities.oauth2.Scope#EMAIL email}'
     * OAuth {@link com.jagrosh.jdautilities.oauth2.Scope Scope}, this will throw a
     * {@link com.jagrosh.jdautilities.oauth2.exceptions.MissingScopeException MissingScopeException}.
     *
     * @return The user's email.
     *
     * @throws com.jagrosh.jdautilities.oauth2.exceptions.MissingScopeException
     *         If the corresponding {@link OAuth2User#getSession() session} does not have the
     *         proper 'email' OAuth2 scope
     */
    String getEmail();

    /**
     * Returns {@code true} if the user's Discord account has been verified via email.
     *
     * <p>This is required to send messages in guilds where certain moderation levels are used.
     *
     * @return {@code true} if the user has verified their account, {@code false} otherwise.
     */
    boolean isVerified();

    /**
     * Returns {@code true} if this user has multi-factor authentication enabled.
     *
     * <p>Some guilds require mfa for administrative actions.
     *
     * @return {@code true} if the user has mfa enabled, {@code false} otherwise.
     */
    boolean isMfaEnabled();

    /**
     * Gets the user's discriminator.
     *
     * @return The user's discriminator.
     */
    String getDiscriminator();

    /**
     * Gets the user's avatar ID, or {@code null} if they have not set one.
     *
     * @return The user's avatar ID, or {@code null} if they have not set one.
     */
    String getAvatarId();

    /**
     * Gets the user's avatar URL, or {@code null} if they have not set one.
     *
     * @return The user's avatar URL, or {@code null} if they have not set one.
     */
    String getAvatarUrl();

    /**
     * Gets the user's avatar URL.
     *
     * @return The user's avatar URL.
     */
    String getDefaultAvatarId();

    /**
     * Gets the user's default avatar ID.
     *
     * @return The user's default avatar ID.
     */
    String getDefaultAvatarUrl();

    /**
     * Gets the user's avatar URL, or their {@link #getDefaultAvatarUrl() default avatar URL}
     * if they do not have a custom avatar set on their account.
     *
     * @return The user's effective avatar URL.
     */
    String getEffectiveAvatarUrl();

    /**
     * Gets whether or not this user is a bot.
     *
     * <p>While, at the time of writing this documentation, bots cannot
     * authenticate applications, there may be a time in the future
     * where they have such an ability.
     *
     * @return {@code false}
     *
     * @deprecated
     *         Due to the nature of OAuth2 at this moment, bots are not
     *         allowed to use the various urls provided.
     *         <br>This method is scheduled for removal upon merging it
     *         with <code>master</code> in JDA-Utilities 2.2
     */
    @Deprecated
    default boolean isBot()
    {
        // Note: the code here has not changed from it's implementation.
        return false;
    }

    /**
     * Gets the user as a discord formatted mention:
     * <br>{@code <@SNOWFLAKE_ID> }
     *
     * @return A discord formatted mention of this user.
     */
    String getAsMention();

    /**
     * Gets the corresponding {@link net.dv8tion.jda.api.entities.User JDA User}
     * from the provided instance of {@link net.dv8tion.jda.api.JDA JDA}.
     *
     * <p>Note that there is no guarantee that this will not return {@code null}
     * as the instance of JDA may not have access to the User.
     *
     * <p>For sharded bots, use {@link OAuth2User#getJDAUser(ShardManager)}.
     *
     * @param  jda
     *         The instance of JDA to get from.
     *
     * @return A JDA User, possibly {@code null}.
     */
    User getJDAUser(JDA jda);

    /**
     * Gets the corresponding {@link net.dv8tion.jda.api.entities.User JDA User}
     * from the provided {@link net.dv8tion.jda.api.sharding.ShardManager ShardManager}.
     *
     * <p>Note that there is no guarantee that this will not return {@code null}
     * as the ShardManager may not have access to the User.
     *
     * <p>For un-sharded bots, use {@link OAuth2User#getJDAUser(JDA)}.
     *
     * @param  shardManager
     *         The ShardManager to get from.
     *
     * @return A JDA User, possibly {@code null}.
     */
    User getJDAUser(ShardManager shardManager);
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/Scope.java
public enum Scope
{
    /**
     * For oauth2 bots, this puts the bot in the user's selected guild by default
     */
    BOT("bot"),
    
    /**
     * Allows /users/@me/connections to return linked third-party accounts
     */
    CONNECTIONS("connections"),
    
    /**
     * Enables /users/@me to return an email
     */
    EMAIL("email"),
    
    /**
     * Allows /users/@me without email
     */
    IDENTIFY("identify"),
    
    /**
     * Allows /users/@me/guilds to return basic information about all of a user's guilds
     */
    GUILDS("guilds"),
    
    /**
     * Allows /invites/{invite.id} to be used for joining users to a guild
     */
    GUILDS_JOIN("guilds.join"),
    
    /**
     * Allows your app to join users to a group dm
     */
    GDM_JOIN("gdm.join"),
    
    /**
     * For local rpc server api access, this allows you to read messages from all
     * client channels (otherwise restricted to channels/guilds your app creates)
     */
    MESSAGES_READ("messages.read"),
    
    /**
     * For local rpc server access, this allows you to control a user's local Discord client
     */
    RPC("rpc"),
    
    /**
     * For local rpc server api access, this allows you to access the API as the local user
     */
    RPC_API("rpc.api"),
    
    /**
     * For local rpc server api access, this allows you to receive notifications pushed out to the user
     */
    RPC_NOTIFICATIONS_READ("rpc.notifications.read"),
    
    /**
     * This generates a webhook that is returned in the oauth token response for authorization code grants
     */
    WEBHOOK_INCOMING("webhook.incoming"),
    
    /**
     * Unknown scope
     */
    UNKNOWN("");
    
    private final String text;
    
    Scope(String text)
    {
        this.text = text;
    }

    /**
     * The text key associated with this scope.
     *
     * @return The text key associated with this scope.
     */
    public String getText()
    {
        return text;
    }

    public static boolean contains(Scope[] scopes, Scope scope)
    {
        if(scopes == null || scopes.length == 0 || scope == null || scope == UNKNOWN)
            return false;
        for(Scope s : scopes)
            if(s == scope)
                return true;
        return false;
    }

    /**
     * Joins the specified scopes properly as they should
     * be represented as part of an authorization URL.
     *
     * @param  scopes
     *         The scopes to join.
     *
     * @return A String representing how the scopes should be
     *         represented as part of an authorization URL.
     */
    public static String join(Scope... scopes)
    {
        return join(false, scopes);
    }

    /**
     * Joins the specified scopes properly as they should
     * be represented as part of an authorization URL.
     *
     * @param  scopes
     *         The scopes to join.
     * @param  bySpace
     *         If the scopes should be joined by " " or "%20" (default: "%20")
     *
     * @return A String representing how the scopes should be
     *         represented as part of an authorization URL.
     */
    public static String join(boolean bySpace, Scope... scopes)
    {
        if(scopes.length == 0)
            return "";
        StringBuilder sb = new StringBuilder(scopes[0].getText());
        for(int i = 1; i < scopes.length; i++)
        {
            if (bySpace)
            {
                sb.append(" ");
            }
            else
            {
                sb.append("%20");
            }
            sb.append(scopes[i].getText());
        }
        return sb.toString();
    }

    /**
     * Gets a scope based on the specified text key.
     *
     * @param  scope
     *         A text key to get a scope by.
     *
     * @return The scope matching the provided text key
     *         ({@link Scope#UNKNOWN UNKNOWN} by default)
     */
    public static Scope from(String scope)
    {
        for(Scope s : values())
            if(s.text.equalsIgnoreCase(scope))
                return s;
        return UNKNOWN;
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/requests/OAuth2Requester.java
public class OAuth2Requester
{
    protected static final Logger LOGGER = JDALogger.getLog(OAuth2Requester.class);
    protected static final String USER_AGENT = "JDA-Utils Oauth2("+JDAUtilitiesInfo.GITHUB+" | "+JDAUtilitiesInfo.VERSION+")";
    protected static final RequestBody EMPTY_BODY = RequestBody.create(null, new byte[0]);

    private final OkHttpClient httpClient;

    public OAuth2Requester(OkHttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    <T> void submitAsync(OAuth2Action<T> request, Consumer<T> success, Consumer<Throwable> failure)
    {
        httpClient.newCall(request.buildRequest()).enqueue(new Callback()
        {
            @Override
            public void onResponse(Call call, Response response)
            {
                try
                {
                    T value = request.handle(response);
                    logSuccessfulRequest(request);

                    // Handle end-user exception differently
                    try
                    {
                        if(value != null)
                            success.accept(value);
                    }
                    catch(Throwable t)
                    {
                        LOGGER.error("OAuth2Action success callback threw an exception!", t);
                    }
                }
                catch(Throwable t)
                {
                    // Handle end-user exception differently
                    try
                    {
                        failure.accept(t);
                    }
                    catch(Throwable t1)
                    {
                        LOGGER.error("OAuth2Action success callback threw an exception!", t1);
                    }
                }
                finally
                {
                    response.close();
                }
            }

            @Override
            public void onFailure(Call call, IOException e)
            {
                LOGGER.error("Requester encountered an error when submitting a request!", e);
            }
        });
    }

    <T> T submitSync(OAuth2Action<T> request) throws IOException
    {
        try(Response response = httpClient.newCall(request.buildRequest()).execute())
        {
            T value = request.handle(response);
            logSuccessfulRequest(request);
            return value;
        }
    }

    private static void logSuccessfulRequest(OAuth2Action request)
    {
        LOGGER.debug("Got a response for {} - {}\nHeaders: {}", request.getMethod(),
            request.getUrl(), request.getHeaders());
    }
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/session/SessionController.java
public interface SessionController<S extends Session>
{
    /**
     * Gets a {@link com.jagrosh.jdautilities.oauth2.session.Session Session} that
     * was previously created using the provided identifier.
     *
     * <p>It is very important for implementations of SessionController to hold
     * a contract that Sessions created using {@link #createSession(SessionData)}
     * will be maintained and retrievable by external sources at any time.
     *
     * <p>Note that Sessions that have elapsed their effective
     * {@link com.jagrosh.jdautilities.oauth2.session.SessionData#getExpiration() expiration}
     * are not necessary to maintain, unless they have been refreshed in which case they
     * should be updated to reflect this.
     *
     * @param  identifier
     *         The identifier to get a Session by.
     *
     * @return The Session mapped to the identifier provided.
     */
    S getSession(String identifier);

    /**
     * Creates a new {@link com.jagrosh.jdautilities.oauth2.session.Session Session} using
     * the specified {@link com.jagrosh.jdautilities.oauth2.session.SessionData SessionData}.
     *
     * <p>Sessions should be kept mapped outside of just creation so that they can be
     * retrieved using {@link SessionController#getSession(String)} later for further
     * manipulation, as well as to keep updated if they are refreshed.
     *
     * @param  data
     *         The data to create a Session using.
     *
     * @return A new Session.
     */
    S createSession(SessionData data);
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/state/StateController.java
public interface StateController
{
    /**
     * Generates a new state string using the provided redirect URI.
     * 
     * @param  redirectUri
     *         The redirect URI that will be used with this state.
     *
     * @return The state string.
     */
    String generateNewState(String redirectUri);
    
    /**
     * Consumes a state to get the corresponding redirect URI.
     *
     * <p>Once this method is called for a specific state, it
     * should return null for all future calls of that same state.
     * 
     * @param  state
     *         The state.
     *
     * @return The redirect URI, or {@code null} if the state does not exist.
     */
    String consumeState(String state);
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/requests/OAuth2Action.java
public abstract class OAuth2Action<T>
{
    protected static final Consumer DEFAULT_SUCCESS = t -> {};
    protected static final Consumer<Throwable> DEFAULT_FAILURE = t -> {
        OAuth2Requester.LOGGER.error("Requester encountered an error while processing response!", t);
    };

    protected final OAuth2ClientImpl client;
    protected final Method method;
    protected final String url;

    public OAuth2Action(OAuth2ClientImpl client, Method method, String url)
    {
        Checks.notNull(client, "OAuth2Client");
        Checks.notNull(method, "Request method");
        Checks.notEmpty(url, "URL");

        this.client = client;
        this.method = method;
        this.url = url;
    }

    protected RequestBody getBody()
    {
        return OAuth2Requester.EMPTY_BODY;
    }

    protected Headers getHeaders()
    {
        return Headers.of();
    }

    protected Request buildRequest()
    {
        Request.Builder builder = new Request.Builder();

        switch(method)
        {
            case GET:
                builder.get();
                break;
            case POST:
                builder.post(getBody());
                break;
            default:
                throw new IllegalArgumentException(method.name() + " requests are not supported!");
        }

        builder.url(url);
        builder.header("User-Agent", OAuth2Requester.USER_AGENT);
        builder.headers(getHeaders());

        return builder.build();
    }

    protected Method getMethod()
    {
        return method;
    }

    protected String getUrl()
    {
        return url;
    }

    /**
     * Asynchronously executes this OAuth2Action.
     */
    public void queue()
    {
        queue(DEFAULT_SUCCESS);
    }

    /**
     * Asynchronously executes this OAuth2Action, providing the value constructed from the response
     * as the parameter given to the success {@link java.util.function.Consumer Consumer}.
     *
     * @param  success
     *         The success consumer, executed when this OAuth2Action gets a successful response.
     */
    public void queue(Consumer<T> success)
    {
        queue(success, DEFAULT_FAILURE);
    }

    /**
     * Asynchronously executes this OAuth2Action, providing the value constructed from the response
     * as the parameter given to the success {@link java.util.function.Consumer Consumer} if the
     * response is successful, or the exception to the failure Consumer if it's not.
     *
     * @param  success
     *         The success consumer, executed when this OAuth2Action gets a successful response.
     * @param  failure
     *         The failure consumer, executed when this OAuth2Action gets a failed response.
     */
    public void queue(Consumer<T> success, Consumer<Throwable> failure)
    {
        client.getRequester().submitAsync(this, success, failure);
    }

    /**
     * Synchronously executes this OAuth2Action, returning the value constructed from the response
     * if it was successful, or throwing the {@link java.lang.Exception Exception} if it was not.
     *
     * <p>Bear in mind when using this, that this method blocks the thread it is called in.
     * @return the value constructed from the response
     * @throws java.io.IOException on unsuccessful execution
     */
    public T complete() throws IOException
    {
        return client.getRequester().submitSync(this);
    }

    /**
     * Gets the {@link com.jagrosh.jdautilities.oauth2.OAuth2Client client} responsible
     * for creating this OAuth2Action.
     *
     * @return The OAuth2Client responsible for creating this.
     */
    public OAuth2ClientImpl getClient()
    {
        return client;
    }

    protected abstract T handle(@WillClose Response response) throws IOException;
}
oauth2/src/main/java/com/jagrosh/jdautilities/oauth2/OAuth2Client.java
public interface OAuth2Client
{
    /**
     * The REST version targeted by JDA-Utilities OAuth2.
     */
    int DISCORD_REST_VERSION = 8;

    /**
     * Generates a formatted authorization URL from the provided redirect URI fragment
     * and {@link com.jagrosh.jdautilities.oauth2.Scope Scopes}.
     *
     * @param  redirectUri
     *         The redirect URI.
     * @param  scopes
     *         The provided scopes.
     *
     * @return The generated authorization URL.
     */
    String generateAuthorizationURL(String redirectUri, Scope... scopes);

    /**
     * Starts a {@link com.jagrosh.jdautilities.oauth2.session.Session Session} with the provided code,
     * state, and identifier. The state provided should be <i>unique</i> and provided through an
     * implementation of {@link com.jagrosh.jdautilities.oauth2.state.StateController StateController}.
     *
     * <p>If the state has already been consumed by the StateController using
     * {@link com.jagrosh.jdautilities.oauth2.state.StateController#consumeState(String) StateController#consumeState},
     * then it should return {@code null} when provided the same state, so that this may throw a
     * {@link InvalidStateException InvalidStateException} to signify it has
     * been consumed.
     *
     * @param  code
     *         The code for the Session to start.
     * @param  state
     *         The state for the Session to start.
     * @param  identifier
     *         The identifier for the Session to start.
     * @param  scopes
     *         The provided scopes.
     *
     * @return A {@link com.jagrosh.jdautilities.oauth2.requests.OAuth2Action OAuth2Action} for the Session to start.
     *
     * @throws InvalidStateException
     *         If the state, when consumed by this client's StateController, results in a {@code null} redirect URI.
     */
    @CheckReturnValue
    OAuth2Action<Session> startSession(String code, String state, String identifier, Scope... scopes) throws InvalidStateException;

    /**
     * Requests a {@link com.jagrosh.jdautilities.oauth2.entities.OAuth2User OAuth2User}
     * from the {@link com.jagrosh.jdautilities.oauth2.session.Session Session}.
     *
     * <p>All Sessions should handle an individual Discord User, and as such this method retrieves
     * data on that User when the session is provided.
     *
     * @param  session
     *         The Session to get a OAuth2User for.
     *
     * @return A {@link com.jagrosh.jdautilities.oauth2.requests.OAuth2Action OAuth2Action} for
     *         the OAuth2User to be retrieved.
     */
    @CheckReturnValue
    OAuth2Action<OAuth2User> getUser(Session session);

    /**
     * Requests a list of {@link com.jagrosh.jdautilities.oauth2.entities.OAuth2Guild OAuth2Guilds}
     * from the {@link com.jagrosh.jdautilities.oauth2.session.Session Session}.
     *
     * <p>All Sessions should handle an individual Discord User, and as such this method retrieves
     * data on all the various Discord Guilds that user is a part of when the session is provided.
     *
     * <p>Note that this can only be performed for Sessions who have the necessary
     * {@link com.jagrosh.jdautilities.oauth2.Scope#GUILDS 'guilds'} scope.
     * <br>Trying to call this using a Session without the scope will cause a
     * {@link com.jagrosh.jdautilities.oauth2.exceptions.MissingScopeException MissingScopeException}
     * to be thrown.
     *
     * @param  session
     *         The Session to get OAuth2Guilds for.
     *
     * @return A {@link com.jagrosh.jdautilities.oauth2.requests.OAuth2Action OAuth2Action} for
     *         the OAuth2Guilds to be retrieved.
     *
     * @throws com.jagrosh.jdautilities.oauth2.exceptions.MissingScopeException
     *         If the provided Session does not have the 'guilds' scope.
     */
    @CheckReturnValue
    OAuth2Action<List<OAuth2Guild>> getGuilds(Session session);

    /**
     * Gets the client ID for this OAuth2Client.
     *
     * @return The client ID.
     */
    long getId();

    /**
     * Gets the client's secret.
     *
     * @return The client's secret.
     */
    String getSecret();

    /**
     * Gets the client's {@link com.jagrosh.jdautilities.oauth2.state.StateController StateController}.
     *
     * @return The client's StateController.
     */
    StateController getStateController();

    /**
     * Gets the client's {@link com.jagrosh.jdautilities.oauth2.session.SessionController SessionController}.
     *
     * @return The client's SessionController.
     */
    SessionController getSessionController();

    /**
     * Builder for creating OAuth2Client instances.
     *
     * <p>At minimum, the developer must provide a
     * valid Client ID, as well as a valid secret.
     */
    class Builder
    {
        private long clientId = -1;
        private String clientSecret;
        private SessionController sessionController;
        private StateController stateController;
        private OkHttpClient client;

        /**
         * Finalizes and builds an {@link com.jagrosh.jdautilities.oauth2.OAuth2Client OAuth2Client}
         * instance using this builder.
         *
         * @return The OAuth2Client instance build.
         *
         * @throws java.lang.IllegalArgumentException
         *         If either:
         *         <ul>
         *             <li>The Client ID is not valid.</li>
         *             <li>The Client Secret is empty.</li>
         *         </ul>
         */
        public OAuth2Client build()
        {
            Checks.check(clientId >= 0, "Client ID is invalid!");
            Checks.notEmpty(clientSecret, "Client Secret");
            return new OAuth2ClientImpl(clientId, clientSecret, sessionController, stateController, client);
        }

        /**
         * Sets the OAuth2Client's ID.
         *
         * @param  clientId
         *         The OAuth2Client's ID.
         *
         * @return This builder.
         */
        public Builder setClientId(long clientId)
        {
            this.clientId = clientId;
            return this;
        }

        /**
         * Sets the OAuth2Client's secret.
         *
         * @param  clientSecret
         *         The OAuth2Client's secret.
         *
         * @return This builder.
         */
        public Builder setClientSecret(String clientSecret)
        {
            this.clientSecret = clientSecret;
            return this;
        }

        /**
         * Sets the OAuth2Client's {@link com.jagrosh.jdautilities.oauth2.session.SessionController SessionController}.
         *
         * @param  sessionController
         *         The OAuth2Client's SessionController.
         *
         * @return This builder.
         */
        public Builder setSessionController(SessionController sessionController)
        {
            this.sessionController = sessionController;
            return this;
        }

        /**
         * Sets the OAuth2Client's {@link com.jagrosh.jdautilities.oauth2.state.StateController StateController}.
         *
         * @param  stateController
         *         The OAuth2Client's StateController.
         *
         * @return This builder.
         */
        public Builder setStateController(StateController stateController)
        {
            this.stateController = stateController;
            return this;
        }

        /**
         * Sets the client's internal {@link okhttp3.OkHttpClient OkHttpClient} used for
         * all requests and interactions with Discord.
         *
         * @param  client
         *         The OAuth2Client's OkHttpClient.
         *
         * @return This builder.
         */
        public Builder setOkHttpClient(OkHttpClient client)
        {
            this.client = client;
            return this;
        }
    }
}


Question: import com.jagrosh.jdautilities.oauth2.OAuth2Client;
import com.jagrosh.jdautilities.oauth2.Scope;
import com.jagrosh.jdautilities.oauth2.entities.OAuth2Guild;
import com.jagrosh.jdautilities.oauth2.entities.OAuth2User;
import com.jagrosh.jdautilities.oauth2.exceptions.InvalidStateException;
import com.jagrosh.jdautilities.oauth2.exceptions.MissingScopeException;
import com.jagrosh.jdautilities.oauth2.requests.OAuth2Action;
import com.jagrosh.jdautilities.oauth2.requests.OAuth2Requester;
import com.jagrosh.jdautilities.oauth2.requests.OAuth2URL;
import com.jagrosh.jdautilities.oauth2.session.DefaultSessionController;
import com.jagrosh.jdautilities.oauth2.session.Session;
import com.jagrosh.jdautilities.oauth2.session.SessionController;
import com.jagrosh.jdautilities.oauth2.session.SessionData;
import com.jagrosh.jdautilities.oauth2.state.DefaultStateController;
import com.jagrosh.jdautilities.oauth2.state.StateController;
import net.dv8tion.jda.api.exceptions.HttpException;
import net.dv8tion.jda.internal.requests.Method;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.EncodingUtil;
import net.dv8tion.jda.internal.utils.IOUtil;
import net.dv8tion.jda.internal.utils.JDALogger;
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.time.OffsetDateTime;
import java.util.LinkedList;
import java.util.List;
/*
 * Copyright 2016-2018 John Grosh (jagrosh) & Kaidan Gustave (TheMonitorLizard)
 *
 * 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 com.jagrosh.jdautilities.oauth2.entities.impl;



/**
 *
 * @author John Grosh (john.a.grosh@gmail.com)
 */
public class OAuth2ClientImpl implements OAuth2Client
{
    private static final Logger LOG = JDALogger.getLog(OAuth2Client.class);
    
    private final long clientId;
    private final String clientSecret;
    private final SessionController sessionController;
    private final StateController stateController;
    private final OkHttpClient httpClient;
Answer:     private final OAuth2Requester requester;