add spotify feature

This commit is contained in:
Lee 2024-06-28 03:01:21 +01:00
parent 5c7a067f7a
commit fa10cf2019
70 changed files with 966 additions and 170 deletions

@ -115,6 +115,11 @@
<artifactId>expiringmap</artifactId> <artifactId>expiringmap</artifactId>
<version>0.5.11</version> <version>0.5.11</version>
</dependency> </dependency>
<dependency>
<groupId>se.michaelthelin.spotify</groupId>
<artifactId>spotify-web-api-java</artifactId>
<version>8.4.0</version>
</dependency>
<!-- Test Dependencies --> <!-- Test Dependencies -->
<dependency> <dependency>

@ -16,7 +16,7 @@ import java.util.Objects;
@EnableScheduling @EnableScheduling
@SpringBootApplication @SpringBootApplication
@Log4j2(topic = "Ember") @Log4j2(topic = "Bat")
public class BatApplication { public class BatApplication {
public static Gson GSON = new GsonBuilder().create(); public static Gson GSON = new GsonBuilder().create();

@ -15,7 +15,8 @@ import java.util.Map;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @Setter @Getter
@Setter
public abstract class BatCommand implements BatCommandExecutor { public abstract class BatCommand implements BatCommandExecutor {
/** /**
* The information about the command * The information about the command

@ -26,5 +26,6 @@ public interface BatCommandExecutor {
@NonNull MessageChannel channel, @NonNull MessageChannel channel,
Member member, Member member,
@NonNull SlashCommandInteraction interaction @NonNull SlashCommandInteraction interaction
) {} ) {
}
} }

@ -8,7 +8,8 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @Setter @Getter
@Setter
public class BatSubCommand implements BatCommandExecutor { public class BatSubCommand implements BatCommandExecutor {
/** /**
* The information about the sub command * The information about the sub command

@ -10,12 +10,14 @@ import java.util.List;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@AllArgsConstructor @Getter @AllArgsConstructor
@Getter
public enum Category { public enum Category {
GENERAL(Emoji.fromUnicode("U+2699"), "General", false), GENERAL(Emoji.fromUnicode("U+2699"), "General", false),
FUN(Emoji.fromFormatted("U+1F973"), "Fun", false), FUN(Emoji.fromFormatted("U+1F973"), "Fun", false),
SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false), SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false),
UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false), UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false),
MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false),
BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false), BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false),
BOT_ADMIN(null, null, true); BOT_ADMIN(null, null, true);

@ -26,9 +26,9 @@ import java.lang.management.RuntimeMXBean;
@Component @Component
@CommandInfo(name = "botstats", description = "Shows the bot statistics", guildOnly = false) @CommandInfo(name = "botstats", description = "Shows the bot statistics", guildOnly = false)
public class BotStatsCommand extends BatCommand { public class BotStatsCommand extends BatCommand {
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
private final GuildService guildService; private final GuildService guildService;
private final UserService userService; private final UserService userService;
private final RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
@Autowired @Autowired
public BotStatsCommand(@NonNull GuildService guildService, @NonNull UserService userService) { public BotStatsCommand(@NonNull GuildService guildService, @NonNull UserService userService) {
@ -42,14 +42,14 @@ public class BotStatsCommand extends BatCommand {
interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription( interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
"**Bot Statistics**\n" + "**Bot Statistics**\n" +
"➜ Guilds: %s\n".formatted(jda.getGuilds().size()) + "➜ Guilds: **%s\n".formatted(jda.getGuilds().size()) +
"➜ Users: %s\n".formatted(jda.getUsers().size()) + "➜ Users: **%s\n".formatted(jda.getUsers().size()) +
"➜ Gateway Ping: %sms\n".formatted(jda.getGatewayPing()) + "➜ Gateway Ping: **%sms**\n".formatted(jda.getGatewayPing()) +
"\n" + "\n" +
"**Bat Statistics**\n" + "**Bat Statistics**\n" +
"➜ Uptime: %s\n".formatted(TimeUtils.format(bean.getUptime())) + "➜ Uptime: **%s**\n".formatted(TimeUtils.format(bean.getUptime())) +
"➜ Cached Guilds: %s\n".formatted(guildService.getGuilds().size()) + "➜ Cached Guilds: **%s**\n".formatted(guildService.getGuilds().size()) +
"➜ Cached Users: %s".formatted(userService.getUsers().size()) "➜ Cached Users: **%s**".formatted(userService.getUsers().size())
).build()).queue(); ).build()).queue();
} }
} }

@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -136,7 +135,7 @@ public class HelpCommand extends BatCommand implements EventListener {
SelectOption.of(category.getName(), category.getName()).withEmoji(category.getEmoji())) SelectOption.of(category.getName(), category.getName()).withEmoji(category.getEmoji()))
.toList()); .toList());
return new LayoutComponent[] { return new LayoutComponent[]{
ActionRow.of( ActionRow.of(
Button.of(ButtonStyle.LINK, Consts.INVITE_URL, "Invite"), Button.of(ButtonStyle.LINK, Consts.INVITE_URL, "Invite"),
Button.of(ButtonStyle.LINK, Consts.SUPPORT_INVITE_URL, "Support") Button.of(ButtonStyle.LINK, Consts.SUPPORT_INVITE_URL, "Support")
@ -148,4 +147,4 @@ public class HelpCommand extends BatCommand implements EventListener {
) )
}; };
} }
} }

@ -57,7 +57,7 @@ public class SetSubCommand extends BatSubCommand {
} }
guildService.saveGuild(batGuild); guildService.saveGuild(batGuild);
if (!infinite) { if (!infinite) {
interaction.reply("The guild **%s** has been set as premium until <t:%s>".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli()/1000)).queue(); interaction.reply("The guild **%s** has been set as premium until <t:%s>".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
} else { } else {
interaction.reply("The guild **%s** has been set as premium indefinitely".formatted(guild.getName())).queue(); interaction.reply("The guild **%s** has been set as premium indefinitely".formatted(guild.getName())).queue();
} }

@ -3,14 +3,12 @@ package cc.fascinated.bat.command.impl.server;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TimeUtils;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -27,9 +25,9 @@ public class PremiumCommand extends BatCommand {
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information"); EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information");
if (premium.hasPremium()) { if (premium.hasPremium()) {
embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true); embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true);
embed.addField("Started On", "<t:%d>".formatted(premium.getActivatedAt().toInstant().toEpochMilli()/1000), true); embed.addField("Started", "<t:%d>".formatted(premium.getActivatedAt().toInstant().toEpochMilli() / 1000), true);
embed.addField("Expires At", premium.isInfinite() ? "Never" : "<t:%d>" embed.addField("Expires", premium.isInfinite() ? "Never" : "<t:%d>"
.formatted(premium.getExpiresAt().toInstant().toEpochMilli()/1000), true); .formatted(premium.getExpiresAt().toInstant().toEpochMilli() / 1000), true);
} else { } else {
embed.setDescription("The guild does not have premium"); embed.setDescription("The guild does not have premium");
} }

@ -8,14 +8,16 @@ import lombok.Setter;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@AllArgsConstructor @AllArgsConstructor
@Getter @Setter @Getter
@Setter
public abstract class Profile { public abstract class Profile {
/** /**
* The key of the profile. * The key of the profile.
*/ */
private String profileKey; private String profileKey;
public Profile() {} public Profile() {
}
/** /**
* Resets the profile * Resets the profile

@ -30,7 +30,7 @@ public class ProfileHolder {
Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null); Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null);
if (profile == null) { if (profile == null) {
try { try {
profile = (Profile) clazz.newInstance(); profile = clazz.newInstance();
profiles.put(profile.getProfileKey(), profile); profiles.put(profile.getProfileKey(), profile);
} catch (InstantiationException | IllegalAccessException e) { } catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace(); e.printStackTrace();

@ -0,0 +1,20 @@
package cc.fascinated.bat.common;
/**
* @author Fascinated (fascinated7)
*/
public class StringUtils {
/**
* Generates a random string
*
* @param length the length of the string
* @return the random string
*/
public static String randomString(int length) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
stringBuilder.append((char) (Math.random() * 26 + 'a'));
}
return stringBuilder.toString();
}
}

@ -20,7 +20,7 @@ public final class TimeUtils {
* @return the formatted time * @return the formatted time
*/ */
public static String format(long millis) { public static String format(long millis) {
return format(millis, WildTimeUnit.FIT); return format(millis, BatTimeFormat.FIT);
} }
/** /**
@ -30,7 +30,7 @@ public final class TimeUtils {
* @param timeUnit the time unit to format the millis to * @param timeUnit the time unit to format the millis to
* @return the formatted time * @return the formatted time
*/ */
public static String format(long millis, WildTimeUnit timeUnit) { public static String format(long millis, BatTimeFormat timeUnit) {
return format(millis, timeUnit, false); return format(millis, timeUnit, false);
} }
@ -42,7 +42,7 @@ public final class TimeUtils {
* @param compact whether to use a compact display * @param compact whether to use a compact display
* @return the formatted time * @return the formatted time
*/ */
public static String format(long millis, WildTimeUnit timeUnit, boolean compact) { public static String format(long millis, BatTimeFormat timeUnit, boolean compact) {
return format(millis, timeUnit, true, compact); return format(millis, timeUnit, true, compact);
} }
@ -55,14 +55,14 @@ public final class TimeUtils {
* @param compact whether to use a compact display * @param compact whether to use a compact display
* @return the formatted time * @return the formatted time
*/ */
public static String format(long millis, WildTimeUnit timeUnit, boolean decimals, boolean compact) { public static String format(long millis, BatTimeFormat timeUnit, boolean decimals, boolean compact) {
if (millis == -1L) { // Format permanent if (millis == -1L) { // Format permanent
return "Perm" + (compact ? "" : "anent"); return "Perm" + (compact ? "" : "anent");
} }
// Format the time to the best fitting time unit // Format the time to the best fitting time unit
if (timeUnit == WildTimeUnit.FIT) { if (timeUnit == BatTimeFormat.FIT) {
for (WildTimeUnit otherTimeUnit : WildTimeUnit.VALUES) { for (BatTimeFormat otherTimeUnit : BatTimeFormat.VALUES) {
if (otherTimeUnit != WildTimeUnit.FIT && millis >= otherTimeUnit.getMillis()) { if (otherTimeUnit != BatTimeFormat.FIT && millis >= otherTimeUnit.getMillis()) {
timeUnit = otherTimeUnit; timeUnit = otherTimeUnit;
break; break;
} }
@ -74,7 +74,7 @@ public final class TimeUtils {
} }
String formatted = time + (compact ? timeUnit.getSuffix() : " " + timeUnit.getDisplay()); // Append the time unit String formatted = time + (compact ? timeUnit.getSuffix() : " " + timeUnit.getDisplay()); // Append the time unit
if (time != 1.0 && !compact) { // Pluralize the time unit if (time != 1.0 && !compact) { // Pluralize the time unit
formatted+= "s"; formatted += "s";
} }
return formatted; return formatted;
} }
@ -89,16 +89,16 @@ public final class TimeUtils {
* @return the time in millis * @return the time in millis
*/ */
public static long fromString(String input) { public static long fromString(String input) {
Matcher matcher = WildTimeUnit.SUFFIX_PATTERN.matcher(input); // Match the given input Matcher matcher = BatTimeFormat.SUFFIX_PATTERN.matcher(input); // Match the given input
long millis = 0; // The total millis long millis = 0; // The total millis
// Match corresponding suffixes and add up the total millis // Match corresponding suffixes and add up the total millis
while (matcher.find()) { while (matcher.find()) {
int amount = Integer.parseInt(matcher.group(1)); // The amount of time to add int amount = Integer.parseInt(matcher.group(1)); // The amount of time to add
String suffix = matcher.group(2); // The unit suffix String suffix = matcher.group(2); // The unit suffix
WildTimeUnit timeUnit = WildTimeUnit.fromSuffix(suffix); // The time unit to add BatTimeFormat timeUnit = BatTimeFormat.fromSuffix(suffix); // The time unit to add
if (timeUnit != null) { // Increment the total millis if (timeUnit != null) { // Increment the total millis
millis+= amount * timeUnit.getMillis(); millis += amount * timeUnit.getMillis();
} }
} }
return millis; return millis;
@ -107,9 +107,11 @@ public final class TimeUtils {
/** /**
* Represents a unit of time. * Represents a unit of time.
*/ */
@NoArgsConstructor @AllArgsConstructor @NoArgsConstructor
@Getter(AccessLevel.PRIVATE) @ToString @AllArgsConstructor
public enum WildTimeUnit { @Getter(AccessLevel.PRIVATE)
@ToString
public enum BatTimeFormat {
FIT, FIT,
YEARS("Year", "y", TimeUnit.DAYS.toMillis(365L)), YEARS("Year", "y", TimeUnit.DAYS.toMillis(365L)),
MONTHS("Month", "mo", TimeUnit.DAYS.toMillis(30L)), MONTHS("Month", "mo", TimeUnit.DAYS.toMillis(30L)),
@ -123,7 +125,7 @@ public final class TimeUtils {
/** /**
* Our cached unit values. * Our cached unit values.
*/ */
public static final WildTimeUnit[] VALUES = values(); public static final BatTimeFormat[] VALUES = values();
/** /**
* Our cached suffix pattern. * Our cached suffix pattern.
@ -152,8 +154,8 @@ public final class TimeUtils {
* @return the time unit, null if not found * @return the time unit, null if not found
*/ */
@Nullable @Nullable
public static WildTimeUnit fromSuffix(String suffix) { public static BatTimeFormat fromSuffix(String suffix) {
for (WildTimeUnit unit : VALUES) { for (BatTimeFormat unit : VALUES) {
if (unit != FIT && unit.getSuffix().equals(suffix)) { if (unit != FIT && unit.getSuffix().equals(suffix)) {
return unit; return unit;
} }

@ -27,14 +27,15 @@ public class WebRequest {
* Gets a response from the given URL. * Gets a response from the given URL.
* *
* @param url the url * @param url the url
* @return the response
* @param <T> the type of the response * @param <T> the type of the response
* @return the response
*/ */
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException { public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
ResponseEntity<T> responseEntity = CLIENT.get() ResponseEntity<T> responseEntity = CLIENT.get()
.uri(url) .uri(url)
.retrieve() .retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error .onStatus(HttpStatusCode::isError, (request, response) -> {
}) // Don't throw exceptions on error
.toEntity(clazz); .toEntity(clazz);
if (responseEntity.getStatusCode().isError()) { if (responseEntity.getStatusCode().isError()) {
@ -56,7 +57,8 @@ public class WebRequest {
return CLIENT.get() return CLIENT.get()
.uri(url) .uri(url)
.retrieve() .retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error .onStatus(HttpStatusCode::isError, (request, response) -> {
}) // Don't throw exceptions on error
.toEntity(clazz); .toEntity(clazz);
} }
@ -70,7 +72,8 @@ public class WebRequest {
return CLIENT.head() return CLIENT.head()
.uri(url) .uri(url)
.retrieve() .retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error .onStatus(HttpStatusCode::isError, (request, response) -> {
}) // Don't throw exceptions on error
.toEntity(clazz); .toEntity(clazz);
} }
} }

@ -8,4 +8,5 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
@ComponentScan(basePackages = "cc.fascinated.bat") @ComponentScan(basePackages = "cc.fascinated.bat")
public class AppConfig { } public class AppConfig {
}

@ -0,0 +1,33 @@
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 String code) {
return ResponseEntity.ok(spotifyService.authorize(code));
}
}

@ -8,6 +8,8 @@ import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
@ -23,7 +25,8 @@ public interface EventListener {
* @param player the player that set the score * @param player the player that set the score
*/ */
default void onScoresaberScoreReceived(@NonNull ScoreSaberPlayerScoreToken score, @NonNull ScoreSaberLeaderboardToken leaderboard, default void onScoresaberScoreReceived(@NonNull ScoreSaberPlayerScoreToken score, @NonNull ScoreSaberLeaderboardToken leaderboard,
@NonNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) {} @NonNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) {
}
/** /**
* Called when a user joins a guild * Called when a user joins a guild
@ -31,7 +34,8 @@ public interface EventListener {
* @param guild the guild the user joined * @param guild the guild the user joined
* @param user the user that joined the guild * @param user the user that joined the guild
*/ */
default void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {} default void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
}
/** /**
* Called when a user leaves a guild * Called when a user leaves a guild
@ -39,7 +43,8 @@ public interface EventListener {
* @param guild the guild the user left * @param guild the guild the user left
* @param user the user that left the guild * @param user the user that left the guild
*/ */
default void onGuildMemberLeave(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberRemoveEvent event) {} default void onGuildMemberLeave(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberRemoveEvent event) {
}
/** /**
* Called when a user types a message * Called when a user types a message
@ -47,7 +52,8 @@ public interface EventListener {
* @param guild the guild that the message was sent in * @param guild the guild that the message was sent in
* @param user the user that sent the message * @param user the user that sent the message
*/ */
default void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {} default void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
}
/** /**
* Called when a user selects a string * Called when a user selects a string
@ -55,5 +61,24 @@ public interface EventListener {
* @param guild the guild that the string was selected in * @param guild the guild that the string was selected in
* @param user the user that selected the string * @param user the user that selected the string
*/ */
default void onStringSelectInteraction(BatGuild guild, @NonNull BatUser user, @NonNull StringSelectInteractionEvent event) {} default void onStringSelectInteraction(BatGuild guild, @NonNull BatUser user, @NonNull StringSelectInteractionEvent event) {
}
/**
* Called when a user interacts with a button
*
* @param guild the guild that the button was interacted with in
* @param user the user that interacted with the button
*/
default void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
}
/**
* Called when a user interacts with a modal
*
* @param guild the guild that the modal was interacted with in
* @param user the user that interacted with the modal
*/
default void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) {
}
} }

@ -6,4 +6,5 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@StandardException @StandardException
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException { } public class BadRequestException extends RuntimeException {
}

@ -6,4 +6,5 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@StandardException @StandardException
@ResponseStatus(HttpStatus.NOT_FOUND) @ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException { } public class ResourceNotFoundException extends RuntimeException {
}

@ -41,7 +41,8 @@ public class AfkProfile extends Profile {
} }
try { try {
member.modifyNickname("[AFK] " + member.getEffectiveName()).queue(); member.modifyNickname("[AFK] " + member.getEffectiveName()).queue();
} catch (Exception ignored) {} } catch (Exception ignored) {
}
} }
/** /**
@ -63,7 +64,8 @@ public class AfkProfile extends Profile {
} }
try { try {
member.modifyNickname(member.getEffectiveName().replace("[AFK] ", "")).queue(); member.modifyNickname(member.getEffectiveName().replace("[AFK] ", "")).queue();
} catch (Exception ignored) {} } catch (Exception ignored) {
}
} }
/** /**

@ -13,7 +13,8 @@ import java.util.List;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Setter @Getter @Setter
@Getter
public class AutoRoleProfile extends Profile { public class AutoRoleProfile extends Profile {
private static final int DEFAULT_MAX_ROLES = 10; private static final int DEFAULT_MAX_ROLES = 10;
private static final int PREMIUM_MAX_ROLES = 25; private static final int PREMIUM_MAX_ROLES = 25;
@ -27,6 +28,19 @@ public class AutoRoleProfile extends Profile {
super("auto-role"); super("auto-role");
} }
/**
* Gets the maximum amount of roles that can be set in the guild
*
* @param guild the guild to check
* @return the amount of role slots
*/
public static int getMaxRoleSlots(BatGuild guild) {
if (guild.getPremium().hasPremium()) {
return PREMIUM_MAX_ROLES;
}
return DEFAULT_MAX_ROLES;
}
/** /**
* Gets the amount of role slots in use * Gets the amount of role slots in use
* *
@ -92,19 +106,6 @@ public class AutoRoleProfile extends Profile {
return roles; return roles;
} }
/**
* Gets the maximum amount of roles that can be set in the guild
*
* @param guild the guild to check
* @return the amount of role slots
*/
public static int getMaxRoleSlots(BatGuild guild) {
if (guild.getPremium().hasPremium()) {
return PREMIUM_MAX_ROLES;
}
return DEFAULT_MAX_ROLES;
}
@Override @Override
public void reset() { public void reset() {
roleIds.clear(); roleIds.clear();

@ -80,7 +80,8 @@ public class MessageSubCommand extends BatSubCommand {
private Date parseBirthday(String birthday) { private Date parseBirthday(String birthday) {
try { try {
return FORMATTER.parse(birthday); return FORMATTER.parse(birthday);
} catch (ParseException ignored) {} } catch (ParseException ignored) {
}
return null; return null;
} }
} }

@ -80,7 +80,8 @@ public class SetSubCommand extends BatSubCommand {
private Date parseBirthday(String birthday) { private Date parseBirthday(String birthday) {
try { try {
return FORMATTER.parse(birthday); return FORMATTER.parse(birthday);
} catch (ParseException ignored) {} } catch (ParseException ignored) {
}
return null; return null;
} }
} }

@ -19,7 +19,8 @@ import org.springframework.stereotype.Component;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Log4j2 @Component
@Log4j2
public class NumberOneScoreFeedListener implements EventListener { public class NumberOneScoreFeedListener implements EventListener {
private final GuildService guildService; private final GuildService guildService;

@ -18,7 +18,8 @@ import org.springframework.stereotype.Component;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Log4j2 @Component
@Log4j2
public class UserScoreFeedListener implements EventListener { public class UserScoreFeedListener implements EventListener {
private final GuildService guildService; private final GuildService guildService;

@ -40,7 +40,7 @@ public class ChannelSubCommand extends BatSubCommand {
if (option == null) { if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications. Please provide a channel to set the feed channel to") .setDescription("There is no channel set for the feed notifications.")
.build()).queue(); .build()).queue();
return; return;
} }

@ -65,7 +65,7 @@ public class LinkSubCommand extends BatSubCommand {
return; return;
} }
((UserScoreSaberProfile) user.getProfile(UserScoreSaberProfile.class)).setSteamId(id); user.getProfile(UserScoreSaberProfile.class).setSteamId(id);
userService.saveUser(user); userService.saveUser(user);
interaction.replyEmbeds(EmbedUtils.successEmbed() interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id))) .setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id)))

@ -41,11 +41,6 @@ public class ScoreSaberCommand extends BatCommand {
super.addSubCommand(context.getBean(ResetSubCommand.class)); super.addSubCommand(context.getBean(ResetSubCommand.class));
} }
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
sendProfileEmbed(true, user, scoreSaberService, interaction);
}
/** /**
* Builds the profile embed for the ScoreSaber profile * Builds the profile embed for the ScoreSaber profile
* *
@ -103,4 +98,9 @@ public class ScoreSaberCommand extends BatCommand {
.build()).queue(); .build()).queue();
} }
} }
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
sendProfileEmbed(true, user, scoreSaberService, interaction);
}
} }

@ -40,7 +40,7 @@ public class ChannelSubCommand extends BatSubCommand {
if (option == null) { if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications. Please provide a channel to set the feed channel to") .setDescription("There is no channel set for the feed notifications.")
.build()).queue(); .build()).queue();
return; return;
} }

@ -39,7 +39,7 @@ public class UserSubCommand extends BatSubCommand {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class); GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class);
OptionMapping option = interaction.getOption("user"); OptionMapping option = interaction.getOption("user");
if (option == null){ if (option == null) {
if (profile.getTrackedUsers().isEmpty()) { if (profile.getTrackedUsers().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no users being tracked in the feed") .setDescription("There are no users being tracked in the feed")

@ -9,7 +9,8 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @Setter @Getter
@Setter
public class GuildNumberOneScoreFeedProfile extends Profile { public class GuildNumberOneScoreFeedProfile extends Profile {
/** /**
* The channel ID of the score feed * The channel ID of the score feed

@ -12,7 +12,8 @@ import java.util.List;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @Setter @Getter
@Setter
public class GuildUserScoreFeedProfile extends Profile { public class GuildUserScoreFeedProfile extends Profile {
/** /**
* The channel ID of the score feed * The channel ID of the score feed

@ -7,7 +7,8 @@ import lombok.Setter;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Setter @Getter @Setter
@Getter
public class UserScoreSaberProfile extends Profile { public class UserScoreSaberProfile extends Profile {
/** /**
* The Account ID of the ScoreSaber profile * The Account ID of the ScoreSaber profile

@ -0,0 +1,33 @@
package cc.fascinated.bat.features.spotify;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class SpotifyFeature extends Feature {
@Autowired
public SpotifyFeature() {
super("Spotify", Category.MUSIC);
}
/**
* The embed for when a user needs to link their Spotify account.
*
* @return The embed.
*/
public static MessageEmbed linkAccountEmbed() {
return EmbedUtils.genericEmbed()
.setDescription("You need to link your Spotify account before you can use this command.")
.build();
}
}

@ -0,0 +1,87 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
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.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Member;
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;
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
@Log4j2
@CommandInfo(name = "current", description = "Gets the currently playing Spotify track")
public class CurrentSubCommand extends BatSubCommand {
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, @NonNull SlashCommandInteraction interaction) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
interaction.replyEmbeds(SpotifyFeature.linkAccountEmbed()).queue();
return;
}
if (!spotifyService.hasTrackPlaying(user)) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You are not currently playing a track.")
.build())
.queue();
return;
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
AlbumSimplified album = track.getAlbum();
String trackUrl = "https://open.spotify.com/track/" + track.getId();
String albumUrl = "https://open.spotify.com/album/" + album.getId();
String description =
"➜ Song: **[%s](%s)**\n".formatted(track.getName(), trackUrl) +
"➜ Album: **[%s](%s)**\n".formatted(album.getName(), albumUrl) +
"➜ Position: %s\n".formatted(getFormattedTime(currentlyPlaying));
Image albumCover = album.getImages()[0];
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Listening to %s".formatted(track.getName()), trackUrl)
.setThumbnail(albumCover.getUrl())
.setDescription(description)
.build()).queue();
}
/**
* Gets the formatted time of the currently playing track
*
* @param currentlyPlaying the currently playing track
* @return the formatted time
*/
private 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);
}
}

@ -0,0 +1,83 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatSubCommand;
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 net.dv8tion.jda.api.entities.Member;
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 BatSubCommand implements EventListener {
private final SpotifyService spotifyService;
@Autowired
public LinkSubCommand(@NonNull SpotifyService spotifyService) {
this.spotifyService = spotifyService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You can link your Spotify account by clicking [here](%s)".formatted(spotifyService.getAuthorizationUrl()))
.build())
.addComponents(ActionRow.of(Button.primary("spotify_link", "Link Spotify")))
.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
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();
spotifyService.linkAccount(user, code);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully linked your Spotify account!")
.build())
.queue();
}
}

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatSubCommand;
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 net.dv8tion.jda.api.entities.Member;
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 = "pause", description = "Pause the current Spotify track")
public class PauseSubCommand extends BatSubCommand {
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, @NonNull SlashCommandInteraction interaction) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
interaction.replyEmbeds(SpotifyFeature.linkAccountEmbed()).queue();
return;
}
if (!spotifyService.hasTrackPlaying(user)) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You need to be playing a track to pause it.")
.build())
.queue();
return;
}
boolean didPause = spotifyService.pausePlayback(user);
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(didPause ? "Paused the current track." : "The current track is already paused.")
.build())
.queue();
}
}

@ -0,0 +1,53 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
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 net.dv8tion.jda.api.entities.Member;
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 BatSubCommand {
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, @NonNull SlashCommandInteraction interaction) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
interaction.replyEmbeds(SpotifyFeature.linkAccountEmbed()).queue();
return;
}
if (!spotifyService.hasTrackPlaying(user)) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You need to be playing a track to pause it.")
.build())
.queue();
return;
}
boolean didPause = spotifyService.resumePlayback(user);
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(didPause ? "Resumed the current track." : "The current track is already playing.")
.build())
.queue();
}
}

@ -0,0 +1,24 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatCommand;
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)
public class SpotifyCommand extends BatCommand {
@Autowired
public SpotifyCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(LinkSubCommand.class));
super.addSubCommand(context.getBean(UnlinkSubCommand.class));
super.addSubCommand(context.getBean(PauseSubCommand.class));
super.addSubCommand(context.getBean(ResumeSubCommand.class));
super.addSubCommand(context.getBean(CurrentSubCommand.class));
}
}

@ -0,0 +1,58 @@
package cc.fascinated.bat.features.spotify.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
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 cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
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 = "unlink", description = "Unlink your Spotify account")
public class UnlinkSubCommand extends BatSubCommand implements EventListener {
private final UserService userService;
@Autowired
public UnlinkSubCommand(@NonNull UserService userService) {
this.userService = userService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You do not have a linked Spotify account.")
.build())
.queue();
return;
}
profile.reset();
userService.saveUser(user);
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("Successfully unlinked your Spotify account.")
.build())
.queue();
}
}

@ -0,0 +1,40 @@
package cc.fascinated.bat.features.spotify.profile;
import cc.fascinated.bat.common.Profile;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@Getter @Setter
public class SpotifyProfile extends Profile {
/**
* The access token
*/
private String accessToken;
/**
* The refresh token
*/
private String refreshToken;
public SpotifyProfile() {
super("spotify");
}
/**
* 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;
}
}

@ -2,12 +2,10 @@ package cc.fascinated.bat.model;
import cc.fascinated.bat.common.ProfileHolder; import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import jakarta.annotation.PostConstruct;
import lombok.*; import lombok.*;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.stereotype.Component;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
@ -16,13 +14,16 @@ import java.util.Date;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
@Getter @Setter @Getter
@Setter
@Document(collection = "guilds") @Document(collection = "guilds")
public class BatGuild extends ProfileHolder { public class BatGuild extends ProfileHolder {
/** /**
* The ID of the guild * The ID of the guild
*/ */
@NonNull @Id private final String id; @NonNull
@Id
private final String id;
/** /**
* The time this guild was joined * The time this guild was joined
@ -64,7 +65,9 @@ public class BatGuild extends ProfileHolder {
return DiscordService.JDA.getGuildById(id); return DiscordService.JDA.getGuildById(id);
} }
@AllArgsConstructor @Getter @Setter @AllArgsConstructor
@Getter
@Setter
public static class Premium { public static class Premium {
/** /**
* The time the premium was activated * The time the premium was activated

@ -16,19 +16,29 @@ import java.util.Date;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
@Getter @Setter @Getter
@Setter
@Document(collection = "users") @Document(collection = "users")
public class BatUser extends ProfileHolder { public class BatUser extends ProfileHolder {
/** /**
* The ID of the user * The ID of the user
*/ */
@NonNull @Id private final String id; @NonNull
@Id
private final String id;
/** /**
* The time this user was created * The time this user was created
*/ */
private Date createdAt = new Date(); private Date createdAt = new Date();
/**
* The name of the user
*/
public String getName() {
return getDiscordUser().getName();
}
/** /**
* Gets the guild as the JDA Guild * Gets the guild as the JDA Guild
* *

@ -88,7 +88,8 @@ public class ScoreSaberAccountToken {
/** /**
* The badge for this account. * The badge for this account.
*/ */
@AllArgsConstructor @Getter @AllArgsConstructor
@Getter
public static class Badge { public static class Badge {
/** /**
* The image for this badge. * The image for this badge.
@ -104,7 +105,8 @@ public class ScoreSaberAccountToken {
/** /**
* The score stats for this account. * The score stats for this account.
*/ */
@AllArgsConstructor @Getter @AllArgsConstructor
@Getter
public static class ScoreStats { public static class ScoreStats {
/** /**
* The total score for this account. * The total score for this account.

@ -5,7 +5,8 @@ import lombok.ToString;
import java.util.List; import java.util.List;
@Getter @ToString @Getter
@ToString
public class ScoreSaberLeaderboardToken { public class ScoreSaberLeaderboardToken {
/** /**
* The ID of the leaderboard. * The ID of the leaderboard.

@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@Getter @ToString @Getter
@ToString
public class ScoreSaberPageMetadataToken { public class ScoreSaberPageMetadataToken {
/** /**
* The total amount of scores. * The total amount of scores.

@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@Getter @ToString @Getter
@ToString
public class ScoreSaberPlayerScoreToken { public class ScoreSaberPlayerScoreToken {
/** /**
* The score that was set. * The score that was set.

@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@Getter @ToString @Getter
@ToString
public class ScoreSaberScoreToken { public class ScoreSaberScoreToken {
/** /**
* The id for this score. * The id for this score.

@ -4,7 +4,8 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
@Getter @Setter @Getter
@Setter
@ToString @ToString
public class ScoreSaberScoresPageToken { public class ScoreSaberScoresPageToken {
/** /**

@ -6,7 +6,8 @@ import lombok.Getter;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @AllArgsConstructor @Getter
@AllArgsConstructor
public class RandomImage { public class RandomImage {
/** /**
* The URL of the image. * The URL of the image.

@ -6,7 +6,8 @@ import lombok.Getter;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @AllArgsConstructor @Getter
@AllArgsConstructor
public class CatImageToken { public class CatImageToken {
/** /**
* The ID of the image. * The ID of the image.

@ -6,4 +6,5 @@ import org.springframework.data.mongodb.repository.MongoRepository;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
public interface GuildRepository extends MongoRepository<BatGuild, String> { } public interface GuildRepository extends MongoRepository<BatGuild, String> {
}

@ -6,4 +6,5 @@ import org.springframework.data.mongodb.repository.MongoRepository;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
public interface UserRepository extends MongoRepository<BatUser, String> { } public interface UserRepository extends MongoRepository<BatUser, String> {
}

@ -17,7 +17,8 @@ import java.util.List;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Getter @Service
@Getter
public class DiscordService { public class DiscordService {
/** /**
* The JDA instance * The JDA instance

@ -7,6 +7,8 @@ import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.hooks.ListenerAdapter;
@ -22,16 +24,16 @@ import java.util.Set;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Log4j2 @Service
@Log4j2
@DependsOn("discordService") @DependsOn("discordService")
public class EventService extends ListenerAdapter { public class EventService extends ListenerAdapter {
private final GuildService guildService;
private final UserService userService;
/** /**
* The list of listeners registered * The list of listeners registered
*/ */
public static final Set<EventListener> LISTENERS = new HashSet<>(); public static final Set<EventListener> LISTENERS = new HashSet<>();
private final GuildService guildService;
private final UserService userService;
@Autowired @Autowired
public EventService(@NonNull GuildService guildService, @NonNull UserService userService, @NonNull ApplicationContext context) { public EventService(@NonNull GuildService guildService, @NonNull UserService userService, @NonNull ApplicationContext context) {
@ -103,4 +105,30 @@ public class EventService extends ListenerAdapter {
listener.onStringSelectInteraction(guild, user, event); listener.onStringSelectInteraction(guild, user, event);
} }
} }
@Override
public void onButtonInteraction(ButtonInteractionEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = event.getGuild() != null ? guildService.getGuild(event.getGuild().getId()) : null;
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onButtonInteraction(guild, user, event);
}
}
@Override
public void onModalInteraction(ModalInteractionEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = event.getGuild() != null ? guildService.getGuild(event.getGuild().getId()) : null;
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onModalInteraction(guild, user, event);
}
}
} }

