diff --git a/build.gradle b/build.gradle index 7e44cd2..aa7ab6c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ plugins { id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.3' id 'org.asciidoctor.jvm.convert' version '3.3.2' - id 'org.flywaydb.flyway' version '10.0.0' } group = 'com.danielvm' @@ -57,36 +56,26 @@ jar { dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc' implementation 'org.springframework.session:spring-session-data-redis' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-devtools' implementation "net.i2p.crypto:eddsa:${i2pCryptoVersion}" implementation "commons-io:commons-io:${IOCommonsVersion}" implementation "org.projectlombok:lombok:${lombokVersion}" implementation "org.aspectj:aspectjrt:${aspectJRTVersion}" - implementation "org.flywaydb:flyway-core:${flywayCoreVersion}" - implementation "org.postgresql:postgresql:${postgresqlVersion}" - implementation "org.postgresql:r2dbc-postgresql:${postgresR2dbcVersion}" implementation "org.aspectj:aspectjweaver:${aspectJWeaverVersion}" implementation "commons-codec:commons-codec:${commonsCodecVersion}" implementation "software.pando.crypto:salty-coffee:${pandoCryptoVersion}" implementation "org.apache.commons:commons-collections4:${apacheCollectionsVersion}" - implementation "org.flywaydb:flyway-database-postgresql:${flywayPostgresVersion}" compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation "org.testcontainers:jdbc:${tcJdbcVersion}" - testImplementation "org.testcontainers:r2dbc:${tcR2dbcVersion}" - testImplementation "org.testcontainers:postgresql:${tcPostgresVersion}" testImplementation "org.testcontainers:junit-jupiter:${tcJunitVersion}" testImplementation "io.projectreactor:reactor-test:${reactorTestVersion}" testImplementation "org.testcontainers:testcontainers:${testContainersVersion}" testImplementation "org.junit.jupiter:junit-jupiter-params:${junitJupiterParamsVersion}" - testImplementation "org.flywaydb.flyway-test-extensions:flyway-spring-test:${flywayTestVersion}" } dependencyManagement { diff --git a/src/main/java/com/danielvm/destiny2bot/Destiny2botApplication.java b/src/main/java/com/danielvm/destiny2bot/Destiny2botApplication.java index 68a4015..0dbef04 100644 --- a/src/main/java/com/danielvm/destiny2bot/Destiny2botApplication.java +++ b/src/main/java/com/danielvm/destiny2bot/Destiny2botApplication.java @@ -2,14 +2,12 @@ import com.danielvm.destiny2bot.exception.ExternalServiceException; import com.danielvm.destiny2bot.exception.InternalServerException; -import com.danielvm.destiny2bot.filter.CachingRequestBodyFilter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.nio.charset.StandardCharsets; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; @@ -44,14 +42,6 @@ CacheManager inMemoryCacheManager() { return new ConcurrentMapCacheManager(); } - @Bean - public FilterRegistrationBean signatureValidationFilterBean() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new CachingRequestBodyFilter()); - registrationBean.addUrlPatterns("/interactions"); - return registrationBean; - } - /** * Prepares a WebClient.Builder bean that has standard status handlers * diff --git a/src/main/java/com/danielvm/destiny2bot/annotation/ValidSignature.java b/src/main/java/com/danielvm/destiny2bot/annotation/ValidSignature.java deleted file mode 100644 index 912fc95..0000000 --- a/src/main/java/com/danielvm/destiny2bot/annotation/ValidSignature.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.danielvm.destiny2bot.annotation; - -import com.danielvm.destiny2bot.validator.SignatureValidator; -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Target(ElementType.PARAMETER) -@Constraint(validatedBy = SignatureValidator.class) -public @interface ValidSignature { - - String message() default "Signature is invalid"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} diff --git a/src/main/java/com/danielvm/destiny2bot/client/BungieClient.java b/src/main/java/com/danielvm/destiny2bot/client/BungieClient.java index 3448b1b..a75d269 100644 --- a/src/main/java/com/danielvm/destiny2bot/client/BungieClient.java +++ b/src/main/java/com/danielvm/destiny2bot/client/BungieClient.java @@ -1,6 +1,11 @@ package com.danielvm.destiny2bot.client; -import com.danielvm.destiny2bot.dto.destiny.GenericResponse; +import com.danielvm.destiny2bot.dto.destiny.ActivitiesResponse; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; +import com.danielvm.destiny2bot.dto.destiny.MemberGroupResponse; +import com.danielvm.destiny2bot.dto.destiny.PostGameCarnageReport; +import com.danielvm.destiny2bot.dto.destiny.SearchResult; +import com.danielvm.destiny2bot.dto.destiny.UserGlobalSearchBody; import com.danielvm.destiny2bot.dto.destiny.characters.CharactersResponse; import com.danielvm.destiny2bot.dto.destiny.manifest.ResponseFields; import com.danielvm.destiny2bot.dto.destiny.membership.MembershipResponse; @@ -10,8 +15,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.PostExchange; import reactor.core.publisher.Mono; /** @@ -47,7 +55,7 @@ Mono getMembershipInfoForCurrentUser( * @return {@link Mono} of {@link ResponseFields} */ @GetExchange("/Destiny2/Manifest/{entityType}/{hashIdentifier}/") - Mono> getManifestEntityRx( + Mono> getManifestEntityRx( @PathVariable(value = "entityType") String entityType, @PathVariable(value = "hashIdentifier") String hashIdentifier); @@ -57,7 +65,7 @@ Mono> getManifestEntityRx( * @return {@link Mono} of Map of {@link MilestoneEntry} */ @GetExchange("/Destiny2/Milestones/") - Mono>> getPublicMilestonesRx(); + Mono>> getPublicMilestonesRx(); /** * Get a user characters @@ -67,9 +75,29 @@ Mono> getManifestEntityRx( * @return {@link Mono} containing {@link CharactersResponse} */ @GetExchange("/Destiny2/{membershipType}/Profile/{destinyMembershipId}/?components=200") - Mono> getUserCharacters( + Mono> getUserCharacters( @PathVariable Integer membershipType, @PathVariable String destinyMembershipId ); + @PostExchange("/User/Search/GlobalName/{page}/") + Mono> searchByGlobalName( + @RequestBody UserGlobalSearchBody searchBody, + @PathVariable Integer page); + + @GetExchange("/GroupV2/User/{membershipType}/{membershipId}/{filter}/{groupType}/") + Mono> getGroupsForMember( + @PathVariable Integer membershipType, @PathVariable String membershipId, + @PathVariable Integer filter, @PathVariable Integer groupType + ); + + @GetExchange("/Destiny2/{membershipType}/Account/{destinyMembershipId}/Character/{characterId}/Stats/Activities/") + Mono> getActivityHistory(@PathVariable Integer membershipType, + @PathVariable String destinyMembershipId, @PathVariable String characterId, + @RequestParam Integer count, @RequestParam Integer mode, @RequestParam Integer page); + + @GetExchange("/Destiny2/Stats/PostGameCarnageReport/{activityId}/") + Mono> getPostGameCarnageReport( + @PathVariable Long activityId + ); } diff --git a/src/main/java/com/danielvm/destiny2bot/client/BungieClientWrapper.java b/src/main/java/com/danielvm/destiny2bot/client/BungieClientWrapper.java index 5d6d60e..47d4c91 100644 --- a/src/main/java/com/danielvm/destiny2bot/client/BungieClientWrapper.java +++ b/src/main/java/com/danielvm/destiny2bot/client/BungieClientWrapper.java @@ -1,33 +1,34 @@ package com.danielvm.destiny2bot.client; -import com.danielvm.destiny2bot.dto.destiny.GenericResponse; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; import com.danielvm.destiny2bot.dto.destiny.manifest.ResponseFields; import com.danielvm.destiny2bot.enums.ManifestEntity; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @Service -@RequiredArgsConstructor @Slf4j public class BungieClientWrapper { - private final BungieClient bungieClient; + private final BungieClient defaultBungieClient; + + public BungieClientWrapper(BungieClient defaultBungieClient) { + this.defaultBungieClient = defaultBungieClient; + } /** * Wraps the client call to the Manifest with a Cacheable method * - * @param entityType The entity type (see - * {@link ManifestEntity}) + * @param entityType The entity type (see {@link ManifestEntity}) * @param hashIdentifier The hash identifier - * @return {@link GenericResponse} of {@link ResponseFields} + * @return {@link BungieResponse} of {@link ResponseFields} */ @Cacheable(cacheNames = "entity", cacheManager = "inMemoryCacheManager") - public Mono> getManifestEntityRx( + public Mono> getManifestEntityRx( ManifestEntity entityType, String hashIdentifier) { - return bungieClient.getManifestEntityRx(entityType.getId(), hashIdentifier).cache(); + return defaultBungieClient.getManifestEntityRx(entityType.getId(), hashIdentifier).cache(); } } diff --git a/src/main/java/com/danielvm/destiny2bot/client/DiscordClient.java b/src/main/java/com/danielvm/destiny2bot/client/DiscordClient.java index f0103ac..2f75acb 100644 --- a/src/main/java/com/danielvm/destiny2bot/client/DiscordClient.java +++ b/src/main/java/com/danielvm/destiny2bot/client/DiscordClient.java @@ -1,10 +1,14 @@ package com.danielvm.destiny2bot.client; import com.danielvm.destiny2bot.dto.discord.DiscordUserResponse; +import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.PatchExchange; import reactor.core.publisher.Mono; /** @@ -21,4 +25,10 @@ public interface DiscordClient { @GetExchange("/users/@me") Mono getUser( @RequestHeader(HttpHeaders.AUTHORIZATION) String bearerToken); + + @PatchExchange(value = "/webhooks/{applicationId}/{interactionToken}/messages/@original", contentType = MediaType.APPLICATION_JSON_VALUE) + Mono editOriginalInteraction( + @PathVariable Long applicationId, + @PathVariable String interactionToken, + @RequestBody InteractionResponseData data); } diff --git a/src/main/java/com/danielvm/destiny2bot/config/BungieConfiguration.java b/src/main/java/com/danielvm/destiny2bot/config/BungieConfiguration.java index 040363d..51323ba 100644 --- a/src/main/java/com/danielvm/destiny2bot/config/BungieConfiguration.java +++ b/src/main/java/com/danielvm/destiny2bot/config/BungieConfiguration.java @@ -5,6 +5,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; @@ -39,6 +43,11 @@ public class BungieConfiguration implements OAuth2Configuration { */ private String baseUrl; + /** + * Base URL for stats endpoint + */ + private String statsBaseUrl; + /** * Url for Bungie Token endpoint */ @@ -54,11 +63,29 @@ public class BungieConfiguration implements OAuth2Configuration { */ private String callbackUrl; - @Bean + @Bean("defaultBungieClient") public BungieClient bungieCharacterClient(WebClient.Builder builder) { var webClient = builder .baseUrl(this.baseUrl) .defaultHeader(API_KEY_HEADER_NAME, this.key) + .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs() + .maxInMemorySize(1024 * 1024)) + .build(); + return HttpServiceProxyFactory.builder() + .exchangeAdapter(WebClientAdapter.create(webClient)) + .build() + .createClient(BungieClient.class); + } + + @Bean(name = "pgcrBungieClient") + public BungieClient pgcrBungieClient(WebClient.Builder builder) { + var webClient = builder + .baseUrl(this.statsBaseUrl) + .defaultHeader(API_KEY_HEADER_NAME, this.key) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs() + .maxInMemorySize(1024 * 1024 * 15)) .build(); return HttpServiceProxyFactory.builder() .exchangeAdapter(WebClientAdapter.create(webClient)) diff --git a/src/main/java/com/danielvm/destiny2bot/config/DiscordConfiguration.java b/src/main/java/com/danielvm/destiny2bot/config/DiscordConfiguration.java index 9cedd50..d3c7352 100644 --- a/src/main/java/com/danielvm/destiny2bot/config/DiscordConfiguration.java +++ b/src/main/java/com/danielvm/destiny2bot/config/DiscordConfiguration.java @@ -60,6 +60,11 @@ public class DiscordConfiguration implements OAuth2Configuration { */ private List scopes; + /** + * The applicationId for the discord bot + */ + private Long applicationId; + @Bean public DiscordClient discordClient(WebClient.Builder defaultBuilder) { var webClient = defaultBuilder diff --git a/src/main/java/com/danielvm/destiny2bot/config/FlywayConfiguration.java b/src/main/java/com/danielvm/destiny2bot/config/FlywayConfiguration.java deleted file mode 100644 index cc8d85e..0000000 --- a/src/main/java/com/danielvm/destiny2bot/config/FlywayConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.danielvm.destiny2bot.config; - -import org.flywaydb.core.Flyway; -import org.springframework.boot.autoconfigure.flyway.FlywayProperties; -import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties({R2dbcProperties.class, FlywayProperties.class}) -public class FlywayConfiguration { - - @Bean(initMethod = "migrate") - public Flyway flyway(FlywayProperties flywayProperties, R2dbcProperties r2dbcProperties) { - return Flyway.configure() - .dataSource( - flywayProperties.getUrl(), - r2dbcProperties.getUsername(), - r2dbcProperties.getPassword()) - .locations(flywayProperties.getLocations().get(0)) - .baselineOnMigrate(true) - .load(); - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/controller/InteractionsController.java b/src/main/java/com/danielvm/destiny2bot/controller/InteractionsController.java index 25c856c..55069cf 100644 --- a/src/main/java/com/danielvm/destiny2bot/controller/InteractionsController.java +++ b/src/main/java/com/danielvm/destiny2bot/controller/InteractionsController.java @@ -2,11 +2,12 @@ import static com.danielvm.destiny2bot.util.HttpUtil.prepareMultipartPayload; -import com.danielvm.destiny2bot.annotation.ValidSignature; import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionResponse; import com.danielvm.destiny2bot.service.ImageAssetService; import com.danielvm.destiny2bot.service.InteractionService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -19,7 +20,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.util.ContentCachingRequestWrapper; import reactor.core.publisher.Mono; @Slf4j @@ -29,12 +29,14 @@ public class InteractionsController { private final InteractionService interactionService; private final ImageAssetService imageAssetService; + private final ObjectMapper objectMapper; public InteractionsController( InteractionService interactionService, - ImageAssetService imageAssetService) { + ImageAssetService imageAssetService, ObjectMapper objectMapper) { this.interactionService = interactionService; this.imageAssetService = imageAssetService; + this.objectMapper = objectMapper; } /** @@ -47,9 +49,7 @@ public InteractionsController( * corresponding bytes, else an {@link InteractionResponse} */ @PostMapping("/interactions") - public Mono> interactions( - @RequestBody Interaction interaction, - @ValidSignature ContentCachingRequestWrapper request) { + public Mono> interactions(@RequestBody Interaction interaction) { return interactionService.handleInteraction(interaction) .flatMap(response -> { boolean containsAttachments = response.getType() != 1 && CollectionUtils.isNotEmpty( @@ -57,8 +57,14 @@ public Mono> interactions( return containsAttachments ? multipartFormResponse(interaction, response) : Mono.just(ResponseEntity.ok(response)); }) - .doOnSubscribe(i -> log.debug("Received interaction: [{}]", interaction)) - .doOnSuccess(i -> log.debug("Completed interaction: [{}]", i)); + .doOnSubscribe(i -> log.info("Received interaction: [{}]", interaction)) + .doOnSuccess(i -> { + try { + log.info("Completed interaction: [{}]", objectMapper.writeValueAsString(i)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); } private Mono>>> multipartFormResponse( diff --git a/src/main/java/com/danielvm/destiny2bot/controller/RegistrationController.java b/src/main/java/com/danielvm/destiny2bot/controller/RegistrationController.java deleted file mode 100644 index f5ffbc2..0000000 --- a/src/main/java/com/danielvm/destiny2bot/controller/RegistrationController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.danielvm.destiny2bot.controller; - -import com.danielvm.destiny2bot.service.UserRegistrationService; -import com.danielvm.destiny2bot.util.OAuth2Params; -import jakarta.servlet.http.HttpSession; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Mono; - -@Slf4j -@RestController -public class RegistrationController { - - private final UserRegistrationService userRegistrationService; - - public RegistrationController( - UserRegistrationService userRegistrationService) { - this.userRegistrationService = userRegistrationService; - } - - /** - * Handle the callback from Discord during OAuth2 authentication - * - * @param authorizationCode the authentication code (short-lived) - * @return Redirect to start Bungie OAuth2 - */ - @GetMapping("/discord/callback") - public Mono> handleCallBackFromDiscord( - @RequestParam(OAuth2Params.CODE) String authorizationCode, - HttpSession httpSession) { - return userRegistrationService - .authenticateDiscordUser(authorizationCode, httpSession); - } - - /** - * Handle the callback from Bungie during OAuth2 authentication - * - * @param authorizationCode the authentication code (short-lived) - * @return Redirect to start Bungie OAuth2 - */ - @GetMapping("/bungie/callback") - public Mono> handleCallBackFromBungie( - @RequestParam(OAuth2Params.CODE) String authorizationCode, - HttpSession httpSession) { - return userRegistrationService - .linkDiscordUserToBungieAccount(authorizationCode, httpSession); - } - -} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/RaidEntry.java b/src/main/java/com/danielvm/destiny2bot/dto/RaidEntry.java new file mode 100644 index 0000000..f2422e6 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/RaidEntry.java @@ -0,0 +1,27 @@ +package com.danielvm.destiny2bot.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RaidEntry { + + private String raidName; + + private Long instanceId; + + private Integer totalDeaths; + + private Integer totalKills; + + private Double kda; + + private Integer duration; + + private Boolean isCompleted; + + private Boolean isFromBeginning; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivitiesResponse.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivitiesResponse.java new file mode 100644 index 0000000..65b5b70 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivitiesResponse.java @@ -0,0 +1,14 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ActivitiesResponse { + + private List activities; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/Activity.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Activity.java new file mode 100644 index 0000000..18a25a6 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Activity.java @@ -0,0 +1,19 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.time.ZonedDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Activity { + + private ZonedDateTime period; + + private ActivityDetails activityDetails; + + private Map values; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityDetails.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityDetails.java new file mode 100644 index 0000000..1472d6e --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityDetails.java @@ -0,0 +1,17 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ActivityDetails { + + private Long directorActivityHash; + + private Long instanceId; + + private Integer mode; +} diff --git a/src/main/java/com/danielvm/destiny2bot/entity/UserCharacter.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityValue.java similarity index 51% rename from src/main/java/com/danielvm/destiny2bot/entity/UserCharacter.java rename to src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityValue.java index 45f735a..2750f9f 100644 --- a/src/main/java/com/danielvm/destiny2bot/entity/UserCharacter.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ActivityValue.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.entity; +package com.danielvm.destiny2bot.dto.destiny; import lombok.AllArgsConstructor; import lombok.Data; @@ -7,13 +7,11 @@ @Data @AllArgsConstructor @NoArgsConstructor -public class UserCharacter { +public class ActivityValue { - private Long characterId; + private DestinyUserInfo destinyUserInfo; - private Integer lightLevel; - - private String destinyClass; + private String characterClass; - private Long discordUserId; + private Integer lightLevel; } diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/Basic.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Basic.java new file mode 100644 index 0000000..903d2d0 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Basic.java @@ -0,0 +1,15 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Basic { + + private Double value; + + private String displayValue; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/GenericResponse.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/BungieResponse.java similarity index 53% rename from src/main/java/com/danielvm/destiny2bot/dto/destiny/GenericResponse.java rename to src/main/java/com/danielvm/destiny2bot/dto/destiny/BungieResponse.java index fbd5866..256f0bf 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/destiny/GenericResponse.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/BungieResponse.java @@ -6,15 +6,16 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Most of the responses from Bungie.net have a Json element named 'Response' with arbitrary info + * depending on the endpoint. This field is just a generic-wrapper for it. It also includes some + * pretty generic info regarding Bungie's APIs. + */ @Data @AllArgsConstructor @NoArgsConstructor -public class GenericResponse { +public class BungieResponse { - /** - * Most of the responses from Bungie.net have a Json element named 'Response' with arbitrary info - * depending on the endpoint. This field is just a generic-wrapper for it. - */ @JsonAlias("Response") @Nullable private T response; diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/DestinyUserInfo.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/DestinyUserInfo.java new file mode 100644 index 0000000..dd4db0b --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/DestinyUserInfo.java @@ -0,0 +1,25 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class DestinyUserInfo { + + private String iconPath; + + private Integer membershipType; + + private Long membershipId; + + private String displayName; + + private Boolean isPublic; + + private String bungieGlobalDisplayName; + + private Integer bungieGlobalDisplayNameCode; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/Group.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Group.java new file mode 100644 index 0000000..f47d2f9 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/Group.java @@ -0,0 +1,16 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Group { + + private String groupId; + + private String name; + +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/GroupResult.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/GroupResult.java new file mode 100644 index 0000000..46fc15b --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/GroupResult.java @@ -0,0 +1,13 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GroupResult { + + Group group; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/MemberGroupResponse.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/MemberGroupResponse.java new file mode 100644 index 0000000..a3f89ad --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/MemberGroupResponse.java @@ -0,0 +1,14 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class MemberGroupResponse { + + private List results; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/PGCREntry.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PGCREntry.java new file mode 100644 index 0000000..f007425 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PGCREntry.java @@ -0,0 +1,18 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PGCREntry { + + private Integer standing; + + private PlayerPGCREntry player; + + private Map values; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/PlayerPGCREntry.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PlayerPGCREntry.java new file mode 100644 index 0000000..89242df --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PlayerPGCREntry.java @@ -0,0 +1,17 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PlayerPGCREntry { + + private DestinyUserInfo destinyUserInfo; + + private String characterClass; + + private Integer lightLevel; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/PostGameCarnageReport.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PostGameCarnageReport.java new file mode 100644 index 0000000..285a046 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/PostGameCarnageReport.java @@ -0,0 +1,19 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.time.Instant; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostGameCarnageReport { + + private Instant period; + + private Boolean activityWasStartedFromBeginning; + + private List entries; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/RaidStatistics.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/RaidStatistics.java new file mode 100644 index 0000000..6e3c080 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/RaidStatistics.java @@ -0,0 +1,88 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RaidStatistics { + + /** + * The name of the raid + */ + private String raidName; + + /** + * Total amount of kills done for a raid + */ + private Integer totalKills; + + /** + * Total amount of deaths done in a raid + */ + private Integer totalDeaths; + + /** + * The fastest time a player has done in a raid + */ + private Integer fastestTime; + + /** + * The average time a player finishes the raid + */ + private Integer averageTime; + + /** + * The number of completed raids that user has for a specific raid + */ + private Integer partialClears; + + /** + * The total amount of times a player has played this raid + */ + private Integer totalClears; + + /** + * The total amount of full clears for a raid + */ + private Integer fullClears; + + public RaidStatistics(String raidName) { + this.raidName = raidName; + this.totalKills = 0; + this.totalDeaths = 0; + this.fastestTime = Integer.MAX_VALUE; + this.averageTime = 0; + this.partialClears = 0; + this.totalClears = 0; + this.fullClears = 0; + } + + public String toString() { + StringBuilder fastestRaidDuration = new StringBuilder(); + int hours = (fastestTime / 3600) % 24; + int minutes = (fastestTime / 60) % 60; + if (hours > 0) { + fastestRaidDuration.append(hours).append("hr(s)").append(" "); + } + if (minutes > 0) { + fastestRaidDuration.append(minutes).append("mins"); + } + StringBuilder raidTemplate = new StringBuilder(); + raidTemplate.append(":crossed_swords: ").append("Kills: ").append(this.totalKills) + .append("\n"); + raidTemplate.append(":skull_crossbones: ").append("Deaths: ").append(this.totalDeaths) + .append("\n"); + if (!fastestRaidDuration.isEmpty()) { + raidTemplate.append(":first_place: ").append("Fastest: ").append(fastestRaidDuration) + .append("\n"); + } + raidTemplate.append(":bar_chart: ").append("Total Clears: ").append(this.totalClears) + .append("\n"); + raidTemplate.append(":trophy: ").append("Full Clears: ").append(this.fullClears) + .append("\n"); + return raidTemplate.toString(); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/SearchResult.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/SearchResult.java new file mode 100644 index 0000000..5b09215 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/SearchResult.java @@ -0,0 +1,18 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SearchResult { + + private List searchResults; + + private Integer page; + + private Boolean hasMore; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserGlobalSearchBody.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserGlobalSearchBody.java new file mode 100644 index 0000000..21b7aa5 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserGlobalSearchBody.java @@ -0,0 +1,13 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserGlobalSearchBody { + + private String displayNamePrefix; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserSearchResult.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserSearchResult.java new file mode 100644 index 0000000..5c2aa82 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/UserSearchResult.java @@ -0,0 +1,21 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import com.danielvm.destiny2bot.dto.destiny.membership.DestinyMembershipData; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserSearchResult { + + private String bungieGlobalDisplayName; + + private Integer bungieGlobalDisplayNameCode; + + private String bungieNetMembershipId; + + private List destinyMemberships; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/destiny/ValueEntry.java b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ValueEntry.java new file mode 100644 index 0000000..cbac9ff --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/destiny/ValueEntry.java @@ -0,0 +1,15 @@ +package com.danielvm.destiny2bot.dto.destiny; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ValueEntry { + + private String statId; + + private Basic basic; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/Channel.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/Channel.java new file mode 100644 index 0000000..5937677 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/Channel.java @@ -0,0 +1,23 @@ +package com.danielvm.destiny2bot.dto.discord; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Channel { + + /** + * ID of the channel + */ + private Long id; + + /** + * ID of the channel the interaction was sent from + */ + @JsonProperty("channel_id") + private Long channelId; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/Embedded.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/Embedded.java index 080024e..ef54a4c 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/discord/Embedded.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/Embedded.java @@ -34,7 +34,7 @@ public class Embedded { private Object video; - private Object provider; + private EmbeddedProvider provider; private EmbeddedAuthor author; diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/EmbeddedProvider.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/EmbeddedProvider.java new file mode 100644 index 0000000..1b9e9da --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/EmbeddedProvider.java @@ -0,0 +1,17 @@ +package com.danielvm.destiny2bot.dto.discord; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class EmbeddedProvider { + + private String name; + + private String url; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/Interaction.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/Interaction.java index 1914a96..d887496 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/discord/Interaction.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/Interaction.java @@ -20,7 +20,7 @@ public class Interaction implements Serializable { /** * The Id of the interaction */ - private Object id; + private Long id; /** * The Id of the application @@ -42,4 +42,9 @@ public class Interaction implements Serializable { * Member Data of the user that invoked the command */ private Member member; + + /** + * Continuation token + */ + private String token; } diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionData.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionData.java index fb7dcb9..9c50c33 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionData.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionData.java @@ -50,4 +50,6 @@ public class InteractionData { */ private List values; + private ResolvedData resolved; + } diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponse.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponse.java index 1f7cb82..8f5215d 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponse.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponse.java @@ -1,6 +1,7 @@ package com.danielvm.destiny2bot.dto.discord; import com.danielvm.destiny2bot.enums.InteractionResponseType; +import java.io.Serializable; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -10,7 +11,7 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class InteractionResponse { +public class InteractionResponse implements Serializable { /** * The type of the InteractionResponse diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponseData.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponseData.java index 98fa88d..8c33901 100644 --- a/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponseData.java +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/InteractionResponseData.java @@ -1,5 +1,6 @@ package com.danielvm.destiny2bot.dto.discord; +import java.io.Serializable; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; @@ -10,7 +11,7 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class InteractionResponseData { +public class InteractionResponseData implements Serializable { /** * The message content of the InteractionResponse diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/Message.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/Message.java new file mode 100644 index 0000000..ce3b66f --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/Message.java @@ -0,0 +1,21 @@ +package com.danielvm.destiny2bot.dto.discord; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Message { + + /** + * ID of the message + */ + private Long id; + + /** + * ID of the channel where the message was sent + */ + private Long channelId; +} diff --git a/src/main/java/com/danielvm/destiny2bot/dto/discord/ResolvedData.java b/src/main/java/com/danielvm/destiny2bot/dto/discord/ResolvedData.java new file mode 100644 index 0000000..e4969a0 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/dto/discord/ResolvedData.java @@ -0,0 +1,6 @@ +package com.danielvm.destiny2bot.dto.discord; + +public class ResolvedData { + + +} diff --git a/src/main/java/com/danielvm/destiny2bot/entity/BotUser.java b/src/main/java/com/danielvm/destiny2bot/entity/BotUser.java deleted file mode 100644 index eddbeaa..0000000 --- a/src/main/java/com/danielvm/destiny2bot/entity/BotUser.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.danielvm.destiny2bot.entity; - -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.Transient; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.Table; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class BotUser { - - private Long discordId; - - private String discordUsername; - - private Long bungieMembershipId; - - private String bungieAccessToken; - - private String bungieRefreshToken; - - private Long bungieTokenExpiration; - - private List characters; -} diff --git a/src/main/java/com/danielvm/destiny2bot/entity/CharacterRaid.java b/src/main/java/com/danielvm/destiny2bot/entity/CharacterRaid.java deleted file mode 100644 index 07cd113..0000000 --- a/src/main/java/com/danielvm/destiny2bot/entity/CharacterRaid.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.danielvm.destiny2bot.entity; - -import java.time.Instant; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CharacterRaid { - - private Long instanceId; - - private Instant raidStartTimestamp; - - private Boolean isFromBeginning; - - private Boolean completed; - - private String raidName; - - private Integer numberOfDeaths; - - private Integer opponentsDefeated; - - private Double kda; - - private Integer raidDuration; - - private Long userCharacterId; - - private List participants; -} diff --git a/src/main/java/com/danielvm/destiny2bot/entity/RaidParticipant.java b/src/main/java/com/danielvm/destiny2bot/entity/RaidParticipant.java deleted file mode 100644 index fd4b89c..0000000 --- a/src/main/java/com/danielvm/destiny2bot/entity/RaidParticipant.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.danielvm.destiny2bot.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class RaidParticipant { - - private Long membershipId; - - private String username; - - private String characterClass; - - private String iconPath; - - private Boolean completed; - - private Long raidInstance; -} diff --git a/src/main/java/com/danielvm/destiny2bot/entity/UserPageInformation.java b/src/main/java/com/danielvm/destiny2bot/entity/UserPageInformation.java deleted file mode 100644 index 2453800..0000000 --- a/src/main/java/com/danielvm/destiny2bot/entity/UserPageInformation.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.danielvm.destiny2bot.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserPageInformation { - - private Long userDiscordId; - - private Integer numberOfPages; - - private Integer lastPageCount; -} diff --git a/src/main/java/com/danielvm/destiny2bot/enums/RaidEncounter.java b/src/main/java/com/danielvm/destiny2bot/enums/RaidEncounter.java index f6973c1..c382a86 100644 --- a/src/main/java/com/danielvm/destiny2bot/enums/RaidEncounter.java +++ b/src/main/java/com/danielvm/destiny2bot/enums/RaidEncounter.java @@ -1,10 +1,11 @@ package com.danielvm.destiny2bot.enums; +import lombok.Getter; +import reactor.core.publisher.Flux; + import java.util.Arrays; import java.util.List; import java.util.Objects; -import lombok.Getter; -import reactor.core.publisher.Flux; public enum RaidEncounter { @@ -62,7 +63,7 @@ public enum RaidEncounter { DAUGHTERS(Raid.KINGS_FALL, "Daughter's of Oryx", "daughters"), ORYX(Raid.KINGS_FALL, "Oryx, the Taken King", "oryx"), - // Crota's End + // Crota's STILLS(Raid.CROTAS_END, "The Stills", "stills"), BRIDGE(Raid.CROTAS_END, "The Bridge", "bridge"), IR_YUT(Raid.CROTAS_END, "Ir Yut, the Deathsinger", "ir_yut"), diff --git a/src/main/java/com/danielvm/destiny2bot/enums/SlashCommand.java b/src/main/java/com/danielvm/destiny2bot/enums/SlashCommand.java index 8fb2ae1..b2f5cb3 100644 --- a/src/main/java/com/danielvm/destiny2bot/enums/SlashCommand.java +++ b/src/main/java/com/danielvm/destiny2bot/enums/SlashCommand.java @@ -10,7 +10,8 @@ public enum SlashCommand { WEEKLY_DUNGEON("weekly_dungeon", false), WEEKLY_RAID("weekly_raid", false), RAID_STATS("raid_stats", true), - RAID_MAP("raid_map", false); + RAID_MAP("raid_map", false), + EXPERIMENTAL_RAID_STATS("experimental_raid_stats", false); @Getter private final String commandName; diff --git a/src/main/java/com/danielvm/destiny2bot/exception/InvalidSignatureException.java b/src/main/java/com/danielvm/destiny2bot/exception/InvalidSignatureException.java index 7546a64..8eb78b9 100644 --- a/src/main/java/com/danielvm/destiny2bot/exception/InvalidSignatureException.java +++ b/src/main/java/com/danielvm/destiny2bot/exception/InvalidSignatureException.java @@ -11,4 +11,8 @@ public class InvalidSignatureException extends BaseException { public InvalidSignatureException(String message, Throwable throwable) { super(message, HttpStatus.BAD_REQUEST, throwable); } + + public InvalidSignatureException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } } diff --git a/src/main/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactory.java b/src/main/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactory.java index 238a3a5..f28b233 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactory.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactory.java @@ -2,11 +2,12 @@ import com.danielvm.destiny2bot.enums.SlashCommand; import com.danielvm.destiny2bot.exception.ResourceNotFoundException; -import com.danielvm.destiny2bot.factory.creator.ApplicationCommandSource; -import com.danielvm.destiny2bot.factory.creator.AuthorizeMessageCreator; -import com.danielvm.destiny2bot.factory.creator.RaidMapMessageCreator; -import com.danielvm.destiny2bot.factory.creator.WeeklyDungeonMessageCreator; -import com.danielvm.destiny2bot.factory.creator.WeeklyRaidMessageCreator; +import com.danielvm.destiny2bot.factory.handler.ApplicationCommandSource; +import com.danielvm.destiny2bot.factory.handler.AuthorizeHandler; +import com.danielvm.destiny2bot.factory.handler.RaidStatsHandler; +import com.danielvm.destiny2bot.factory.handler.RaidMapHandler; +import com.danielvm.destiny2bot.factory.handler.WeeklyDungeonHandler; +import com.danielvm.destiny2bot.factory.handler.WeeklyRaidHandler; import java.util.Map; import java.util.Objects; import org.springframework.stereotype.Component; @@ -16,20 +17,22 @@ * their corresponding message creation services. */ @Component -public class ApplicationCommandFactory implements InteractionFactory { +public class ApplicationCommandFactory implements SlashCommandHandler { private final Map messageFactory; public ApplicationCommandFactory( - RaidMapMessageCreator raidMapMessageCreator, - WeeklyRaidMessageCreator weeklyRaidMessageCreator, - WeeklyDungeonMessageCreator weeklyDungeonMessageCreator, - AuthorizeMessageCreator authorizeMessageCreator) { + RaidMapHandler raidMapHandler, + WeeklyRaidHandler weeklyRaidHandler, + WeeklyDungeonHandler weeklyDungeonHandler, + AuthorizeHandler authorizeMessageHandler, + RaidStatsHandler raidStatsHandler) { this.messageFactory = Map.of( - SlashCommand.WEEKLY_RAID, weeklyRaidMessageCreator, - SlashCommand.WEEKLY_DUNGEON, weeklyDungeonMessageCreator, - SlashCommand.AUTHORIZE, authorizeMessageCreator, - SlashCommand.RAID_MAP, raidMapMessageCreator); + SlashCommand.WEEKLY_RAID, weeklyRaidHandler, + SlashCommand.WEEKLY_DUNGEON, weeklyDungeonHandler, + SlashCommand.AUTHORIZE, authorizeMessageHandler, + SlashCommand.RAID_MAP, raidMapHandler, + SlashCommand.EXPERIMENTAL_RAID_STATS, raidStatsHandler); } @Override diff --git a/src/main/java/com/danielvm/destiny2bot/factory/AutocompleteFactory.java b/src/main/java/com/danielvm/destiny2bot/factory/AutocompleteFactory.java index 2e47faa..a288013 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/AutocompleteFactory.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/AutocompleteFactory.java @@ -2,24 +2,24 @@ import com.danielvm.destiny2bot.enums.SlashCommand; import com.danielvm.destiny2bot.exception.ResourceNotFoundException; -import com.danielvm.destiny2bot.factory.creator.AutocompleteSource; -import com.danielvm.destiny2bot.factory.creator.RaidMapMessageCreator; -import com.danielvm.destiny2bot.factory.creator.RaidStatsMessageCreator; +import com.danielvm.destiny2bot.factory.handler.AutocompleteSource; +import com.danielvm.destiny2bot.factory.handler.RaidMapHandler; +import com.danielvm.destiny2bot.factory.handler.RaidStatsHandler; import java.util.Map; import java.util.Objects; import org.springframework.stereotype.Component; @Component -public class AutocompleteFactory implements InteractionFactory { +public class AutocompleteFactory implements SlashCommandHandler { private final Map autocompleteFactory; public AutocompleteFactory( - RaidStatsMessageCreator raidStatsMessageCreator, - RaidMapMessageCreator raidMapMessageCreator) { + RaidMapHandler raidMapHandler, + RaidStatsHandler raidStatsHandler) { this.autocompleteFactory = Map.of( - SlashCommand.RAID_STATS, raidStatsMessageCreator, - SlashCommand.RAID_MAP, raidMapMessageCreator); + SlashCommand.RAID_MAP, raidMapHandler, + SlashCommand.EXPERIMENTAL_RAID_STATS, raidStatsHandler); } @Override diff --git a/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentFactory.java b/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentFactory.java new file mode 100644 index 0000000..4151425 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentFactory.java @@ -0,0 +1,22 @@ +package com.danielvm.destiny2bot.factory; + +import com.danielvm.destiny2bot.factory.handler.MessageComponentSource; +import com.danielvm.destiny2bot.factory.handler.RaidStatsButtonHandler; +import java.util.Map; +import org.springframework.stereotype.Component; + +@Component +public class MessageComponentFactory implements MessageComponentHandler { + + private final Map messageComponentFactory; + + public MessageComponentFactory( + RaidStatsButtonHandler raidStatsButtonHandler) { + messageComponentFactory = Map.of("raid_stats_comprehension", raidStatsButtonHandler); + } + + @Override + public MessageComponentSource handle(String componentId) { + return messageComponentFactory.get(componentId); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentHandler.java b/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentHandler.java new file mode 100644 index 0000000..6fca557 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/factory/MessageComponentHandler.java @@ -0,0 +1,12 @@ +package com.danielvm.destiny2bot.factory; + +public interface MessageComponentHandler { + + /** + * Return a message component of type T based on a componentId + * + * @param componentId The componentId of the button + * @return Type of the button handler + */ + T handle(String componentId); +} diff --git a/src/main/java/com/danielvm/destiny2bot/factory/InteractionFactory.java b/src/main/java/com/danielvm/destiny2bot/factory/SlashCommandHandler.java similarity index 71% rename from src/main/java/com/danielvm/destiny2bot/factory/InteractionFactory.java rename to src/main/java/com/danielvm/destiny2bot/factory/SlashCommandHandler.java index fb2f3c1..4a4afa9 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/InteractionFactory.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/SlashCommandHandler.java @@ -2,10 +2,10 @@ import com.danielvm.destiny2bot.enums.SlashCommand; -public interface InteractionFactory { +public interface SlashCommandHandler { /** - * Return a message creator of type T based on a slash-command + * Return a handler of type T based on a slash-command * * @param slashCommand The slash command that is invoked * @return Message creator of type T diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/RaidStatsMessageCreator.java b/src/main/java/com/danielvm/destiny2bot/factory/creator/RaidStatsMessageCreator.java deleted file mode 100644 index fd9b2d5..0000000 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/RaidStatsMessageCreator.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.danielvm.destiny2bot.factory.creator; - -import com.danielvm.destiny2bot.dto.discord.Choice; -import com.danielvm.destiny2bot.dto.discord.Interaction; -import com.danielvm.destiny2bot.dto.discord.InteractionResponse; -import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; -import com.danielvm.destiny2bot.service.DestinyCharacterService; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -@Component -public class RaidStatsMessageCreator implements ApplicationCommandSource, - AutocompleteSource { - - private static final String CHOICE_FORMAT = "[%s] %s - %s"; - private final DestinyCharacterService destinyCharacterService; - - public RaidStatsMessageCreator( - DestinyCharacterService destinyCharacterService) { - this.destinyCharacterService = destinyCharacterService; - } - - @Override - public Mono createResponse(Interaction interaction) { - return null; - } - - @Override - public Mono autocompleteResponse(Interaction interaction) { - String userId = interaction.getMember().getUser().getId(); - return destinyCharacterService.getCharactersForUser(userId) - .map(character -> new Choice(CHOICE_FORMAT.formatted( - character.getLightLevel(), character.getCharacterRace(), character.getCharacterClass()), - character.getCharacterId())) - .collectList() - .map(choices -> { - if (choices.size() > 1) { - choices.add(new Choice("All", "Gets stats for all characters")); - } - return new InteractionResponse(8, InteractionResponseData.builder() - .choices(choices) - .build()); - }); - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/ApplicationCommandSource.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/ApplicationCommandSource.java similarity index 92% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/ApplicationCommandSource.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/ApplicationCommandSource.java index eb9c30d..40dfd16 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/ApplicationCommandSource.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/ApplicationCommandSource.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionResponse; diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/AuthorizeMessageCreator.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/AuthorizeHandler.java similarity index 88% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/AuthorizeMessageCreator.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/AuthorizeHandler.java index 9f53abf..216be6e 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/AuthorizeMessageCreator.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/AuthorizeHandler.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; @@ -8,12 +8,13 @@ import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionResponse; import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; +import com.danielvm.destiny2bot.util.MessageUtil; import com.danielvm.destiny2bot.util.OAuth2Util; import java.util.List; import reactor.core.publisher.Mono; @org.springframework.stereotype.Component -public class AuthorizeMessageCreator implements ApplicationCommandSource { +public class AuthorizeHandler implements ApplicationCommandSource { public static final String MESSAGE_TITLE = "**Link Bungie and Discord accounts here**"; public static final String MESSAGE_DESCRIPTION = """ @@ -21,10 +22,9 @@ public class AuthorizeMessageCreator implements ApplicationCommandSource { However, in order for her to do that you must authorize her to read a sub-set of your Destiny 2 data beforehand. """; - private static final Integer EPHEMERAL_BYTE = 1000000; private final DiscordConfiguration discordConfiguration; - public AuthorizeMessageCreator(DiscordConfiguration discordConfiguration) { + public AuthorizeHandler(DiscordConfiguration discordConfiguration) { this.discordConfiguration = discordConfiguration; } @@ -44,7 +44,7 @@ public Mono createResponse(Interaction interaction) { .type(CHANNEL_MESSAGE_WITH_SOURCE.getType()) .data(InteractionResponseData.builder() .embeds(List.of(accountLinkEmbed)) - .flags(EPHEMERAL_BYTE) + .flags(MessageUtil.EPHEMERAL_BYTE) .components( List.of(Component.builder() .type(1) @@ -61,8 +61,7 @@ public Mono createResponse(Interaction interaction) { .label("Why?") .type(2) .style(1) - .build()) - ) + .build())) .build())) .build()) .build()); diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/AutocompleteSource.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/AutocompleteSource.java similarity index 93% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/AutocompleteSource.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/AutocompleteSource.java index ed5b8cc..f3f1c27 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/AutocompleteSource.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/AutocompleteSource.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionResponse; diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/MessageComponentSource.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/MessageComponentSource.java similarity index 83% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/MessageComponentSource.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/MessageComponentSource.java index 82e5ab5..44cc929 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/MessageComponentSource.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/MessageComponentSource.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionResponse; @@ -17,5 +17,5 @@ public interface MessageComponentSource { * @param interaction Interaction data in-case the message component response needs context * @return {@link InteractionResponse} */ - Mono messageComponentResponse(Interaction interaction); + Mono respond(Interaction interaction); } diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/RaidMapMessageCreator.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidMapHandler.java similarity index 96% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/RaidMapMessageCreator.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/RaidMapHandler.java index 26dfa0a..2ece280 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/RaidMapMessageCreator.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidMapHandler.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import com.danielvm.destiny2bot.dto.discord.Attachment; import com.danielvm.destiny2bot.dto.discord.Choice; @@ -29,7 +29,7 @@ @Slf4j @org.springframework.stereotype.Component -public class RaidMapMessageCreator implements ApplicationCommandSource, AutocompleteSource { +public class RaidMapHandler implements ApplicationCommandSource, AutocompleteSource { private static final String EMBED_BINDING_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; private static final String RAID_OPTION_NAME = "raid"; @@ -37,7 +37,7 @@ public class RaidMapMessageCreator implements ApplicationCommandSource, Autocomp private final ImageAssetService imageAssetService; - public RaidMapMessageCreator( + public RaidMapHandler( ImageAssetService imageAssetService) { this.imageAssetService = imageAssetService; } @@ -110,7 +110,7 @@ private static List extractAttachments(Map map) { public Mono createResponse(Interaction interaction) { try { return imageAssetService.retrieveEncounterImages(interaction) - .map(RaidMapMessageCreator::extractAttachments) + .map(RaidMapHandler::extractAttachments) .map(attachments -> formatInteractionResponse(interaction, attachments)); } catch (IOException e) { String raidName = InteractionUtil.retrieveInteractionOption(interaction.getData() diff --git a/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsButtonHandler.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsButtonHandler.java new file mode 100644 index 0000000..58844a1 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsButtonHandler.java @@ -0,0 +1,32 @@ +package com.danielvm.destiny2bot.factory.handler; + +import com.danielvm.destiny2bot.dto.discord.Interaction; +import com.danielvm.destiny2bot.dto.discord.InteractionResponse; +import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; +import com.danielvm.destiny2bot.enums.InteractionResponseType; +import com.danielvm.destiny2bot.util.MessageUtil; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class RaidStatsButtonHandler implements MessageComponentSource { + + private final static String RAID_STATS_EXPLANATION = """ + These aggregated raid statistics represent some crunched numbers Bungie has put out for you \ + such as total kills and total deaths in a raid. All the stats you are seeing right now are \ + an aggregated total throughout **all your characters for a single raid**. + + Please note that the ":first_place: fastest" stat as of now is calculated only on full raid clears. \ + That means raids that did not start from the beginning **do not count**."""; + + @Override + public Mono respond(Interaction interaction) { + return Mono.just(InteractionResponse.builder() + .type(InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE.getType()) + .data(InteractionResponseData.builder() + .content(RAID_STATS_EXPLANATION) + .flags(MessageUtil.EPHEMERAL_BYTE) + .build()) + .build()); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsHandler.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsHandler.java new file mode 100644 index 0000000..045464c --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/RaidStatsHandler.java @@ -0,0 +1,159 @@ +package com.danielvm.destiny2bot.factory.handler; + +import com.danielvm.destiny2bot.client.BungieClient; +import com.danielvm.destiny2bot.client.DiscordClient; +import com.danielvm.destiny2bot.config.DiscordConfiguration; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; +import com.danielvm.destiny2bot.dto.destiny.MemberGroupResponse; +import com.danielvm.destiny2bot.dto.destiny.UserGlobalSearchBody; +import com.danielvm.destiny2bot.dto.destiny.UserSearchResult; +import com.danielvm.destiny2bot.dto.discord.Choice; +import com.danielvm.destiny2bot.dto.discord.Component; +import com.danielvm.destiny2bot.dto.discord.Embedded; +import com.danielvm.destiny2bot.dto.discord.EmbeddedAuthor; +import com.danielvm.destiny2bot.dto.discord.EmbeddedField; +import com.danielvm.destiny2bot.dto.discord.EmbeddedFooter; +import com.danielvm.destiny2bot.dto.discord.Interaction; +import com.danielvm.destiny2bot.dto.discord.InteractionResponse; +import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; +import com.danielvm.destiny2bot.enums.InteractionResponseType; +import com.danielvm.destiny2bot.service.RaidStatsService; +import java.time.Instant; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@Slf4j +@org.springframework.stereotype.Component +public class RaidStatsHandler implements AutocompleteSource, ApplicationCommandSource { + + private static final String STATS_TITLE = "Raid Stats for %s"; + private static final String CHOICE_VALUE_TEMPLATE = "%s:%s:%s"; + + private final BungieClient defaultBungieClient; + private final DiscordClient discordClient; + private final RaidStatsService raidStatsService; + private final DiscordConfiguration discordConfiguration; + + public RaidStatsHandler( + BungieClient defaultBungieClient, + DiscordClient discordClient, + RaidStatsService raidStatsService, + DiscordConfiguration discordConfiguration) { + this.defaultBungieClient = defaultBungieClient; + this.discordClient = discordClient; + this.raidStatsService = raidStatsService; + this.discordConfiguration = discordConfiguration; + } + + private static StringBuilder buildChoiceDefaultName(UserSearchResult result, + BungieResponse groupResponse) { + StringBuilder defaultName = new StringBuilder() + .append(result.getBungieGlobalDisplayName()) + .append("#") + .append(result.getBungieGlobalDisplayNameCode()); + if (CollectionUtils.isNotEmpty(groupResponse.getResponse().getResults())) { + String clanName = groupResponse.getResponse().getResults() + .get(0).getGroup().getName(); + defaultName.append("["); + defaultName.append(clanName); + defaultName.append("]"); + } + return defaultName; + } + + @Override + public Mono autocompleteResponse(Interaction interaction) { + Object focusedOption = interaction.getData().getOptions().get(0).getValue(); + return defaultBungieClient.searchByGlobalName(new UserGlobalSearchBody((String) focusedOption), + 0) + .flatMapIterable(response -> response.getResponse().getSearchResults()) + .take(25) + .filter(result -> CollectionUtils.isNotEmpty(result.getDestinyMemberships())) + .flatMap(this::createUserChoices) + .collectList() + .map(choices -> new InteractionResponse( + InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT.getType(), + InteractionResponseData.builder() + .choices(choices) + .build() + )); + } + + private Mono createUserChoices(UserSearchResult result) { + String membershipId = result.getDestinyMemberships().get(0).membershipId(); + Integer membershipType = result.getDestinyMemberships().get(0).membershipType(); + return defaultBungieClient.getGroupsForMember(membershipType, membershipId, 0, 1) + .map(groupResponse -> { + StringBuilder defaultName = buildChoiceDefaultName(result, groupResponse); + return new Choice(defaultName.toString(), + CHOICE_VALUE_TEMPLATE.formatted(membershipId, membershipType, + defaultName.toString())); + }); + } + + @Override + public Mono createResponse(Interaction interaction) { + var asyncScheduler = Schedulers.boundedElastic(); + var raidsAsync = processRaidsResponseUser(interaction) + .flatMap(response -> discordClient.editOriginalInteraction( + discordConfiguration.getApplicationId(), interaction.getToken(), response)) + .subscribeOn(asyncScheduler); + + raidsAsync.subscribe(); + + return Mono.just(InteractionResponse.builder() + .type(5) + .data(new InteractionResponseData()) + .build()); + } + + private Mono processRaidsResponseUser(Interaction interaction) { + String playerName = ((String) interaction.getData().getOptions().get(0).getValue()).split( + ":")[2]; + return raidStatsService.calculateRaidLevelStats(interaction) + .map(response -> response.entrySet().stream() + .map(entry -> EmbeddedField.builder() + .name(entry.getKey()) + .value(entry.getValue().toString()) + .inline(true) + .build()) + .toList()) + .map(embeddedFields -> InteractionResponseData.builder() + .embeds(List.of( + Embedded.builder() + .author(EmbeddedAuthor.builder() + .name("Riven of a Thousand Servers") + .iconUrl( + "https://ih1.redbubble.net/image.2953200665.7291/st,small,507x507-pad,600x600,f8f8f8.jpg") + .build()) + .title(STATS_TITLE.formatted(playerName)) + .description(""" + + General crunched numbers regarding all the raid clears you've done so far guardian. + """.formatted(Instant.now().getEpochSecond())) + .fields(embeddedFields) + .color(10070709) + .footer(EmbeddedFooter.builder() + .text(""" + Keep in mind this command is still being developed and the data displayed may be inaccurate. \ + + For example, fastest clears for Last Wish could be incorrect because of the Wall of Wishes, \ + meaning that if the raid was started from the beginning, but you used a wish to get to \ + Riven and finish the raid in under ~10 minutes, the bot would still count that as your fastest clear.""") + .build()) + .build())) + .components(List.of(Component.builder() + .type(1) + .components(List.of(Component.builder() + .type(2) + .customId("raid_stats_comprehension") + .style(1) + .label("What is this?") + .build())) + .build())) + .build()); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyDungeonMessageCreator.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyDungeonHandler.java similarity index 86% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyDungeonMessageCreator.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyDungeonHandler.java index cb70989..9eda3a8 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyDungeonMessageCreator.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyDungeonHandler.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; @@ -12,7 +12,7 @@ import reactor.core.publisher.Mono; @Component -public class WeeklyDungeonMessageCreator implements ApplicationCommandSource { +public class WeeklyDungeonHandler implements ApplicationCommandSource { public static final String MESSAGE_TEMPLATE = """ This week's dungeon is: %s. @@ -20,7 +20,7 @@ public class WeeklyDungeonMessageCreator implements ApplicationCommandSource { """; private final WeeklyActivitiesService weeklyActivitiesService; - public WeeklyDungeonMessageCreator(WeeklyActivitiesService weeklyActivitiesService) { + public WeeklyDungeonHandler(WeeklyActivitiesService weeklyActivitiesService) { this.weeklyActivitiesService = weeklyActivitiesService; } diff --git a/src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyRaidMessageCreator.java b/src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyRaidHandler.java similarity index 87% rename from src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyRaidMessageCreator.java rename to src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyRaidHandler.java index 42772ad..c967f22 100644 --- a/src/main/java/com/danielvm/destiny2bot/factory/creator/WeeklyRaidMessageCreator.java +++ b/src/main/java/com/danielvm/destiny2bot/factory/handler/WeeklyRaidHandler.java @@ -1,4 +1,4 @@ -package com.danielvm.destiny2bot.factory.creator; +package com.danielvm.destiny2bot.factory.handler; import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; @@ -12,7 +12,7 @@ import reactor.core.publisher.Mono; @Component -public class WeeklyRaidMessageCreator implements ApplicationCommandSource { +public class WeeklyRaidHandler implements ApplicationCommandSource { public static final String MESSAGE_TEMPLATE = """ This week's raid is: %s. @@ -21,7 +21,7 @@ public class WeeklyRaidMessageCreator implements ApplicationCommandSource { private final WeeklyActivitiesService weeklyActivitiesService; - public WeeklyRaidMessageCreator(WeeklyActivitiesService weeklyActivitiesService) { + public WeeklyRaidHandler(WeeklyActivitiesService weeklyActivitiesService) { this.weeklyActivitiesService = weeklyActivitiesService; } diff --git a/src/main/java/com/danielvm/destiny2bot/filter/CachingRequestBodyFilter.java b/src/main/java/com/danielvm/destiny2bot/filter/CachingRequestBodyFilter.java deleted file mode 100644 index 782b71a..0000000 --- a/src/main/java/com/danielvm/destiny2bot/filter/CachingRequestBodyFilter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.danielvm.destiny2bot.filter; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import org.springframework.web.util.ContentCachingRequestWrapper; - -/** - * Filter intended to cache the request body's bytes before being bound using @RequestBody This is - * needed as the annotation {@link com.danielvm.destiny2bot.annotation.ValidSignature} validates - * signatures from requests as they are received, and the InputStream from the request body can only - * be consumed once. This class caches it, so it can be used more than once, to have the request - * body at hand, and to validate the request signature. - */ -public class CachingRequestBodyFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws ServletException, IOException { - if (request instanceof HttpServletRequest httpRequest) { - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(httpRequest); - chain.doFilter(wrappedRequest, response); - } else { - chain.doFilter(request, response); - } - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/filter/RequestBodyCacheFilter.java b/src/main/java/com/danielvm/destiny2bot/filter/RequestBodyCacheFilter.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/danielvm/destiny2bot/filter/SignatureFilter.java b/src/main/java/com/danielvm/destiny2bot/filter/SignatureFilter.java new file mode 100644 index 0000000..5d16837 --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/filter/SignatureFilter.java @@ -0,0 +1,76 @@ +package com.danielvm.destiny2bot.filter; + +import com.danielvm.destiny2bot.config.DiscordConfiguration; +import com.danielvm.destiny2bot.exception.InvalidSignatureException; +import com.danielvm.destiny2bot.util.CryptoUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +@Order(1) +@Slf4j +public class SignatureFilter implements WebFilter { + + private static final String SIGNATURE_HEADER_NAME = "X-Signature-Ed25519"; + private static final String TIMESTAMP_HEADER_NAME = "X-Signature-Timestamp"; + + private final DiscordConfiguration discordConfiguration; + + public SignatureFilter(DiscordConfiguration discordConfiguration) { + this.discordConfiguration = discordConfiguration; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + Flux body = exchange.getRequest().getBody(); + + return DataBufferUtils.join(body) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + return bytes; + }) + .flatMap(bytes -> { + Assert.notNull(request.getHeaders().get(SIGNATURE_HEADER_NAME), + "Signature header is null"); + Assert.notNull(request.getHeaders().get(TIMESTAMP_HEADER_NAME), + "Signature timestamp is null"); + + String signature = request.getHeaders().get(SIGNATURE_HEADER_NAME).getFirst(); + String timestamp = request.getHeaders().get(TIMESTAMP_HEADER_NAME).getFirst(); + String publicKey = discordConfiguration.getBotPublicKey(); + boolean isValid = CryptoUtil.validateSignature(bytes, signature, publicKey, timestamp); + if (!isValid) { + log.error( + "There was a request with invalid signature. Signature: [{}], Timestamp: [{}]", + signature, timestamp); + return Mono.error(new InvalidSignatureException("The signature passed in was invalid")); + } + + DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + ServerHttpRequest modifiedRequest = new ServerHttpRequestDecorator(request) { + @Override + public Flux getBody() { + return Flux.just(factory.wrap(bytes)); + } + }; + + ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build(); + return chain.filter(modifiedExchange); + }); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/mapper/BotUserMapper.java b/src/main/java/com/danielvm/destiny2bot/mapper/BotUserMapper.java deleted file mode 100644 index 2d26a64..0000000 --- a/src/main/java/com/danielvm/destiny2bot/mapper/BotUserMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.danielvm.destiny2bot.mapper; - -import com.danielvm.destiny2bot.entity.BotUser; -import io.r2dbc.spi.Row; -import java.util.ArrayList; -import java.util.function.BiFunction; - -public class BotUserMapper implements BiFunction { - - @Override - public BotUser apply(Row row, Object o) { - Long discordId = row.get("discord_id", Long.class); - String discordUsername = row.get("discord_username", String.class); - Long membershipId = row.get("bungie_membership_id", Long.class); - String accessToken = row.get("bungie_access_token", String.class); - String refreshToken = row.get("bungie_refresh_token", String.class); - Long tokenExpiration = row.get("bungie_token_expiration", Long.class); - return new BotUser(discordId, discordUsername, membershipId, accessToken, refreshToken, - tokenExpiration, new ArrayList<>()); - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/mapper/UserCharacterMapper.java b/src/main/java/com/danielvm/destiny2bot/mapper/UserCharacterMapper.java deleted file mode 100644 index a2c109e..0000000 --- a/src/main/java/com/danielvm/destiny2bot/mapper/UserCharacterMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.danielvm.destiny2bot.mapper; - -import com.danielvm.destiny2bot.entity.UserCharacter; -import io.r2dbc.spi.Row; -import java.util.function.BiFunction; - -public class UserCharacterMapper implements BiFunction { - - @Override - public UserCharacter apply(Row row, Object o) { - Long characterId = row.get("character_id", Long.class); - Integer lightLevel = row.get("light_level", Integer.class); - String destinyClass = row.get("destiny_class", String.class); - Long discordUserId = row.get("discord_user_id", Long.class); - return new UserCharacter(characterId, lightLevel, destinyClass, discordUserId); - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepository.java b/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepository.java deleted file mode 100644 index 08576ef..0000000 --- a/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import com.danielvm.destiny2bot.entity.BotUser; -import reactor.core.publisher.Mono; - -public interface BotUserRepository { - - /** - * Retrieve a {@link BotUser} by a DiscordId - * - * @param discordId The DiscordId to find by - * @return {@link BotUser} - */ - Mono findBotUserByDiscordId(Long discordId); - - /** - * Save a {@link BotUser} to the corresponding table - * - * @param user The {@link BotUser} to save - * @return The saved {@link BotUser} - */ - Mono save(BotUser user); -} diff --git a/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImpl.java b/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImpl.java deleted file mode 100644 index f868f2c..0000000 --- a/src/main/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import com.danielvm.destiny2bot.entity.BotUser; -import com.danielvm.destiny2bot.exception.ResourceNotFoundException; -import com.danielvm.destiny2bot.mapper.BotUserMapper; -import com.danielvm.destiny2bot.mapper.UserCharacterMapper; -import java.util.Map; -import org.springframework.r2dbc.core.DatabaseClient; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Mono; - -@Repository -public class BotUserRepositoryImpl implements BotUserRepository { - - - private static final BotUserMapper BOT_MAPPER = new BotUserMapper(); - private static final UserCharacterMapper CHARACTER_MAPPER = new UserCharacterMapper(); - - private static final String RETRIEVE_CHARACTERS_QUERY = """ - SELECT buc.character_id, - buc.light_level, - buc.destiny_class, - buc.discord_user_id - FROM bot_user bu - INNER JOIN bungie_user_character buc on - bu.discord_id = buc.discord_user_id - WHERE bu.discord_id = :discordId - """; - - private static final String BOT_USER_QUERY = """ - SELECT bu.discord_id, - bu.discord_username, - bu.bungie_membership_id, - bu.bungie_access_token, - bu.bungie_refresh_token, - bu.bungie_token_expiration - FROM bot_user bu - WHERE bu.discord_id = :discordId - """; - - public static final String INSERT_USER_QUERY = """ - INSERT INTO bot_user (discord_id, discord_username, bungie_membership_id, - bungie_access_token, bungie_refresh_token, bungie_token_expiration) - VALUES (:discordId, :discordUsername, :membershipId, :accessToken, - :refreshToken, :tokenExpiration) - """; - - private final DatabaseClient databaseClient; - - public BotUserRepositoryImpl( - DatabaseClient databaseClient) { - this.databaseClient = databaseClient; - } - - @Override - public Mono findBotUserByDiscordId(Long discordId) { - return databaseClient.sql(BOT_USER_QUERY) - .bind("discordId", discordId) - .map(BOT_MAPPER::apply) - .first() - .switchIfEmpty(Mono.error(new ResourceNotFoundException( - "Discord user with Id [%s] not found".formatted(discordId)))) - .flatMap(botUser -> databaseClient.sql(RETRIEVE_CHARACTERS_QUERY) - .bind("discordId", discordId) - .map(CHARACTER_MAPPER::apply) - .all().collectList() - .map(userCharacters -> { - botUser.setCharacters(userCharacters); - return botUser; - }) - ); - } - - @Override - public Mono save(BotUser botUser) { - Map params = Map.of( - "discordId", botUser.getDiscordId(), - "discordUsername", botUser.getDiscordUsername(), - "membershipId", botUser.getBungieMembershipId(), - "accessToken", botUser.getBungieAccessToken(), - "refreshToken", botUser.getBungieRefreshToken(), - "tokenExpiration", botUser.getBungieTokenExpiration() - ); - return databaseClient.sql(INSERT_USER_QUERY) - .bindValues(params) - .map(BOT_MAPPER::apply) - .one(); - } - -} diff --git a/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepository.java b/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepository.java deleted file mode 100644 index e7bd203..0000000 --- a/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import com.danielvm.destiny2bot.entity.UserCharacter; -import reactor.core.publisher.Mono; - -public interface UserCharacterRepository { - - /** - * Save a single user character to the corresponding table - * - * @param userCharacter The {@link UserCharacter} object to save - * @return The saved {@link UserCharacter} - */ - Mono save(UserCharacter userCharacter); - -} diff --git a/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImpl.java b/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImpl.java deleted file mode 100644 index 1d65206..0000000 --- a/src/main/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import com.danielvm.destiny2bot.entity.UserCharacter; -import com.danielvm.destiny2bot.mapper.UserCharacterMapper; -import java.util.Map; -import org.springframework.r2dbc.core.DatabaseClient; -import org.springframework.stereotype.Repository; -import reactor.core.publisher.Mono; - -@Repository -public class UserCharacterRepositoryImpl implements UserCharacterRepository { - - private static final UserCharacterMapper USER_CHARACTER_MAPPER = new UserCharacterMapper(); - public static final String INSERT_CHARACTER_QUERY = """ - INSERT INTO bungie_user_character (character_id, light_level, destiny_class, discord_user_id) - VALUES (:characterId, :lightLevel, :destinyClass, :discordUserId)"""; - - private final DatabaseClient databaseClient; - - public UserCharacterRepositoryImpl( - DatabaseClient databaseClient) { - this.databaseClient = databaseClient; - } - - @Override - public Mono save(UserCharacter userCharacter) { - Map saveParameters = Map.of( - "characterId", userCharacter.getCharacterId(), - "lightLevel", userCharacter.getLightLevel(), - "destinyClass", userCharacter.getDestinyClass(), - "discordUserId", userCharacter.getDiscordUserId() - ); - return databaseClient.sql(INSERT_CHARACTER_QUERY) - .bindValues(saveParameters) - .map(USER_CHARACTER_MAPPER::apply) - .one(); - } -} diff --git a/src/main/java/com/danielvm/destiny2bot/service/BungieMembershipService.java b/src/main/java/com/danielvm/destiny2bot/service/BungieMembershipService.java index a5d39a2..6fbe709 100644 --- a/src/main/java/com/danielvm/destiny2bot/service/BungieMembershipService.java +++ b/src/main/java/com/danielvm/destiny2bot/service/BungieMembershipService.java @@ -14,10 +14,10 @@ @Slf4j public class BungieMembershipService { - private final BungieClient bungieClient; + private final BungieClient defaultBungieClient; - public BungieMembershipService(BungieClient bungieClient) { - this.bungieClient = bungieClient; + public BungieMembershipService(BungieClient defaultBungieClient) { + this.defaultBungieClient = defaultBungieClient; } /** @@ -27,7 +27,7 @@ public BungieMembershipService(BungieClient bungieClient) { * @return {@link MembershipResponse} */ public MembershipResponse getCurrentUserMembershipInformation(String bearerToken) { - var membershipData = bungieClient.getMembershipForCurrentUser( + var membershipData = defaultBungieClient.getMembershipForCurrentUser( bearerToken).getBody(); Assert.notNull(membershipData, "The membership characters for the current user is null"); @@ -45,7 +45,7 @@ public MembershipResponse getCurrentUserMembershipInformation(String bearerToken * @return {@link MembershipResponse} */ public Mono getUserMembershipInformation(String bearerToken) { - return bungieClient.getMembershipInfoForCurrentUser(bearerToken) + return defaultBungieClient.getMembershipInfoForCurrentUser(bearerToken) .filter(Objects::nonNull) .filter(membership -> Objects.nonNull(MembershipUtil.extractMembershipType(membership)) diff --git a/src/main/java/com/danielvm/destiny2bot/service/DestinyCharacterService.java b/src/main/java/com/danielvm/destiny2bot/service/DestinyCharacterService.java index 347b756..6c69d56 100644 --- a/src/main/java/com/danielvm/destiny2bot/service/DestinyCharacterService.java +++ b/src/main/java/com/danielvm/destiny2bot/service/DestinyCharacterService.java @@ -17,15 +17,15 @@ @Slf4j public class DestinyCharacterService { - private final BungieClient bungieClient; + private final BungieClient defaultBungieClient; private final UserDetailsReactiveDao userDetailsReactiveDao; private final BungieMembershipService bungieMembershipService; public DestinyCharacterService( - BungieClient bungieClient, + BungieClient defaultBungieClient, UserDetailsReactiveDao userDetailsReactiveDao, BungieMembershipService bungieMembershipService) { - this.bungieClient = bungieClient; + this.defaultBungieClient = defaultBungieClient; this.userDetailsReactiveDao = userDetailsReactiveDao; this.bungieMembershipService = bungieMembershipService; } @@ -45,7 +45,7 @@ public Flux getCharactersForUser(String userId) { .flatMap(membershipResponse -> { String membershipId = MembershipUtil.extractMembershipId(membershipResponse); Integer membershipType = MembershipUtil.extractMembershipType(membershipResponse); - return bungieClient.getUserCharacters(membershipType, membershipId); + return defaultBungieClient.getUserCharacters(membershipType, membershipId); }) .flatMapIterable(characters -> characters.getResponse().getCharacters().getData().entrySet()) diff --git a/src/main/java/com/danielvm/destiny2bot/service/InteractionService.java b/src/main/java/com/danielvm/destiny2bot/service/InteractionService.java index eccd9d0..30e110d 100644 --- a/src/main/java/com/danielvm/destiny2bot/service/InteractionService.java +++ b/src/main/java/com/danielvm/destiny2bot/service/InteractionService.java @@ -6,6 +6,7 @@ import com.danielvm.destiny2bot.enums.SlashCommand; import com.danielvm.destiny2bot.factory.ApplicationCommandFactory; import com.danielvm.destiny2bot.factory.AutocompleteFactory; +import com.danielvm.destiny2bot.factory.MessageComponentFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -16,12 +17,14 @@ public class InteractionService { private final ApplicationCommandFactory applicationCommandFactory; private final AutocompleteFactory autocompleteFactory; + private final MessageComponentFactory messageComponentFactory; public InteractionService( ApplicationCommandFactory applicationCommandFactory, - AutocompleteFactory autocompleteFactory) { + AutocompleteFactory autocompleteFactory, MessageComponentFactory messageComponentFactory) { this.applicationCommandFactory = applicationCommandFactory; this.autocompleteFactory = autocompleteFactory; + this.messageComponentFactory = messageComponentFactory; } /** @@ -34,7 +37,11 @@ public InteractionService( public Mono handleInteraction(Interaction interaction) { InteractionType interactionType = InteractionType.findByValue(interaction.getType()); return switch (interactionType) { - case MODAL_SUBMIT, MESSAGE_COMPONENT -> Mono.just(new InteractionResponse()); + case MESSAGE_COMPONENT -> { + String componentId = interaction.getData().getCustomId(); + yield messageComponentFactory.handle(componentId).respond(interaction); + } + case MODAL_SUBMIT -> Mono.just(new InteractionResponse()); case APPLICATION_COMMAND_AUTOCOMPLETE -> { SlashCommand command = SlashCommand.findByName(interaction.getData().getName()); yield autocompleteFactory.messageCreator(command).autocompleteResponse(interaction); diff --git a/src/main/java/com/danielvm/destiny2bot/service/MessageService.java b/src/main/java/com/danielvm/destiny2bot/service/MessageService.java index 87d9c28..64d430d 100644 --- a/src/main/java/com/danielvm/destiny2bot/service/MessageService.java +++ b/src/main/java/com/danielvm/destiny2bot/service/MessageService.java @@ -13,5 +13,4 @@ public interface MessageService { * @return {@link com.danielvm.destiny2bot.dto.discord.InteractionResponse} */ Mono createResponse(Interaction interaction); - } diff --git a/src/main/java/com/danielvm/destiny2bot/service/RaidStatsService.java b/src/main/java/com/danielvm/destiny2bot/service/RaidStatsService.java new file mode 100644 index 0000000..2d4f15c --- /dev/null +++ b/src/main/java/com/danielvm/destiny2bot/service/RaidStatsService.java @@ -0,0 +1,126 @@ +package com.danielvm.destiny2bot.service; + +import com.danielvm.destiny2bot.client.BungieClient; +import com.danielvm.destiny2bot.client.BungieClientWrapper; +import com.danielvm.destiny2bot.dto.RaidEntry; +import com.danielvm.destiny2bot.dto.destiny.Activity; +import com.danielvm.destiny2bot.dto.destiny.RaidStatistics; +import com.danielvm.destiny2bot.dto.discord.Interaction; +import com.danielvm.destiny2bot.enums.ManifestEntity; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Service +@Slf4j +public class RaidStatsService { + + private static final Integer MAX_PAGE_COUNT = 250; + private static final Integer RAID_MODE = 4; + + private final BungieClient defaultBungieClient; + private final BungieClient pgcrBungieClient; + private final BungieClientWrapper bungieClientWrapper; + + public RaidStatsService( + BungieClient defaultBungieClient, + BungieClient pgcrBungieClient, + BungieClientWrapper bungieClientWrapper) { + this.defaultBungieClient = defaultBungieClient; + this.pgcrBungieClient = pgcrBungieClient; + this.bungieClientWrapper = bungieClientWrapper; + } + + private static RaidStatistics createRaidStatistics(RaidStatistics stats, RaidEntry raidEntry) { + stats.setTotalKills(stats.getTotalKills() + raidEntry.getTotalKills()); + stats.setTotalDeaths(stats.getTotalDeaths() + raidEntry.getTotalDeaths()); + if (raidEntry.getIsCompleted()) { + stats.setTotalClears(stats.getTotalClears() + 1); + if (raidEntry.getIsFromBeginning()) { + stats.setFastestTime(Math.min(stats.getFastestTime(), raidEntry.getDuration())); + stats.setFullClears(stats.getFullClears() + 1); + } + } + return stats; + } + + /** + * Retrieve user raid statistics based on the given interaction data + * + * @param interaction The Discord command interaction + * @return Map of Raid Statistics, the key will be the raid you want stats for + */ + public Mono> calculateRaidLevelStats( + Interaction interaction) { + var parsedData = ((String) interaction.getData().getOptions().get(0).getValue()).split(":"); + String membershipId = parsedData[0]; + Integer membershipType = Integer.valueOf(parsedData[1]); + + return defaultBungieClient.getUserCharacters(membershipType, membershipId) + .flatMapMany(userCharacter -> Flux.fromIterable( + userCharacter.getResponse().getCharacters().getData().keySet())) + .flatMap(characterId -> getActivities(membershipType, membershipId, characterId)) + .flatMap(this::createRaidEntry) + .flatMap(this::addPGCRDetails, 5) + .groupBy(RaidEntry::getRaidName) + .flatMap(group -> group.reduce(new RaidStatistics(group.key()), + RaidStatsService::createRaidStatistics)) + .collectMap(RaidStatistics::getRaidName, raidStatistics -> { + if (raidStatistics.getFastestTime() == Integer.MAX_VALUE) { + raidStatistics.setFastestTime(0); + } + return raidStatistics; + }) + .doOnSubscribe(c -> log.info("Calculating raid statistics for user [{}]", parsedData[2])) + .doOnSuccess( + c -> log.info("Finished calculating raid statistics for user [{}]", parsedData[2])); + } + + private Mono addPGCRDetails(RaidEntry raidEntry) { + return pgcrBungieClient.getPostGameCarnageReport(raidEntry.getInstanceId()) + .map(pgcr -> { + raidEntry.setIsFromBeginning(pgcr.getResponse().getActivityWasStartedFromBeginning()); + return raidEntry; + }); + } + + private Flux getActivities(Integer membershipType, String membershipId, + String characterId) { + return Flux.range(0, 25) + .flatMapSequential( + page -> defaultBungieClient.getActivityHistory(membershipType, membershipId, + characterId, MAX_PAGE_COUNT, RAID_MODE, page)) + .filter(activities -> CollectionUtils.isNotEmpty(activities.getResponse().getActivities())) + .takeUntil(activities -> activities.getResponse().getActivities().size() < MAX_PAGE_COUNT) + .flatMapIterable(response -> response.getResponse().getActivities()); + } + + private Mono createRaidEntry(Activity activity) { + return bungieClientWrapper.getManifestEntityRx(ManifestEntity.ACTIVITY_DEFINITION, + String.valueOf(activity.getActivityDetails().getDirectorActivityHash())) + .map(entity -> { + if (entity.getResponse() == null || entity.getResponse().getDisplayProperties() == null + || entity.getResponse().getDisplayProperties().getName() == null) { + return ""; + } + return resolveRaidName(entity.getResponse().getDisplayProperties().getName()); + }) + .map(raidName -> new RaidEntry(raidName, + activity.getActivityDetails().getInstanceId(), + activity.getValues().get("deaths").getBasic().getValue().intValue(), + activity.getValues().get("kills").getBasic().getValue().intValue(), + activity.getValues().get("killsDeathsAssists").getBasic().getValue(), + activity.getValues().get("activityDurationSeconds").getBasic().getValue().intValue(), + activity.getValues().get("completed").getBasic().getValue() != 0, + null + )); + } + + private String resolveRaidName(String name) { + String[] tokens = name.split(":"); + return tokens[0].trim(); + } +} diff --git a/src/main/java/com/danielvm/destiny2bot/service/UserRegistrationService.java b/src/main/java/com/danielvm/destiny2bot/service/UserRegistrationService.java deleted file mode 100644 index d2cf1b8..0000000 --- a/src/main/java/com/danielvm/destiny2bot/service/UserRegistrationService.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.danielvm.destiny2bot.service; - -import com.danielvm.destiny2bot.client.DiscordClient; -import com.danielvm.destiny2bot.config.BungieConfiguration; -import com.danielvm.destiny2bot.config.DiscordConfiguration; -import com.danielvm.destiny2bot.config.OAuth2Configuration; -import com.danielvm.destiny2bot.dao.UserDetailsReactiveDao; -import com.danielvm.destiny2bot.dto.oauth2.TokenResponse; -import com.danielvm.destiny2bot.entity.UserDetails; -import com.danielvm.destiny2bot.exception.InternalServerException; -import com.danielvm.destiny2bot.util.OAuth2Util; -import jakarta.servlet.http.HttpSession; -import java.time.Instant; -import java.util.Objects; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@Service -@Slf4j -public class UserRegistrationService { - - private static final String DISCORD_USER_ID_KEY = "discordUserId"; - private static final String DISCORD_USER_ALIAS_KEY = "discordUserAlias"; - - private final DiscordConfiguration discordConfiguration; - private final BungieConfiguration bungieConfiguration; - private final DiscordClient discordClient; - private final UserDetailsReactiveDao userDetailsReactiveDao; - private final WebClient.Builder defaultWebClientBuilder; - - - public UserRegistrationService( - DiscordConfiguration discordConfiguration, - BungieConfiguration bungieConfiguration, - DiscordClient discordClient, - UserDetailsReactiveDao userDetailsReactiveDao, - WebClient.Builder defaultWebClientBuilder) { - this.discordConfiguration = discordConfiguration; - this.bungieConfiguration = bungieConfiguration; - this.discordClient = discordClient; - this.userDetailsReactiveDao = userDetailsReactiveDao; - this.defaultWebClientBuilder = defaultWebClientBuilder; - } - - /** - * Retrieve DiscordUserId from authenticated user and save it to Session - * - * @param authorizationCode The authorization code from Discord - * @param session The HttpSession the user is linked to - * @return ResponseEntity with redirection to begin Bungie OAuth2 flow - */ - public Mono> authenticateDiscordUser(String authorizationCode, - HttpSession session) { - return getTokenResponse(authorizationCode, discordConfiguration) - .flatMap(token -> discordClient.getUser( - OAuth2Util.formatBearerToken(token.getAccessToken()))) - .switchIfEmpty( - Mono.error(new IllegalStateException("The user response from Discord is empty"))) - .flatMap(token -> { - if (Objects.isNull(token.getId()) || Objects.isNull(token.getUsername())) { - var errorMessage = "Some required arguments for registration are null for the current user"; - return Mono.error(new IllegalStateException(errorMessage)); - } - return Mono.just(token); - }) - .doOnSuccess(user -> { - session.setAttribute(DISCORD_USER_ALIAS_KEY, user.getUsername()); - session.setAttribute(DISCORD_USER_ID_KEY, user.getId()); - }) - .then(Mono.just(ResponseEntity - .status(HttpStatus.FOUND) // on success relocate to Bungie Auth URL - .header(HttpHeaders.LOCATION, OAuth2Util.bungieAuthorizationUrl( - bungieConfiguration.getAuthorizationUrl(), - bungieConfiguration.getClientId())) - .build()) - ); - } - - /** - * Link the Bungie credentials to the current DiscordUserId and create a database entry - * - * @param authorizationCode The authorization code from Bungie - * @param httpSession The HttpSession the user is linked to - * @return ResponseEntity with no content when the user is persisted successfully - */ - public Mono> linkDiscordUserToBungieAccount(String authorizationCode, - HttpSession httpSession) { - return getTokenResponse(authorizationCode, bungieConfiguration) - .flatMap(token -> { - UserDetails userDetails = UserDetails.builder() - .discordUsername((String) httpSession.getAttribute(DISCORD_USER_ALIAS_KEY)) - .discordId((String) httpSession.getAttribute(DISCORD_USER_ID_KEY)) - .accessToken(token.getAccessToken()) - .refreshToken(token.getRefreshToken()) - .expiration(Instant.now().plusSeconds(token.getExpiresIn())) - .build(); - httpSession.invalidate(); - return userDetailsReactiveDao.save(userDetails); - }) - .flatMap(result -> result ? Mono.empty() : Mono.error( - new InternalServerException("Something went wrong when persisting a user into Redis", - HttpStatus.INTERNAL_SERVER_ERROR))) - .then(Mono.just(ResponseEntity.noContent().build())); - } - - private Mono getTokenResponse(String authorizationCode, - OAuth2Configuration oAuth2Configuration) { - MultiValueMap map = - OAuth2Util.buildTokenExchangeParameters(authorizationCode, - oAuth2Configuration.getCallbackUrl(), oAuth2Configuration.getClientSecret(), - oAuth2Configuration.getClientId()); - - var tokenClient = defaultWebClientBuilder - .baseUrl(oAuth2Configuration.getTokenUrl()) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .build(); - - return tokenClient.post() - .body(BodyInserters.fromFormData(map)) - .retrieve() - .bodyToMono(TokenResponse.class) - .flatMap(token -> { - if (Objects.isNull(token.getAccessToken()) || Objects.isNull(token.getRefreshToken()) - || Objects.isNull(token.getExpiresIn())) { - String errorMessage = "The access token, the refresh token or the expiration are null for user"; - return Mono.error(new IllegalStateException(errorMessage)); - } - return Mono.just(token); - }); - } - -} diff --git a/src/main/java/com/danielvm/destiny2bot/service/WeeklyActivitiesService.java b/src/main/java/com/danielvm/destiny2bot/service/WeeklyActivitiesService.java index e9a4042..48ea4af 100644 --- a/src/main/java/com/danielvm/destiny2bot/service/WeeklyActivitiesService.java +++ b/src/main/java/com/danielvm/destiny2bot/service/WeeklyActivitiesService.java @@ -8,7 +8,7 @@ import com.danielvm.destiny2bot.client.BungieClientWrapper; import com.danielvm.destiny2bot.dto.MilestoneResponse; import com.danielvm.destiny2bot.dto.WeeklyActivity; -import com.danielvm.destiny2bot.dto.destiny.GenericResponse; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; import com.danielvm.destiny2bot.dto.destiny.milestone.ActivitiesDto; import com.danielvm.destiny2bot.dto.destiny.milestone.MilestoneEntry; import com.danielvm.destiny2bot.enums.ActivityMode; @@ -26,13 +26,13 @@ @Slf4j public class WeeklyActivitiesService { - private final BungieClient bungieClient; + private final BungieClient defaultBungieClient; private final BungieClientWrapper bungieClientWrapper; public WeeklyActivitiesService( - BungieClient bungieClient, + BungieClient defaultBungieClient, BungieClientWrapper bungieClientWrapper) { - this.bungieClient = bungieClient; + this.defaultBungieClient = defaultBungieClient; this.bungieClientWrapper = bungieClientWrapper; } @@ -43,8 +43,8 @@ public WeeklyActivitiesService( * @return {@link MilestoneResponse} */ public Mono getWeeklyActivity(ActivityMode activityMode) { - return bungieClient.getPublicMilestonesRx() - .map(GenericResponse::getResponse) + return defaultBungieClient.getPublicMilestonesRx() + .map(BungieResponse::getResponse) .flatMapIterable(Map::values) .filter(this::hasWeeklyObjectives) .filterWhen(milestoneEntry -> activityModeMatches(milestoneEntry, activityMode)) diff --git a/src/main/java/com/danielvm/destiny2bot/util/MessageUtil.java b/src/main/java/com/danielvm/destiny2bot/util/MessageUtil.java index bd442b5..ad14314 100644 --- a/src/main/java/com/danielvm/destiny2bot/util/MessageUtil.java +++ b/src/main/java/com/danielvm/destiny2bot/util/MessageUtil.java @@ -13,13 +13,13 @@ public class MessageUtil { private static final LocalTime DESTINY_2_STANDARD_RESET_TIME = LocalTime.of(9, 0); private static final ZoneId STANDARD_TIMEZONE = ZoneId.of("America/Los_Angeles"); + public static final Integer EPHEMERAL_BYTE = 1000000; public static final ZonedDateTime NEXT_TUESDAY = ZonedDateTime.of( LocalDate.now(STANDARD_TIMEZONE), DESTINY_2_STANDARD_RESET_TIME, STANDARD_TIMEZONE) .with(TemporalAdjusters.next(DayOfWeek.TUESDAY)); public static final ZonedDateTime PREVIOUS_TUESDAY = ZonedDateTime.of( LocalDate.now(STANDARD_TIMEZONE), DESTINY_2_STANDARD_RESET_TIME, STANDARD_TIMEZONE) .with(TemporalAdjusters.previous(DayOfWeek.TUESDAY)); - /** * Formatter used to format the date for a message Example: the LocalDate object with date * 2023-01-01 would be formatted to "Sunday 1st, January 2023" diff --git a/src/main/java/com/danielvm/destiny2bot/validator/SignatureValidator.java b/src/main/java/com/danielvm/destiny2bot/validator/SignatureValidator.java deleted file mode 100644 index d6529b7..0000000 --- a/src/main/java/com/danielvm/destiny2bot/validator/SignatureValidator.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.danielvm.destiny2bot.validator; - -import com.danielvm.destiny2bot.annotation.ValidSignature; -import com.danielvm.destiny2bot.config.DiscordConfiguration; -import com.danielvm.destiny2bot.util.CryptoUtil; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import org.springframework.web.util.ContentCachingRequestWrapper; - -public class SignatureValidator implements - ConstraintValidator { - - private static final String SIGNATURE_HEADER_NAME = "X-Signature-Ed25519"; - private static final String TIMESTAMP_HEADER_NAME = "X-Signature-Timestamp"; - - private final DiscordConfiguration discordConfiguration; - - public SignatureValidator(DiscordConfiguration discordConfiguration) { - this.discordConfiguration = discordConfiguration; - } - - @Override - public boolean isValid(ContentCachingRequestWrapper value, ConstraintValidatorContext context) { - String signature = value.getHeader(SIGNATURE_HEADER_NAME); - String timestamp = value.getHeader(TIMESTAMP_HEADER_NAME); - String botPublicKey = discordConfiguration.getBotPublicKey(); - byte[] bodyBytes = value.getContentAsByteArray(); - return CryptoUtil.validateSignature(bodyBytes, signature, botPublicKey, timestamp); - } -} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 133a8b3..ff4ddef 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -3,19 +3,6 @@ spring: redis: port: 6379 host: localhost - r2dbc: - url: r2dbc:postgresql://localhost:5432/postgres - username: postgres - password: mysecretpassword - h2: - console: - enabled: true - flyway: - enabled: true - url: jdbc:postgresql://localhost:5432/postgres - user: postgres - password: mysecretpassword - locations: classpath:db/migration bungie: api: @@ -33,4 +20,8 @@ server: application: callback: # Replace with your own callback url masking port 8080 to run locally - url: https://c3ee-2600-1700-4390-a930-19b5-f155-e766-6d6.ngrok-free.app \ No newline at end of file + url: https://c3ee-2600-1700-4390-a930-19b5-f155-e766-6d6.ngrok-free.app + +logging: + level: + org.springframework.http.converter.json: DEBUG \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c4e5cc6..77637b0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,6 +23,7 @@ bungie: clientSecret: ${BUNGIE_CLIENT_SECRET} clientId: ${BUNGIE_CLIENT_ID} baseUrl: https://www.bungie.net/Platform + statsBaseUrl: https://stats.bungie.net/Platform authorizationUrl: https://www.bungie.net/en/oauth/authorize tokenUrl: https://www.bungie.net/platform/app/oauth/token/ callbackUrl: ${application.callback.url}/bungie/callback @@ -39,6 +40,7 @@ discord: authorizationUrl: https://discord.com/oauth2/authorize botToken: ${DISCORD_BOT_TOKEN} botPublicKey: ${DISCORD_BOT_PUBLIC_KEY} + applicationId: ${DISCORD_APPLICATION_ID} permissionsInteger: 137439217728 clientId: ${DISCORD_CLIENT_ID} clientSecret: ${DISCORD_CLIENT_SECRET} diff --git a/src/main/resources/static/.DS_Store b/src/main/resources/static/.DS_Store new file mode 100644 index 0000000..a23b4ce Binary files /dev/null and b/src/main/resources/static/.DS_Store differ diff --git a/src/main/resources/static/raids/.DS_Store b/src/main/resources/static/raids/.DS_Store new file mode 100644 index 0000000..65b59a5 Binary files /dev/null and b/src/main/resources/static/raids/.DS_Store differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/.DS_Store b/src/main/resources/static/raids/deep_stone_crypt/.DS_Store new file mode 100644 index 0000000..6c262c1 Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/.DS_Store differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-ground-level.jpg b/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-ground-level.jpg new file mode 100644 index 0000000..723fcda Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-ground-level.jpg differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-space.jpg b/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-space.jpg new file mode 100644 index 0000000..b21503b Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/atraks_1/atraks-1-space.jpg differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/crypt_security/.DS_Store b/src/main/resources/static/raids/deep_stone_crypt/crypt_security/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/crypt_security/.DS_Store differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/crypt_security/crypt-security.jpg b/src/main/resources/static/raids/deep_stone_crypt/crypt_security/crypt-security.jpg new file mode 100644 index 0000000..45f0c7f Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/crypt_security/crypt-security.jpg differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/entrance/.DS_Store b/src/main/resources/static/raids/deep_stone_crypt/entrance/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/entrance/.DS_Store differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/entrance/entrance.jpeg b/src/main/resources/static/raids/deep_stone_crypt/entrance/entrance.jpeg new file mode 100644 index 0000000..980707a Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/entrance/entrance.jpeg differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/taniks_reborn/taniks-reborn.jpg b/src/main/resources/static/raids/deep_stone_crypt/taniks_reborn/taniks-reborn.jpg new file mode 100644 index 0000000..1675161 Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/taniks_reborn/taniks-reborn.jpg differ diff --git a/src/main/resources/static/raids/deep_stone_crypt/taniks_the_abonimation/taniks-last-encounter.jpg b/src/main/resources/static/raids/deep_stone_crypt/taniks_the_abonimation/taniks-last-encounter.jpg new file mode 100644 index 0000000..a6da801 Binary files /dev/null and b/src/main/resources/static/raids/deep_stone_crypt/taniks_the_abonimation/taniks-last-encounter.jpg differ diff --git a/src/test/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactoryTest.java b/src/test/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactoryTest.java index 10b0fd8..8c6c1a9 100644 --- a/src/test/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactoryTest.java +++ b/src/test/java/com/danielvm/destiny2bot/factory/ApplicationCommandFactoryTest.java @@ -1,14 +1,13 @@ package com.danielvm.destiny2bot.factory; -import static org.assertj.core.api.Assertions.assertThat; - import com.danielvm.destiny2bot.enums.SlashCommand; import com.danielvm.destiny2bot.exception.ResourceNotFoundException; -import com.danielvm.destiny2bot.factory.creator.ApplicationCommandSource; -import com.danielvm.destiny2bot.factory.creator.AuthorizeMessageCreator; -import com.danielvm.destiny2bot.factory.creator.RaidMapMessageCreator; -import com.danielvm.destiny2bot.factory.creator.WeeklyDungeonMessageCreator; -import com.danielvm.destiny2bot.factory.creator.WeeklyRaidMessageCreator; +import com.danielvm.destiny2bot.factory.handler.ApplicationCommandSource; +import com.danielvm.destiny2bot.factory.handler.AuthorizeHandler; +import com.danielvm.destiny2bot.factory.handler.RaidMapHandler; +import com.danielvm.destiny2bot.factory.handler.RaidStatsHandler; +import com.danielvm.destiny2bot.factory.handler.WeeklyDungeonHandler; +import com.danielvm.destiny2bot.factory.handler.WeeklyRaidHandler; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,17 +16,21 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import static org.assertj.core.api.Assertions.assertThat; + @ExtendWith(MockitoExtension.class) public class ApplicationCommandFactoryTest { @Mock - private WeeklyRaidMessageCreator weeklyRaidMessageCreator; + private WeeklyRaidHandler weeklyRaidHandler; + @Mock + private WeeklyDungeonHandler weeklyDungeonHandler; @Mock - private WeeklyDungeonMessageCreator weeklyDungeonMessageCreator; + private AuthorizeHandler authorizeHandler; @Mock - private AuthorizeMessageCreator authorizeMessageCreator; + private RaidMapHandler raidMapHandler; @Mock - private RaidMapMessageCreator raidMapMessageCreator; + private RaidStatsHandler raidStatsHandler; @InjectMocks private ApplicationCommandFactory sut; @@ -42,8 +45,8 @@ public void messageCreatorWorksForAuthorize() { // then: the correct message creator is returned assertThat(creator) - .isInstanceOf(AuthorizeMessageCreator.class) - .isEqualTo(authorizeMessageCreator); + .isInstanceOf(AuthorizeHandler.class) + .isEqualTo(authorizeHandler); } @Test @@ -57,8 +60,8 @@ public void messageCreatorWorksForWeeklyDungeon() { // then: the correct message creator is returned assertThat(creator) - .isInstanceOf(WeeklyDungeonMessageCreator.class) - .isEqualTo(weeklyDungeonMessageCreator); + .isInstanceOf(WeeklyDungeonHandler.class) + .isEqualTo(weeklyDungeonHandler); } @Test @@ -72,8 +75,8 @@ public void messageCreatorWorksForWeeklyRaid() { // then: the correct message creator is returned assertThat(creator) - .isInstanceOf(WeeklyRaidMessageCreator.class) - .isEqualTo(weeklyRaidMessageCreator); + .isInstanceOf(WeeklyRaidHandler.class) + .isEqualTo(weeklyRaidHandler); } @Test diff --git a/src/test/java/com/danielvm/destiny2bot/factory/AuthorizeMessageCreatorTest.java b/src/test/java/com/danielvm/destiny2bot/factory/AuthorizeHandlerTest.java similarity index 91% rename from src/test/java/com/danielvm/destiny2bot/factory/AuthorizeMessageCreatorTest.java rename to src/test/java/com/danielvm/destiny2bot/factory/AuthorizeHandlerTest.java index 087e5cd..65861b9 100644 --- a/src/test/java/com/danielvm/destiny2bot/factory/AuthorizeMessageCreatorTest.java +++ b/src/test/java/com/danielvm/destiny2bot/factory/AuthorizeHandlerTest.java @@ -6,7 +6,7 @@ import com.danielvm.destiny2bot.config.DiscordConfiguration; import com.danielvm.destiny2bot.dto.discord.Component; import com.danielvm.destiny2bot.enums.InteractionResponseType; -import com.danielvm.destiny2bot.factory.creator.AuthorizeMessageCreator; +import com.danielvm.destiny2bot.factory.handler.AuthorizeHandler; import com.danielvm.destiny2bot.util.OAuth2Util; import java.util.List; import java.util.Objects; @@ -20,13 +20,13 @@ import reactor.test.StepVerifier.FirstStep; @ExtendWith(MockitoExtension.class) -public class AuthorizeMessageCreatorTest { +public class AuthorizeHandlerTest { @Mock DiscordConfiguration discordConfiguration; @InjectMocks - AuthorizeMessageCreator sut; + AuthorizeHandler sut; @Test @DisplayName("Create message is successful") @@ -77,8 +77,8 @@ public void createMessageIsSuccessful() { InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE.getType()); assertThat(embedded.getDescription()).isEqualTo( - AuthorizeMessageCreator.MESSAGE_DESCRIPTION); - assertThat(embedded.getTitle()).isEqualTo(AuthorizeMessageCreator.MESSAGE_TITLE); + AuthorizeHandler.MESSAGE_DESCRIPTION); + assertThat(embedded.getTitle()).isEqualTo(AuthorizeHandler.MESSAGE_TITLE); assertThat(whyComponent).isNotNull(); assertThat(whyComponent).isEqualTo(whyButton); diff --git a/src/test/java/com/danielvm/destiny2bot/factory/RaidMapMessageCreatorTest.java b/src/test/java/com/danielvm/destiny2bot/factory/RaidMapHandlerTest.java similarity index 85% rename from src/test/java/com/danielvm/destiny2bot/factory/RaidMapMessageCreatorTest.java rename to src/test/java/com/danielvm/destiny2bot/factory/RaidMapHandlerTest.java index 55872f9..b83c5c9 100644 --- a/src/test/java/com/danielvm/destiny2bot/factory/RaidMapMessageCreatorTest.java +++ b/src/test/java/com/danielvm/destiny2bot/factory/RaidMapHandlerTest.java @@ -1,27 +1,13 @@ package com.danielvm.destiny2bot.factory; -import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; -import static org.assertj.core.api.Assertions.assertThat; - import com.danielvm.destiny2bot.TestUtils; -import com.danielvm.destiny2bot.dto.discord.Attachment; -import com.danielvm.destiny2bot.dto.discord.Choice; -import com.danielvm.destiny2bot.dto.discord.Embedded; -import com.danielvm.destiny2bot.dto.discord.EmbeddedImage; -import com.danielvm.destiny2bot.dto.discord.Interaction; -import com.danielvm.destiny2bot.dto.discord.InteractionData; -import com.danielvm.destiny2bot.dto.discord.InteractionResponse; -import com.danielvm.destiny2bot.dto.discord.InteractionResponseData; -import com.danielvm.destiny2bot.dto.discord.Option; +import com.danielvm.destiny2bot.dto.discord.*; import com.danielvm.destiny2bot.enums.InteractionResponseType; import com.danielvm.destiny2bot.enums.InteractionType; import com.danielvm.destiny2bot.enums.Raid; import com.danielvm.destiny2bot.enums.RaidEncounter; -import com.danielvm.destiny2bot.factory.creator.RaidMapMessageCreator; +import com.danielvm.destiny2bot.factory.handler.RaidMapHandler; import com.danielvm.destiny2bot.service.ImageAssetService; -import java.io.IOException; -import java.util.List; -import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,14 +19,21 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; +import static org.assertj.core.api.Assertions.assertThat; + @ExtendWith(MockitoExtension.class) -public class RaidMapMessageCreatorTest { +public class RaidMapHandlerTest { @Mock private ImageAssetService imageAssetService; @InjectMocks - private RaidMapMessageCreator sut; + private RaidMapHandler sut; @Test @DisplayName("Creating autocomplete response works correctly") @@ -51,7 +44,7 @@ public void creatingAutocompleteResponseWorksCorrectly() { .id(1).name("raid_map").options(options).type(1) .build(); Interaction interaction = Interaction.builder() - .id(1).data(data).type(InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE.getType()) + .id(1L).data(data).type(InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE.getType()) .build(); List choices = RaidEncounter.getRaidEncounters(Raid.LAST_WISH) @@ -85,7 +78,7 @@ public void creatingApplicationCommandResponseWorksCorrectly() throws IOExceptio .id(1).name("raid_map").options(options).type(1) .build(); Interaction interaction = Interaction.builder() - .id(1).data(data).type(InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE.getType()) + .id(1L).data(data).type(InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE.getType()) .build(); Map resourcesMap = Map.of( diff --git a/src/test/java/com/danielvm/destiny2bot/factory/RaidStatsMessageCreatorCreatorTest.java b/src/test/java/com/danielvm/destiny2bot/factory/RaidStatsMessageCreatorCreatorTest.java deleted file mode 100644 index 2fbc150..0000000 --- a/src/test/java/com/danielvm/destiny2bot/factory/RaidStatsMessageCreatorCreatorTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.danielvm.destiny2bot.factory; - -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.dto.DestinyCharacter; -import com.danielvm.destiny2bot.dto.discord.Choice; -import com.danielvm.destiny2bot.dto.discord.DiscordUser; -import com.danielvm.destiny2bot.dto.discord.Interaction; -import com.danielvm.destiny2bot.dto.discord.InteractionResponse; -import com.danielvm.destiny2bot.dto.discord.Member; -import com.danielvm.destiny2bot.factory.creator.RaidStatsMessageCreator; -import com.danielvm.destiny2bot.service.DestinyCharacterService; -import java.util.List; -import java.util.stream.Collectors; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; -import reactor.test.StepVerifier.FirstStep; - -@ExtendWith(MockitoExtension.class) -public class RaidStatsMessageCreatorCreatorTest { - - @Mock - DestinyCharacterService destinyCharacterService; - - @InjectMocks - RaidStatsMessageCreator sut; - - @Test - @DisplayName("Create message is successful for users with more than one character") - public void createMessageIsSuccessful() { - // given: data from character service and userId - String userId = "someUserId"; - DiscordUser user = new DiscordUser(userId, "deahtstroke"); - Interaction interaction = Interaction.builder().member(new Member(user)).build(); - List characters = List.of( - new DestinyCharacter("1", "Titan", 1890, "Human"), - new DestinyCharacter("2", "Warlock", 1890, "Awoken"), - new DestinyCharacter("3", "Hunter", 1890, "Exo") - ); - - when(destinyCharacterService.getCharactersForUser(userId)) - .thenReturn(Flux.fromIterable(characters)); - - // when: createMessage is called - FirstStep response = StepVerifier - .create(sut.autocompleteResponse(interaction)); - - // then: the created Discord interaction has correct fields and the 'all' choice is added - List expectedChoices = characters.stream() - .map(ch -> new Choice("[%s] %s - %s".formatted(ch.getLightLevel(), ch.getCharacterRace(), - ch.getCharacterClass()), ch.getCharacterId())) - .collect(Collectors.toList()); - // Add the all flag - expectedChoices.add(new Choice("All", "Gets stats for all characters")); - - response - .assertNext(interactionResponse -> { - Assertions.assertThat(interactionResponse.getType()).isEqualTo(8); - Assertions.assertThat(interactionResponse.getData()).isNotNull(); - Assertions.assertThat(interactionResponse.getData().getChoices().size()).isEqualTo(4); - Assertions.assertThat(interactionResponse.getData().getChoices()) - .containsExactlyElementsOf(expectedChoices); - }) - .verifyComplete(); - } - - @Test - @DisplayName("Create message works for players with only one character") - public void createMessageForPlayersWithOneCharacter() { - // given: data from character service and userId - String userId = "someUserId"; - DiscordUser user = new DiscordUser(userId, "deahtstroke"); - Interaction interaction = Interaction.builder().member(new Member(user)).build(); - List characters = List.of( - new DestinyCharacter("1", "Titan", 1890, "Human") - ); - - when(destinyCharacterService.getCharactersForUser(userId)).thenReturn( - Flux.fromIterable(characters)); - - // when: createMessage is called - FirstStep response = StepVerifier - .create(sut.autocompleteResponse(interaction)); - - // then: the created Discord interaction has correct fields but no 'all' choice is added - List expectedChoices = characters.stream() - .map(ch -> new Choice("[%s] %s - %s".formatted(ch.getLightLevel(), ch.getCharacterRace(), - ch.getCharacterClass()), ch.getCharacterId())) - .collect(Collectors.toList()); - - response - .assertNext(interactionResponse -> { - Assertions.assertThat(interactionResponse.getType()).isEqualTo(8); - Assertions.assertThat(interactionResponse.getData()).isNotNull(); - Assertions.assertThat(interactionResponse.getData().getChoices().size()).isEqualTo(1); - Assertions.assertThat(interactionResponse.getData().getChoices()) - .containsExactlyElementsOf(expectedChoices); - }) - .verifyComplete(); - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonMessageCreatorTest.java b/src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonHandlerTest.java similarity index 88% rename from src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonMessageCreatorTest.java rename to src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonHandlerTest.java index ac51e40..b033636 100644 --- a/src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonMessageCreatorTest.java +++ b/src/test/java/com/danielvm/destiny2bot/factory/WeeklyDungeonHandlerTest.java @@ -6,7 +6,7 @@ import com.danielvm.destiny2bot.dto.discord.InteractionResponse; import com.danielvm.destiny2bot.enums.ActivityMode; import com.danielvm.destiny2bot.enums.InteractionResponseType; -import com.danielvm.destiny2bot.factory.creator.WeeklyDungeonMessageCreator; +import com.danielvm.destiny2bot.factory.handler.WeeklyDungeonHandler; import com.danielvm.destiny2bot.service.WeeklyActivitiesService; import com.danielvm.destiny2bot.util.MessageUtil; import java.time.ZonedDateTime; @@ -22,13 +22,13 @@ import reactor.test.StepVerifier.FirstStep; @ExtendWith(MockitoExtension.class) -public class WeeklyDungeonMessageCreatorTest { +public class WeeklyDungeonHandlerTest { @Mock WeeklyActivitiesService weeklyActivitiesService; @InjectMocks - WeeklyDungeonMessageCreator sut; + WeeklyDungeonHandler sut; @Test @DisplayName("Create message is successful") @@ -43,7 +43,7 @@ public void createMessageIsSuccessful() { sut.createResponse(null)); // then: the message created is correct - String expectedMessage = WeeklyDungeonMessageCreator.MESSAGE_TEMPLATE.formatted( + String expectedMessage = WeeklyDungeonHandler.MESSAGE_TEMPLATE.formatted( weeklyActivity.getName(), MessageUtil.formatDate(weeklyActivity.getEndDate().toLocalDate())); response diff --git a/src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidMessageCreatorTest.java b/src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidHandlerTest.java similarity index 89% rename from src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidMessageCreatorTest.java rename to src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidHandlerTest.java index a128e8a..dc8318a 100644 --- a/src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidMessageCreatorTest.java +++ b/src/test/java/com/danielvm/destiny2bot/factory/WeeklyRaidHandlerTest.java @@ -5,7 +5,7 @@ import com.danielvm.destiny2bot.dto.WeeklyActivity; import com.danielvm.destiny2bot.enums.ActivityMode; import com.danielvm.destiny2bot.enums.InteractionResponseType; -import com.danielvm.destiny2bot.factory.creator.WeeklyRaidMessageCreator; +import com.danielvm.destiny2bot.factory.handler.WeeklyRaidHandler; import com.danielvm.destiny2bot.service.WeeklyActivitiesService; import com.danielvm.destiny2bot.util.MessageUtil; import java.time.ZonedDateTime; @@ -21,13 +21,13 @@ import reactor.test.StepVerifier.FirstStep; @ExtendWith(MockitoExtension.class) -public class WeeklyRaidMessageCreatorTest { +public class WeeklyRaidHandlerTest { @Mock WeeklyActivitiesService weeklyActivitiesService; @InjectMocks - WeeklyRaidMessageCreator sut; + WeeklyRaidHandler sut; @Test @DisplayName("Create message is successful") @@ -42,7 +42,7 @@ public void createMessageIsSuccessful() { sut.createResponse(null)); // then: the message created is correct - String expectedMessage = WeeklyRaidMessageCreator.MESSAGE_TEMPLATE.formatted( + String expectedMessage = WeeklyRaidHandler.MESSAGE_TEMPLATE.formatted( weeklyActivity.getName(), MessageUtil.formatDate(weeklyActivity.getEndDate().toLocalDate())); response diff --git a/src/test/java/com/danielvm/destiny2bot/integration/BaseIntegrationTest.java b/src/test/java/com/danielvm/destiny2bot/integration/BaseIntegrationTest.java index 0a67bc7..c03dcbc 100644 --- a/src/test/java/com/danielvm/destiny2bot/integration/BaseIntegrationTest.java +++ b/src/test/java/com/danielvm/destiny2bot/integration/BaseIntegrationTest.java @@ -5,11 +5,6 @@ import com.danielvm.destiny2bot.dto.discord.Interaction; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.r2dbc.spi.ConnectionFactoryOptions; -import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.PublicKey; -import java.time.Instant; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.json.JSONException; @@ -17,8 +12,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.http.MediaType; import org.springframework.test.context.DynamicPropertyRegistry; @@ -27,25 +25,22 @@ import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; import org.springframework.web.reactive.function.BodyInserters; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import software.pando.crypto.nacl.Crypto; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.PublicKey; +import java.time.Instant; + @ExtendWith(MockitoExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=reactive") @AutoConfigureWireMock(files = "/build/resources/test/__files") @Testcontainers +@AutoConfigureWebTestClient public abstract class BaseIntegrationTest { - @Container - public static final PostgreSQLContainer POSTGRES_SQL_CONTAINER = new PostgreSQLContainer<>( - "postgres:16.1") - .withDatabaseName("riven_of_a_thousand_servers") - .withUsername("username") - .withPassword("password"); - @Container private static final GenericContainer REDIS_CONTAINER = new GenericContainer<>( "redis:5.0.3-alpine").withExposedPorts(6379); @@ -57,6 +52,9 @@ public abstract class BaseIntegrationTest { @LocalServerPort protected int localServerPort; + @Autowired + ReactiveWebApplicationContext reactiveWebApplicationContext; + @Autowired WebTestClient webTestClient; diff --git a/src/test/java/com/danielvm/destiny2bot/integration/InteractionControllerTest.java b/src/test/java/com/danielvm/destiny2bot/integration/InteractionControllerTest.java index 76ef6f6..43c791d 100644 --- a/src/test/java/com/danielvm/destiny2bot/integration/InteractionControllerTest.java +++ b/src/test/java/com/danielvm/destiny2bot/integration/InteractionControllerTest.java @@ -1,24 +1,9 @@ package com.danielvm.destiny2bot.integration; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.containsString; - import com.danielvm.destiny2bot.dao.UserDetailsReactiveDao; -import com.danielvm.destiny2bot.dto.destiny.GenericResponse; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; import com.danielvm.destiny2bot.dto.destiny.milestone.MilestoneEntry; -import com.danielvm.destiny2bot.dto.discord.Choice; -import com.danielvm.destiny2bot.dto.discord.DiscordUser; -import com.danielvm.destiny2bot.dto.discord.Interaction; -import com.danielvm.destiny2bot.dto.discord.InteractionData; -import com.danielvm.destiny2bot.dto.discord.InteractionResponse; -import com.danielvm.destiny2bot.dto.discord.Member; -import com.danielvm.destiny2bot.dto.discord.Option; +import com.danielvm.destiny2bot.dto.discord.*; import com.danielvm.destiny2bot.entity.UserDetails; import com.danielvm.destiny2bot.enums.InteractionType; import com.danielvm.destiny2bot.enums.ManifestEntity; @@ -32,12 +17,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.io.File; -import java.io.IOException; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.Objects; import org.apache.commons.codec.DecoderException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -49,6 +28,17 @@ import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; + public class InteractionControllerTest extends BaseIntegrationTest { // Static mapper to be used on the @BeforeAll static method @@ -71,7 +61,7 @@ public class InteractionControllerTest extends BaseIntegrationTest { @BeforeAll public static void before() throws IOException { File milestoneFile = new File("src/test/resources/__files/bungie/milestone-response.json"); - TypeReference>> typeReference = new TypeReference<>() { + TypeReference>> typeReference = new TypeReference<>() { }; var milestoneResponse = OBJECT_MAPPER.readValue(milestoneFile, typeReference); @@ -81,7 +71,7 @@ public static void before() throws IOException { OBJECT_MAPPER.writeValue(milestoneFile, milestoneResponse); } - private static void replaceDates(GenericResponse> response, + private static void replaceDates(BungieResponse> response, String hash) { response.getResponse().entrySet().stream() .filter(entry -> Objects.equals(entry.getKey(), hash)) @@ -104,7 +94,7 @@ public void getWeeklyDungeonWorksSuccessfully() throws JsonProcessingException, InteractionData weeklyDungeonData = InteractionData.builder() .id(2).name("weekly_dungeon").type(1) .build(); - Interaction body = Interaction.builder().id(1) + Interaction body = Interaction.builder().id(1L) .applicationId("theApplicationId").data(weeklyDungeonData).type(2) .build(); @@ -179,7 +169,7 @@ public void getWeeklyRaidWorksSuccessfully() throws JsonProcessingException, Dec InteractionData weeklyRaidData = InteractionData.builder() .id(2).name("weekly_raid").type(1) .build(); - Interaction body = Interaction.builder().id(1).applicationId("theApplicationId").type(2) + Interaction body = Interaction.builder().id(1L).applicationId("theApplicationId").type(2) .data(weeklyRaidData).build(); stubFor(get(urlPathEqualTo("/bungie/Destiny2/Milestones/")) @@ -244,7 +234,7 @@ public void getWeeklyRaidsShouldThrowErrors() throws JsonProcessingException, De InteractionData weeklyRaidData = InteractionData.builder() .id(2).name("weekly_raid").type(1) .build(); - Interaction body = Interaction.builder().id(1).applicationId("theApplicationId").type(2) + Interaction body = Interaction.builder().id(1L).applicationId("theApplicationId").type(2) .data(weeklyRaidData).build(); stubFor(get(urlPathEqualTo("/bungie/Destiny2/Milestones/")) @@ -282,7 +272,7 @@ public void getWeeklyRaidInvalidSignature() throws JsonProcessingException, Deco InteractionData data = InteractionData.builder() .id(2).name("weekly_raid").type(1).build(); Interaction body = Interaction.builder() - .id(1).applicationId("theApplicationId").type(2).data(data) + .id(1L).applicationId("theApplicationId").type(2).data(data) .build(); // when: the request is sent @@ -300,7 +290,7 @@ public void getWeeklyRaidInvalidSignature() throws JsonProcessingException, Deco @DisplayName("PING interactions with valid signatures are ack'd correctly") public void pingRequestsAreAckdCorrectly() throws JsonProcessingException, DecoderException { // given: an interaction with an invalid signature - Interaction body = Interaction.builder().id(1).applicationId("theApplicationId").type(1) + Interaction body = Interaction.builder().id(1L).applicationId("theApplicationId").type(1) .build(); // when: the request is sent @@ -317,7 +307,7 @@ public void pingRequestsAreAckdCorrectly() throws JsonProcessingException, Decod @DisplayName("PING interactions with invalid signatures are not ack'd") public void invalidPingRequestsAreNotAckd() throws JsonProcessingException, DecoderException { // given: an interaction with an invalid signature - Interaction body = Interaction.builder().id(1) + Interaction body = Interaction.builder().id(1L) .applicationId("theApplicationId").type(1) .build(); @@ -344,7 +334,7 @@ public void autocompleteRequestsForRaidStats() throws DecoderException, JsonProc InteractionData data = InteractionData.builder().id("2").name("raid_stats").type(1) .build(); Interaction body = Interaction.builder() - .id("1").applicationId("theApplicationId").type(4).data(data).member(memberInfo) + .id(1L).applicationId("theApplicationId").type(4).data(data).member(memberInfo) .build(); // dummy entity in Redis @@ -404,7 +394,7 @@ public void autocompleteRequestForUsersWithOneCharacter() .id("2").name("raid_stats").type(1) .build(); Interaction body = Interaction.builder() - .id("1").applicationId("theApplicationId").type(4).data(data).member(memberInfo) + .id(1L).applicationId("theApplicationId").type(4).data(data).member(memberInfo) .build(); // dummy entity in Redis @@ -458,7 +448,7 @@ public void autocompleteRequestFailsForEmptyCharactersFromBungie() .id("1").name("raid_stats").type(1) .build(); Interaction body = Interaction.builder() - .id("1").applicationId("theApplicationId").data(data).member(memberInfo).type(4) + .id(1L).applicationId("theApplicationId").data(data).member(memberInfo).type(4) .build(); // dummy entity in Redis @@ -509,7 +499,7 @@ public void autocompleteRequestsForRaidMapAreSuccessful() .name("raid_map") .options(options).build(); Interaction body = Interaction.builder() - .id("someInteractionID") + .id(1L) .data(data) .type(InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE.getType()) .build(); @@ -550,7 +540,7 @@ public void commandRequestsForRaidMapAreSuccessful() .name("raid_map") .options(options).build(); Interaction body = Interaction.builder() - .id("someInteractionID") + .id(1L) .data(data) .type(InteractionType.APPLICATION_COMMAND.getType()) .build(); diff --git a/src/test/java/com/danielvm/destiny2bot/mapper/BotUserMapperTest.java b/src/test/java/com/danielvm/destiny2bot/mapper/BotUserMapperTest.java deleted file mode 100644 index 3224a98..0000000 --- a/src/test/java/com/danielvm/destiny2bot/mapper/BotUserMapperTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.danielvm.destiny2bot.mapper; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.entity.BotUser; -import io.r2dbc.spi.Row; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class BotUserMapperTest { - - private final BotUserMapper sut = new BotUserMapper(); - - @Test - @DisplayName("Mapping an SQL row to BotUser is successful") - public void mappingRowToBotUserIsSuccessful() { - // given: a Table Row from an SQL query - Row rowMock = mock(Row.class); - when(rowMock.get("discord_id", Long.class)).thenReturn(172731L); - when(rowMock.get("discord_username", String.class)).thenReturn("Deahtstroke"); - when(rowMock.get("bungie_membership_id", Long.class)).thenReturn(28134L); - when(rowMock.get("bungie_access_token", String.class)).thenReturn("someAccessToken"); - when(rowMock.get("bungie_refresh_token", String.class)).thenReturn("someRefreshToken"); - when(rowMock.get("bungie_token_expiration", Long.class)).thenReturn(3600L); - - // when: applying the BiFunction - BotUser result = sut.apply(rowMock, null); - - // then: the resulting BotUser object has correct fields - assertThat(result.getDiscordId()).isEqualTo(172731L); - assertThat(result.getDiscordUsername()).isEqualTo("Deahtstroke"); - assertThat(result.getBungieMembershipId()).isEqualTo(28134L); - assertThat(result.getBungieAccessToken()).isEqualTo("someAccessToken"); - assertThat(result.getBungieRefreshToken()).isEqualTo("someRefreshToken"); - assertThat(result.getBungieTokenExpiration()).isEqualTo(3600L); - assertThat(result.getCharacters()).isEmpty(); - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/mapper/UserCharacterMapperTest.java b/src/test/java/com/danielvm/destiny2bot/mapper/UserCharacterMapperTest.java deleted file mode 100644 index 10f64e6..0000000 --- a/src/test/java/com/danielvm/destiny2bot/mapper/UserCharacterMapperTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.danielvm.destiny2bot.mapper; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.entity.UserCharacter; -import io.r2dbc.spi.Row; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class UserCharacterMapperTest { - - private final UserCharacterMapper sut = new UserCharacterMapper(); - - @Test - @DisplayName("Mapping Row to UserCharacter works successfully") - public void successfulMappingRowToUserCharacter() { - // given: an SQL row that contains user character data - Row row = mock(Row.class); - when(row.get("character_id", Long.class)).thenReturn(892812L); - when(row.get("light_level", Integer.class)).thenReturn(1810); - when(row.get("destiny_class", String.class)).thenReturn("Titan"); - when(row.get("discord_user_id", Long.class)).thenReturn(172731L); - - // when: applying the BiFunction - UserCharacter result = sut.apply(row, null); - - // then: the result has correct mappings - assertThat(result.getCharacterId()).isEqualTo(892812L); - assertThat(result.getLightLevel()).isEqualTo(1810); - assertThat(result.getDestinyClass()).isEqualTo("Titan"); - assertThat(result.getDiscordUserId()).isEqualTo(172731L); - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImplTest.java b/src/test/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImplTest.java deleted file mode 100644 index 2a1e72d..0000000 --- a/src/test/java/com/danielvm/destiny2bot/repository/BotUserRepositoryImplTest.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import static com.danielvm.destiny2bot.repository.BotUserRepositoryImpl.INSERT_USER_QUERY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.entity.BotUser; -import com.danielvm.destiny2bot.entity.UserCharacter; -import com.danielvm.destiny2bot.exception.ResourceNotFoundException; -import com.danielvm.destiny2bot.mapper.BotUserMapper; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.r2dbc.core.DatabaseClient; -import org.springframework.r2dbc.core.DatabaseClient.GenericExecuteSpec; -import org.springframework.r2dbc.core.RowsFetchSpec; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; -import reactor.test.StepVerifier.FirstStep; - -@ExtendWith(MockitoExtension.class) -public class BotUserRepositoryImplTest { - - @Mock - DatabaseClient databaseClient; - - @InjectMocks - BotUserRepositoryImpl sut; - - @Test - @DisplayName("Retrieving Bot user w/characters is successful") - public void retrieveBotUserTest() { - // given: a bot user to save to the DB - BotUser user = new BotUser(1234L, "Deahtstroke", 56789L, - "someAccessToken", "someRefreshToken", 3600L, null); - BotUserMapper mapper = new BotUserMapper(); - - Map params = Map.of( - "discordId", user.getDiscordId(), - "discordUsername", user.getDiscordUsername(), - "membershipId", user.getBungieMembershipId(), - "accessToken", user.getBungieAccessToken(), - "refreshToken", user.getBungieRefreshToken(), - "tokenExpiration", user.getBungieTokenExpiration() - ); - - RowsFetchSpec botUserRowsFetchSpec = mock(RowsFetchSpec.class); - GenericExecuteSpec genericExecuteSpec = mock(GenericExecuteSpec.class); - when(databaseClient.sql(INSERT_USER_QUERY)) - .thenReturn(genericExecuteSpec); - when(genericExecuteSpec.bindValues(params)) - .thenReturn(genericExecuteSpec); - - when(genericExecuteSpec.map(any(BiFunction.class))) - .thenReturn(botUserRowsFetchSpec); - - when(botUserRowsFetchSpec.one()) - .thenReturn(Mono.just(user)); - - // when: save is called with the user - FirstStep result = StepVerifier.create(sut.save(user)); - - // then: the given user is returned successfully - result.assertNext(botUser -> { - assertThat(botUser.getDiscordId()).isEqualTo(user.getDiscordId()); - assertThat(botUser.getBungieAccessToken()).isEqualTo(user.getBungieAccessToken()); - assertThat(botUser.getBungieRefreshToken()).isEqualTo(user.getBungieRefreshToken()); - assertThat(botUser.getBungieTokenExpiration()).isEqualTo(user.getBungieTokenExpiration()); - }).verifyComplete(); - } - - @Test - @DisplayName("findByDiscordId is successful") - public void findByDiscordIdIsSuccessful() { - // given: some DiscordId - Long discordId = 173312L; - - List characters = List.of( - new UserCharacter(1L, 1810, "Titan", 12L), - new UserCharacter(2L, 1789, "Hunter", 12L) - ); - BotUser user = new BotUser(12L, "deaht", 34L, - "someAccessToken", "someRefreshToken", 3600L, null); - - GenericExecuteSpec botUserGenericSpec = mock(GenericExecuteSpec.class); - RowsFetchSpec botUserRowsFetchSpec = mock(RowsFetchSpec.class); - GenericExecuteSpec userCharacterGenericSpec = mock(GenericExecuteSpec.class); - RowsFetchSpec userCharacterRowsFetchSpec = mock(RowsFetchSpec.class); - when(databaseClient.sql(""" - SELECT bu.discord_id, - bu.discord_username, - bu.bungie_membership_id, - bu.bungie_access_token, - bu.bungie_refresh_token, - bu.bungie_token_expiration - FROM bot_user bu - WHERE bu.discord_id = :discordId - """)).thenReturn(botUserGenericSpec); - when(botUserGenericSpec.bind("discordId", discordId)).thenReturn(botUserGenericSpec); - when(botUserGenericSpec.map(any(BiFunction.class))).thenReturn(botUserRowsFetchSpec); - when(botUserRowsFetchSpec.first()).thenReturn(Mono.just(user)); - - when(databaseClient.sql(""" - SELECT buc.character_id, - buc.light_level, - buc.destiny_class, - buc.discord_user_id - FROM bot_user bu - INNER JOIN bungie_user_character buc on - bu.discord_id = buc.discord_user_id - WHERE bu.discord_id = :discordId - """)).thenReturn(userCharacterGenericSpec); - when(userCharacterGenericSpec.bind("discordId", discordId)).thenReturn( - userCharacterGenericSpec); - when(userCharacterGenericSpec.map(any(BiFunction.class))).thenReturn( - userCharacterRowsFetchSpec); - when(userCharacterRowsFetchSpec.all()).thenReturn(Flux.fromIterable(characters)); - - // when: findByDiscordId is called - var result = StepVerifier.create(sut.findBotUserByDiscordId(discordId)); - - // then: the found botUser has the correct fields - result.assertNext(botUser -> { - assertThat(botUser.getBungieTokenExpiration()).isEqualTo(user.getBungieTokenExpiration()); - assertThat(botUser.getBungieMembershipId()).isEqualTo(user.getBungieMembershipId()); - assertThat(botUser.getBungieRefreshToken()).isEqualTo(user.getBungieRefreshToken()); - assertThat(botUser.getBungieAccessToken()).isEqualTo(user.getBungieAccessToken()); - assertThat(botUser.getDiscordUsername()).isEqualTo(user.getDiscordUsername()); - assertThat(botUser.getDiscordId()).isEqualTo(user.getDiscordId()); - botUser.getCharacters().forEach(character -> { - if (character.getCharacterId().equals(characters.get(0).getCharacterId())) { - assertThat(character.getDiscordUserId()).isEqualTo(characters.get(0).getDiscordUserId()); - assertThat(character.getDestinyClass()).isEqualTo(characters.get(0).getDestinyClass()); - assertThat(character.getLightLevel()).isEqualTo(characters.get(0).getLightLevel()); - } else { - assertThat(character.getDiscordUserId()).isEqualTo(characters.get(1).getDiscordUserId()); - assertThat(character.getDestinyClass()).isEqualTo(characters.get(1).getDestinyClass()); - assertThat(character.getLightLevel()).isEqualTo(characters.get(1).getLightLevel()); - } - }); - }).verifyComplete(); - } - - @Test - @DisplayName("findByDiscordId throws exception when Bot User is not found") - public void findByDiscordIdShouldThrowException() { - // given: some DiscordId - Long discordId = 173312L; - - GenericExecuteSpec botUserGenericSpec = mock(GenericExecuteSpec.class); - RowsFetchSpec botUserRowsFetchSpec = mock(RowsFetchSpec.class); - when(databaseClient.sql(""" - SELECT bu.discord_id, - bu.discord_username, - bu.bungie_membership_id, - bu.bungie_access_token, - bu.bungie_refresh_token, - bu.bungie_token_expiration - FROM bot_user bu - WHERE bu.discord_id = :discordId - """)).thenReturn(botUserGenericSpec); - when(botUserGenericSpec.bind("discordId", discordId)).thenReturn(botUserGenericSpec); - when(botUserGenericSpec.map(any(BiFunction.class))).thenReturn(botUserRowsFetchSpec); - when(botUserRowsFetchSpec.first()).thenReturn(Mono.empty()); - - // when: findByDiscordId is called - // then: a ResourceNotFoundException is thrown as a result of an empty Mono - StepVerifier.create(sut.findBotUserByDiscordId(discordId)) - .expectError(ResourceNotFoundException.class) - .verify(); - - // and: the appropriate error message is thrown with the exception - StepVerifier.create(sut.findBotUserByDiscordId(discordId)) - .expectErrorMessage("Discord user with Id [%s] not found".formatted(discordId)) - .verify(); - - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImplTest.java b/src/test/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImplTest.java deleted file mode 100644 index 15b4c96..0000000 --- a/src/test/java/com/danielvm/destiny2bot/repository/UserCharacterRepositoryImplTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.danielvm.destiny2bot.repository; - -import static com.danielvm.destiny2bot.repository.UserCharacterRepositoryImpl.INSERT_CHARACTER_QUERY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.entity.UserCharacter; -import java.util.Map; -import java.util.function.BiFunction; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.r2dbc.core.DatabaseClient; -import org.springframework.r2dbc.core.DatabaseClient.GenericExecuteSpec; -import org.springframework.r2dbc.core.RowsFetchSpec; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -@ExtendWith(MockitoExtension.class) -public class UserCharacterRepositoryImplTest { - - @Mock - private DatabaseClient databaseClient; - - @InjectMocks - private UserCharacterRepositoryImpl sut; - - @Test - @DisplayName("Save user character is successful") - public void saveUserCharacterIsSuccessful() { - // given: a user character to save - UserCharacter userCharacter = new UserCharacter( - 1L, 1801, "Titan", 12345L); - - Map parameters = Map.of( - "characterId", userCharacter.getCharacterId(), - "lightLevel", userCharacter.getLightLevel(), - "discordUserId", userCharacter.getDiscordUserId(), - "destinyClass", userCharacter.getDestinyClass() - ); - - GenericExecuteSpec genericExecuteSpec = mock(GenericExecuteSpec.class); - RowsFetchSpec rowsFetchSpec = mock(RowsFetchSpec.class); - - when(databaseClient.sql(INSERT_CHARACTER_QUERY)).thenReturn(genericExecuteSpec); - when(genericExecuteSpec.bindValues(parameters)).thenReturn(genericExecuteSpec); - when(genericExecuteSpec.map(any(BiFunction.class))).thenReturn(rowsFetchSpec); - when(rowsFetchSpec.one()).thenReturn(Mono.just(userCharacter)); - - // when: save is called for a user character - var result = StepVerifier.create(sut.save(userCharacter)); - - // then: the saved character is returned - result.assertNext(user -> { - assertThat(user.getDiscordUserId()).isEqualTo(userCharacter.getDiscordUserId()); - assertThat(user.getCharacterId()).isEqualTo(userCharacter.getCharacterId()); - assertThat(user.getLightLevel()).isEqualTo(userCharacter.getLightLevel()); - assertThat(user.getDestinyClass()).isEqualTo(userCharacter.getDestinyClass()); - }).verifyComplete(); - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/service/InteractionServiceTest.java b/src/test/java/com/danielvm/destiny2bot/service/InteractionServiceTest.java index 3c8fbd0..3c065eb 100644 --- a/src/test/java/com/danielvm/destiny2bot/service/InteractionServiceTest.java +++ b/src/test/java/com/danielvm/destiny2bot/service/InteractionServiceTest.java @@ -1,12 +1,10 @@ package com.danielvm.destiny2bot.service; -import static com.danielvm.destiny2bot.enums.InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT; import static com.danielvm.destiny2bot.enums.InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE; -import static com.danielvm.destiny2bot.factory.creator.WeeklyDungeonMessageCreator.MESSAGE_TEMPLATE; +import static com.danielvm.destiny2bot.factory.handler.WeeklyDungeonHandler.MESSAGE_TEMPLATE; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; -import com.danielvm.destiny2bot.dto.discord.Choice; import com.danielvm.destiny2bot.dto.discord.DiscordUser; import com.danielvm.destiny2bot.dto.discord.Interaction; import com.danielvm.destiny2bot.dto.discord.InteractionData; @@ -16,11 +14,9 @@ import com.danielvm.destiny2bot.enums.SlashCommand; import com.danielvm.destiny2bot.factory.ApplicationCommandFactory; import com.danielvm.destiny2bot.factory.AutocompleteFactory; -import com.danielvm.destiny2bot.factory.creator.RaidStatsMessageCreator; -import com.danielvm.destiny2bot.factory.creator.WeeklyDungeonMessageCreator; +import com.danielvm.destiny2bot.factory.handler.WeeklyDungeonHandler; import com.danielvm.destiny2bot.util.MessageUtil; import java.time.LocalDate; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,12 +30,8 @@ @ExtendWith(MockitoExtension.class) public class InteractionServiceTest { - - @Mock - RaidStatsMessageCreator raidStatsMessageCreator; - @Mock - WeeklyDungeonMessageCreator weeklyDungeonMessageCreator; + WeeklyDungeonHandler weeklyDungeonHandler; @Mock ApplicationCommandFactory applicationCommandFactory; @@ -91,9 +83,9 @@ public void handleInteractionFor() { .build(); when(applicationCommandFactory.messageCreator(SlashCommand.WEEKLY_DUNGEON)) - .thenReturn(weeklyDungeonMessageCreator); + .thenReturn(weeklyDungeonHandler); - when(weeklyDungeonMessageCreator.createResponse(interaction)) + when(weeklyDungeonHandler.createResponse(interaction)) .thenReturn(Mono.just(message)); // when: the interaction is received @@ -109,48 +101,4 @@ public void handleInteractionFor() { .verifyComplete(); } - @Test - @DisplayName("Create response is successful for APPLICATION_COMMAND_AUTOCOMPLETE interaction request that requires authentication") - public void handleInteractionForAuthorizedAutocomplete() { - // given: interaction data from a slash-command autocomplete - String userId = "userId"; - Member member = new Member(new DiscordUser(userId, "myUsername")); - InteractionData data = InteractionData.builder() - .id("someId").name("raid_stats").type(1) - .build(); - Interaction interaction = Interaction.builder() - .applicationId("myApplicationId").type(4).data(data).member(member) - .build(); - - List choices = List.of( - new Choice("Character 1", "1"), - new Choice("Character 1", "1"), - new Choice("Character 1", "1"), - new Choice("All", "All") - ); - InteractionResponse message = InteractionResponse.builder() - .type(APPLICATION_COMMAND_AUTOCOMPLETE_RESULT.getType()) - .data(InteractionResponseData.builder() - .choices(choices) - .build()) - .build(); - - when(autocompleteFactory.messageCreator(SlashCommand.RAID_STATS)) - .thenReturn(raidStatsMessageCreator); - - when(raidStatsMessageCreator.autocompleteResponse(interaction)) - .thenReturn(Mono.just(message)); - - // when: the interaction is received - FirstStep response = StepVerifier.create( - interactionService.handleInteraction(interaction)); - - // then: the response received is correct and the message returned has the correct content - response - .assertNext(result -> { - assertThat(result.getType()).isEqualTo(APPLICATION_COMMAND_AUTOCOMPLETE_RESULT.getType()); - assertThat(result.getData().getChoices()).containsAll(choices); - }) - .verifyComplete(); - } } diff --git a/src/test/java/com/danielvm/destiny2bot/service/UserRegistrationServiceTest.java b/src/test/java/com/danielvm/destiny2bot/service/UserRegistrationServiceTest.java deleted file mode 100644 index ec85ddb..0000000 --- a/src/test/java/com/danielvm/destiny2bot/service/UserRegistrationServiceTest.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.danielvm.destiny2bot.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.when; - -import com.danielvm.destiny2bot.client.DiscordClient; -import com.danielvm.destiny2bot.config.BungieConfiguration; -import com.danielvm.destiny2bot.config.DiscordConfiguration; -import com.danielvm.destiny2bot.dao.UserDetailsReactiveDao; -import com.danielvm.destiny2bot.dto.discord.DiscordUserResponse; -import com.danielvm.destiny2bot.dto.oauth2.TokenResponse; -import com.danielvm.destiny2bot.entity.UserDetails; -import com.danielvm.destiny2bot.util.OAuth2Util; -import java.util.Locale; -import java.util.Objects; -import java.util.stream.Stream; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClient.RequestBodyUriSpec; -import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; -import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -@ExtendWith(MockitoExtension.class) -public class UserRegistrationServiceTest { - - private static final TokenResponse GENERIC_TOKEN_RESPONSE = new TokenResponse( - "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", - "IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", - 3600L, - "Bearer"); - @Mock - private DiscordConfiguration discordConfigurationMock; - @Mock - private BungieConfiguration bungieConfigurationMock; - @Mock - private DiscordClient discordClientMock; - @Mock - private UserDetailsReactiveDao userDetailsReactiveDaoMock; - @Spy - private WebClient.Builder defaultWebClientBuilderMock; - @Mock - private RequestBodyUriSpec requestBodyUriSpec; - @Mock - private RequestHeadersSpec requestHeadersSpec; - @Mock - private ResponseSpec responseSpec; - @Mock - private WebClient webClientMock; - @InjectMocks - private UserRegistrationService sut; - - static Stream discordUsersWithMissingAttributes() { - return Stream.of( - arguments(new DiscordUserResponse(null, "someUsername", "avatar", "en_US")), - arguments(new DiscordUserResponse("someId", null, "avatar", "en_US"))); - } - - static Stream bungieTokenMissingAttributes() { - return Stream.of( - arguments(new TokenResponse(null, "Bearer", 3600L, "someRefreshToken")), - arguments(new TokenResponse("someAccessToken", "Bearer", null, "someRefreshToken")), - arguments(new TokenResponse("someAccessToken", "Bearer", 3600L, null))); - } - - @Test - @DisplayName("authenticate Discord user is successful") - public void authenticateDiscordUserIsSuccessful() { - // given: authorization code and an HttpSession - var authorizationCode = "someAuthorizationCode"; - var httpSession = new MockHttpSession(); - - var discordUserId = "88012312784012"; - var discordUsername = "generic_discord_user"; - var discordAvatar = "generic_avatar"; - var locale = Locale.US; - DiscordUserResponse discordUser = new DiscordUserResponse(discordUserId, discordUsername, - discordAvatar, locale.toString()); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(GENERIC_TOKEN_RESPONSE)); - - when(discordClientMock.getUser( - OAuth2Util.formatBearerToken(GENERIC_TOKEN_RESPONSE.getAccessToken()))) - .thenReturn(Mono.just(discordUser)); - - // when: authenticateDiscordUser is called - StepVerifier.create(sut.authenticateDiscordUser(authorizationCode, httpSession)) - .assertNext( - entity -> { - var status = entity.getStatusCode(); - var relocationUrl = Objects.requireNonNull( - entity.getHeaders().get(HttpHeaders.LOCATION)).get(0); - Assertions.assertThat(relocationUrl) - .isEqualTo("http://discord.auth.url/oauth/auth?response_type=code&client_id"); - Assertions.assertThat(status).isEqualTo(HttpStatus.FOUND); - }) - .verifyComplete(); - - // then: the HttpSession has session attributes related to it - assertThat(httpSession.getAttribute("discordUserId")).isEqualTo(discordUserId); - assertThat(httpSession.getAttribute("discordUserAlias")).isEqualTo(discordUsername); - } - - @Test - @DisplayName("authenticate Discord fails if access_token is null") - public void authenticateDiscordUserFailsIfAccessTokenIsNull() { - // given: authorization code and an HttpSession - var authorizationCode = "someAuthorizationCode"; - var httpSession = new MockHttpSession(); - - TokenResponse nullAccessToken = new TokenResponse( - null, null, null, null); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(nullAccessToken)); - - // when: authenticateDiscordUser is called, an IllegalArgumentException is thrown - StepVerifier.create(sut.authenticateDiscordUser(authorizationCode, httpSession)) - .expectErrorMessage( - "The access token, the refresh token or the expiration are null for user") - .verify(); - } - - @ParameterizedTest - @MethodSource("discordUsersWithMissingAttributes") - @DisplayName("authenticate Discord user fails if user response is missing attributes") - public void authenticateDiscordUserFailsIfUserIsMissingAttributes( - DiscordUserResponse userArgument) { - // given: authorization code and an HttpSession - var authorizationCode = "someAuthorizationCode"; - var httpSession = new MockHttpSession(); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(GENERIC_TOKEN_RESPONSE)); - - when(discordClientMock.getUser( - OAuth2Util.formatBearerToken(GENERIC_TOKEN_RESPONSE.getAccessToken()))) - .thenReturn(Mono.just(userArgument)); - - // when: authenticateDiscordUser is called an IllegalStateException is thrown - StepVerifier.create(sut.authenticateDiscordUser(authorizationCode, httpSession)) - .expectErrorMessage( - "Some required arguments for registration are null for the current user") - .verify(); - } - - @Test - @DisplayName("authenticate Discord user fails if user response empty") - public void authenticateDiscordUserFailsIfUserIsEmpty() { - // given: authorization code and an HttpSession - var authorizationCode = "someAuthorizationCode"; - var httpSession = new MockHttpSession(); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(GENERIC_TOKEN_RESPONSE)); - - when(discordClientMock.getUser( - OAuth2Util.formatBearerToken(GENERIC_TOKEN_RESPONSE.getAccessToken()))) - .thenReturn(Mono.empty()); - - // when: authenticateDiscordUser is called an IllegalStateException is thrown - StepVerifier.create(sut.authenticateDiscordUser(authorizationCode, httpSession)) - .expectErrorMessage("The user response from Discord is empty") - .verify(); - } - - @Test - @DisplayName("registering a new user using Bungie and Discord data is successful") - public void registerDiscordAndBungieUserIsSuccessful() { - // given: authorization code and an HttpSession with previous Discord data - var authorizationCode = "someAuthorizationCode"; - - var discordUserId = "88012312784012"; - var discordUsername = "generic_discord_user"; - - var httpSession = new MockHttpSession(); - httpSession.setAttribute("discordUserId", discordUserId); - httpSession.setAttribute("discordUserAlias", discordUsername); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(GENERIC_TOKEN_RESPONSE)); - - UserDetails databaseEntity = UserDetails.builder() - .discordId(discordUserId) - .discordUsername(discordUsername) - .expiration(any()) - .refreshToken(GENERIC_TOKEN_RESPONSE.getRefreshToken()) - .accessToken(GENERIC_TOKEN_RESPONSE.getAccessToken()) - .build(); - - lenient().when(userDetailsReactiveDaoMock.save(databaseEntity)) - .thenReturn(Mono.just(Boolean.TRUE)); - - // when: linkDiscordUserToBungieAccount is called - StepVerifier.create(sut.linkDiscordUserToBungieAccount(authorizationCode, httpSession)) - .assertNext(next -> assertThat(httpSession.isInvalid()).isTrue()); - } - - @ParameterizedTest - @MethodSource("bungieTokenMissingAttributes") - @DisplayName("Linking and registering user fails if Bungie's access token is missing fields") - public void linkDiscordUserToBungieFailsIfSomeFieldsAreNotPresent( - TokenResponse bungieToken) { - // given: authorization code and an HttpSession with previous Discord data - var authorizationCode = "someAuthorizationCode"; - - var discordUserId = "88012312784012"; - var discordUsername = "generic_discord_user"; - - var httpSession = new MockHttpSession(); - httpSession.setAttribute("discordUserId", discordUserId); - httpSession.setAttribute("discordUserAlias", discordUsername); - - mockGetTokenMethodCalls(); - - when(responseSpec.bodyToMono(TokenResponse.class)) - .thenReturn(Mono.just(bungieToken)); - - // when: linkDiscordUserToBungieAccount is called, an exception is thrown with an error message - StepVerifier.create(sut.linkDiscordUserToBungieAccount(authorizationCode, httpSession)) - .expectErrorMessage( - "The access token, the refresh token or the expiration are null for user") - .verify(); - } - - public void mockGetTokenMethodCalls() { - // Leniency is needed for these two mock - lenient().when(discordConfigurationMock.getAuthorizationUrl()) - .thenReturn("http://discord.auth.url/oauth/auth"); - - lenient().when(bungieConfigurationMock.getAuthorizationUrl()) - .thenReturn("http://discord.auth.url/oauth/auth"); - - lenient().when(discordConfigurationMock.getTokenUrl()) - .thenReturn("http://discord.token.url/oauth/token"); - - lenient().when(bungieConfigurationMock.getTokenUrl()) - .thenReturn("http://bungie.token.url/oauth/token"); - - when(defaultWebClientBuilderMock.baseUrl(anyString())) - .thenReturn(defaultWebClientBuilderMock); - - when(defaultWebClientBuilderMock.defaultHeader(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE)) - .thenReturn(defaultWebClientBuilderMock); - - when(defaultWebClientBuilderMock.defaultHeader(HttpHeaders.ACCEPT, - MediaType.APPLICATION_JSON_VALUE)) - .thenReturn(defaultWebClientBuilderMock); - - when(defaultWebClientBuilderMock.build()) - .thenReturn(webClientMock); - - when(webClientMock.post()) - .thenReturn(requestBodyUriSpec); - - when(requestBodyUriSpec.body(any())) - .thenReturn(requestHeadersSpec); - - when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - } - -} diff --git a/src/test/java/com/danielvm/destiny2bot/service/WeeklyActivitiesServiceTest.java b/src/test/java/com/danielvm/destiny2bot/service/WeeklyActivitiesServiceTest.java index 88e7056..ce74c2a 100644 --- a/src/test/java/com/danielvm/destiny2bot/service/WeeklyActivitiesServiceTest.java +++ b/src/test/java/com/danielvm/destiny2bot/service/WeeklyActivitiesServiceTest.java @@ -5,7 +5,7 @@ import com.danielvm.destiny2bot.client.BungieClient; import com.danielvm.destiny2bot.client.BungieClientWrapper; import com.danielvm.destiny2bot.dto.WeeklyActivity; -import com.danielvm.destiny2bot.dto.destiny.GenericResponse; +import com.danielvm.destiny2bot.dto.destiny.BungieResponse; import com.danielvm.destiny2bot.dto.destiny.manifest.DisplayProperties; import com.danielvm.destiny2bot.dto.destiny.manifest.ResponseFields; import com.danielvm.destiny2bot.dto.destiny.milestone.ActivitiesDto; @@ -46,7 +46,7 @@ public void retrieveWeeklyRaidWorksSuccessfully() { var endTime = ZonedDateTime.now().plusDays(2L); var activitiesNoWeekly = List.of(new ActivitiesDto("1262462921", Collections.emptyList())); var activitiesWeekly = List.of(new ActivitiesDto("2823159265", List.of("897950155"))); - var milestoneResponse = new GenericResponse<>( + var milestoneResponse = new BungieResponse<>( Map.of( "526718853", new MilestoneEntry("526718853", startTime, endTime, activitiesNoWeekly), @@ -64,7 +64,7 @@ public void retrieveWeeklyRaidWorksSuccessfully() { var activityWithType = new ResponseFields(); activityWithType.setActivityTypeHash("608898761"); - var raidActivityEntity = new GenericResponse<>(activityWithType); + var raidActivityEntity = new BungieResponse<>(activityWithType); when( bungieClientWrapper.getManifestEntityRx(ManifestEntity.ACTIVITY_DEFINITION, "2823159265")) @@ -73,7 +73,7 @@ public void retrieveWeeklyRaidWorksSuccessfully() { var raidResponseFields = new ResponseFields(); raidResponseFields.setDisplayProperties( new DisplayProperties("someDescription", "Raid", null, null, false)); - var activityTypeEntity = new GenericResponse<>(raidResponseFields); + var activityTypeEntity = new BungieResponse<>(raidResponseFields); when(bungieClientWrapper.getManifestEntityRx(ManifestEntity.ACTIVITY_TYPE_DEFINITION, "608898761")) @@ -83,7 +83,7 @@ public void retrieveWeeklyRaidWorksSuccessfully() { var lastWishDisplayProperties = new DisplayProperties("Delve into the Last Wish raid", "The Last Wish", null, null, false); milestoneResponseFields.setDisplayProperties(lastWishDisplayProperties); - var milestoneEntity = new GenericResponse<>(milestoneResponseFields); + var milestoneEntity = new BungieResponse<>(milestoneResponseFields); when( bungieClientWrapper.getManifestEntityRx(ManifestEntity.MILESTONE_DEFINITION, "3618845105")) @@ -111,7 +111,7 @@ public void retrieveWeeklyDungeonIsSuccessful() { var endTime = ZonedDateTime.now().plusDays(2L); var activitiesNoWeekly = List.of(new ActivitiesDto("1262462921", Collections.emptyList())); var activitiesWeekly = List.of(new ActivitiesDto("2823159265", List.of("897950155"))); - var milestoneResponse = new GenericResponse<>( + var milestoneResponse = new BungieResponse<>( Map.of( "526718853", new MilestoneEntry("526718853", startTime, endTime, activitiesNoWeekly), @@ -129,7 +129,7 @@ public void retrieveWeeklyDungeonIsSuccessful() { var activityWithType = new ResponseFields(); activityWithType.setActivityTypeHash("608898761"); - var raidActivityEntity = new GenericResponse<>(activityWithType); + var raidActivityEntity = new BungieResponse<>(activityWithType); when(bungieClientWrapper.getManifestEntityRx(ManifestEntity.ACTIVITY_DEFINITION, "2823159265")) .thenReturn(Mono.just(raidActivityEntity)); @@ -137,7 +137,7 @@ public void retrieveWeeklyDungeonIsSuccessful() { var dungeonResponseFields = new ResponseFields(); dungeonResponseFields.setDisplayProperties( new DisplayProperties("someDescription", "Dungeon", null, null, false)); - var activityTypeEntity = new GenericResponse<>(dungeonResponseFields); + var activityTypeEntity = new BungieResponse<>(dungeonResponseFields); when(bungieClientWrapper.getManifestEntityRx(ManifestEntity.ACTIVITY_TYPE_DEFINITION, "608898761")) @@ -147,7 +147,7 @@ public void retrieveWeeklyDungeonIsSuccessful() { var dualityDisplayProperties = new DisplayProperties("Calus' mind as a dungeon lol", "Duality", null, null, false); milestoneResponseFields.setDisplayProperties(dualityDisplayProperties); - var milestoneEntity = new GenericResponse<>(milestoneResponseFields); + var milestoneEntity = new BungieResponse<>(milestoneResponseFields); when(bungieClientWrapper.getManifestEntityRx(ManifestEntity.MILESTONE_DEFINITION, "3618845105")) .thenReturn(Mono.just(milestoneEntity)); @@ -173,7 +173,7 @@ public void getWeeklyActivityError() { var startTime = ZonedDateTime.now(); var endTime = ZonedDateTime.now().plusDays(2L); var activitiesNoWeekly = List.of(new ActivitiesDto("1262462921", Collections.emptyList())); - var milestoneResponse = new GenericResponse<>( + var milestoneResponse = new BungieResponse<>( Map.of( "526718853", new MilestoneEntry("526718853", startTime, endTime, activitiesNoWeekly), diff --git a/src/test/resources/__files/bungie/milestone-response.json b/src/test/resources/__files/bungie/milestone-response.json index e518d83..0cda0c0 100644 --- a/src/test/resources/__files/bungie/milestone-response.json +++ b/src/test/resources/__files/bungie/milestone-response.json @@ -1 +1 @@ -{"response":{"3021174356":{"milestoneHash":"3021174356","startDate":null,"endDate":null,"activities":null},"644555645":{"milestoneHash":"644555645","startDate":null,"endDate":null,"activities":null},"3181387331":{"milestoneHash":"3181387331","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":[{"activityHash":"2122313384","challengeObjectiveHashes":[]}]},"4253138191":{"milestoneHash":"4253138191","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":null},"3603098564":{"milestoneHash":"3603098564","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":null},"3802603984":{"milestoneHash":"3802603984","startDate":null,"endDate":null,"activities":null},"2709491520":{"milestoneHash":"2709491520","startDate":null,"endDate":null,"activities":null},"2594202463":{"milestoneHash":"2594202463","startDate":null,"endDate":null,"activities":null},"3899487295":{"milestoneHash":"3899487295","startDate":null,"endDate":null,"activities":null},"2712317338":{"milestoneHash":"2712317338","startDate":1706029200.000000000,"endDate":1707238800.000000000,"activities":[{"activityHash":"1042180643","challengeObjectiveHashes":["2398860795"]}]},"541780856":{"milestoneHash":"541780856","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":[{"activityHash":"910380154","challengeObjectiveHashes":[]}]},"526718853":{"milestoneHash":"526718853","startDate":1706029200.000000000,"endDate":1707238800.000000000,"activities":[{"activityHash":"1262462921","challengeObjectiveHashes":["3211393925"]},{"activityHash":"2296818662","challengeObjectiveHashes":["3211393925"]}]}}} \ No newline at end of file +{"response":{"3021174356":{"milestoneHash":"3021174356","startDate":null,"endDate":null,"activities":null},"644555645":{"milestoneHash":"644555645","startDate":null,"endDate":null,"activities":null},"3181387331":{"milestoneHash":"3181387331","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":[{"activityHash":"2122313384","challengeObjectiveHashes":[]}]},"4253138191":{"milestoneHash":"4253138191","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":null},"3603098564":{"milestoneHash":"3603098564","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":null},"3802603984":{"milestoneHash":"3802603984","startDate":null,"endDate":null,"activities":null},"2709491520":{"milestoneHash":"2709491520","startDate":null,"endDate":null,"activities":null},"2594202463":{"milestoneHash":"2594202463","startDate":null,"endDate":null,"activities":null},"3899487295":{"milestoneHash":"3899487295","startDate":null,"endDate":null,"activities":null},"2712317338":{"milestoneHash":"2712317338","startDate":1707843600.000000000,"endDate":1708448400.000000000,"activities":[{"activityHash":"1042180643","challengeObjectiveHashes":["2398860795"]}]},"541780856":{"milestoneHash":"541780856","startDate":1701795600.000000000,"endDate":1702400400.000000000,"activities":[{"activityHash":"910380154","challengeObjectiveHashes":[]}]},"526718853":{"milestoneHash":"526718853","startDate":1707843600.000000000,"endDate":1708448400.000000000,"activities":[{"activityHash":"1262462921","challengeObjectiveHashes":["3211393925"]},{"activityHash":"2296818662","challengeObjectiveHashes":["3211393925"]}]}}} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 1140133..f5f50f9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,14 +1,9 @@ spring: + main: + web-application-type: reactive data: redis: port: 6379 - r2dbc: - url: r2dbc:tc:postgresql:///riven_of_a_thousand_servers?postgres=16.1 - username: username - password: password - flyway: - url: jdbc:tc:postgresql:16.1:///riven_of_a_thousand_servers - enabled: true bungie: api: