much stuff
This commit is contained in:
parent
eeb09ee1fd
commit
0b176c3b2a
22
HELP.md
22
HELP.md
@ -1,22 +0,0 @@
|
||||
# Getting Started
|
||||
|
||||
### Reference Documentation
|
||||
For further reference, please consider the following sections:
|
||||
|
||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
||||
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.1/maven-plugin/reference/html/)
|
||||
* [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.1/maven-plugin/reference/html/#build-image)
|
||||
* [Spring Data MongoDB](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#data.nosql.mongodb)
|
||||
|
||||
### Guides
|
||||
The following guides illustrate how to use some features concretely:
|
||||
|
||||
* [Accessing Data with MongoDB](https://spring.io/guides/gs/accessing-data-mongodb/)
|
||||
|
||||
### Maven Parent overrides
|
||||
|
||||
Due to Maven's design, elements are inherited from the parent POM to the project POM.
|
||||
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
|
||||
To prevent this, the project POM contains empty overrides for these elements.
|
||||
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
|
||||
|
23
pom.xml
23
pom.xml
@ -29,17 +29,16 @@
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Libraries -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
@ -47,6 +46,11 @@
|
||||
<version>1.18.32</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<dependency>
|
||||
@ -54,5 +58,12 @@
|
||||
<artifactId>JDA</artifactId>
|
||||
<version>5.0.0-beta.24</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -9,7 +9,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
|
||||
@SpringBootApplication()
|
||||
|
89
src/main/java/cc/fascinated/bat/command/BatCommand.java
Normal file
89
src/main/java/cc/fascinated/bat/command/BatCommand.java
Normal file
@ -0,0 +1,89 @@
|
||||
package cc.fascinated.bat.command;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Getter @Setter
|
||||
public abstract class BatCommand implements BatCommandExecutor {
|
||||
/**
|
||||
* The name of the command
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The description of the command
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* The category of the command
|
||||
*/
|
||||
private final Category category;
|
||||
|
||||
/**
|
||||
* The command data for the slash command
|
||||
*/
|
||||
private CommandDataImpl commandData;
|
||||
|
||||
/**
|
||||
* The sub commands of the command
|
||||
*/
|
||||
private Map<String, BatSubCommand> subCommands = new HashMap<>();
|
||||
|
||||
public BatCommand(@NonNull String name) {
|
||||
this.name = name;
|
||||
|
||||
// Default values
|
||||
this.description = "No description provided.";
|
||||
this.category = Category.GENERAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a sub command to the command
|
||||
*
|
||||
* @param name The name of the sub command
|
||||
* @param subCommand The sub command
|
||||
*/
|
||||
public BatCommand addSubCommand(@NonNull String name, @NonNull BatSubCommand subCommand) {
|
||||
this.subCommands.put(name.toLowerCase(), subCommand);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the options for the command
|
||||
*
|
||||
* @param interaction The slash command interaction
|
||||
* @return The option strings
|
||||
*/
|
||||
public List<String> getOptions(SlashCommandInteraction interaction) {
|
||||
return interaction.getOptions().stream().map(OptionMapping::getName).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* The category of the command
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
private enum Category {
|
||||
GENERAL("General"),
|
||||
MODERATION("Moderation"),
|
||||
SERVER("Server");
|
||||
|
||||
/**
|
||||
* The name of the category
|
||||
*/
|
||||
private final String name;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package cc.fascinated.bat.command;
|
||||
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.service.GuildService;
|
||||
import cc.fascinated.bat.service.UserService;
|
||||
import lombok.NonNull;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public interface BatCommandExecutor {
|
||||
|
||||
/**
|
||||
* Executes the command using a slash command interaction.
|
||||
*
|
||||
* @param guild the bat guild the command was executed in
|
||||
* @param user the bat user that executed the command
|
||||
* @param channel the channel the command was executed in
|
||||
* @param member the member that executed the command
|
||||
* @param interaction the slash command interaction
|
||||
* @param option the option that was used in the command, or null if no option was used
|
||||
*/
|
||||
default void execute(
|
||||
@NonNull BatGuild guild,
|
||||
@NonNull BatUser user,
|
||||
@NonNull TextChannel channel,
|
||||
@NonNull Member member,
|
||||
@NonNull SlashCommandInteraction interaction,
|
||||
OptionMapping option
|
||||
) {}
|
||||
}
|
18
src/main/java/cc/fascinated/bat/command/BatSubCommand.java
Normal file
18
src/main/java/cc/fascinated/bat/command/BatSubCommand.java
Normal file
@ -0,0 +1,18 @@
|
||||
package cc.fascinated.bat.command;
|
||||
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.service.GuildService;
|
||||
import cc.fascinated.bat.service.UserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public class BatSubCommand implements BatCommandExecutor { }
|
64
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/LinkSubCommand.java
Normal file
64
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/LinkSubCommand.java
Normal file
@ -0,0 +1,64 @@
|
||||
package cc.fascinated.bat.command.impl.global.beatsaber.scoresaber;
|
||||
|
||||
import cc.fascinated.bat.command.BatSubCommand;
|
||||
import cc.fascinated.bat.common.Profile;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberAccountToken;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.model.user.profiles.ScoreSaberProfile;
|
||||
import cc.fascinated.bat.service.ScoreSaberService;
|
||||
import cc.fascinated.bat.service.UserService;
|
||||
import lombok.NonNull;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Component
|
||||
public class LinkSubCommand extends BatSubCommand {
|
||||
|
||||
private final ScoreSaberService scoreSaberService;
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService) {
|
||||
this.scoreSaberService = scoreSaberService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull TextChannel channel, @NonNull Member member, @NonNull SlashCommandInteraction interaction, OptionMapping option) {
|
||||
if (option == null) {
|
||||
interaction.reply("You must provide a ScoreSaber profile link to link your profile").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String link = option.getAsString();
|
||||
if (!link.contains("scoresaber.com/u/")) {
|
||||
interaction.reply("Invalid ScoreSaber profile link").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String id = link.split("scoresaber.com/u/")[1];
|
||||
if (id.contains("/")) {
|
||||
id = id.split("/")[0];
|
||||
}
|
||||
|
||||
ScoreSaberAccountToken account = scoreSaberService.getAccount(id);
|
||||
if (account == null) {
|
||||
interaction.reply("Invalid ScoreSaber profile link").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
((ScoreSaberProfile) user.getProfile(ScoreSaberProfile.class)).setId(id);
|
||||
userService.saveUser(user);
|
||||
interaction.reply("Successfully linked your ScoreSaber profile").queue();
|
||||
}
|
||||
}
|
72
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/ScoreSaberCommand.java
Normal file
72
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/ScoreSaberCommand.java
Normal file
@ -0,0 +1,72 @@
|
||||
package cc.fascinated.bat.command.impl.global.beatsaber.scoresaber;
|
||||
|
||||
import cc.fascinated.bat.command.BatCommand;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberAccountToken;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.model.user.profiles.ScoreSaberProfile;
|
||||
import cc.fascinated.bat.service.GuildService;
|
||||
import cc.fascinated.bat.service.ScoreSaberService;
|
||||
import cc.fascinated.bat.service.UserService;
|
||||
import lombok.NonNull;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Component
|
||||
public class ScoreSaberCommand extends BatCommand {
|
||||
private final ScoreSaberService scoreSaberService;
|
||||
|
||||
@Autowired
|
||||
public ScoreSaberCommand(@NonNull ScoreSaberService scoreSaberService) {
|
||||
super("scoresaber");
|
||||
this.scoreSaberService = scoreSaberService;
|
||||
|
||||
super.setDescription("View a user's ScoreSaber profile");
|
||||
super.setCommandData(new CommandDataImpl(this.getName(), this.getDescription())
|
||||
.addOptions(new OptionData(OptionType.STRING, "link", "Link your ScoreSaber profile", false))
|
||||
.addOptions(new OptionData(OptionType.USER, "user", "The user to view the ScoreSaber profile of", false))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull TextChannel channel, @NonNull Member member, @NonNull SlashCommandInteraction interaction, OptionMapping option) {
|
||||
ScoreSaberProfile profile = user.getProfile(ScoreSaberProfile.class);
|
||||
if (profile.getId() == null) {
|
||||
interaction.reply("You must link your ScoreSaber profile first").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: handle rate limits
|
||||
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getId());
|
||||
if (account == null) {
|
||||
interaction.reply("Invalid ScoreSaber profile, please re-link your account.").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
interaction.replyEmbeds(buildProfileEmbed(account)).queue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the profile embed for the ScoreSaber profile
|
||||
*
|
||||
* @param account The account to build the embed for
|
||||
* @return The built embed
|
||||
*/
|
||||
public static MessageEmbed buildProfileEmbed(ScoreSaberAccountToken account) {
|
||||
return new EmbedBuilder()
|
||||
.addField("Name", account.getName(), true)
|
||||
.build();
|
||||
}
|
||||
}
|
25
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/UserSubCommand.java
Normal file
25
src/main/java/cc/fascinated/bat/command/impl/global/beatsaber/scoresaber/UserSubCommand.java
Normal file
@ -0,0 +1,25 @@
|
||||
package cc.fascinated.bat.command.impl.global.beatsaber.scoresaber;
|
||||
|
||||
import cc.fascinated.bat.command.BatSubCommand;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.service.GuildService;
|
||||
import cc.fascinated.bat.service.UserService;
|
||||
import lombok.NonNull;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Component
|
||||
public class UserSubCommand extends BatSubCommand {
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull TextChannel channel, @NonNull Member member, @NonNull SlashCommandInteraction interaction, OptionMapping option) {
|
||||
interaction.reply("view someone elses profile").queue();
|
||||
}
|
||||
}
|
23
src/main/java/cc/fascinated/bat/common/DateUtils.java
Normal file
23
src/main/java/cc/fascinated/bat/common/DateUtils.java
Normal file
@ -0,0 +1,23 @@
|
||||
package cc.fascinated.bat.common;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
@UtilityClass
|
||||
public class DateUtils {
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT;
|
||||
|
||||
/**
|
||||
* Gets the date from a string.
|
||||
*
|
||||
* @param date The date string.
|
||||
* @return The date.
|
||||
*/
|
||||
public static Date getDateFromString(String date) {
|
||||
return Date.from(Instant.from(FORMATTER.parse(date)));
|
||||
}
|
||||
}
|
50
src/main/java/cc/fascinated/bat/common/EmbedUtils.java
Normal file
50
src/main/java/cc/fascinated/bat/common/EmbedUtils.java
Normal file
@ -0,0 +1,50 @@
|
||||
package cc.fascinated.bat.common;
|
||||
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public class EmbedUtils {
|
||||
|
||||
/**
|
||||
* Builds a generic embed
|
||||
*
|
||||
* @param description the description of the embed
|
||||
* @return the embed builder
|
||||
*/
|
||||
public static EmbedBuilder buildGenericEmbed(String description) {
|
||||
return new EmbedBuilder()
|
||||
.setDescription(description)
|
||||
.setTimestamp(LocalDateTime.now())
|
||||
.setColor(0x2F3136);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an error embed
|
||||
*
|
||||
* @param description the description of the embed
|
||||
* @return the embed builder
|
||||
*/
|
||||
public static EmbedBuilder buildErrorEmbed(String description) {
|
||||
return new EmbedBuilder()
|
||||
.setDescription(description)
|
||||
.setTimestamp(LocalDateTime.now())
|
||||
.setColor(0xFF0000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a success embed
|
||||
*
|
||||
* @param description the description of the embed
|
||||
* @return the embed builder
|
||||
*/
|
||||
public static EmbedBuilder buildSuccessEmbed(String description) {
|
||||
return new EmbedBuilder()
|
||||
.setDescription(description)
|
||||
.setTimestamp(LocalDateTime.now())
|
||||
.setColor(0x00FF00);
|
||||
}
|
||||
}
|
16
src/main/java/cc/fascinated/bat/common/Profile.java
Normal file
16
src/main/java/cc/fascinated/bat/common/Profile.java
Normal file
@ -0,0 +1,16 @@
|
||||
package cc.fascinated.bat.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.data.annotation.Transient;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public class Profile {
|
||||
/**
|
||||
* The key of the profile.
|
||||
*/
|
||||
@Transient private final String profileKey;
|
||||
}
|
77
src/main/java/cc/fascinated/bat/common/WebRequest.java
Normal file
77
src/main/java/cc/fascinated/bat/common/WebRequest.java
Normal file
@ -0,0 +1,77 @@
|
||||
package cc.fascinated.bat.common;
|
||||
|
||||
import cc.fascinated.bat.exception.RateLimitException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@UtilityClass
|
||||
public class WebRequest {
|
||||
|
||||
/**
|
||||
* The web client.
|
||||
*/
|
||||
private static final RestClient CLIENT;
|
||||
|
||||
static {
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(2500); // 2.5 seconds
|
||||
CLIENT = RestClient.builder()
|
||||
.requestFactory(requestFactory)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
* @param <T> the type of the response
|
||||
*/
|
||||
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
|
||||
ResponseEntity<T> responseEntity = CLIENT.get()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
|
||||
if (responseEntity.getStatusCode().isError()) {
|
||||
return null;
|
||||
}
|
||||
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
|
||||
throw new RateLimitException("Rate limit reached");
|
||||
}
|
||||
return responseEntity.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
*/
|
||||
public static ResponseEntity<?> get(String url, Class<?> clazz) {
|
||||
return CLIENT.get()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
*/
|
||||
public static ResponseEntity<?> head(String url, Class<?> clazz) {
|
||||
return CLIENT.head()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
}
|
||||
}
|
11
src/main/java/cc/fascinated/bat/config/AppConfig.java
Normal file
11
src/main/java/cc/fascinated/bat/config/AppConfig.java
Normal file
@ -0,0 +1,11 @@
|
||||
package cc.fascinated.bat.config;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "cc.fascinated.bat")
|
||||
public class AppConfig { }
|
32
src/main/java/cc/fascinated/bat/config/MongoConfig.java
Normal file
32
src/main/java/cc/fascinated/bat/config/MongoConfig.java
Normal file
@ -0,0 +1,32 @@
|
||||
package cc.fascinated.bat.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.convert.DbRefResolver;
|
||||
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
|
||||
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
|
||||
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Configuration
|
||||
public class MongoConfig {
|
||||
@Bean
|
||||
public MappingMongoConverter mongoConverter(MongoDatabaseFactory mongoFactory, MongoMappingContext mongoMappingContext) {
|
||||
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoFactory);
|
||||
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
|
||||
mongoConverter.setMapKeyDotReplacement("-DOT");
|
||||
return mongoConverter;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.fascinated.bat.exception;
|
||||
|
||||
import lombok.experimental.StandardException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@StandardException
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public class BadRequestException extends RuntimeException { }
|
@ -0,0 +1,12 @@
|
||||
package cc.fascinated.bat.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
|
||||
public class RateLimitException extends RuntimeException {
|
||||
|
||||
public RateLimitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.fascinated.bat.exception;
|
||||
|
||||
import lombok.experimental.StandardException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@StandardException
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public class ResourceNotFoundException extends RuntimeException { }
|
33
src/main/java/cc/fascinated/bat/model/BatGuild.java
Normal file
33
src/main/java/cc/fascinated/bat/model/BatGuild.java
Normal file
@ -0,0 +1,33 @@
|
||||
package cc.fascinated.bat.model;
|
||||
|
||||
import cc.fascinated.bat.service.DiscordService;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter @Setter
|
||||
@Document(collection = "guilds")
|
||||
public class BatGuild {
|
||||
|
||||
/**
|
||||
* The ID of the guild
|
||||
*/
|
||||
@NonNull @Id private final String id;
|
||||
|
||||
/**
|
||||
* Gets the guild as the JDA Guild
|
||||
*
|
||||
* @return the guild
|
||||
*/
|
||||
private Guild getDiscordGuild() {
|
||||
return DiscordService.JDA.getGuildById(id);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cc.fascinated.bat.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class Guild {
|
||||
|
||||
/**
|
||||
* The ID of the guild
|
||||
*/
|
||||
@NonNull @Id private final String id;
|
||||
}
|
139
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberAccountToken.java
Normal file
139
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberAccountToken.java
Normal file
@ -0,0 +1,139 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class ScoreSaberAccountToken {
|
||||
/**
|
||||
* The id for this ScoreSaber account.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The name for this account.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The profile picture for this account.
|
||||
*/
|
||||
private String profilePicture;
|
||||
|
||||
/**
|
||||
* The bio for this account.
|
||||
*/
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* The country for this account.
|
||||
*/
|
||||
private String country;
|
||||
|
||||
/**
|
||||
* The PP for this account.
|
||||
*/
|
||||
private double pp;
|
||||
|
||||
/**
|
||||
* The rank for this account.
|
||||
*/
|
||||
private int rank;
|
||||
|
||||
/**
|
||||
* The country rank for this account.
|
||||
*/
|
||||
private int countryRank;
|
||||
|
||||
/**
|
||||
* The role for this account.
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* The badges for this account.
|
||||
*/
|
||||
private Badge[] badges;
|
||||
|
||||
/**
|
||||
* The history of the rank for this account.
|
||||
*/
|
||||
private String histories;
|
||||
|
||||
/**
|
||||
* The permissions for this account.
|
||||
*/
|
||||
private int permissions;
|
||||
|
||||
/**
|
||||
* The banned status for this account.
|
||||
*/
|
||||
private boolean banned;
|
||||
|
||||
/**
|
||||
* The inactive status for this account.
|
||||
*/
|
||||
private boolean inactive;
|
||||
|
||||
/**
|
||||
* The score stats for this account.
|
||||
*/
|
||||
private ScoreStats scoreStats;
|
||||
|
||||
/**
|
||||
* The first time this account was seen.
|
||||
*/
|
||||
private String firstSeen;
|
||||
|
||||
/**
|
||||
* The badge for this account.
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public static class Badge {
|
||||
/**
|
||||
* The image for this badge.
|
||||
*/
|
||||
private String image;
|
||||
|
||||
/**
|
||||
* The description for this badge.
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The score stats for this account.
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public static class ScoreStats {
|
||||
/**
|
||||
* The total score for this account.
|
||||
*/
|
||||
private long totalScore;
|
||||
|
||||
/**
|
||||
* The total ranked score for this account.
|
||||
*/
|
||||
private long totalRankedScore;
|
||||
|
||||
/**
|
||||
* The average ranked accuracy for this account.
|
||||
*/
|
||||
private double averageRankedAccuracy;
|
||||
|
||||
/**
|
||||
* The total play count for this account.
|
||||
*/
|
||||
private int totalPlayCount;
|
||||
|
||||
/**
|
||||
* The ranked play count for this account.
|
||||
*/
|
||||
private int rankedPlayCount;
|
||||
|
||||
/**
|
||||
* The replays watched for this account.
|
||||
*/
|
||||
private int replaysWatched;
|
||||
}
|
||||
}
|
145
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java
Normal file
145
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java
Normal file
@ -0,0 +1,145 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberLeaderboardToken {
|
||||
/**
|
||||
* The ID of the leaderboard.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The hash of the song.
|
||||
*/
|
||||
private String songHash;
|
||||
|
||||
/**
|
||||
* The name of the song.
|
||||
*/
|
||||
private String songName;
|
||||
|
||||
/**
|
||||
* The sub name of the song.
|
||||
*/
|
||||
private String songSubName;
|
||||
|
||||
/**
|
||||
* The author of the song.
|
||||
*/
|
||||
private String songAuthorName;
|
||||
|
||||
/**
|
||||
* The mapper of the song.
|
||||
*/
|
||||
private String levelAuthorName;
|
||||
|
||||
/**
|
||||
* The difficulty of the song.
|
||||
*/
|
||||
private Difficulty difficulty;
|
||||
|
||||
/**
|
||||
* The maximum score of the song.
|
||||
*/
|
||||
private int maxScore;
|
||||
|
||||
/**
|
||||
* The date the leaderboard was created.
|
||||
*/
|
||||
private String createdDate;
|
||||
|
||||
/**
|
||||
* The date the song was ranked.
|
||||
*/
|
||||
private String rankedDate;
|
||||
|
||||
/**
|
||||
* The date the song was qualified.
|
||||
*/
|
||||
private String qualifiedDate;
|
||||
|
||||
/**
|
||||
* The date the song's status was changed to loved.
|
||||
*/
|
||||
private String lovedDate;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is ranked.
|
||||
*/
|
||||
private boolean ranked;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is qualified to be ranked.
|
||||
*/
|
||||
private boolean qualified;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is in a loved state.
|
||||
*/
|
||||
private boolean loved;
|
||||
|
||||
/**
|
||||
* The maximum PP for this leaderboard.
|
||||
*/
|
||||
private int maxPP;
|
||||
|
||||
/**
|
||||
* The star rating for this leaderboard.
|
||||
*/
|
||||
private double stars;
|
||||
|
||||
/**
|
||||
* The amount of plays for this leaderboard.
|
||||
*/
|
||||
private int plays;
|
||||
|
||||
/**
|
||||
* The amount of daily plays for this leaderboard.
|
||||
*/
|
||||
private int dailyPlays;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard has positive modifiers.
|
||||
*/
|
||||
private boolean positiveModifiers;
|
||||
|
||||
/**
|
||||
* The cover image for this leaderboard.
|
||||
*/
|
||||
private String coverImage;
|
||||
|
||||
/**
|
||||
* The difficulties for this leaderboard.
|
||||
*/
|
||||
private List<Difficulty> difficulties;
|
||||
|
||||
/**
|
||||
* The difficulty of the leaderboard.
|
||||
*/
|
||||
@Getter
|
||||
public static class Difficulty {
|
||||
/**
|
||||
* The leaderboard ID.
|
||||
*/
|
||||
private int leaderboardId;
|
||||
|
||||
/**
|
||||
* The difficulty of the leaderboard.
|
||||
*/
|
||||
private int difficulty;
|
||||
|
||||
/**
|
||||
* The game mode of the leaderboard.
|
||||
*/
|
||||
private String gameMode;
|
||||
|
||||
/**
|
||||
* The difficulty raw of the leaderboard.
|
||||
*/
|
||||
private String difficultyRaw;
|
||||
}
|
||||
}
|
22
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java
Normal file
22
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java
Normal file
@ -0,0 +1,22 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberPageMetadataToken {
|
||||
/**
|
||||
* The total amount of scores.
|
||||
*/
|
||||
private int total;
|
||||
|
||||
/**
|
||||
* The current page.
|
||||
*/
|
||||
private int page;
|
||||
|
||||
/**
|
||||
* The amount of scores per page.
|
||||
*/
|
||||
private int itemsPerPage;
|
||||
}
|
17
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java
Normal file
17
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java
Normal file
@ -0,0 +1,17 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberPlayerScoreToken {
|
||||
/**
|
||||
* The score that was set.
|
||||
*/
|
||||
private ScoreSaberScoreToken score;
|
||||
|
||||
/**
|
||||
* The leaderboard that the score was set on.
|
||||
*/
|
||||
private ScoreSaberLeaderboardToken leaderboard;
|
||||
}
|
135
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberScoreToken.java
Normal file
135
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberScoreToken.java
Normal file
@ -0,0 +1,135 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberScoreToken {
|
||||
/**
|
||||
* The id for this score.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The player info for this score.
|
||||
*/
|
||||
private LeaderboardPlayerInfo leaderboardPlayerInfo;
|
||||
|
||||
/**
|
||||
* The rank of this score.
|
||||
*/
|
||||
private int rank;
|
||||
|
||||
/**
|
||||
* The base score for this score.
|
||||
*/
|
||||
private int baseScore;
|
||||
|
||||
/**
|
||||
* The modified score for this score.
|
||||
*/
|
||||
private int modifiedScore;
|
||||
|
||||
/**
|
||||
* The PP for this score.
|
||||
*/
|
||||
private double pp;
|
||||
|
||||
/**
|
||||
* The weight for this score.
|
||||
*/
|
||||
private int weight;
|
||||
|
||||
/**
|
||||
* The modifiers for this score.
|
||||
*/
|
||||
private String modifiers;
|
||||
|
||||
/**
|
||||
* The multiplier for this score.
|
||||
*/
|
||||
private int multiplier;
|
||||
|
||||
/**
|
||||
* How many bad cuts this score has.
|
||||
*/
|
||||
private int badCuts;
|
||||
|
||||
/**
|
||||
* How many misses this score has.
|
||||
*/
|
||||
private int missedNotes;
|
||||
|
||||
/**
|
||||
* The maximum combo for this score.
|
||||
*/
|
||||
private int maxCombo;
|
||||
|
||||
/**
|
||||
* Whether this score was a full combo.
|
||||
*/
|
||||
private boolean fullCombo;
|
||||
|
||||
/**
|
||||
* The HMD that was used to set this score.
|
||||
*/
|
||||
private int hmd;
|
||||
|
||||
/**
|
||||
* The time set for this score.
|
||||
*/
|
||||
private String timeSet;
|
||||
|
||||
/**
|
||||
* Whether this score has a replay.
|
||||
*/
|
||||
private boolean hasReplay;
|
||||
|
||||
/**
|
||||
* The full HMD name that was used to set this score.
|
||||
*/
|
||||
private String deviceHmd;
|
||||
|
||||
/**
|
||||
* The controller that was used on the left hand.
|
||||
*/
|
||||
private String deviceControllerLeft;
|
||||
|
||||
/**
|
||||
* The controller that was used on the right hand.
|
||||
*/
|
||||
private String deviceControllerRight;
|
||||
|
||||
@Getter
|
||||
public class LeaderboardPlayerInfo {
|
||||
/**
|
||||
* The ID of the player.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The name of the player.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The profile picture of the player.
|
||||
*/
|
||||
private String profilePicture;
|
||||
|
||||
/**
|
||||
* The country of the player.
|
||||
*/
|
||||
private String country;
|
||||
|
||||
/**
|
||||
* The permissions for the player.
|
||||
*/
|
||||
private int permissions;
|
||||
|
||||
/**
|
||||
* The role for the player.
|
||||
*/
|
||||
private String role;
|
||||
}
|
||||
}
|
19
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberScoresPageToken.java
Normal file
19
src/main/java/cc/fascinated/bat/model/beatsaber/scoresaber/ScoreSaberScoresPageToken.java
Normal file
@ -0,0 +1,19 @@
|
||||
package cc.fascinated.bat.model.beatsaber.scoresaber;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @Setter
|
||||
@ToString
|
||||
public class ScoreSaberScoresPageToken {
|
||||
/**
|
||||
* The scores on this page.
|
||||
*/
|
||||
private ScoreSaberPlayerScoreToken[] playerScores;
|
||||
|
||||
/**
|
||||
* The metadata for this page.
|
||||
*/
|
||||
private ScoreSaberPageMetadataToken metadata;
|
||||
}
|
66
src/main/java/cc/fascinated/bat/model/user/BatUser.java
Normal file
66
src/main/java/cc/fascinated/bat/model/user/BatUser.java
Normal file
@ -0,0 +1,66 @@
|
||||
package cc.fascinated.bat.model.user;
|
||||
|
||||
import cc.fascinated.bat.common.Profile;
|
||||
import cc.fascinated.bat.service.DiscordService;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter @Setter
|
||||
@Document(collection = "users")
|
||||
public class BatUser {
|
||||
|
||||
/**
|
||||
* The ID of the user
|
||||
*/
|
||||
@NonNull @Id private final String id;
|
||||
|
||||
/**
|
||||
* The profiles for this user
|
||||
*/
|
||||
private Map<String, Profile> profiles;
|
||||
|
||||
/**
|
||||
* Gets the profile for the user
|
||||
*
|
||||
* @param clazz The class of the profile
|
||||
* @param <T> The type of the profile
|
||||
* @return The profile
|
||||
*/
|
||||
public <T extends Profile> T getProfile(Class<?> clazz) {
|
||||
if (profiles == null) {
|
||||
profiles = new HashMap<>();
|
||||
}
|
||||
|
||||
Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null);
|
||||
if (profile == null) {
|
||||
try {
|
||||
profile = (Profile) clazz.newInstance();
|
||||
profiles.put(profile.getProfileKey(), profile);
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return (T) profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the guild as the JDA Guild
|
||||
*
|
||||
* @return the guild
|
||||
*/
|
||||
private User getDiscordUser() {
|
||||
return DiscordService.JDA.getUserById(id);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package cc.fascinated.bat.model.user.profiles;
|
||||
|
||||
import cc.fascinated.bat.common.Profile;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Setter @Getter
|
||||
public class ScoreSaberProfile extends Profile {
|
||||
/**
|
||||
* The Account ID of the ScoreSaber profile
|
||||
*/
|
||||
private String id;
|
||||
|
||||
public ScoreSaberProfile() {
|
||||
super("scoresaber");
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package cc.fascinated.bat.repository;
|
||||
|
||||
import cc.fascinated.bat.model.Guild;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public interface GuildRepository extends MongoRepository<Guild, String> { }
|
||||
public interface GuildRepository extends MongoRepository<BatGuild, String> { }
|
@ -0,0 +1,9 @@
|
||||
package cc.fascinated.bat.repository;
|
||||
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public interface UserRepository extends MongoRepository<BatUser, String> { }
|
156
src/main/java/cc/fascinated/bat/service/CommandService.java
Normal file
156
src/main/java/cc/fascinated/bat/service/CommandService.java
Normal file
@ -0,0 +1,156 @@
|
||||
package cc.fascinated.bat.service;
|
||||
|
||||
import cc.fascinated.bat.command.BatCommand;
|
||||
import cc.fascinated.bat.command.BatSubCommand;
|
||||
import cc.fascinated.bat.command.impl.global.beatsaber.scoresaber.LinkSubCommand;
|
||||
import cc.fascinated.bat.command.impl.global.beatsaber.scoresaber.ScoreSaberCommand;
|
||||
import cc.fascinated.bat.command.impl.global.beatsaber.scoresaber.UserSubCommand;
|
||||
import cc.fascinated.bat.common.EmbedUtils;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service @Log4j2
|
||||
@DependsOn("discordService")
|
||||
public class CommandService extends ListenerAdapter {
|
||||
/**
|
||||
* The registered commands
|
||||
*/
|
||||
private final Map<String, BatCommand> commands = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The guild service to use
|
||||
*/
|
||||
private final GuildService guildService;
|
||||
|
||||
/**
|
||||
* The user service to use
|
||||
*/
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* The application context to use
|
||||
*/
|
||||
private final ApplicationContext context;
|
||||
|
||||
@Autowired
|
||||
public CommandService(@NonNull GuildService guildService, @NonNull UserService userService, @NonNull ApplicationContext context) {
|
||||
this.guildService = guildService;
|
||||
this.userService = userService;
|
||||
this.context = context;
|
||||
DiscordService.JDA.addEventListener(this);
|
||||
|
||||
// Guild commands
|
||||
// todo: add some, duh
|
||||
|
||||
// Global commands
|
||||
registerCommand(context.getBean(ScoreSaberCommand.class)
|
||||
.addSubCommand("link", context.getBean(LinkSubCommand.class))
|
||||
.addSubCommand("user", context.getBean(UserSubCommand.class)
|
||||
));
|
||||
|
||||
registerSlashCommands(); // Register all slash commands
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a command
|
||||
*
|
||||
* @param command The command to register
|
||||
*/
|
||||
public void registerCommand(@NonNull BatCommand command) {
|
||||
commands.put(command.getName().toLowerCase(), command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all slash commands
|
||||
*/
|
||||
public void registerSlashCommands() {
|
||||
log.info("Registering all slash commands");
|
||||
JDA jda = DiscordService.JDA;
|
||||
long before = System.currentTimeMillis();
|
||||
|
||||
// Unregister all commands that Discord has but we don't
|
||||
jda.retrieveCommands().complete().forEach(command -> {
|
||||
if (commands.containsKey(command.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
jda.deleteCommandById(command.getId()).complete(); // Unregister the command on Discord
|
||||
log.info("Unregistered unknown command \"{}\" from Discord", command.getName());
|
||||
});
|
||||
|
||||
// Register all commands
|
||||
for (BatCommand command : commands.values()) {
|
||||
if (command.getCommandData() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
jda.upsertCommand(command.getCommandData()).complete(); // Register the command on Discord
|
||||
}
|
||||
log.info("Registered all slash commands in {}ms", System.currentTimeMillis() - before);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
|
||||
Guild discordGuild = event.getGuild();
|
||||
if (discordGuild == null) {
|
||||
return;
|
||||
}
|
||||
if (event.getUser().isBot()) {
|
||||
return;
|
||||
}
|
||||
if (event.getMember() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String commandName = event.getName();
|
||||
BatCommand command = commands.get(commandName);
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BatGuild guild = guildService.getGuild(discordGuild.getId());
|
||||
BatUser user = userService.getUser(event.getUser().getId());
|
||||
|
||||
// No args provided, use the main command executor
|
||||
List<OptionMapping> options = event.getInteraction().getOptions();
|
||||
|
||||
try {
|
||||
if (options.isEmpty()) {
|
||||
command.execute(guild, user, event.getChannel().asTextChannel(), event.getMember(), event.getInteraction(), null);
|
||||
}
|
||||
|
||||
// Check if the sub command exists
|
||||
for (Map.Entry<String, BatSubCommand> subCommand : command.getSubCommands().entrySet()) {
|
||||
for (OptionMapping option : options) {
|
||||
if (subCommand.getKey().equalsIgnoreCase(option.getName())) {
|
||||
subCommand.getValue().execute(guild, user, event.getChannel().asTextChannel(), event.getMember(), event.getInteraction(), option);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("An error occurred while executing command \"{}\"", commandName, ex);
|
||||
event.replyEmbeds(EmbedUtils.buildErrorEmbed("An error occurred while executing the command\n\n" +
|
||||
ex.getLocalizedMessage()).build()).queue();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.entities.Activity;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -19,10 +20,13 @@ public class DiscordService {
|
||||
/**
|
||||
* The JDA instance
|
||||
*/
|
||||
private final JDA jda;
|
||||
public static JDA JDA;
|
||||
|
||||
public DiscordService(@Value("${discord.token}") String token) throws Exception {
|
||||
jda = JDABuilder.createLight(token, EnumSet.of(
|
||||
@Autowired
|
||||
public DiscordService(
|
||||
@Value("${discord.token}") String token
|
||||
) throws Exception {
|
||||
JDA = JDABuilder.createLight(token, EnumSet.of(
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT
|
||||
)).build()
|
||||
@ -32,11 +36,12 @@ public class DiscordService {
|
||||
TimerUtils.scheduleRepeating(this::updateActivity, 0, 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the activity of the bot
|
||||
*/
|
||||
public void updateActivity() {
|
||||
int guildCount = jda.getGuilds().size();
|
||||
jda.getPresence().setActivity(Activity.playing("with %s guilds".formatted(guildCount)));
|
||||
int guildCount = JDA.getGuilds().size();
|
||||
JDA.getPresence().setActivity(Activity.playing("with %s guilds".formatted(guildCount)));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
package cc.fascinated.bat.service;
|
||||
|
||||
import cc.fascinated.bat.model.Guild;
|
||||
import cc.fascinated.bat.model.BatGuild;
|
||||
import cc.fascinated.bat.repository.GuildRepository;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -15,23 +16,17 @@ import java.util.Optional;
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service @Log4j2
|
||||
@DependsOn("discordService")
|
||||
public class GuildService extends ListenerAdapter {
|
||||
/**
|
||||
* The guild repository to use
|
||||
*/
|
||||
private final GuildRepository guildRepository;
|
||||
|
||||
/**
|
||||
* The discord service to use
|
||||
*/
|
||||
private final DiscordService discordService;
|
||||
|
||||
@Autowired
|
||||
public GuildService(@NonNull GuildRepository guildRepository, @NonNull DiscordService discordService) {
|
||||
public GuildService(@NonNull GuildRepository guildRepository) {
|
||||
this.guildRepository = guildRepository;
|
||||
this.discordService = discordService;
|
||||
|
||||
discordService.getJda().addEventListener(this);
|
||||
DiscordService.JDA.addEventListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,19 +35,28 @@ public class GuildService extends ListenerAdapter {
|
||||
* @param id The ID of the guild
|
||||
* @return The guild
|
||||
*/
|
||||
public Guild getGuild(@NonNull String id) {
|
||||
public BatGuild getGuild(@NonNull String id) {
|
||||
long start = System.currentTimeMillis();
|
||||
Optional<Guild> optionalGuild = guildRepository.findById(id);
|
||||
Optional<BatGuild> optionalGuild = guildRepository.findById(id);
|
||||
if (optionalGuild.isPresent()) {
|
||||
return optionalGuild.get();
|
||||
}
|
||||
Guild guild = guildRepository.save(new Guild(id));
|
||||
BatGuild guild = guildRepository.save(new BatGuild(id));
|
||||
log.info("Created guild \"{}\" in {}ms", id, System.currentTimeMillis() - start);
|
||||
return guild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a guild
|
||||
*
|
||||
* @param guild The guild to save
|
||||
*/
|
||||
public void saveGuild(@NonNull BatGuild guild) {
|
||||
guildRepository.save(guild);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildJoin(GuildJoinEvent event) {
|
||||
public final void onGuildJoin(GuildJoinEvent event) {
|
||||
log.info("Joined guild \"{}\"", event.getGuild().getId());
|
||||
getGuild(event.getGuild().getId()); // Ensure the guild is in the database
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
package cc.fascinated.bat.service;
|
||||
|
||||
import cc.fascinated.bat.common.DateUtils;
|
||||
import cc.fascinated.bat.common.WebRequest;
|
||||
import cc.fascinated.bat.exception.BadRequestException;
|
||||
import cc.fascinated.bat.exception.ResourceNotFoundException;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberAccountToken;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberPageMetadataToken;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
|
||||
import cc.fascinated.bat.model.beatsaber.scoresaber.ScoreSaberScoresPageToken;
|
||||
import cc.fascinated.bat.model.user.profiles.ScoreSaberProfile;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service @Log4j2(topic = "ScoreSaber Service")
|
||||
public class ScoreSaberService {
|
||||
private static final String SCORESABER_API = "https://scoresaber.com/api/";
|
||||
private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full";
|
||||
private static final String GET_PLAYER_SCORES_ENDPOINT = SCORESABER_API + "player/%s/scores?limit=100&sort=%s&page=%s&withMetadata=true";
|
||||
|
||||
/**
|
||||
* Gets the account from the ScoreSaber API.
|
||||
*
|
||||
* @param id The id of the account.
|
||||
* @return The account.
|
||||
* @throws ResourceNotFoundException If the account is not found.
|
||||
* @throws cc.fascinated.bat.exception.RateLimitException If the ScoreSaber rate limit is reached.
|
||||
*/
|
||||
public ScoreSaberAccountToken getAccount(String id) {
|
||||
ScoreSaberAccountToken account = WebRequest.getAsEntity(String.format(GET_PLAYER_ENDPOINT, id), ScoreSaberAccountToken.class);
|
||||
if (account == null) { // Check if the account doesn't exist.
|
||||
log.info("Account with id '{}' not found.", id);
|
||||
throw new ResourceNotFoundException("Account with id '%s' not found.".formatted(id));
|
||||
}
|
||||
if (account.isBanned()) {
|
||||
throw new BadRequestException("Account with id '%s' is banned.".formatted(id));
|
||||
}
|
||||
if (account.isInactive()) {
|
||||
throw new BadRequestException("Account with id '%s' is inactive.".formatted(id));
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scores for the account.
|
||||
*
|
||||
* @param profile The profile.
|
||||
* @param page The page to get the scores from.
|
||||
* @return The scores.
|
||||
*/
|
||||
public ScoreSaberScoresPageToken getPageScores(ScoreSaberProfile profile, int page) {
|
||||
log.info("Fetching scores for account '{}' from page {}.", profile.getId(), page);
|
||||
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, profile.getId(), "recent", page), ScoreSaberScoresPageToken.class);
|
||||
if (pageToken == null) { // Check if the page doesn't exist.
|
||||
return null;
|
||||
}
|
||||
// Sort the scores by newest time set.
|
||||
pageToken.setPlayerScores(Arrays.stream(pageToken.getPlayerScores())
|
||||
.sorted((a, b) -> DateUtils.getDateFromString(b.getScore().getTimeSet()).compareTo(DateUtils.getDateFromString(a.getScore().getTimeSet())))
|
||||
.toArray(ScoreSaberPlayerScoreToken[]::new));
|
||||
return pageToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scores for the account.
|
||||
*
|
||||
* @param profile The profile.
|
||||
* @return The scores.
|
||||
*/
|
||||
public List<ScoreSaberScoresPageToken> getScores(ScoreSaberProfile profile) {
|
||||
List<ScoreSaberScoresPageToken> scores = new ArrayList<>(List.of(getPageScores(profile, 1)));
|
||||
ScoreSaberPageMetadataToken metadata = scores.get(0).getMetadata();
|
||||
int totalPages = (int) Math.ceil((double) metadata.getTotal() / metadata.getItemsPerPage());
|
||||
log.info("Fetching {} pages of scores for account '{}'.", totalPages, profile.getId());
|
||||
for (int i = 2; i <= totalPages; i++) {
|
||||
scores.add(getPageScores(profile, i));
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
}
|
54
src/main/java/cc/fascinated/bat/service/UserService.java
Normal file
54
src/main/java/cc/fascinated/bat/service/UserService.java
Normal file
@ -0,0 +1,54 @@
|
||||
package cc.fascinated.bat.service;
|
||||
|
||||
import cc.fascinated.bat.model.user.BatUser;
|
||||
import cc.fascinated.bat.repository.UserRepository;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service @Log4j2
|
||||
@DependsOn("discordService")
|
||||
public class UserService {
|
||||
/**
|
||||
* The user repository to use
|
||||
*/
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserService(@NonNull UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user by their ID
|
||||
*
|
||||
* @param id The ID of the user
|
||||
* @return The user
|
||||
*/
|
||||
public BatUser getUser(@NonNull String id) {
|
||||
long start = System.currentTimeMillis();
|
||||
Optional<BatUser> optionalUser = userRepository.findById(id);
|
||||
if (optionalUser.isPresent()) {
|
||||
return optionalUser.get();
|
||||
}
|
||||
BatUser user = userRepository.save(new BatUser(id));
|
||||
log.info("Created user \"{}\" in {}ms", id, System.currentTimeMillis() - start);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a user
|
||||
*
|
||||
* @param user The user to save
|
||||
*/
|
||||
public void saveUser(@NonNull BatUser user) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@ discord:
|
||||
|
||||
# Spring Configuration
|
||||
spring:
|
||||
# Disable the Spring Web Server
|
||||
main:
|
||||
web-application-type: none
|
||||
data:
|
||||
# MongoDB Configuration
|
||||
mongodb:
|
||||
|
Loading…
Reference in New Issue
Block a user