remove some things and bump depends
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m14s
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m14s
This commit is contained in:
parent
577c895169
commit
5f099a97f0
37
pom.xml
37
pom.xml
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user