remove some things and bump depends

This commit is contained in:
Lee 2024-12-27 12:50:13 +00:00
parent 577c895169
commit 5f099a97f0
50 changed files with 5 additions and 3174 deletions

37
pom.xml
View File

@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<version>3.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
@ -75,11 +75,6 @@
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>fascinated-repo-public</id>
<name>Fascinated's Repository</name>
<url>https://repo.fascinated.cc/public</url>
</repository>
</repositories>
<dependencies>
@ -101,25 +96,6 @@
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.16.0</version>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-bom</artifactId>
<version>5.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-springboot-v3</artifactId>
<version>5.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongodb-springdata-v4-driver</artifactId>
<version>5.5.0</version>
<scope>compile</scope>
</dependency>
<!-- Redis for caching -->
<dependency>
@ -167,12 +143,6 @@
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.11</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>se.michaelthelin.spotify</groupId>
<artifactId>spotify-web-api-java</artifactId>
<version>8.4.1</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -180,11 +150,6 @@
<artifactId>commons-text</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>xyz.mcutils</groupId>
<artifactId>mcutils-java-library</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>com.github.Steppschuh</groupId>
<artifactId>Java-Markdown-Generator</artifactId>

View File

@ -5,7 +5,6 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.service.EventService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.mongock.runner.springboot.EnableMongock;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
@ -21,7 +20,6 @@ import java.util.Objects;
@EnableScheduling
@SpringBootApplication
@EnableMongock
@Log4j2(topic = "Bat")
public class BatApplication {
public static Gson GSON = new GsonBuilder().create();

View File

@ -14,10 +14,12 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
import java.util.*;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* @author Braydon

View File

@ -1,25 +1,14 @@
package cc.fascinated.bat.common;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.InteractionService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.interactions.components.ActionComponent;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.interactions.components.selections.SelectMenu;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import net.dv8tion.jda.api.utils.data.SerializableData;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
@ -27,8 +16,6 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static net.dv8tion.jda.api.interactions.components.Component.Type.STRING_SELECT;
/**
* @author Fascinated (fascinated7)
*/

View File

@ -1,71 +0,0 @@
package cc.fascinated.bat.common;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import se.michaelthelin.spotify.model_objects.specification.Track;
/**
* @author Fascinated (fascinated7)
*/
@UtilityClass
@Log4j2(topic = "Spotify Utils")
public class SpotifyUtils {
/**
* Gets the URL of the track that is currently playing.
*
* @param currentlyPlaying The currently playing object.
* @return The URL of the track that is currently playing.
*/
public static String getTrackUrl(CurrentlyPlaying currentlyPlaying) {
return "https://open.spotify.com/track/" + currentlyPlaying.getItem().getId();
}
/**
* Gets the formatted time of the currently playing track
*
* @param currentlyPlaying the currently playing track
* @return the formatted time
*/
public static String getFormattedTime(@NonNull CurrentlyPlaying currentlyPlaying) {
Track track = (Track) currentlyPlaying.getItem();
int currentMinutes = currentlyPlaying.getProgress_ms() / 1000 / 60;
int currentSeconds = currentlyPlaying.getProgress_ms() / 1000 % 60;
int totalMinutes = track.getDurationMs() / 1000 / 60;
int totalSeconds = track.getDurationMs() / 1000 % 60;
return "`%02d:%02d`/`%02d:%02d`".formatted(currentMinutes, currentSeconds, totalMinutes, totalSeconds);
}
/**
* Get the next track that is playing
*
* @param user The user to get the track for
* @param oldName The name of the old track
* @return The new track
*/
public static CurrentlyPlaying getNewTrack(@NonNull SpotifyService spotifyService, @NonNull BatUser user, @NonNull String oldName) {
int checks = 0;
try {
Thread.sleep(150);
while (checks < 10) {
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
if (track.getName().equals(oldName)) {
Thread.sleep(250);
checks++;
} else {
log.info("Found new track \"{}\" in {} check{}", track.getName(), checks, checks == 1 ? "" : "s");
return currentlyPlaying;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -1,33 +0,0 @@
package cc.fascinated.bat.controller;
import cc.fascinated.bat.service.SpotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Fascinated (fascinated7)
*/
@RestController
@RequestMapping(value = "/spotify")
public class SpotifyController {
private final SpotifyService spotifyService;
@Autowired
public SpotifyController(SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
/**
* A GET request to authorize the user with Spotify.
*
* @return the response entity
*/
@GetMapping(value = "/callback")
public ResponseEntity<String> authorizationCallback(@RequestParam(required = false) String code) {
return ResponseEntity.ok(spotifyService.authorize(code));
}
}

View File

@ -1,48 +0,0 @@
package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.minecraft.command.minecraft.MinecraftCommand;
import cc.fascinated.bat.features.minecraft.command.serverwatcher.ServerWatcherCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class MinecraftFeature extends Feature {
private final GuildService guildService;
@Autowired
public MinecraftFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Minecraft", FeatureProfile.FeatureState.DISABLED, true);
this.guildService = guildService;
super.registerCommand(commandService, context.getBean(MinecraftCommand.class));
super.registerCommand(commandService, context.getBean(ServerWatcherCommand.class));
}
/**
* Check servers every minute
*/
@Scheduled(cron = "0 * * * * *")
public void checkServers() {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild.getFeatureProfile().isFeatureDisabled(this)) { // Check if the feature is disabled
continue;
}
batGuild.getMinecraftProfile().checkServers();
}
}
}

View File

@ -1,143 +0,0 @@
package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.common.*;
import com.google.gson.Gson;
import lombok.Getter;
import net.dv8tion.jda.api.EmbedBuilder;
import org.bson.Document;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.models.server.MinecraftServer;
import xyz.mcutils.models.server.ServerPlatform;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Getter
public class MinecraftProfile extends Serializable {
/**
* The servers that are getting their status watched
*/
private final List<ServerWatcher> serverWatchers = new ArrayList<>();
/**
* Adds a server watcher
*
* @param serverWatcher - The server watcher to add
*/
public void addServerWatcher(ServerWatcher serverWatcher) {
serverWatchers.add(serverWatcher);
}
/**
* Removes a server watcher
*
* @param serverWatcher - The server watcher to remove
*/
public void removeServerWatcher(ServerWatcher serverWatcher) {
serverWatchers.remove(serverWatcher);
}
/**
* Gets a server watcher by hostname
*
* @param hostname the hostname of the server
* @param platform the platform of the server
* @return the server watcher
*/
public ServerWatcher getServerWatcher(String hostname, ServerPlatform platform) {
for (ServerWatcher serverWatcher : serverWatchers) {
if (serverWatcher.getHostname().equalsIgnoreCase(hostname) && serverWatcher.getPlatform() == platform) {
return serverWatcher;
}
}
return null;
}
public void checkServers() {
for (ServerWatcher server : serverWatchers) {
int platformDefaultPort = server.getPlatform() == ServerPlatform.JAVA ? 25565 : 19132;
String hostname = server.getHostname() + (server.getPort() != platformDefaultPort ? ":" + server.getPort() : "");
boolean isOnline = true;
MinecraftServer minecraftServer = null;
switch (server.getPlatform()) {
case JAVA -> {
try {
minecraftServer = McUtilsAPI.getJavaServer(hostname);
} catch (Exception e) {
isOnline = false;
}
}
case BEDROCK -> {
try {
minecraftServer = McUtilsAPI.getBedrockServer(hostname);
} catch (Exception e) {
isOnline = false;
}
}
}
if (isOnline == server.isLastState()) {
continue;
}
server.setLastState(isOnline);
EmbedBuilder embedBuilder = isOnline ? EmbedUtils.successEmbed() : EmbedUtils.errorEmbed();
DescriptionBuilder description = new DescriptionBuilder("Server Watcher");
description.appendLine("%s %s server `%s` is now **%s**".formatted(
isOnline ? Emojis.CHECK_MARK_EMOJI : Emojis.CROSS_MARK_EMOJI,
EnumUtils.getEnumName(server.getPlatform()),
hostname,
isOnline ? "online" : "offline"
), false);
if (minecraftServer != null) {
description.appendLine("Players: `%s/%s`".formatted(
NumberFormatter.simpleFormat(minecraftServer.getPlayers().getOnline()),
NumberFormatter.simpleFormat(minecraftServer.getPlayers().getMax())
), true);
}
server.getChannel().sendMessageEmbeds(embedBuilder
.setDescription(description.build())
.setThumbnail("https://api.mcutils.xyz/server/icon/%s".formatted(hostname))
.build()).queue();
}
}
@Override
public void load(Document document, Gson gson) {
for (Document watcherDocument : document.getList("serverWatchers", Document.class, new ArrayList<>())) {
serverWatchers.add(new ServerWatcher(
watcherDocument.getString("hostname"),
watcherDocument.getInteger("port"),
ServerPlatform.valueOf(watcherDocument.getString("platform")),
watcherDocument.getString("channelId"),
watcherDocument.getBoolean("lastState", false)
));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
List<Document> watcherDocuments = new ArrayList<>();
for (ServerWatcher serverWatcher : serverWatchers) {
Document watcherDocument = new Document();
watcherDocument.append("hostname", serverWatcher.getHostname());
watcherDocument.append("port", serverWatcher.getPort());
watcherDocument.append("platform", serverWatcher.getPlatform().name());
watcherDocument.append("channelId", serverWatcher.getChannelId());
watcherDocument.append("lastState", serverWatcher.isLastState());
watcherDocuments.add(watcherDocument);
}
document.append("serverWatchers", watcherDocuments);
return document;
}
@Override
public void reset() {
serverWatchers.clear();
}
}

View File

@ -1,54 +0,0 @@
package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.common.ChannelUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class ServerWatcher {
/**
* The hostname of the server
*/
private final String hostname;
/**
* The port of the server
*/
private final int port;
/**
* The platform of the server
*/
private final ServerPlatform platform;
/**
* The channel id to send notifications in
*/
private final String channelId;
/**
* The last state of the server
* <p>
* true = online
* false = offline
* </p>
*/
private boolean lastState;
/**
* Gets the channel
*
* @return - The channel
*/
public TextChannel getChannel() {
return ChannelUtils.getTextChannel(channelId);
}
}

View File

@ -1,75 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.DescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.stereotype.Component;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.exception.ErrorResponse;
import xyz.mcutils.models.cache.CachedPlayer;
import xyz.mcutils.models.player.Skin;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "lookup-player",
description = "Lookup a Minecraft player"
)
public class LookupPlayerSubCommand extends BatCommand {
public LookupPlayerSubCommand() {
super.addOptions(
new OptionData(OptionType.STRING, "player", "The player to lookup", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping playerOption = event.getOption("player");
assert playerOption != null;
String player = playerOption.getAsString();
// Check if the player id is valid
if (player.length() > 16 || player.contains(" ")) {
event.reply("The player id `%s` is invalid".formatted(player)).queue();
return;
}
// Fetch the player from the API
CachedPlayer cachedPlayer = null;
try {
cachedPlayer = McUtilsAPI.getPlayer(player);
} catch (ErrorResponse ignored) { } // The error response is handled below
if (cachedPlayer == null) {
event.reply("The player `%s` could not be found".formatted(player)).queue();
return;
}
String headUrl = cachedPlayer.getSkin() != null ? cachedPlayer.getSkin().getParts().get(Skin.SkinPart.HEAD.getName()) : null;
DescriptionBuilder description = new DescriptionBuilder("Player Lookup")
.appendLine("Username: `%s`".formatted(cachedPlayer.getUsername()), true)
.appendLine("UUID: `%s`".formatted(cachedPlayer.getUniqueId().toString()), true);
if (cachedPlayer.getSkin() != null) {
description.appendLine("Skin: [Click Here](%s)".formatted(headUrl), true);
}
if (cachedPlayer.getCape() != null) {
description.appendLine("Cape: [Click Here](%s)".formatted(cachedPlayer.getCape().getUrl()), true);
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.setThumbnail(headUrl)
.build()).queue();
}
}

View File

@ -1,104 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.DescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.stereotype.Component;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.models.cache.CachedBedrockMinecraftServer;
import xyz.mcutils.models.cache.CachedJavaMinecraftServer;
import xyz.mcutils.models.server.MinecraftServer;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "lookup-server",
description = "Lookup a Minecraft server"
)
public class LookupServerSubCommand extends BatCommand {
public LookupServerSubCommand() {
super.addOptions(
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java")
.addChoice("Bedrock", "bedrock"),
new OptionData(OptionType.STRING, "host", "The host/ip of the server to lookup", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping platformOption = event.getOption("platform");
assert platformOption != null;
OptionMapping hostOption = event.getOption("host");
assert hostOption != null;
String platform = platformOption.getAsString();
String host = hostOption.getAsString();
MinecraftServer server;
try {
if (platform.equalsIgnoreCase("java")) {
server = McUtilsAPI.getJavaServer(host);
} else {
server = McUtilsAPI.getBedrockServer(host);
}
} catch (Exception ex) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is invalid or offline".formatted(host))
.build()).queue();
return;
}
int platformDefaultPort = platform.equalsIgnoreCase("java") ? 25565 : 19132;
String hostname = server.getHostname() + (server.getPort() != platformDefaultPort ? ":" + server.getPort() : "");
DescriptionBuilder description = new DescriptionBuilder("Server Lookup [(Raw Data)](%s)".formatted(
"https://api.mcutils.xyz/server/%s/%s".formatted(platform, hostname)
));
description.appendLine("Host: `%s`".formatted(hostname), true);
MinecraftServer.GeoLocation location = server.getLocation();
if (server instanceof CachedJavaMinecraftServer javaServer) {
description.appendLine("Version: `%s`".formatted(javaServer.getVersion().getName()), true);
if (javaServer.getForgeData() != null) {
description.appendLine("Forge Mods: `%s`".formatted(javaServer.getForgeData().getMods().length), true);
}
if (javaServer.isMojangBlocked()) {
description.appendLine("Mojang Blocked: `Yes`", true);
}
if (javaServer.isEnforcesSecureChat()) {
description.appendLine("Enforces Secure Chat: `Yes`", true);
}
if (javaServer.isPreventsChatReports()) {
description.appendLine("Prevents Chat Reports: `Yes`", true);
}
}
if (server instanceof CachedBedrockMinecraftServer bedrockServer) {
description.appendLine("Version: `%s`".formatted(bedrockServer.getVersion().getName()), true);
}
if (location != null) {
description.appendLine("Location: [%s](%s)".formatted(
(location.getCity() == null ? "" : location.getCity() + ", ") + location.getCountry(),
"https://www.google.com/maps/search/?api=1&query=%s,%s".formatted(location.getLatitude(), location.getLongitude())
), true);
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.setThumbnail("https://api.mcutils.xyz/server/icon/%s".formatted(hostname))
.setImage(server.getMotd().getPreview())
.setFooter("Powered by mcutils.xyz")
.build()).queue();
}
}

View File

@ -1,29 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "minecraft",
description = "Minecraft related commands",
userInstall = true,
category = Category.UTILITY
)
public class MinecraftCommand extends BatCommand {
@Autowired
public MinecraftCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(LookupPlayerSubCommand.class),
context.getBean(LookupServerSubCommand.class)
);
}
}

View File

@ -1,101 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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 org.springframework.stereotype.Component;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.models.server.MinecraftServer;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.add:sub")
@CommandInfo(
name = "add",
description = "Add a server to the server watcher"
)
public class AddSubCommand extends BatCommand {
public AddSubCommand() {
super.addOptions(
new OptionData(OptionType.CHANNEL, "channel", "The channel to send the server watcher notifications", true),
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java")
.addChoice("Bedrock", "bedrock"),
new OptionData(OptionType.STRING, "host", "The host/ip of the server to add", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping platformOption = event.getOption("platform");
assert platformOption != null;
OptionMapping hostOption = event.getOption("host");
assert hostOption != null;
OptionMapping channelOption = event.getOption("channel");
assert channelOption != null;
String platform = platformOption.getAsString();
String host = hostOption.getAsString();
GuildChannelUnion channelUnion = channelOption.getAsChannel();
TextChannel textChannel = channelUnion.asTextChannel();
MinecraftServer server;
try {
if (platform.equalsIgnoreCase("java")) {
server = McUtilsAPI.getJavaServer(host);
} else {
server = McUtilsAPI.getBedrockServer(host);
}
} catch (Exception ex) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is invalid or offline".formatted(host))
.build()).queue();
return;
}
MinecraftProfile profile = guild.getMinecraftProfile();
if (profile.getServerWatchers().size() >= 10) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You can only have a maximum of `10` server watchers")
.build()).queue();
return;
}
if (profile.getServerWatcher(host, ServerPlatform.valueOf(platform.toUpperCase())) != null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is already being watched".formatted(host))
.build()).queue();
return;
}
profile.addServerWatcher(new ServerWatcher(
server.getHostname(),
server.getPort(),
ServerPlatform.valueOf(platform.toUpperCase()),
textChannel.getId(),
false
));
profile.checkServers(); // Force check the servers
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Setup the server watcher for `%s` in %s".formatted(
server.getHostname(),
textChannel.getAsMention()
)).build()).queue();
}
}

View File

@ -1,61 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.DescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.EnumUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import xyz.mcutils.models.server.ServerPlatform;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.list:sub")
@CommandInfo(
name = "list",
description = "Shows a list of all the servers being watched"
)
public class ListSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
MinecraftProfile profile = guild.getMinecraftProfile();
Map<ServerPlatform, List<ServerWatcher>> watchers = new HashMap<>();
for (ServerWatcher server : profile.getServerWatchers()) {
watchers.computeIfAbsent(server.getPlatform(), k -> new ArrayList<>()).add(server);
}
DescriptionBuilder description = new DescriptionBuilder("Server Watcher");
description.appendLine("Here is a list of all the servers being watched", false);
description.emptyLine();
for (Map.Entry<ServerPlatform, List<ServerWatcher>> entry : watchers.entrySet()) {
description.appendLine("**%s**".formatted(EnumUtils.getEnumName(entry.getKey())), false);
for (ServerWatcher server : entry.getValue()) {
description.appendLine("`%s` - %s".formatted(
server.getHostname(),
server.getChannel().getAsMention()
), true);
}
description.emptyLine();
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
}
}

View File

@ -1,63 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.stereotype.Component;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.remove:sub")
@CommandInfo(
name = "remove",
description = "Remove a server from the server watcher"
)
public class RemoveSubCommand extends BatCommand {
public RemoveSubCommand() {
super.addOptions(
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java")
.addChoice("Bedrock", "bedrock"),
new OptionData(OptionType.STRING, "host", "The host/ip of the server to remove", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping platformOption = event.getOption("platform");
assert platformOption != null;
OptionMapping hostOption = event.getOption("host");
assert hostOption != null;
String platform = platformOption.getAsString();
String host = hostOption.getAsString();
MinecraftProfile profile = guild.getMinecraftProfile();
ServerWatcher serverWatcher = profile.getServerWatcher(host, ServerPlatform.valueOf(platform.toUpperCase()));
if (serverWatcher == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is not being watched".formatted(host))
.build()).queue();
return;
}
profile.removeServerWatcher(serverWatcher);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The server `%s` has been removed from the server watcher".formatted(host))
.build()).queue();
}
}

View File

@ -1,31 +0,0 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "minecraft-server-watcher",
description = "Configure the server watcher for Minecraft servers",
requiredPermissions = Permission.MANAGE_SERVER,
category = Category.UTILITY
)
public class ServerWatcherCommand extends BatCommand {
@Autowired
public ServerWatcherCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(AddSubCommand.class),
context.getBean(RemoveSubCommand.class),
context.getBean(ListSubCommand.class)
);
}
}

View File

@ -1,73 +0,0 @@
package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "NumberOneScoreFeed Listener")
public class NumberOneScoreFeedListener implements EventListener {
private final GuildService guildService;
private final FeatureService featureService;
@Autowired
public NumberOneScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService;
this.featureService = featureService;
}
@Override
public void onScoresaberScoreReceived(@NotNull ScoreSaberPlayerScoreToken score, @NotNull ScoreSaberLeaderboardToken leaderboard,
@NotNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) {
if (score.getScore().getRank() != 1) { // Only send if the score is a #1 score
return;
}
if (!leaderboard.isRanked()) { // Only send if the leaderboard is ranked
return;
}
log.info("A new #1 score has been set by {} on {} ({})!",
player.getName(),
leaderboard.getSongName(),
"%s⭐".formatted(NumberFormatter.simpleFormat(leaderboard.getStars()))
);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
ScoreSaberFeature scoreSaberFeature = featureService.getFeature(ScoreSaberFeature.class);
if (!batGuild.getFeatureProfile().isFeatureEnabled(scoreSaberFeature)) { // Check if the feature is enabled
return;
}
NumberOneScoreFeedProfile profile = batGuild.getProfile(NumberOneScoreFeedProfile.class);
if (profile == null || profile.getChannelId() == null) {
continue;
}
TextChannel channel = profile.getTextChannel();
if (channel == null) {
continue;
}
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue();
}
}
}

View File

@ -1,82 +0,0 @@
package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.common.DateUtils;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.ScoreSaberUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.scoresaber.command.numberone.NumberOneFeedCommand;
import cc.fascinated.bat.features.scoresaber.command.scoresaber.ScoreSaberCommand;
import cc.fascinated.bat.features.scoresaber.command.userfeed.UserFeedCommand;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class ScoreSaberFeature extends Feature {
@Autowired
public ScoreSaberFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("ScoreSaber", FeatureProfile.FeatureState.DISABLED, true);
registerCommand(commandService, context.getBean(ScoreSaberCommand.class));
registerCommand(commandService, context.getBean(UserFeedCommand.class));
registerCommand(commandService, context.getBean(NumberOneFeedCommand.class));
}
/**
* Builds an embed for a score.
*
* @param score The score.
* @return The embed.
*/
public static MessageEmbed buildScoreEmbed(ScoreSaberPlayerScoreToken score) {
ScoreSaberScoreToken scoreToken = score.getScore();
ScoreSaberLeaderboardToken leaderboardToken = score.getLeaderboard();
ScoreSaberScoreToken.LeaderboardPlayerInfo playerInfo = scoreToken.getLeaderboardPlayerInfo();
String thumbnailUrl = String.format("https://cdn.scoresaber.com/covers/%s.png", leaderboardToken.getSongHash());
String authorUrl = String.format("https://scoresaber.com/u/%s", playerInfo.getId());
String description = String.format("**%s** (%s%s)\n[[Map Link]](%s) [[SS Profile]](%s)",
leaderboardToken.getSongName(),
ScoreSaberUtils.getFormattedDifficulty(leaderboardToken.getDifficulty().getDifficulty()),
leaderboardToken.isRanked() ? " " + leaderboardToken.getStars() + "" : "",
String.format("https://scoresaber.com/leaderboard/%s", leaderboardToken.getId()),
authorUrl
);
String accuracy = leaderboardToken.getMaxScore() == 0 ? "N/A" :
String.format("%s%%", NumberFormatter.simpleFormat(((double) scoreToken.getBaseScore() / leaderboardToken.getMaxScore()) * 100));
String rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberFormatter.simpleFormat(scoreToken.getPp());
String rank = String.format("#%s", NumberFormatter.simpleFormat(scoreToken.getRank()));
String misses = String.format("%s", scoreToken.getMissedNotes());
String badCuts = String.format("%s", scoreToken.getBadCuts());
String maxCombo = String.format("%s %s",
scoreToken.getMaxCombo(),
scoreToken.getMaxCombo() == leaderboardToken.getMaxScore() ? "(FC)" : ""
);
return EmbedUtils.genericEmbed()
.setThumbnail(thumbnailUrl)
.setAuthor(playerInfo.getName() + " just set a new score!", authorUrl, playerInfo.getProfilePicture())
.setDescription(description)
.addField("Accuracy", accuracy, true)
.addField("Raw PP", rawPp, true)
.addField("Rank", rank, true)
.addField("Misses", misses, true)
.addField("Bad Cuts", badCuts, true)
.addField("Max Combo", maxCombo, true)
.setTimestamp(DateUtils.getDateFromString(scoreToken.getTimeSet()).toInstant())
.build();
}
}

View File

@ -1,59 +0,0 @@
package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "UserScoreFeed Listener")
public class UserScoreFeedListener implements EventListener {
private final GuildService guildService;
private final FeatureService featureService;
@Autowired
public UserScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService;
this.featureService = featureService;
}
@Override
public void onScoresaberScoreReceived(@NotNull ScoreSaberPlayerScoreToken score, @NotNull ScoreSaberLeaderboardToken leaderboard,
@NotNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
ScoreSaberFeature scoreSaberFeature = featureService.getFeature(ScoreSaberFeature.class);
if (!batGuild.getFeatureProfile().isFeatureEnabled(scoreSaberFeature)) { // Check if the feature is enabled
return;
}
UserScoreFeedProfile profile = batGuild.getProfile(UserScoreFeedProfile.class);
if (profile == null || profile.getChannelId() == null || !profile.getTrackedUsers().contains(player.getId())) {
continue;
}
TextChannel channel = profile.getTextChannel();
if (channel == null) {
continue;
}
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue();
}
}
}

View File

@ -1,64 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.numberone;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-number-one-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatCommand {
@Autowired
public ChannelSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel scores are sent in", false));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.")
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
}
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}
}

View File

@ -1,23 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.numberone;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-number-one-feed")
@CommandInfo(name = "scoresaber-number-one-feed", description = "Modifies the settings for the feed.", requiredPermissions = Permission.MANAGE_SERVER, category = Category.BEAT_SABER)
public class NumberOneFeedCommand extends BatCommand {
public NumberOneFeedCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(ChannelSubCommand.class),
context.getBean(ResetSubCommand.class)
);
}
}

View File

@ -1,31 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.numberone;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-number-one-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.")
.build()).queue();
}
}

View File

@ -1,72 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
import cc.fascinated.bat.service.ScoreSaberService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber:link.sub")
@CommandInfo(name = "link", description = "Links your ScoreSaber profile")
public class LinkSubCommand extends BatCommand {
private final ScoreSaberService scoreSaberService;
@Autowired
public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService) {
super.addOptions(new OptionData(OptionType.STRING, "link", "Link your ScoreSaber profile", true));
this.scoreSaberService = scoreSaberService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping linkOption = event.getOption("link");
assert linkOption != null;
String link = linkOption.getAsString();
if (!link.contains("scoresaber.com/u/")) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link")
.build())
.setEphemeral(true)
.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) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link")
.build())
.setEphemeral(true)
.queue();
return;
}
user.getScoreSaberProfile().setAccountId(id);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id)))
.build())
.setEphemeral(true)
.queue();
}
}

View File

@ -1,33 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.ScoreSaberService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber:me.sub")
@CommandInfo(name = "me", description = "Gets your ScoreSaber profile")
public class MeSubCommand extends BatCommand {
private final ScoreSaberService scoreSaberService;
@Autowired
public MeSubCommand(@NonNull ScoreSaberService scoreSaberService) {
this.scoreSaberService = scoreSaberService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
ScoreSaberCommand.sendProfileEmbed(true, user, scoreSaberService, event);
}
}

View File

@ -1,33 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber:reset.sub")
@CommandInfo(name = "reset", description = "Reset your settings")
public class ResetSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
ScoreSaberProfile profile = user.getScoreSaberProfile();
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset your settings.")
.build())
.setEphemeral(true)
.queue();
}
}

View File

@ -1,144 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.*;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
import cc.fascinated.bat.service.ScoreSaberService;
import lombok.NonNull;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "scoresaber", description = "General ScoreSaber commands", userInstall = true, category = Category.BEAT_SABER)
public class ScoreSaberCommand extends BatCommand {
@Autowired
public ScoreSaberCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(LinkSubCommand.class),
context.getBean(UserSubCommand.class),
context.getBean(MeSubCommand.class),
context.getBean(ResetSubCommand.class),
context.getBean(ScoresSummarySubCommand.class)
);
}
/**
* Builds the profile embed for the ScoreSaber profile
*
* @param user The user to build the profile embed for
* @param scoreSaberService The ScoreSaber service
* @param event The interaction
*/
public static void sendProfileEmbed(boolean isSelf, BatUser user, ScoreSaberService scoreSaberService, SlashCommandInteraction event) {
ScoreSaberProfile profile = user.getScoreSaberProfile();
if (profile.getAccountId() == null) {
if (!isSelf) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
.build())
.setEphemeral(true)
.queue();
}
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have a linked ScoreSaber account")
.build())
.setEphemeral(true)
.queue();
return;
}
long before = System.currentTimeMillis();
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getAccountId());
if (account == null) {
if (!isSelf) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s has an invalid ScoreSaber account linked, please ask them to re-link their account"
.formatted(user.getDiscordUser().getAsMention()))
.build())
.setEphemeral(true)
.queue();
}
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have an invalid ScoreSaber account linked, please re-link your account")
.build())
.setEphemeral(true)
.queue();
return;
}
if (profile.getAccountId() == null) {
if (!isSelf) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
.build())
.setEphemeral(true)
.queue();
}
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have a linked ScoreSaber account")
.build())
.setEphemeral(true)
.queue();
return;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("Loading profile for %s...".formatted(user.getDiscordUser().getAsMention()))
.build()).queue(message -> {
ScoreSaberService.RawPerGlobal rawPerGlobal = scoreSaberService.getRawPerGlobal(profile, (callback) -> {
int currentPage = callback.getCurrentPage();
int totalPages = callback.getTotalPages();
// Only update every 5 pages, but show the first page
if (currentPage % 5 != 0 && currentPage != 1) {
return;
}
message.editOriginalEmbeds(EmbedUtils.genericEmbed()
.setDescription("""
Loading profile for %s...
Page `%s`/`%s`
""".formatted(
user.getDiscordUser().getAsMention(),
currentPage,
totalPages
))
.build()).queue();
});
String name = account.getName();
String country = account.getCountry();
String rank = NumberFormatter.simpleFormat(account.getRank());
String countryRank = NumberFormatter.simpleFormat(account.getCountryRank());
String pp = NumberFormatter.simpleFormat(account.getPp());
String ppPerRawGlobal = NumberFormatter.simpleFormat(rawPerGlobal.getRawPerGlobal());
long joinedTimeInSeconds = DateUtils.getDateFromString(account.getFirstSeen()).toInstant().getEpochSecond();
String timeTaken = TimeUtils.format(System.currentTimeMillis() - before, TimeUtils.BatTimeFormat.FIT, true);
message.editOriginalEmbeds(EmbedUtils.successEmbed()
.setDescription(new DescriptionBuilder("%s's ScoreSaber Account".formatted(user.getDiscordUser().getAsMention()))
.appendLine("Name: `%s`".formatted(name), true)
.appendLine("Country: `%s`".formatted(country), true)
.appendLine("Rank: `#%s`".formatted(rank), true)
.appendLine("Country Rank: `#%s`".formatted(countryRank), true)
.appendLine("PP: `%spp`".formatted(pp), true)
.appendLine("Raw PP Per Global: `%spp`".formatted(ppPerRawGlobal), true)
.appendLine("Joined: <t:%s>".formatted(joinedTimeInSeconds), true)
.build())
.setThumbnail(account.getProfilePicture())
.setFooter("Took %s (%s/%s cached pages)".formatted(
timeTaken,
rawPerGlobal.getCachedPages(),
rawPerGlobal.getTotalPages()
), null)
.build()).queue();
});
}
}

View File

@ -1,122 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.ScoreSaberService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.utils.FileUpload;
import net.steppschuh.markdowngenerator.table.Table;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber:scores-summary.sub")
@CommandInfo(name = "scores-summary", description = "Generate a summary of your scores")
public class ScoresSummarySubCommand extends BatCommand {
private final ScoreSaberService scoreSaberService;
@Autowired
public ScoresSummarySubCommand(@NonNull ScoreSaberService scoreSaberService) {
this.scoreSaberService = scoreSaberService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
ScoreSaberProfile profile = user.getScoreSaberProfile();
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("Loading profile for %s...".formatted(user.getDiscordUser().getAsMention()))
.build())
.queue(message -> {
List<ScoreSaberService.CachedPage> pages = scoreSaberService.getScores(profile, (currentPage -> {
// Only update every 5 pages, but show the first page
if (currentPage.getCurrentPage() % 5 != 0 && currentPage.getCurrentPage() != 1) {
return;
}
message.editOriginalEmbeds(EmbedUtils.genericEmbed()
.setDescription("Loading profile for %s... (Page %s/%s)".formatted(
user.getDiscordUser().getAsMention(),
currentPage.getCurrentPage(),
currentPage.getTotalPages()
))
.build()).queue();
}));
Table.Builder tableBuilder = new Table.Builder()
.withAlignments(Table.ALIGN_LEFT, Table.ALIGN_LEFT, Table.ALIGN_LEFT, Table.ALIGN_LEFT)
.addRow("Song", "PP", "Accuracy", "Rank");
int totalRankedScores = 0;
List<ScoreSaberPlayerScoreToken> scores = new ArrayList<>();
for (ScoreSaberService.CachedPage page : pages) {
Collections.addAll(scores, page.getPage().getPlayerScores());
}
// Sort by highest PP first
scores.sort((score1, score2) -> {
if (score1.getScore().getPp() > score2.getScore().getPp()) {
return -1;
} else if (score1.getScore().getPp() < score2.getScore().getPp()) {
return 1;
}
return 0;
});
// Add the scores to the table
for (ScoreSaberPlayerScoreToken scoreToken : scores) {
ScoreSaberScoreToken score = scoreToken.getScore();
ScoreSaberLeaderboardToken leaderboard = scoreToken.getLeaderboard();
double acc = leaderboard.getMaxScore() == 0 ? 0 : ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100;
tableBuilder.addRow(
"[%s](https://scoresaber.com/leaderboard/%s)".formatted(
leaderboard.getSongName(),
leaderboard.getId()
),
NumberFormatter.simpleFormat(score.getPp()) + "pp",
NumberFormatter.simpleFormat(acc) + "%",
"#" + NumberFormatter.simpleFormat(score.getRank())
);
if (score.getPp() != 0) {
totalRankedScores++;
}
}
message.editOriginalEmbeds(EmbedUtils.genericEmbed()
.setDescription("""
**Scores Summary**
Here is a summary of score for %s
\s
Total Scores: `%s`
Total Ranked Scores: `%s`
""".formatted(
user.getDiscordUser().getAsMention(),
NumberFormatter.simpleFormat(scores.size()),
NumberFormatter.simpleFormat(totalRankedScores)
))
.build())
.setFiles(FileUpload.fromData(tableBuilder.build().toString().getBytes(), "scores-summary.md"))
.queue();
});
}
}

View File

@ -1,53 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
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.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber:user.sub")
@CommandInfo(name = "user", description = "Gets a ScoreSaber profile")
public class UserSubCommand extends BatCommand {
private final ScoreSaberService scoreSaberService;
private final UserService userService;
@Autowired
public UserSubCommand(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService) {
super.addOptions(new OptionData(OptionType.USER, "user", "The user to view the ScoreSaber profile of", true));
this.scoreSaberService = scoreSaberService;
this.userService = userService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
assert userOption != null;
BatUser target = userService.getUser(userOption.getAsUser().getId());
if (target == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Unknown user")
.build())
.setEphemeral(true)
.queue();
return;
}
ScoreSaberCommand.sendProfileEmbed(false, target, scoreSaberService, event);
}
}

View File

@ -1,64 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-user-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatCommand {
@Autowired
public ChannelSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel scores are sent in", false));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.")
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
}
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}
}

View File

@ -1,30 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-user-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.")
.build()).queue();
}
}

View File

@ -1,24 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-user-feed.command")
@CommandInfo(name = "scoresaber-user-feed", description = "Modifies the settings for the feed.", requiredPermissions = Permission.MANAGE_CHANNEL, category = Category.BEAT_SABER)
public class UserFeedCommand extends BatCommand {
public UserFeedCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(UserSubCommand.class),
context.getBean(ChannelSubCommand.class),
context.getBean(ResetSubCommand.class)
);
}
}

View File

@ -1,86 +0,0 @@
package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("scoresaber-user-feed:user.sub")
@CommandInfo(name = "user", description = "Adds or removes a user from the feed")
public class UserSubCommand extends BatCommand {
private final UserService userService;
@Autowired
public UserSubCommand(UserService userService) {
super.addOptions(new OptionData(OptionType.USER, "user", "Add or remove a user from the score feed", false));
this.userService = userService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = event.getOption("user");
if (option == null) {
if (profile.getTrackedUsers().isEmpty()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no users being tracked in the feed")
.build()).queue();
return;
}
StringBuilder stringBuilder = new StringBuilder();
for (String accountId : profile.getTrackedUsers()) {
stringBuilder.append("[%s](%s)".formatted(
accountId,
"https://scoresaber.com/u/%s".formatted(accountId)
));
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current users being tracked in the feed are:\n%s".formatted(stringBuilder.toString()))
.build()).queue();
return;
}
User target = option.getAsUser();
BatUser targetUser = userService.getUser(target.getId());
ScoreSaberProfile targetProfile = targetUser.getScoreSaberProfile();
if (targetProfile.getAccountId() == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The user you are trying to track does not have a linked ScoreSaber profile")
.build()).queue();
return;
}
boolean added = false;
if (profile.isUserTracked(targetProfile.getAccountId())) {
profile.removeTrackedUser(targetProfile.getAccountId());
} else {
profile.addTrackedUser(targetProfile.getAccountId());
added = true;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully %s %s from the feed".formatted(
added ? "added" : "removed",
target.getAsMention()
))
.build()).queue();
}
}

View File

@ -1,49 +0,0 @@
package cc.fascinated.bat.features.scoresaber.profile.guild;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
@NoArgsConstructor
public class NumberOneScoreFeedProfile extends Serializable {
/**
* The channel ID of the score feed
*/
private String channelId;
/**
* Gets the channel as a TextChannel
*
* @return the channel as a TextChannel
*/
public TextChannel getTextChannel() {
return DiscordService.JDA.getTextChannelById(channelId);
}
@Override
public void reset() {
this.channelId = null;
}
@Override
public void load(Document document, Gson gson) {
this.channelId = document.getString("channelId");
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("channelId", this.channelId);
return document;
}
}

View File

@ -1,109 +0,0 @@
package cc.fascinated.bat.features.scoresaber.profile.guild;
import cc.fascinated.bat.common.ChannelUtils;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
@NoArgsConstructor
public class UserScoreFeedProfile extends Serializable {
/**
* The channel ID of the score feed
*/
private String channelId;
/**
* The users that are being tracked
*/
private List<String> trackedUsers;
/**
* Gets the tracked users
*
* @return the tracked users
*/
public List<String> getTrackedUsers() {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
return trackedUsers;
}
/**
* Checks if a user is being tracked
*
* @param userId the user ID to check
* @return if the user is being tracked
*/
public boolean isUserTracked(String userId) {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
return trackedUsers.contains(userId);
}
/**
* Adds a user to be tracked
*
* @param userId the user ID to add
*/
public void addTrackedUser(String userId) {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
trackedUsers.add(userId);
}
/**
* Removes a user from being tracked
*
* @param userId the user ID to remove
*/
public void removeTrackedUser(String userId) {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
trackedUsers.remove(userId);
}
/**
* Gets the channel as a TextChannel
*
* @return the channel as a TextChannel
*/
public TextChannel getTextChannel() {
return ChannelUtils.getTextChannel(channelId);
}
@Override
public void reset() {
this.channelId = null;
this.trackedUsers = null;
}
@Override
public void load(Document document, Gson gson) {
this.channelId = document.getString("channelId");
this.trackedUsers = document.getList("trackedUsers", String.class, new ArrayList<>());
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("channelId", this.channelId);
document.put("trackedUsers", this.trackedUsers);
return document;
}
}

View File

@ -1,38 +0,0 @@
package cc.fascinated.bat.features.scoresaber.profile.user;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Setter
@Getter
@NoArgsConstructor
public class ScoreSaberProfile extends Serializable {
/**
* The Account ID of the ScoreSaber profile
*/
private String accountId;
@Override
public void reset() {
this.accountId = null;
}
@Override
public void load(Document document, Gson gson) {
this.accountId = document.getString("accountId");
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("accountId", this.accountId);
return document;
}
}

View File

@ -1,196 +0,0 @@
package cc.fascinated.bat.features.spotify;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.SpotifyUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.spotify.command.SpotifyCommand;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.SneakyThrows;
import net.dv8tion.jda.api.EmbedBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import se.michaelthelin.spotify.enums.Action;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import se.michaelthelin.spotify.model_objects.specification.AlbumSimplified;
import se.michaelthelin.spotify.model_objects.specification.Image;
import se.michaelthelin.spotify.model_objects.specification.Track;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class SpotifyFeature extends Feature {
@Autowired
public SpotifyFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Spotify", FeatureProfile.FeatureState.DISABLED, true);
super.registerCommand(commandService, context.getBean(SpotifyCommand.class));
}
/**
* Pre-checks for Spotify commands.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
public static EmbedBuilder checkSpotify(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
return EmbedUtils.errorEmbed()
.setDescription("%s You need to link your Spotify account before you can use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
if (!spotifyService.hasTrackPlaying(user)) {
return EmbedUtils.errorEmbed()
.setDescription("%s You need to have Spotify Premium to use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
return null;
}
/**
* Gets the currently playing song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder currentSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount() || !spotifyService.hasTrackPlaying(user)) {
return checkSpotify(spotifyService, user);
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
AlbumSimplified album = track.getAlbum();
String trackUrl = SpotifyUtils.getTrackUrl(currentlyPlaying);
String albumUrl = "https://open.spotify.com/album/" + album.getId();
StringBuilder artists = new StringBuilder();
for (int i = 0; i < track.getArtists().length; i++) {
artists.append("**[%s](%s)**".formatted(track.getArtists()[i].getName(), "https://open.spotify.com/artist/" + track.getArtists()[i].getId()));
if (i != track.getArtists().length - 1) {
artists.append(", ");
}
}
Image albumCover = album.getImages()[0];
return EmbedUtils.genericEmbed()
.setAuthor("Listening to %s | %s".formatted(track.getName(), track.getArtists()[0].getName()), trackUrl)
.setThumbnail(albumCover.getUrl())
.setDescription("""
Song: **[%s](%s)**
Album: **[%s](%s)**
Artist%s: %s
Position: %s
""".formatted(
track.getName(), trackUrl,
album.getName(), albumUrl,
track.getArtists().length > 1 ? "s" : "", artists,
SpotifyUtils.getFormattedTime(currentlyPlaying)
));
}
/**
* Skips the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder skipSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount() || !spotifyService.hasTrackPlaying(user)) {
return checkSpotify(spotifyService, user);
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
String trackName = track.getName();
spotifyService.skipTrack(user); // Skip the track
// Get the new track
CurrentlyPlaying newCurrentlyPlaying = SpotifyUtils.getNewTrack(spotifyService, user, trackName);
if (newCurrentlyPlaying == null) {
return EmbedUtils.errorEmbed()
.setDescription("%s There are no more tracks in the queue.".formatted(Emojis.CROSS_MARK_EMOJI));
}
Track newTrack = (Track) newCurrentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription("""
:track_next: Skipped the track: **[%s | %s](%s)**
%s New Track: **[%s | %s](%s)**
""".formatted(
trackName, track.getArtists()[0].getName(), SpotifyUtils.getTrackUrl(currentlyPlaying),
Emojis.CHECK_MARK_EMOJI, newTrack.getName(), newTrack.getArtists()[0].getName(), SpotifyUtils.getTrackUrl(newCurrentlyPlaying)
));
}
/**
* Pauses the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder pauseSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount() || !spotifyService.hasTrackPlaying(user)) {
return checkSpotify(spotifyService, user);
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
for (Action action : currentlyPlaying.getActions().getDisallows().getDisallowedActions()) {
if (action.equals(Action.PAUSING)) {
return EmbedUtils.errorEmbed()
.setDescription("%s This track is already paused.".formatted(Emojis.CROSS_MARK_EMOJI));
}
}
spotifyService.pausePlayback(user);
Track track = (Track) currentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription("%s Paused the track **[%s | %s](%s)**".formatted(
Emojis.CHECK_MARK_EMOJI,
track.getName(),
track.getArtists()[0].getName(),
SpotifyUtils.getTrackUrl(currentlyPlaying)
));
}
/**
* Resumes the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder resumeSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount() || !spotifyService.hasTrackPlaying(user)) {
return checkSpotify(spotifyService, user);
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
for (Action action : currentlyPlaying.getActions().getDisallows().getDisallowedActions()) {
if (action.equals(Action.RESUMING)) {
return EmbedUtils.errorEmbed()
.setDescription("%s This track is already playing.".formatted(Emojis.CROSS_MARK_EMOJI));
}
}
spotifyService.resumePlayback(user);
Track track = (Track) currentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription("%s Resumed the track **[%s | %s](%s)**".formatted(
Emojis.CHECK_MARK_EMOJI,
track.getName(),
track.getArtists()[0].getName(),
SpotifyUtils.getTrackUrl(currentlyPlaying)
));
}
}

View File

@ -1,37 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.spotify.SpotifyFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "Spotify Current Command")
@CommandInfo(name = "current", description = "Gets the currently playing Spotify track")
public class CurrentSubCommand extends BatCommand implements EventListener {
private final SpotifyService spotifyService;
@Autowired
public CurrentSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.currentSong(spotifyService, user).build()).queue();
}
}

View File

@ -1,110 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.SneakyThrows;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.interactions.components.text.TextInput;
import net.dv8tion.jda.api.interactions.components.text.TextInputStyle;
import net.dv8tion.jda.api.interactions.modals.Modal;
import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "link", description = "Link your Spotify account")
public class LinkSubCommand extends BatCommand implements EventListener {
private final SpotifyService spotifyService;
@Autowired
public LinkSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override @SneakyThrows
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
// if (!user.getId().equals(Consts.BOT_OWNER)) {
// event.replyEmbeds(EmbedUtils.errorEmbed()
// .setDescription("""
// %s We are currently awaiting Spotify's approval for our application. Please check back later.
// Submitted on: <t:1719583353>
// """.formatted(Emojis.CROSS_MARK_EMOJI))
// .build())
// .setEphemeral(true)
// .queue();
// return;
// }
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("%s You can link your Spotify account by clicking [here](%s)".formatted(
Emojis.SPOTIFY_EMOJI.getFormatted(),
spotifyService.getAuthorizationUrl()
)).build())
.addComponents(ActionRow.of(Button.primary("spotify_link", "Link Code").withEmoji(Emojis.SPOTIFY_EMOJI)))
.setEphemeral(true)
.queue();
}
@Override
public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
if (!event.getComponentId().equals("spotify_link")) {
return;
}
TextInput code = TextInput.create("code", "Link Code", TextInputStyle.SHORT)
.setPlaceholder("Your link code")
.setMinLength(0)
.setMaxLength(16)
.build();
Modal modal = Modal.create("link_modal", "Link Spotify Account")
.addComponents(ActionRow.of(code))
.build();
event.replyModal(modal).queue();
}
@Override @SneakyThrows
public void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) {
if (!event.getModalId().equals("link_modal")) {
return;
}
ModalMapping codeMapping = event.getValue("code");
if (codeMapping == null) {
return;
}
String code = codeMapping.getAsString();
if (!spotifyService.isValidLinkCode(code)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The link code you provided is invalid.".formatted(Emojis.CROSS_MARK_EMOJI))
.build())
.setEphemeral(true)
.queue();
return;
}
spotifyService.linkAccount(user, code);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s You have linked your Spotify account!".formatted(Emojis.CHECK_MARK_EMOJI.getFormatted()))
.build())
.setEphemeral(true)
.queue();
}
}

View File

@ -1,34 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.features.spotify.SpotifyFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "pause", description = "Pause the current Spotify track")
public class PauseSubCommand extends BatCommand {
private final SpotifyService spotifyService;
@Autowired
public PauseSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.pauseSong(spotifyService, user).build()).queue();
}
}

View File

@ -1,34 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.features.spotify.SpotifyFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "resume", description = "Resume the current Spotify track")
public class ResumeSubCommand extends BatCommand {
private final SpotifyService spotifyService;
@Autowired
public ResumeSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.resumeSong(spotifyService, user).build()).queue();
}
}

View File

@ -1,37 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.features.spotify.SpotifyFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "skip", description = "Skip the current Spotify track")
public class SkipSubCommand extends BatCommand {
private static final Logger log = LoggerFactory.getLogger(SkipSubCommand.class);
private final SpotifyService spotifyService;
@Autowired
public SkipSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.skipSong(spotifyService, user).build()).queue();
}
}

View File

@ -1,28 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "spotify", description = "Change your Spotify settings", guildOnly = false, userInstall = true, category = Category.MEDIA)
public class SpotifyCommand extends BatCommand {
@Autowired
public SpotifyCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(LinkSubCommand.class),
context.getBean(UnlinkSubCommand.class),
context.getBean(PauseSubCommand.class),
context.getBean(ResumeSubCommand.class),
context.getBean(CurrentSubCommand.class),
context.getBean(SkipSubCommand.class)
);
}
}

View File

@ -1,50 +0,0 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.spotify.SpotifyFeature;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.SneakyThrows;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "unlink", description = "Unlink your Spotify account")
public class UnlinkSubCommand extends BatCommand implements EventListener {
private final SpotifyService spotifyService;
@Autowired
public UnlinkSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override @SneakyThrows
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, Message commandMessage, String[] arguments, SlashCommandInteraction event) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount() || !spotifyService.hasTrackPlaying(user)) {
event.replyEmbeds(SpotifyFeature.checkSpotify(spotifyService, user).build()).queue();
return;
}
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s Successfully unlinked your Spotify account.".formatted(Emojis.CHECK_MARK_EMOJI))
.build())
.setEphemeral(true)
.queue();
}
}

View File

@ -1,59 +0,0 @@
package cc.fascinated.bat.features.spotify.profile;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Getter @Setter
public class SpotifyProfile extends Serializable {
/**
* The access token
*/
private String accessToken;
/**
* The refresh token
*/
private String refreshToken;
/**
* When the access token expires
*/
private Long expiresAt;
/**
* Checks if the account has a linked account
*
* @return if the account has a linked account
*/
public boolean hasLinkedAccount() {
return this.accessToken != null && this.refreshToken != null;
}
@Override
public void reset() {
this.accessToken = null;
this.refreshToken = null;
}
@Override
public void load(Document document, Gson gson) {
this.accessToken = document.getString("accessToken");
this.refreshToken = document.getString("refreshToken");
this.expiresAt = document.getLong("expiresAt");
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("accessToken", this.accessToken);
document.put("refreshToken", this.refreshToken);
document.put("expiresAt", this.expiresAt);
return document;
}
}

View File

@ -1,16 +0,0 @@
package cc.fascinated.bat.migrations;
import io.mongock.runner.spring.base.events.SpringMigrationSuccessEvent;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Log4j2(topic = "Mongock Listener")
public class MongockSuccessEventListener implements ApplicationListener<SpringMigrationSuccessEvent> {
@Override
public void onApplicationEvent(@NotNull SpringMigrationSuccessEvent event) {
log.info("Successfully ran Mongock migrations");
}
}

View File

@ -8,7 +8,6 @@ import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.features.counter.CounterProfile;
import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.moderation.punish.PunishmentProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile;
@ -23,8 +22,6 @@ import lombok.NonNull;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id;
import java.util.Date;
@ -182,15 +179,6 @@ public class BatGuild extends ProfileHolder {
return getProfile(LevelingProfile.class);
}
/**
* Gets the minecraft profile
*
* @return the minecraft profile
*/
public MinecraftProfile getMinecraftProfile() {
return getProfile(MinecraftProfile.class);
}
/**
* Gets the stats channel profile
*

View File

@ -5,7 +5,6 @@ import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.common.UserUtils;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.MongoService;
import com.mongodb.client.model.ReplaceOptions;
@ -87,15 +86,6 @@ public class BatUser extends ProfileHolder {
return user;
}
/**
* Gets the user's ScoreSaber profile
*
* @return the user's ScoreSaber profile
*/
public ScoreSaberProfile getScoreSaberProfile() {
return getProfile(ScoreSaberProfile.class);
}
/**
* Gets the user's name history profile
*

View File

@ -1,9 +1,6 @@
package cc.fascinated.bat.service;
import cc.fascinated.bat.common.InteractionBuilder;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;

View File

@ -1,254 +0,0 @@
package cc.fascinated.bat.service;
import cc.fascinated.bat.BatApplication;
import cc.fascinated.bat.common.DateUtils;
import cc.fascinated.bat.common.WebRequest;
import cc.fascinated.bat.common.beatsaber.leaderboard.impl.ScoreSaberLeaderboard;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.exception.BadRequestException;
import cc.fascinated.bat.exception.ResourceNotFoundException;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.*;
import com.google.gson.JsonObject;
import lombok.*;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Service
@Log4j2(topic = "ScoreSaber Service")
@DependsOn("discordService")
public class ScoreSaberService extends TextWebSocketHandler {
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";
/**
* The cached accounts.
*/
private final Map<String, ScoreSaberAccountToken> cachedAccounts = ExpiringMap.builder()
.expiration(5, TimeUnit.MINUTES)
.build();
/**
* The cached score pages.
*/
private final Map<String, CachedPage> cachedScorePages = ExpiringMap.builder()
.expiration(30, TimeUnit.MINUTES)
.build();
@Autowired
public ScoreSaberService() {
connectWebSocket();
}
/**
* Checks if the account is cached.
*
* @param id The id of the account.
* @return If the account is cached.
*/
public boolean isCached(String id) {
return cachedAccounts.containsKey(id);
}
/**
* 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) {
if (cachedAccounts.containsKey(id)) {
return cachedAccounts.get(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));
}
cachedAccounts.put(id, account);
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 CachedPage getPageScores(ScoreSaberProfile profile, int page) {
String key = profile.getAccountId() + "." + page;
// Check if the page is cached, but don't cache the first page as it's the most likely to change.
CachedPage cachedPage = cachedScorePages.get(key);
if (cachedScorePages.containsKey(key) && page != 1) {
cachedPage.setCached(true);
return cachedPage;
}
if (page == 1 || page % 5 == 0 || (cachedPage != null && cachedPage.getPage().getMetadata().getTotal() == page)) {
log.info("Fetching scores for account '{}' from page {}.", profile.getAccountId(), page);
}
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, profile.getAccountId(), "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));
cachedPage = new CachedPage(pageToken, false);
cachedScorePages.put(key, cachedPage);
return cachedPage;
}
/**
* Gets the scores for the account.
*
* @param profile The profile.
* @return The scores.
*/
public List<CachedPage> getScores(ScoreSaberProfile profile, Consumer<CurrentPageCallback> currentPageCallback) {
List<CachedPage> scores = new ArrayList<>(List.of(getPageScores(profile, 1)));
ScoreSaberPageMetadataToken metadata = scores.get(0).getPage().getMetadata();
int totalPages = (int) Math.ceil((double) metadata.getTotal() / metadata.getItemsPerPage());
log.info("Fetching {} pages of scores for account '{}'.", totalPages, profile.getAccountId());
for (int i = 2; i <= totalPages; i++) {
scores.add(getPageScores(profile, i));
currentPageCallback.accept(new CurrentPageCallback(i, totalPages));
}
return scores;
}
/**
* Gets the raw pp per global pp for the given account.
*
* @param account The account.
* @return The raw pp per global pp.
*/
public RawPerGlobal getRawPerGlobal(ScoreSaberProfile account, Consumer<CurrentPageCallback> currentPageCallback) {
List<CachedPage> scores = getScores(account, currentPageCallback);
List<ScoreSaberScoreToken> playerScores = new ArrayList<>();
for (CachedPage score : scores) {
for (ScoreSaberPlayerScoreToken playerScore : score.getPage().getPlayerScores()) {
playerScores.add(playerScore.getScore());
}
}
return new RawPerGlobal(
ScoreSaberLeaderboard.INSTANCE.getRawPerGlobalPP(playerScores, 1),
(int) scores.stream().filter(CachedPage::isCached).count(),
scores.size()
);
}
/**
* Connects to the ScoreSaber WebSocket.
*/
@SneakyThrows
private void connectWebSocket() {
log.info("Connecting to the ScoreSaber WSS.");
new StandardWebSocketClient().execute(this, "wss://scoresaber.com/ws").get();
}
@Override
public void afterConnectionEstablished(@NonNull WebSocketSession session) {
log.info("Connected to the ScoreSaber WSS.");
}
@Override
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
log.info("Disconnected from the ScoreSaber WSS.");
connectWebSocket(); // Reconnect to the WebSocket.
}
@Override
@SneakyThrows
protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
// Ignore the connection message.
if (message.getPayload().equals("Connected to the ScoreSaber WSS")) {
return;
}
try {
JsonObject json = BatApplication.GSON.fromJson(message.getPayload(), JsonObject.class);
String command = json.get("commandName").getAsString();
JsonObject data = json.get("commandData").getAsJsonObject();
if (command.equals("score")) {
ScoreSaberPlayerScoreToken score = BatApplication.GSON.fromJson(data, ScoreSaberPlayerScoreToken.class);
ScoreSaberScoreToken.LeaderboardPlayerInfo playerInfo = score.getScore().getLeaderboardPlayerInfo();
for (EventListener listener : EventService.LISTENERS) {
listener.onScoresaberScoreReceived(score, score.getLeaderboard(), playerInfo);
}
}
} catch (Exception ex) {
log.error("An error occurred while handling the message.", ex);
}
}
@AllArgsConstructor
@Getter
public static class CurrentPageCallback {
private int currentPage;
private int totalPages;
}
@AllArgsConstructor
@Getter
@Setter
public static class CachedPage {
/**
* The page of scores.
*/
private ScoreSaberScoresPageToken page;
/**
* Whether the page is cached.
*/
private boolean cached;
}
@AllArgsConstructor
@Getter
public static class RawPerGlobal {
/**
* The raw pp per global pp.
*/
private double rawPerGlobal;
/**
* The amount of pages that were cached.
*/
private int cachedPages;
/**
* The total amount of pages.
*/
private int totalPages;
}
}

View File

@ -1,239 +0,0 @@
package cc.fascinated.bat.service;
import cc.fascinated.bat.common.StringUtils;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatUser;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.enums.AuthorizationScope;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Service
@Getter
@Log4j2(topic = "Spotify Service")
@DependsOn("discordService")
public class SpotifyService {
/**
* The access token map.
*/
private final Map<String, AuthorizationCodeCredentials> accessToken = ExpiringMap.builder()
.expiration(30, TimeUnit.MINUTES)
.build();
/**
* A cache of the currently playing track for each user.
*/
private final Map<BatUser, CurrentlyPlaying> currentlyPlayingCache = ExpiringMap.builder()
.expiration(30, TimeUnit.SECONDS)
.build();
/**
* The client ID.
*/
private final String clientId;
/**
* The client secret.
*/
private final String clientSecret;
/**
* The Spotify API instance.
*/
private final SpotifyApi spotifyApi;
/**
* The user service.
*/
private final UserService userService;
/**
* The authorization URL.
*/
private final String authorizationUrl;
public SpotifyService(@Value("${spotify.client-id}") String clientId, @Value("${spotify.client-secret}") String clientSecret,
@Value("${spotify.redirect-uri}") String redirectUri, @NonNull UserService userService) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.userService = userService;
this.spotifyApi = new SpotifyApi.Builder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setRedirectUri(URI.create(redirectUri))
.build();
this.authorizationUrl = spotifyApi.authorizationCodeUri()
.response_type("code")
.scope(AuthorizationScope.values())
.build().execute().toString();
}
/**
* Starts playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void skipTrack(BatUser user) {
getSpotifyApi(user).skipUsersPlaybackToNextTrack().build().execute();
}
/**
* Gets the currently playing track for the user.
*
* @param user the user to check
* @return the currently playing track
*/
@SneakyThrows
public CurrentlyPlaying getCurrentlyPlayingTrack(BatUser user) {
CurrentlyPlaying currentlyPlaying = currentlyPlayingCache.get(user);
// If the track is still playing return the cache otherwise fetch the track
if (currentlyPlaying != null && currentlyPlaying.getTimestamp() + currentlyPlaying.getProgress_ms() < System.currentTimeMillis()) {
return currentlyPlaying;
}
currentlyPlaying = getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute();
currentlyPlayingCache.put(user, currentlyPlaying);
return currentlyPlaying;
}
/**
* Checks if the user has a track playing.
*
* @param user the user to check
* @return whether a track is playing
*/
public boolean hasTrackPlaying(BatUser user) {
return getCurrentlyPlayingTrack(user) != null;
}
/**
* Pauses playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void pausePlayback(BatUser user) {
getSpotifyApi(user).pauseUsersPlayback().build().execute();
}
/**
* Pauses playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void resumePlayback(BatUser user) {
getSpotifyApi(user).startResumeUsersPlayback().build().execute();
}
/**
* Gets the authorization key to link the user's
* Spotify account with their Discord account.
*
* @param code the code to authorize with
* @return the authorization details
*/
@SneakyThrows
public String authorize(String code) {
if (code == null) {
return """
<a href="%s">Click here to authorize your Spotify account</a>
""".formatted(authorizationUrl);
}
AuthorizationCodeCredentials credentials = spotifyApi.authorizationCode(code).build().execute();
String key = StringUtils.randomString(16);
accessToken.put(key, credentials);
return """
<p>Successfully authorized your Spotify account!</p>
<p>Your key is: <strong>%s</strong></p>
""".formatted(key);
}
/**
* Gets a new Spotify API instance.
*
* @return the Spotify API
*/
@SneakyThrows
public SpotifyApi getSpotifyApi(BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
ensureValidToken(profile, user);
return new SpotifyApi.Builder().setAccessToken(profile.getAccessToken()).build();
}
/**
* Ensures the user has a valid Spotify access token.
* <p>
* If the token is expired, it will be refreshed.
* </p>
*
* @param user the user to get the token for
*/
@SneakyThrows
public void ensureValidToken(SpotifyProfile profile, BatUser user) {
// If the token is still valid, return
if (profile.getExpiresAt() > System.currentTimeMillis()) {
return;
}
SpotifyApi api = new SpotifyApi.Builder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setAccessToken(profile.getAccessToken())
.setRefreshToken(profile.getRefreshToken())
.build();
try {
AuthorizationCodeCredentials credentials = api.authorizationCodeRefresh().build().execute();
profile.setAccessToken(credentials.getAccessToken());
profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000));
log.info("Refreshed Spotify token for user \"{}\"", user.getName());
} catch (SpotifyWebApiException ex) {
log.error("Failed to refresh Spotify token", ex);
}
}
/**
* Links the user's Spotify account with their Discord account.
*
* @param user the user to link the account with
* @param key the key to link the account with
*/
public void linkAccount(BatUser user, String key) {
AuthorizationCodeCredentials credentials = accessToken.get(key);
if (credentials == null) {
return;
}
// Link the user's Spotify account
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
profile.setAccessToken(credentials.getAccessToken());
profile.setRefreshToken(credentials.getRefreshToken());
profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000));
log.info("Linked Spotify account for user {}", user.getName());
}
/**
* Checks if the link code is valid.
*
* @param code the code to check
* @return if the code is valid
*/
public boolean isValidLinkCode(String code) {
return accessToken.containsKey(code);
}
}