@ -16,7 +16,9 @@ import java.util.List;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Getter @Log4j2 @Service
@Getter
@Log4j2
@DependsOn("commandService") @DependsOn("commandService")
public class FeatureService { public class FeatureService {
/** /**

@ -20,7 +20,9 @@ import java.util.concurrent.TimeUnit;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Log4j2 @Getter @Service
@Log4j2
@Getter
@DependsOn("discordService") @DependsOn("discordService")
public class GuildService extends ListenerAdapter { public class GuildService extends ListenerAdapter {
/** /**

@ -27,7 +27,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Service @Log4j2(topic = "ScoreSaber Service") @Service
@Log4j2(topic = "ScoreSaber Service")
public class ScoreSaberService extends TextWebSocketHandler { public class ScoreSaberService extends TextWebSocketHandler {
private static final String SCORESABER_API = "https://scoresaber.com/api/"; 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_ENDPOINT = SCORESABER_API + "player/%s/full";
@ -131,7 +132,8 @@ public class ScoreSaberService extends TextWebSocketHandler {
connectWebSocket(); // Reconnect to the WebSocket. connectWebSocket(); // Reconnect to the WebSocket.
} }
@Override @SneakyThrows @Override
@SneakyThrows
protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) { protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
// Ignore the connection message. // Ignore the connection message.
if (message.getPayload().equals("Connected to the ScoreSaber WSS")) { if (message.getPayload().equals("Connected to the ScoreSaber WSS")) {

@ -0,0 +1,188 @@
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 net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.enums.AuthorizationScope;
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import se.michaelthelin.spotify.requests.data.player.GetUsersCurrentlyPlayingTrackRequest;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Service
@Getter
public class SpotifyService {
private static final String REDIRECT_URI = "http://localhost:8080/spotify/callback";
/**
* The access token map.
*/
private final Map<String, AuthorizationCodeCredentials> accessToken = ExpiringMap.builder()
.expiration(30, TimeUnit.MINUTES)
.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, @NonNull UserService userService) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.userService = userService;
this.spotifyApi = new SpotifyApi.Builder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setRedirectUri(URI.create(REDIRECT_URI))
.build();
this.authorizationUrl = spotifyApi.authorizationCodeUri()
.response_type("code")
.scope(
AuthorizationScope.APP_REMOTE_CONTROL,
AuthorizationScope.USER_READ_PLAYBACK_POSITION,
AuthorizationScope.USER_READ_PLAYBACK_STATE,
AuthorizationScope.USER_MODIFY_PLAYBACK_STATE,
AuthorizationScope.USER_READ_CURRENTLY_PLAYING,
AuthorizationScope.APP_REMOTE_CONTROL,
AuthorizationScope.STREAMING
)
.build().execute().toString();
}
/**
* Gets the currently playing track for the user.
*
* @param user the user to check
* @return the currently playing track
*/
public CurrentlyPlaying getCurrentlyPlayingTrack(BatUser user) {
try {
return getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute();
} catch (Exception e) {
return null;
}
}
/**
* 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
* @return if the playback was paused
*/
@SneakyThrows
public boolean pausePlayback(BatUser user) {
try {
getSpotifyApi(user).pauseUsersPlayback().build().execute();
return true;
} catch (Exception e) {
return false;
}
}
/**
* Pauses playback for the user.
*
* @param user the user to start playback for
* @return if the playback was paused
*/
@SneakyThrows
public boolean resumePlayback(BatUser user) {
try {
getSpotifyApi(user).startResumeUsersPlayback().build().execute();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 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) {
AuthorizationCodeCredentials credentials = spotifyApi.authorizationCode(code).build().execute();
String key = StringUtils.randomString(16);
accessToken.put(key, credentials);
return "Authorization key: " + key;
}
/**
* 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());
userService.saveUser(user);
}
/**
* Gets a new Spotify API instance.
*
* @return the Spotify API
*/
public SpotifyApi getSpotifyApi(BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
return new SpotifyApi.Builder()
.setAccessToken(profile.getAccessToken())
.setClientSecret(clientSecret)
.build();
}
}

@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -18,7 +17,9 @@ import java.util.concurrent.TimeUnit;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Log4j2 @Getter @Service
@Log4j2
@Getter
@DependsOn("discordService") @DependsOn("discordService")
public class UserService { public class UserService {
/** /**

@ -1,11 +1,14 @@
# Discord Configuration
discord: discord:
token: "oh my goodnesssssssssss" token: "oh my goodnesssssssssss"
# Spotify Configuration
spotify:
client-id: "spotify-client-id"
client-secret: "spotify-client-secret"
# Spring Configuration # Spring Configuration
spring: spring:
# Disable the Spring Web Server
main:
web-application-type: none
data: data:
# MongoDB Configuration # MongoDB Configuration
mongodb: mongodb: