All Downloads are FREE. Search and download functionalities are using the official Maven repository.

discord4j.oauth2.DiscordOAuth2Client Maven / Gradle / Ivy

There is a newer version: 3.3.0-RC2
Show newest version
/*
 * This file is part of Discord4J.
 *
 * Discord4J 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.
 *
 * Discord4J 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 Discord4J. If not, see .
 */

package discord4j.oauth2;

import com.fasterxml.jackson.databind.ObjectMapper;
import discord4j.common.annotations.Experimental;
import discord4j.discordjson.json.*;
import discord4j.oauth2.object.AccessToken;
import discord4j.oauth2.service.OAuth2Service;
import discord4j.rest.RestClient;
import discord4j.rest.request.DiscordWebRequest;
import discord4j.rest.request.Router;
import discord4j.rest.route.Routes;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.Logger;
import reactor.util.Loggers;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/**
 * A client registration capable of making requests to the Discord API on behalf of a single user using
 * their OAuth2 access token.
 * 

* If you use a {@link DiscordOAuth2Server}, you can retrieve working instances by defining a * {@link DiscordOAuth2SuccessHandler}. In other cases use a factory method depending on how your OAuth2 application * is structured: *

    *
  • {@link #createFromCode(RestClient, AuthorizationCodeGrantRequest)} if you want to defer the token fetching * process until the first authorized request
  • *
  • {@link #createFromToken(RestClient, long, String, AccessTokenData)} if access token data is already * available, like after calling {@link OAuth2Service#exchangeAuthorizationCode(AuthorizationCodeGrantRequest)}
  • *
  • {@link #createFromCredentials(RestClient, ClientCredentialsGrantRequest)} if you are a bot developer and * want to issue a Bearer token on your behalf for testing purposes
  • *
* * @see Discord Docs */ @Experimental public class DiscordOAuth2Client { private static final Logger log = Loggers.getLogger(DiscordOAuth2Client.class); private final RestClient restClient; private final OAuth2Service oAuth2Service; private final long clientId; private final String clientSecret; private final Function> tokenFactory; private final AtomicReference accessToken; DiscordOAuth2Client(RestClient restClient, long clientId, String clientSecret, Function> tokenFactory) { this.restClient = restClient; this.oAuth2Service = new OAuth2Service(restClient.getRestResources().getRouter()); this.clientId = clientId; this.clientSecret = clientSecret; this.tokenFactory = tokenFactory; this.accessToken = new AtomicReference<>(); } /** * Create an OAuth2 client by completing an authorization code grant flow. This is useful if you have a custom * HTTP server that receives the {@code code} parameter. Build a * {@link AuthorizationCodeGrantRequest} making sure to include the code parameter and the access token with refresh * capabilities will be stored once the first request is made using one of the API methods in this class or * {@link #withAuthorizedClient(DiscordWebRequest)}. *

* For an example server implementation, check {@link DiscordOAuth2Server}, which uses this method. * * @param restClient a Discord REST API client for performing requests * @param request an object with all parameters required to complete an authorization code grant request * @return a client that can work with a valid user token to perform API requests */ public static DiscordOAuth2Client createFromCode(RestClient restClient, AuthorizationCodeGrantRequest request) { return new DiscordOAuth2Client(restClient, request.clientId(), request.clientSecret(), service -> service.exchangeAuthorizationCode(request).map(AccessToken::new)); } /** * Create an OAuth2 client by performing a client credentials flow. This is a quick and easy way as a bot developer * to get an access token for testing purposes. Build a {@link ClientCredentialsGrantRequest} using your * application parameters and intended {@link Scope} values, separated by spaces. * * @param restClient a Discord REST API client for performing requests * @param request an object with all parameters required to complete a client credentials grant request * @return a client that can work with a valid token for your user to perform API requests */ public static DiscordOAuth2Client createFromCredentials(RestClient restClient, ClientCredentialsGrantRequest request) { return new DiscordOAuth2Client(restClient, request.clientId(), request.clientSecret(), service -> service.exchangeClientCredentials(request).map(AccessToken::new)); } /** * Create an OAuth2 client with the raw {@link AccessTokenData} returned from * {@link OAuth2Service#exchangeAuthorizationCode(AuthorizationCodeGrantRequest)}. Useful if a token is requested * directly by your HTTP server. * * @param restClient a Discord REST API client for performing requests * @param clientId your application's client ID * @param clientSecret your application's client secret * @param data the access token object * @return a client that can work with a valid token to perform API requests */ public static DiscordOAuth2Client createFromToken(RestClient restClient, long clientId, String clientSecret, AccessTokenData data) { return new DiscordOAuth2Client(restClient, clientId, clientSecret, __ -> Mono.just(new AccessToken(data))); } /** * Return the current access token for this client. Depending on the OAuth2 scopes used, you can extract information * from this object to perform additional tasks. For instance, if you used {@link Scope#BOT}, * {@link AccessToken#getGuild()} will be available; if you used {@link Scope#WEBHOOK_INCOMING}, * {@link AccessToken#getWebhook()} will be available. * * @return the current access token authorized for this client. This is an immutable instance, so you may need to * call this again if the token is refreshed. * @throws IllegalStateException if this access token was invalidated, indicating you might require reauthorization */ public AccessToken getAccessToken() { return Optional.ofNullable(accessToken.get()) .orElseThrow(() -> new IllegalStateException("No valid token present")); } /** * Return the Jackson {@link ObjectMapper} tied to this instance for JSON handling purposes. * * @return an object that can provide JSON processing */ public ObjectMapper getObjectMapper() { return restClient.getRestResources().getJacksonResources().getObjectMapper(); } /** * Return the {@link Router} tied to this instance to execute requests to Discord API. * * @return an abstraction to perform API requests */ private Router getRouter() { return restClient.getRestResources().getRouter(); } /** * Returns info about the current authorization. Uses {@link #withAuthorizedClient(DiscordWebRequest)} to retrieve * and use the Bearer token tied to this client. * * @return a Mono with authorization details given by this token in this client, or an * error Mono in case any request fails */ public Mono getAuthorizationInfo() { return exchange(Routes.AUTHORIZATION_INFO_GET.newRequest(), AuthorizationInfoData.class); } /** * Returns the user object of the requesting account. For OAuth2, this requires the {@link Scope#IDENTIFY} * scope, which will return the object without an email, and optionally the {@link Scope#EMAIL} scope, which returns * the object with an email. * * @return a Mono with user details if successful, otherwise an error Mono */ public Mono getCurrentUser() { return exchange(Routes.CURRENT_USER_GET.newRequest(), UserData.class); } /** * Returns a list of partial guild objects the requesting account is a member of. Requires the * {@link Scope#GUILDS} scope. * * @param queryParams optional query parameters for this endpoint, allowing pagination using {@code before}, * {@code after} and {@code limit} (defaults to 200) * @return a Flux with partial guild information for the user, otherwise an error Flux */ public Flux getCurrentUserGuilds(Map queryParams) { return exchange(Routes.CURRENT_USER_GUILDS_GET.newRequest().query(queryParams), UserGuildData[].class) .flatMapMany(Flux::fromArray); } /** * Returns a member object from the current user in the given guild. Request the * {@link Scope#GUILDS_MEMBERS_READ} scope. * * @param guildId the guild to query the current user member object * @return a Mono with member information, otherwise an error Mono */ public Mono getCurrentUserGuildMember(long guildId) { return exchange(Routes.CURRENT_USER_GUILD_MEMBER_GET.newRequest(guildId), MemberData.class); } /** * Return a list of {@link ConnectionData} objects. Requires this client was authorized to use the * {@link Scope#CONNECTIONS} scope. Uses {@link #withAuthorizedClient(DiscordWebRequest)} to retrieve and use the * Bearer token tied to this client. * * @return a Mono with user connections, or an error Mono in case any request fails */ public Flux getUserConnections() { return exchange(Routes.USER_CONNECTIONS_GET.newRequest(), ConnectionData[].class) .flatMapMany(Flux::fromArray); } /** * Fetches permissions for a specific command for your application in a guild. Returns a guild application command * permissions object. * * @param applicationId your application ID * @param guildId the guild ID * @param commandId the command ID * @return a Mono with command permissions object for the requested guild, or an error Mono in case a request fails */ public Mono getApplicationCommandPermissions(long applicationId, long guildId, long commandId) { return exchange(Routes.APPLICATION_COMMAND_PERMISSIONS_GET.newRequest(applicationId, guildId, commandId), GuildApplicationCommandPermissionsData.class); } /** * Edits command permissions for a specific command for your application in a guild and returns a guild * application command permissions object. You can add up to 100 permission overwrites for a command. * * @param applicationId your application ID * @param guildId the guild ID * @param commandId the command ID * @param request a request body containing the permissions to be set * @return a Mono with command permissions object for the requested guild, or an error Mono in case a request fails */ public Mono modifyApplicationCommandPermissions(long applicationId, long guildId, long commandId, ApplicationCommandPermissionsRequest request) { return exchange(Routes.APPLICATION_COMMAND_PERMISSIONS_MODIFY.newRequest(applicationId, guildId, commandId), GuildApplicationCommandPermissionsData.class); } /** * Execute a given {@link DiscordWebRequest} on behalf of a user and mapping the response to the given type, using * the credentials stored under this client. The token fetching, refreshing (if required) and API request are run * once this Mono is subscribed. For more control on the authorized request and response see * {@link #withAuthorizedClient(DiscordWebRequest)}. * * @param request the compiled Discord REST API request to be run on behalf of a user * @param responseType the expected response type from the API * @param the response type * @return a Mono with the mapped response if successful, otherwise an error Mono */ public Mono exchange(DiscordWebRequest request, Class responseType) { return withAuthorizedClient(request) .map(it -> it.exchange(getRouter())) .flatMap(res -> res.bodyToMono(responseType)); } /** * Prepare a given {@link DiscordWebRequest} on behalf of a user, using the credentials stored under this client. * The token fetching, refreshing (if required) and API request are run once this Mono is subscribed. * * @param request the compiled Discord REST API request to be run on behalf of a user * @return a Mono of a request including required steps to include proper authorization */ public Mono withAuthorizedClient(DiscordWebRequest request) { return getBearerToken().map(token -> request.copy().bearerAuth(token)); } private Mono getBearerToken() { return Mono.fromCallable(accessToken::get) .flatMap(token -> { if (token.hasExpired()) { return oAuth2Service.refreshToken(TokenRefreshRequest.builder() .clientId(clientId) .clientSecret(clientSecret) .refreshToken(token.getRefreshToken().orElseThrow(UnsupportedOperationException::new)) .build()) .map(data -> { accessToken.set(new AccessToken(data)); return data.accessToken(); }); } return Mono.just(token.getAccessToken()); }) .switchIfEmpty(Mono.defer(() -> tokenFactory.apply(oAuth2Service) .map(newToken -> { accessToken.set(newToken); return newToken.getAccessToken(); }))) .onErrorResume(ex -> { accessToken.set(null); return Mono.error(ex); }); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy