From fa10cf20191743c460cf3795b65ba0acbcb402bd Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 28 Jun 2024 03:01:21 +0100 Subject: [PATCH] add spotify feature --- pom.xml | 5 + .../cc/fascinated/bat/BatApplication.java | 34 ++-- .../cc/fascinated/bat/command/BatCommand.java | 9 +- .../bat/command/BatCommandExecutor.java | 11 +- .../fascinated/bat/command/BatSubCommand.java | 9 +- .../cc/fascinated/bat/command/Category.java | 4 +- .../bat/command/impl/BotStatsCommand.java | 18 +- .../bat/command/impl/HelpCommand.java | 5 +- .../bat/command/impl/PingCommand.java | 2 +- .../impl/botadmin/premium/SetSubCommand.java | 2 +- .../command/impl/server/PremiumCommand.java | 8 +- .../cc/fascinated/bat/common/MathUtils.java | 2 +- .../cc/fascinated/bat/common/MemberUtils.java | 2 +- .../cc/fascinated/bat/common/NumberUtils.java | 2 +- .../cc/fascinated/bat/common/Profile.java | 6 +- .../fascinated/bat/common/ProfileHolder.java | 4 +- .../cc/fascinated/bat/common/RoleUtils.java | 4 +- .../cc/fascinated/bat/common/StringUtils.java | 20 ++ .../cc/fascinated/bat/common/TimeUtils.java | 46 +++-- .../cc/fascinated/bat/common/TimerUtils.java | 2 +- .../cc/fascinated/bat/common/WebRequest.java | 11 +- .../cc/fascinated/bat/config/AppConfig.java | 3 +- .../bat/controller/SpotifyController.java | 33 +++ .../fascinated/bat/event/EventListener.java | 47 ++++- .../bat/exception/BadRequestException.java | 3 +- .../exception/ResourceNotFoundException.java | 3 +- .../cc/fascinated/bat/features/Feature.java | 2 +- .../bat/features/afk/profile/AfkProfile.java | 10 +- .../autorole/command/AddSubCommand.java | 2 +- .../autorole/profile/AutoRoleProfile.java | 29 +-- .../birthday/command/MessageSubCommand.java | 3 +- .../birthday/command/SetSubCommand.java | 3 +- .../NumberOneScoreFeedListener.java | 3 +- .../scoresaber/UserScoreFeedListener.java | 3 +- .../command/numberone/ChannelSubCommand.java | 2 +- .../command/scoresaber/LinkSubCommand.java | 2 +- .../command/scoresaber/ScoreSaberCommand.java | 10 +- .../command/userfeed/ChannelSubCommand.java | 2 +- .../command/userfeed/UserSubCommand.java | 2 +- .../GuildNumberOneScoreFeedProfile.java | 3 +- .../profile/GuildUserScoreFeedProfile.java | 3 +- .../profile/UserScoreSaberProfile.java | 3 +- .../bat/features/spotify/SpotifyFeature.java | 33 +++ .../spotify/command/CurrentSubCommand.java | 87 ++++++++ .../spotify/command/LinkSubCommand.java | 83 ++++++++ .../spotify/command/PauseSubCommand.java | 62 ++++++ .../spotify/command/ResumeSubCommand.java | 53 +++++ .../spotify/command/SpotifyCommand.java | 24 +++ .../spotify/command/UnlinkSubCommand.java | 58 ++++++ .../spotify/profile/SpotifyProfile.java | 40 ++++ .../cc/fascinated/bat/model/BatGuild.java | 13 +- .../java/cc/fascinated/bat/model/BatUser.java | 14 +- .../scoresaber/ScoreSaberAccountToken.java | 6 +- .../ScoreSaberLeaderboardToken.java | 3 +- .../ScoreSaberPageMetadataToken.java | 3 +- .../ScoreSaberPlayerScoreToken.java | 3 +- .../scoresaber/ScoreSaberScoreToken.java | 3 +- .../scoresaber/ScoreSaberScoresPageToken.java | 3 +- .../bat/model/token/dogceo/RandomImage.java | 3 +- .../model/token/thecatapi/CatImageToken.java | 3 +- .../bat/repository/GuildRepository.java | 3 +- .../bat/repository/UserRepository.java | 3 +- .../bat/service/DiscordService.java | 3 +- .../fascinated/bat/service/EventService.java | 36 +++- .../bat/service/FeatureService.java | 4 +- .../fascinated/bat/service/GuildService.java | 4 +- .../bat/service/ScoreSaberService.java | 10 +- .../bat/service/SpotifyService.java | 188 ++++++++++++++++++ .../fascinated/bat/service/UserService.java | 5 +- src/main/resources/application.yml | 9 +- 70 files changed, 966 insertions(+), 170 deletions(-) create mode 100644 src/main/java/cc/fascinated/bat/common/StringUtils.java create mode 100644 src/main/java/cc/fascinated/bat/controller/SpotifyController.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/command/UnlinkSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/spotify/profile/SpotifyProfile.java create mode 100644 src/main/java/cc/fascinated/bat/service/SpotifyService.java diff --git a/pom.xml b/pom.xml index 8f1af76..2a61f67 100644 --- a/pom.xml +++ b/pom.xml @@ -115,6 +115,11 @@ expiringmap 0.5.11 + + se.michaelthelin.spotify + spotify-web-api-java + 8.4.0 + diff --git a/src/main/java/cc/fascinated/bat/BatApplication.java b/src/main/java/cc/fascinated/bat/BatApplication.java index 8782999..330697e 100644 --- a/src/main/java/cc/fascinated/bat/BatApplication.java +++ b/src/main/java/cc/fascinated/bat/BatApplication.java @@ -16,24 +16,24 @@ import java.util.Objects; @EnableScheduling @SpringBootApplication -@Log4j2(topic = "Ember") +@Log4j2(topic = "Bat") public class BatApplication { - public static Gson GSON = new GsonBuilder().create(); + public static Gson GSON = new GsonBuilder().create(); - @SneakyThrows - public static void main(@NonNull String[] args) { - // Handle loading of our configuration file - File config = new File("application.yml"); - if (!config.exists()) { // Saving the default config if it doesn't exist locally - Files.copy(Objects.requireNonNull(BatApplication.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING); - log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved - config.getAbsolutePath() - ); - return; - } - log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config + @SneakyThrows + public static void main(@NonNull String[] args) { + // Handle loading of our configuration file + File config = new File("application.yml"); + if (!config.exists()) { // Saving the default config if it doesn't exist locally + Files.copy(Objects.requireNonNull(BatApplication.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved + config.getAbsolutePath() + ); + return; + } + log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config - // Start the app - SpringApplication.run(BatApplication.class, args); - } + // Start the app + SpringApplication.run(BatApplication.class, args); + } } \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/BatCommand.java b/src/main/java/cc/fascinated/bat/command/BatCommand.java index 3fc7d4d..a1af652 100644 --- a/src/main/java/cc/fascinated/bat/command/BatCommand.java +++ b/src/main/java/cc/fascinated/bat/command/BatCommand.java @@ -15,7 +15,8 @@ import java.util.Map; /** * @author Fascinated (fascinated7) */ -@Getter @Setter +@Getter +@Setter public abstract class BatCommand implements BatCommandExecutor { /** * The information about the command @@ -69,10 +70,10 @@ public abstract class BatCommand implements BatCommandExecutor { /** * Adds an option to the sub command * - * @param optionType the type of the option - * @param name the name of the option + * @param optionType the type of the option + * @param name the name of the option * @param description the description of the option - * @param required whether the option is required + * @param required whether the option is required */ protected void addOption(OptionType optionType, String name, String description, boolean required) { this.commandData.addOption(optionType, name, description, required); diff --git a/src/main/java/cc/fascinated/bat/command/BatCommandExecutor.java b/src/main/java/cc/fascinated/bat/command/BatCommandExecutor.java index 4221850..613ba83 100644 --- a/src/main/java/cc/fascinated/bat/command/BatCommandExecutor.java +++ b/src/main/java/cc/fascinated/bat/command/BatCommandExecutor.java @@ -14,10 +14,10 @@ public interface BatCommandExecutor { /** * Executes the command using a slash command interaction. * - * @param guild the bat guild the command was executed in (null if the command was executed in a DM) - * @param user the bat user that executed the command - * @param channel the channel the command was executed in - * @param member the member that executed the command + * @param guild the bat guild the command was executed in (null if the command was executed in a DM) + * @param user the bat user that executed the command + * @param channel the channel the command was executed in + * @param member the member that executed the command * @param interaction the slash command interaction */ default void execute( @@ -26,5 +26,6 @@ public interface BatCommandExecutor { @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction - ) {} + ) { + } } diff --git a/src/main/java/cc/fascinated/bat/command/BatSubCommand.java b/src/main/java/cc/fascinated/bat/command/BatSubCommand.java index c866cf7..744a41c 100644 --- a/src/main/java/cc/fascinated/bat/command/BatSubCommand.java +++ b/src/main/java/cc/fascinated/bat/command/BatSubCommand.java @@ -8,7 +8,8 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; /** * @author Fascinated (fascinated7) */ -@Getter @Setter +@Getter +@Setter public class BatSubCommand implements BatCommandExecutor { /** * The information about the sub command @@ -33,10 +34,10 @@ public class BatSubCommand implements BatCommandExecutor { /** * Adds an option to the sub command * - * @param optionType the type of the option - * @param name the name of the option + * @param optionType the type of the option + * @param name the name of the option * @param description the description of the option - * @param required whether the option is required + * @param required whether the option is required */ public void addOption(OptionType optionType, String name, String description, boolean required) { this.commandData.addOption(optionType, name, description, required); diff --git a/src/main/java/cc/fascinated/bat/command/Category.java b/src/main/java/cc/fascinated/bat/command/Category.java index 9d188ff..c60650f 100644 --- a/src/main/java/cc/fascinated/bat/command/Category.java +++ b/src/main/java/cc/fascinated/bat/command/Category.java @@ -10,12 +10,14 @@ import java.util.List; /** * @author Fascinated (fascinated7) */ -@AllArgsConstructor @Getter +@AllArgsConstructor +@Getter public enum Category { GENERAL(Emoji.fromUnicode("U+2699"), "General", false), FUN(Emoji.fromFormatted("U+1F973"), "Fun", false), SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false), UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false), + MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false), BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false), BOT_ADMIN(null, null, true); diff --git a/src/main/java/cc/fascinated/bat/command/impl/BotStatsCommand.java b/src/main/java/cc/fascinated/bat/command/impl/BotStatsCommand.java index e96f0c9..e4061fb 100644 --- a/src/main/java/cc/fascinated/bat/command/impl/BotStatsCommand.java +++ b/src/main/java/cc/fascinated/bat/command/impl/BotStatsCommand.java @@ -26,9 +26,9 @@ import java.lang.management.RuntimeMXBean; @Component @CommandInfo(name = "botstats", description = "Shows the bot statistics", guildOnly = false) public class BotStatsCommand extends BatCommand { - RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean(); private final GuildService guildService; private final UserService userService; + private final RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean(); @Autowired public BotStatsCommand(@NonNull GuildService guildService, @NonNull UserService userService) { @@ -42,14 +42,14 @@ public class BotStatsCommand extends BatCommand { interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription( "**Bot Statistics**\n" + - "➜ Guilds: %s\n".formatted(jda.getGuilds().size()) + - "➜ Users: %s\n".formatted(jda.getUsers().size()) + - "➜ Gateway Ping: %sms\n".formatted(jda.getGatewayPing()) + - "\n" + - "**Bat Statistics**\n" + - "➜ Uptime: %s\n".formatted(TimeUtils.format(bean.getUptime())) + - "➜ Cached Guilds: %s\n".formatted(guildService.getGuilds().size()) + - "➜ Cached Users: %s".formatted(userService.getUsers().size()) + "➜ Guilds: **%s\n".formatted(jda.getGuilds().size()) + + "➜ Users: **%s\n".formatted(jda.getUsers().size()) + + "➜ Gateway Ping: **%sms**\n".formatted(jda.getGatewayPing()) + + "\n" + + "**Bat Statistics**\n" + + "➜ Uptime: **%s**\n".formatted(TimeUtils.format(bean.getUptime())) + + "➜ Cached Guilds: **%s**\n".formatted(guildService.getGuilds().size()) + + "➜ Cached Users: **%s**".formatted(userService.getUsers().size()) ).build()).queue(); } } diff --git a/src/main/java/cc/fascinated/bat/command/impl/HelpCommand.java b/src/main/java/cc/fascinated/bat/command/impl/HelpCommand.java index de68216..28d68bc 100644 --- a/src/main/java/cc/fascinated/bat/command/impl/HelpCommand.java +++ b/src/main/java/cc/fascinated/bat/command/impl/HelpCommand.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -136,7 +135,7 @@ public class HelpCommand extends BatCommand implements EventListener { SelectOption.of(category.getName(), category.getName()).withEmoji(category.getEmoji())) .toList()); - return new LayoutComponent[] { + return new LayoutComponent[]{ ActionRow.of( Button.of(ButtonStyle.LINK, Consts.INVITE_URL, "Invite"), Button.of(ButtonStyle.LINK, Consts.SUPPORT_INVITE_URL, "Support") @@ -148,4 +147,4 @@ public class HelpCommand extends BatCommand implements EventListener { ) }; } - } +} diff --git a/src/main/java/cc/fascinated/bat/command/impl/PingCommand.java b/src/main/java/cc/fascinated/bat/command/impl/PingCommand.java index 55be0a4..b513a4d 100644 --- a/src/main/java/cc/fascinated/bat/command/impl/PingCommand.java +++ b/src/main/java/cc/fascinated/bat/command/impl/PingCommand.java @@ -23,7 +23,7 @@ public class PingCommand extends BatCommand { interaction.reply("Pinging...").queue(response -> { response.editOriginal("Gateway response time: `%sms`\nAPI response time `%sms`".formatted( DiscordService.JDA.getGatewayPing(), - System.currentTimeMillis() - time + System.currentTimeMillis() - time )).queue(); }); } diff --git a/src/main/java/cc/fascinated/bat/command/impl/botadmin/premium/SetSubCommand.java b/src/main/java/cc/fascinated/bat/command/impl/botadmin/premium/SetSubCommand.java index 4563eac..ad3b0f8 100644 --- a/src/main/java/cc/fascinated/bat/command/impl/botadmin/premium/SetSubCommand.java +++ b/src/main/java/cc/fascinated/bat/command/impl/botadmin/premium/SetSubCommand.java @@ -57,7 +57,7 @@ public class SetSubCommand extends BatSubCommand { } guildService.saveGuild(batGuild); if (!infinite) { - interaction.reply("The guild **%s** has been set as premium until ".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli()/1000)).queue(); + interaction.reply("The guild **%s** has been set as premium until ".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue(); } else { interaction.reply("The guild **%s** has been set as premium indefinitely".formatted(guild.getName())).queue(); } diff --git a/src/main/java/cc/fascinated/bat/command/impl/server/PremiumCommand.java b/src/main/java/cc/fascinated/bat/command/impl/server/PremiumCommand.java index 3b6f13d..b5a35cf 100644 --- a/src/main/java/cc/fascinated/bat/command/impl/server/PremiumCommand.java +++ b/src/main/java/cc/fascinated/bat/command/impl/server/PremiumCommand.java @@ -3,14 +3,12 @@ package cc.fascinated.bat.command.impl.server; import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.common.EmbedUtils; -import cc.fascinated.bat.common.TimeUtils; import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatUser; import lombok.NonNull; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; 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.interactions.commands.SlashCommandInteraction; import org.springframework.stereotype.Component; @@ -27,9 +25,9 @@ public class PremiumCommand extends BatCommand { EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information"); if (premium.hasPremium()) { embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true); - embed.addField("Started On", "".formatted(premium.getActivatedAt().toInstant().toEpochMilli()/1000), true); - embed.addField("Expires At", premium.isInfinite() ? "Never" : "" - .formatted(premium.getExpiresAt().toInstant().toEpochMilli()/1000), true); + embed.addField("Started", "".formatted(premium.getActivatedAt().toInstant().toEpochMilli() / 1000), true); + embed.addField("Expires", premium.isInfinite() ? "Never" : "" + .formatted(premium.getExpiresAt().toInstant().toEpochMilli() / 1000), true); } else { embed.setDescription("The guild does not have premium"); } diff --git a/src/main/java/cc/fascinated/bat/common/MathUtils.java b/src/main/java/cc/fascinated/bat/common/MathUtils.java index 4dfbd8e..e977726 100644 --- a/src/main/java/cc/fascinated/bat/common/MathUtils.java +++ b/src/main/java/cc/fascinated/bat/common/MathUtils.java @@ -13,7 +13,7 @@ public final class MathUtils { /** * Format a number to a specific amount of decimal places. * - * @param number the number to format + * @param number the number to format * @param additional the additional decimal places to format * @return the formatted number */ diff --git a/src/main/java/cc/fascinated/bat/common/MemberUtils.java b/src/main/java/cc/fascinated/bat/common/MemberUtils.java index 85c96dc..287e288 100644 --- a/src/main/java/cc/fascinated/bat/common/MemberUtils.java +++ b/src/main/java/cc/fascinated/bat/common/MemberUtils.java @@ -15,7 +15,7 @@ public class MemberUtils { * Checks if a user has permission to edit another user * * @param guild the guild to check - * @param user the user to check + * @param user the user to check * @return if the user has permission to edit another user */ public static boolean hasPermissionToEdit(BatGuild guild, BatUser user) { diff --git a/src/main/java/cc/fascinated/bat/common/NumberUtils.java b/src/main/java/cc/fascinated/bat/common/NumberUtils.java index 5784e62..21770d1 100644 --- a/src/main/java/cc/fascinated/bat/common/NumberUtils.java +++ b/src/main/java/cc/fascinated/bat/common/NumberUtils.java @@ -12,7 +12,7 @@ public class NumberUtils { /** * Formats a number with commas. *

- * Example: 1000 -> 1,000 | Example: 1000.5 -> 1,000.5 + * Example: 1000 -> 1,000 | Example: 1000.5 -> 1,000.5 *

* * @param number the number to format diff --git a/src/main/java/cc/fascinated/bat/common/Profile.java b/src/main/java/cc/fascinated/bat/common/Profile.java index 42e089b..79e0624 100644 --- a/src/main/java/cc/fascinated/bat/common/Profile.java +++ b/src/main/java/cc/fascinated/bat/common/Profile.java @@ -8,14 +8,16 @@ import lombok.Setter; * @author Fascinated (fascinated7) */ @AllArgsConstructor -@Getter @Setter +@Getter +@Setter public abstract class Profile { /** * The key of the profile. */ private String profileKey; - public Profile() {} + public Profile() { + } /** * Resets the profile diff --git a/src/main/java/cc/fascinated/bat/common/ProfileHolder.java b/src/main/java/cc/fascinated/bat/common/ProfileHolder.java index 33aaa5b..75e490c 100644 --- a/src/main/java/cc/fascinated/bat/common/ProfileHolder.java +++ b/src/main/java/cc/fascinated/bat/common/ProfileHolder.java @@ -19,7 +19,7 @@ public class ProfileHolder { * Gets a profile for the holder * * @param clazz The class of the profile - * @param The type of the profile + * @param The type of the profile * @return The profile */ public T getProfile(Class clazz) { @@ -30,7 +30,7 @@ public class ProfileHolder { Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null); if (profile == null) { try { - profile = (Profile) clazz.newInstance(); + profile = clazz.newInstance(); profiles.put(profile.getProfileKey(), profile); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); diff --git a/src/main/java/cc/fascinated/bat/common/RoleUtils.java b/src/main/java/cc/fascinated/bat/common/RoleUtils.java index fbe0626..95294ad 100644 --- a/src/main/java/cc/fascinated/bat/common/RoleUtils.java +++ b/src/main/java/cc/fascinated/bat/common/RoleUtils.java @@ -11,9 +11,9 @@ public class RoleUtils { /** * Checks if a member has permission to give the role to another member * - * @param guild the guild to check + * @param guild the guild to check * @param member the member to check - * @param role the role to check + * @param role the role to check * @return if the member has permission to give the role */ public static boolean hasPermissionToGiveRole(BatGuild guild, Member member, Role role) { diff --git a/src/main/java/cc/fascinated/bat/common/StringUtils.java b/src/main/java/cc/fascinated/bat/common/StringUtils.java new file mode 100644 index 0000000..ad3af10 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/common/StringUtils.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/common/TimeUtils.java b/src/main/java/cc/fascinated/bat/common/TimeUtils.java index f3b902e..736f1aa 100644 --- a/src/main/java/cc/fascinated/bat/common/TimeUtils.java +++ b/src/main/java/cc/fascinated/bat/common/TimeUtils.java @@ -20,49 +20,49 @@ public final class TimeUtils { * @return the formatted time */ public static String format(long millis) { - return format(millis, WildTimeUnit.FIT); + return format(millis, BatTimeFormat.FIT); } /** * Format a time in millis to a readable time format. * - * @param millis the millis to format + * @param millis the millis to format * @param timeUnit the time unit to format the millis to * @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); } /** * Format a time in millis to a readable time format. * - * @param millis the millis to format + * @param millis the millis to format * @param timeUnit the time unit to format the millis to - * @param compact whether to use a compact display + * @param compact whether to use a compact display * @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); } /** * Format a time in millis to a readable time format. * - * @param millis the millis to format + * @param millis the millis to format * @param timeUnit the time unit to format the millis to * @param decimals whether to include decimals - * @param compact whether to use a compact display + * @param compact whether to use a compact display * @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 return "Perm" + (compact ? "" : "anent"); } // Format the time to the best fitting time unit - if (timeUnit == WildTimeUnit.FIT) { - for (WildTimeUnit otherTimeUnit : WildTimeUnit.VALUES) { - if (otherTimeUnit != WildTimeUnit.FIT && millis >= otherTimeUnit.getMillis()) { + if (timeUnit == BatTimeFormat.FIT) { + for (BatTimeFormat otherTimeUnit : BatTimeFormat.VALUES) { + if (otherTimeUnit != BatTimeFormat.FIT && millis >= otherTimeUnit.getMillis()) { timeUnit = otherTimeUnit; break; } @@ -74,7 +74,7 @@ public final class TimeUtils { } String formatted = time + (compact ? timeUnit.getSuffix() : " " + timeUnit.getDisplay()); // Append the time unit if (time != 1.0 && !compact) { // Pluralize the time unit - formatted+= "s"; + formatted += "s"; } return formatted; } @@ -89,16 +89,16 @@ public final class TimeUtils { * @return the time in millis */ 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 // Match corresponding suffixes and add up the total millis while (matcher.find()) { int amount = Integer.parseInt(matcher.group(1)); // The amount of time to add 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 - millis+= amount * timeUnit.getMillis(); + millis += amount * timeUnit.getMillis(); } } return millis; @@ -107,9 +107,11 @@ public final class TimeUtils { /** * Represents a unit of time. */ - @NoArgsConstructor @AllArgsConstructor - @Getter(AccessLevel.PRIVATE) @ToString - public enum WildTimeUnit { + @NoArgsConstructor + @AllArgsConstructor + @Getter(AccessLevel.PRIVATE) + @ToString + public enum BatTimeFormat { FIT, YEARS("Year", "y", TimeUnit.DAYS.toMillis(365L)), MONTHS("Month", "mo", TimeUnit.DAYS.toMillis(30L)), @@ -123,7 +125,7 @@ public final class TimeUtils { /** * Our cached unit values. */ - public static final WildTimeUnit[] VALUES = values(); + public static final BatTimeFormat[] VALUES = values(); /** * Our cached suffix pattern. @@ -152,8 +154,8 @@ public final class TimeUtils { * @return the time unit, null if not found */ @Nullable - public static WildTimeUnit fromSuffix(String suffix) { - for (WildTimeUnit unit : VALUES) { + public static BatTimeFormat fromSuffix(String suffix) { + for (BatTimeFormat unit : VALUES) { if (unit != FIT && unit.getSuffix().equals(suffix)) { return unit; } diff --git a/src/main/java/cc/fascinated/bat/common/TimerUtils.java b/src/main/java/cc/fascinated/bat/common/TimerUtils.java index 4d1d997..ad37ffe 100644 --- a/src/main/java/cc/fascinated/bat/common/TimerUtils.java +++ b/src/main/java/cc/fascinated/bat/common/TimerUtils.java @@ -14,7 +14,7 @@ public class TimerUtils { * Runs a repeating task on a schedule * * @param runnable the task to run - * @param delay the delay before the task runs + * @param delay the delay before the task runs */ public static void scheduleRepeating(Runnable runnable, long delay, long period) { new Timer().scheduleAtFixedRate(new TimerTask() { diff --git a/src/main/java/cc/fascinated/bat/common/WebRequest.java b/src/main/java/cc/fascinated/bat/common/WebRequest.java index f59dd98..5ff5841 100644 --- a/src/main/java/cc/fascinated/bat/common/WebRequest.java +++ b/src/main/java/cc/fascinated/bat/common/WebRequest.java @@ -27,14 +27,15 @@ public class WebRequest { * Gets a response from the given URL. * * @param url the url - * @return the response * @param the type of the response + * @return the response */ public static T getAsEntity(String url, Class clazz) throws RateLimitException { ResponseEntity responseEntity = CLIENT.get() .uri(url) .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); if (responseEntity.getStatusCode().isError()) { @@ -56,7 +57,8 @@ public class WebRequest { return CLIENT.get() .uri(url) .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); } @@ -70,7 +72,8 @@ public class WebRequest { return CLIENT.head() .uri(url) .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); } } diff --git a/src/main/java/cc/fascinated/bat/config/AppConfig.java b/src/main/java/cc/fascinated/bat/config/AppConfig.java index 58ba464..8409e30 100644 --- a/src/main/java/cc/fascinated/bat/config/AppConfig.java +++ b/src/main/java/cc/fascinated/bat/config/AppConfig.java @@ -8,4 +8,5 @@ import org.springframework.context.annotation.Configuration; */ @Configuration @ComponentScan(basePackages = "cc.fascinated.bat") -public class AppConfig { } \ No newline at end of file +public class AppConfig { +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/controller/SpotifyController.java b/src/main/java/cc/fascinated/bat/controller/SpotifyController.java new file mode 100644 index 0000000..4f23bf7 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/controller/SpotifyController.java @@ -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 authorizationCallback(@RequestParam String code) { + return ResponseEntity.ok(spotifyService.authorize(code)); + } +} diff --git a/src/main/java/cc/fascinated/bat/event/EventListener.java b/src/main/java/cc/fascinated/bat/event/EventListener.java index 6a9c3ff..e6caa6f 100644 --- a/src/main/java/cc/fascinated/bat/event/EventListener.java +++ b/src/main/java/cc/fascinated/bat/event/EventListener.java @@ -8,6 +8,8 @@ import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken; import lombok.NonNull; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; 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.message.MessageReceivedEvent; @@ -18,42 +20,65 @@ public interface EventListener { /** * Called when a ScoreSaber score is received * - * @param score the score that was set + * @param score the score that was set * @param leaderboard the leaderboard that the score was set on - * @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, - @NonNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) {} + @NonNull ScoreSaberScoreToken.LeaderboardPlayerInfo player) { + } /** * Called when a user joins a guild * * @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 * * @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 * * @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 * * @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) { + } } diff --git a/src/main/java/cc/fascinated/bat/exception/BadRequestException.java b/src/main/java/cc/fascinated/bat/exception/BadRequestException.java index f629d9f..966f467 100644 --- a/src/main/java/cc/fascinated/bat/exception/BadRequestException.java +++ b/src/main/java/cc/fascinated/bat/exception/BadRequestException.java @@ -6,4 +6,5 @@ import org.springframework.web.bind.annotation.ResponseStatus; @StandardException @ResponseStatus(HttpStatus.BAD_REQUEST) -public class BadRequestException extends RuntimeException { } +public class BadRequestException extends RuntimeException { +} diff --git a/src/main/java/cc/fascinated/bat/exception/ResourceNotFoundException.java b/src/main/java/cc/fascinated/bat/exception/ResourceNotFoundException.java index f864cef..3dd3a76 100644 --- a/src/main/java/cc/fascinated/bat/exception/ResourceNotFoundException.java +++ b/src/main/java/cc/fascinated/bat/exception/ResourceNotFoundException.java @@ -6,4 +6,5 @@ import org.springframework.web.bind.annotation.ResponseStatus; @StandardException @ResponseStatus(HttpStatus.NOT_FOUND) -public class ResourceNotFoundException extends RuntimeException { } +public class ResourceNotFoundException extends RuntimeException { +} diff --git a/src/main/java/cc/fascinated/bat/features/Feature.java b/src/main/java/cc/fascinated/bat/features/Feature.java index 0fc395c..ecdcfd7 100644 --- a/src/main/java/cc/fascinated/bat/features/Feature.java +++ b/src/main/java/cc/fascinated/bat/features/Feature.java @@ -29,7 +29,7 @@ public abstract class Feature { * Registers the command for the feature * * @param commandService The command service - * @param command The command to register + * @param command The command to register */ public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) { command.setCategory(category); diff --git a/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java b/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java index 02ce819..20aeecb 100644 --- a/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java +++ b/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java @@ -24,7 +24,7 @@ public class AfkProfile extends Profile { /** * Adds a user to the AFK list * - * @param guild the guild enable afk mode for + * @param guild the guild enable afk mode for * @param userId the user ID to add * @param reason the reason for being AFK */ @@ -41,13 +41,14 @@ public class AfkProfile extends Profile { } try { member.modifyNickname("[AFK] " + member.getEffectiveName()).queue(); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } /** * Removes a user from the AFK list * - * @param guild the guild to remove the user from + * @param guild the guild to remove the user from * @param userId the user ID to remove */ public void removeAfkUser(BatGuild guild, String userId) { @@ -63,7 +64,8 @@ public class AfkProfile extends Profile { } try { member.modifyNickname(member.getEffectiveName().replace("[AFK] ", "")).queue(); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } /** diff --git a/src/main/java/cc/fascinated/bat/features/autorole/command/AddSubCommand.java b/src/main/java/cc/fascinated/bat/features/autorole/command/AddSubCommand.java index 07600cf..fd3586d 100644 --- a/src/main/java/cc/fascinated/bat/features/autorole/command/AddSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/autorole/command/AddSubCommand.java @@ -40,7 +40,7 @@ public class AddSubCommand extends BatSubCommand { if (profile.getRoleSlotsInUse() >= maxRoleSlots) { interaction.replyEmbeds(EmbedUtils.errorEmbed() .setDescription("The guild can only have a maximum of %d auto roles" - .formatted(maxRoleSlots)) + .formatted(maxRoleSlots)) .build()).queue(); return; } diff --git a/src/main/java/cc/fascinated/bat/features/autorole/profile/AutoRoleProfile.java b/src/main/java/cc/fascinated/bat/features/autorole/profile/AutoRoleProfile.java index d7e4544..80608dd 100644 --- a/src/main/java/cc/fascinated/bat/features/autorole/profile/AutoRoleProfile.java +++ b/src/main/java/cc/fascinated/bat/features/autorole/profile/AutoRoleProfile.java @@ -13,7 +13,8 @@ import java.util.List; /** * @author Fascinated (fascinated7) */ -@Setter @Getter +@Setter +@Getter public class AutoRoleProfile extends Profile { private static final int DEFAULT_MAX_ROLES = 10; private static final int PREMIUM_MAX_ROLES = 25; @@ -27,6 +28,19 @@ public class AutoRoleProfile extends Profile { 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 * @@ -92,19 +106,6 @@ public class AutoRoleProfile extends Profile { 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 public void reset() { roleIds.clear(); diff --git a/src/main/java/cc/fascinated/bat/features/birthday/command/MessageSubCommand.java b/src/main/java/cc/fascinated/bat/features/birthday/command/MessageSubCommand.java index 1ae7295..216b0d3 100644 --- a/src/main/java/cc/fascinated/bat/features/birthday/command/MessageSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/birthday/command/MessageSubCommand.java @@ -80,7 +80,8 @@ public class MessageSubCommand extends BatSubCommand { private Date parseBirthday(String birthday) { try { return FORMATTER.parse(birthday); - } catch (ParseException ignored) {} + } catch (ParseException ignored) { + } return null; } } diff --git a/src/main/java/cc/fascinated/bat/features/birthday/command/SetSubCommand.java b/src/main/java/cc/fascinated/bat/features/birthday/command/SetSubCommand.java index 5d3de41..ecf6a46 100644 --- a/src/main/java/cc/fascinated/bat/features/birthday/command/SetSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/birthday/command/SetSubCommand.java @@ -80,7 +80,8 @@ public class SetSubCommand extends BatSubCommand { private Date parseBirthday(String birthday) { try { return FORMATTER.parse(birthday); - } catch (ParseException ignored) {} + } catch (ParseException ignored) { + } return null; } } diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/NumberOneScoreFeedListener.java b/src/main/java/cc/fascinated/bat/features/scoresaber/NumberOneScoreFeedListener.java index 041050b..b20744f 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/NumberOneScoreFeedListener.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/NumberOneScoreFeedListener.java @@ -19,7 +19,8 @@ import org.springframework.stereotype.Component; /** * @author Fascinated (fascinated7) */ -@Component @Log4j2 +@Component +@Log4j2 public class NumberOneScoreFeedListener implements EventListener { private final GuildService guildService; diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/UserScoreFeedListener.java b/src/main/java/cc/fascinated/bat/features/scoresaber/UserScoreFeedListener.java index e20cb1d..0af493f 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/UserScoreFeedListener.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/UserScoreFeedListener.java @@ -18,7 +18,8 @@ import org.springframework.stereotype.Component; /** * @author Fascinated (fascinated7) */ -@Component @Log4j2 +@Component +@Log4j2 public class UserScoreFeedListener implements EventListener { private final GuildService guildService; diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/command/numberone/ChannelSubCommand.java b/src/main/java/cc/fascinated/bat/features/scoresaber/command/numberone/ChannelSubCommand.java index 85d79b2..984e583 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/command/numberone/ChannelSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/command/numberone/ChannelSubCommand.java @@ -40,7 +40,7 @@ public class ChannelSubCommand extends BatSubCommand { if (option == null) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { 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(); return; } diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/LinkSubCommand.java b/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/LinkSubCommand.java index 6dd81b6..491428d 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/LinkSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/LinkSubCommand.java @@ -65,7 +65,7 @@ public class LinkSubCommand extends BatSubCommand { return; } - ((UserScoreSaberProfile) user.getProfile(UserScoreSaberProfile.class)).setSteamId(id); + user.getProfile(UserScoreSaberProfile.class).setSteamId(id); userService.saveUser(user); interaction.replyEmbeds(EmbedUtils.successEmbed() .setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id))) diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/ScoreSaberCommand.java b/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/ScoreSaberCommand.java index 3a084e8..27106ec 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/ScoreSaberCommand.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/command/scoresaber/ScoreSaberCommand.java @@ -41,11 +41,6 @@ public class ScoreSaberCommand extends BatCommand { 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 * @@ -103,4 +98,9 @@ public class ScoreSaberCommand extends BatCommand { .build()).queue(); } } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { + sendProfileEmbed(true, user, scoreSaberService, interaction); + } } diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/ChannelSubCommand.java b/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/ChannelSubCommand.java index 42ab983..7af65ac 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/ChannelSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/ChannelSubCommand.java @@ -40,7 +40,7 @@ public class ChannelSubCommand extends BatSubCommand { if (option == null) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { 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(); return; } diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/UserSubCommand.java b/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/UserSubCommand.java index 8b90d8d..789d6ab 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/UserSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/command/userfeed/UserSubCommand.java @@ -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) { GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class); OptionMapping option = interaction.getOption("user"); - if (option == null){ + if (option == null) { if (profile.getTrackedUsers().isEmpty()) { interaction.replyEmbeds(EmbedUtils.errorEmbed() .setDescription("There are no users being tracked in the feed") diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildNumberOneScoreFeedProfile.java b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildNumberOneScoreFeedProfile.java index 873dfc3..1291bee 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildNumberOneScoreFeedProfile.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildNumberOneScoreFeedProfile.java @@ -9,7 +9,8 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; /** * @author Fascinated (fascinated7) */ -@Getter @Setter +@Getter +@Setter public class GuildNumberOneScoreFeedProfile extends Profile { /** * The channel ID of the score feed diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildUserScoreFeedProfile.java b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildUserScoreFeedProfile.java index e68b2de..3d6ade3 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildUserScoreFeedProfile.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/GuildUserScoreFeedProfile.java @@ -12,7 +12,8 @@ import java.util.List; /** * @author Fascinated (fascinated7) */ -@Getter @Setter +@Getter +@Setter public class GuildUserScoreFeedProfile extends Profile { /** * The channel ID of the score feed diff --git a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/UserScoreSaberProfile.java b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/UserScoreSaberProfile.java index 1a4fa04..799faf1 100644 --- a/src/main/java/cc/fascinated/bat/features/scoresaber/profile/UserScoreSaberProfile.java +++ b/src/main/java/cc/fascinated/bat/features/scoresaber/profile/UserScoreSaberProfile.java @@ -7,7 +7,8 @@ import lombok.Setter; /** * @author Fascinated (fascinated7) */ -@Setter @Getter +@Setter +@Getter public class UserScoreSaberProfile extends Profile { /** * The Account ID of the ScoreSaber profile diff --git a/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java b/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java new file mode 100644 index 0000000..ed38a38 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java new file mode 100644 index 0000000..d54bead --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java @@ -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); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java new file mode 100644 index 0000000..f41a9fb --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java new file mode 100644 index 0000000..b1b948c --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java new file mode 100644 index 0000000..492a407 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java new file mode 100644 index 0000000..3d743ab --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java @@ -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)); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/UnlinkSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/UnlinkSubCommand.java new file mode 100644 index 0000000..9bbc911 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/UnlinkSubCommand.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/profile/SpotifyProfile.java b/src/main/java/cc/fascinated/bat/features/spotify/profile/SpotifyProfile.java new file mode 100644 index 0000000..03e3e0f --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/profile/SpotifyProfile.java @@ -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; + } +} diff --git a/src/main/java/cc/fascinated/bat/model/BatGuild.java b/src/main/java/cc/fascinated/bat/model/BatGuild.java index fe8b769..c80a008 100644 --- a/src/main/java/cc/fascinated/bat/model/BatGuild.java +++ b/src/main/java/cc/fascinated/bat/model/BatGuild.java @@ -2,12 +2,10 @@ package cc.fascinated.bat.model; import cc.fascinated.bat.common.ProfileHolder; import cc.fascinated.bat.service.DiscordService; -import jakarta.annotation.PostConstruct; import lombok.*; import net.dv8tion.jda.api.entities.Guild; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.stereotype.Component; import java.util.Calendar; import java.util.Date; @@ -16,13 +14,16 @@ import java.util.Date; * @author Fascinated (fascinated7) */ @RequiredArgsConstructor -@Getter @Setter +@Getter +@Setter @Document(collection = "guilds") public class BatGuild extends ProfileHolder { /** * The ID of the guild */ - @NonNull @Id private final String id; + @NonNull + @Id + private final String id; /** * The time this guild was joined @@ -64,7 +65,9 @@ public class BatGuild extends ProfileHolder { return DiscordService.JDA.getGuildById(id); } - @AllArgsConstructor @Getter @Setter + @AllArgsConstructor + @Getter + @Setter public static class Premium { /** * The time the premium was activated diff --git a/src/main/java/cc/fascinated/bat/model/BatUser.java b/src/main/java/cc/fascinated/bat/model/BatUser.java index 72d0242..9347bce 100644 --- a/src/main/java/cc/fascinated/bat/model/BatUser.java +++ b/src/main/java/cc/fascinated/bat/model/BatUser.java @@ -16,19 +16,29 @@ import java.util.Date; * @author Fascinated (fascinated7) */ @RequiredArgsConstructor -@Getter @Setter +@Getter +@Setter @Document(collection = "users") public class BatUser extends ProfileHolder { /** * The ID of the user */ - @NonNull @Id private final String id; + @NonNull + @Id + private final String id; /** * The time this user was created */ private Date createdAt = new Date(); + /** + * The name of the user + */ + public String getName() { + return getDiscordUser().getName(); + } + /** * Gets the guild as the JDA Guild * diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberAccountToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberAccountToken.java index f470417..a9e2584 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberAccountToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberAccountToken.java @@ -88,7 +88,8 @@ public class ScoreSaberAccountToken { /** * The badge for this account. */ - @AllArgsConstructor @Getter + @AllArgsConstructor + @Getter public static class Badge { /** * The image for this badge. @@ -104,7 +105,8 @@ public class ScoreSaberAccountToken { /** * The score stats for this account. */ - @AllArgsConstructor @Getter + @AllArgsConstructor + @Getter public static class ScoreStats { /** * The total score for this account. diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java index 4d5dd49..e8ca838 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberLeaderboardToken.java @@ -5,7 +5,8 @@ import lombok.ToString; import java.util.List; -@Getter @ToString +@Getter +@ToString public class ScoreSaberLeaderboardToken { /** * The ID of the leaderboard. diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java index 7698a19..7782d36 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPageMetadataToken.java @@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber; import lombok.Getter; import lombok.ToString; -@Getter @ToString +@Getter +@ToString public class ScoreSaberPageMetadataToken { /** * The total amount of scores. diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java index 83d7873..b090ce4 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberPlayerScoreToken.java @@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber; import lombok.Getter; import lombok.ToString; -@Getter @ToString +@Getter +@ToString public class ScoreSaberPlayerScoreToken { /** * The score that was set. diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoreToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoreToken.java index b1afa3b..12f69c0 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoreToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoreToken.java @@ -3,7 +3,8 @@ package cc.fascinated.bat.model.token.beatsaber.scoresaber; import lombok.Getter; import lombok.ToString; -@Getter @ToString +@Getter +@ToString public class ScoreSaberScoreToken { /** * The id for this score. diff --git a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoresPageToken.java b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoresPageToken.java index f552fff..712cd77 100644 --- a/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoresPageToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/beatsaber/scoresaber/ScoreSaberScoresPageToken.java @@ -4,7 +4,8 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; -@Getter @Setter +@Getter +@Setter @ToString public class ScoreSaberScoresPageToken { /** diff --git a/src/main/java/cc/fascinated/bat/model/token/dogceo/RandomImage.java b/src/main/java/cc/fascinated/bat/model/token/dogceo/RandomImage.java index e70243a..f89af7b 100644 --- a/src/main/java/cc/fascinated/bat/model/token/dogceo/RandomImage.java +++ b/src/main/java/cc/fascinated/bat/model/token/dogceo/RandomImage.java @@ -6,7 +6,8 @@ import lombok.Getter; /** * @author Fascinated (fascinated7) */ -@Getter @AllArgsConstructor +@Getter +@AllArgsConstructor public class RandomImage { /** * The URL of the image. diff --git a/src/main/java/cc/fascinated/bat/model/token/thecatapi/CatImageToken.java b/src/main/java/cc/fascinated/bat/model/token/thecatapi/CatImageToken.java index 55b5846..f4b4dbb 100644 --- a/src/main/java/cc/fascinated/bat/model/token/thecatapi/CatImageToken.java +++ b/src/main/java/cc/fascinated/bat/model/token/thecatapi/CatImageToken.java @@ -6,7 +6,8 @@ import lombok.Getter; /** * @author Fascinated (fascinated7) */ -@Getter @AllArgsConstructor +@Getter +@AllArgsConstructor public class CatImageToken { /** * The ID of the image. diff --git a/src/main/java/cc/fascinated/bat/repository/GuildRepository.java b/src/main/java/cc/fascinated/bat/repository/GuildRepository.java index 415891d..c072a1c 100644 --- a/src/main/java/cc/fascinated/bat/repository/GuildRepository.java +++ b/src/main/java/cc/fascinated/bat/repository/GuildRepository.java @@ -6,4 +6,5 @@ import org.springframework.data.mongodb.repository.MongoRepository; /** * @author Fascinated (fascinated7) */ -public interface GuildRepository extends MongoRepository { } \ No newline at end of file +public interface GuildRepository extends MongoRepository { +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/repository/UserRepository.java b/src/main/java/cc/fascinated/bat/repository/UserRepository.java index 4637764..959ec3b 100644 --- a/src/main/java/cc/fascinated/bat/repository/UserRepository.java +++ b/src/main/java/cc/fascinated/bat/repository/UserRepository.java @@ -6,4 +6,5 @@ import org.springframework.data.mongodb.repository.MongoRepository; /** * @author Fascinated (fascinated7) */ -public interface UserRepository extends MongoRepository { } \ No newline at end of file +public interface UserRepository extends MongoRepository { +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/service/DiscordService.java b/src/main/java/cc/fascinated/bat/service/DiscordService.java index 58b2871..fa0d4c8 100644 --- a/src/main/java/cc/fascinated/bat/service/DiscordService.java +++ b/src/main/java/cc/fascinated/bat/service/DiscordService.java @@ -17,7 +17,8 @@ import java.util.List; /** * @author Fascinated (fascinated7) */ -@Service @Getter +@Service +@Getter public class DiscordService { /** * The JDA instance diff --git a/src/main/java/cc/fascinated/bat/service/EventService.java b/src/main/java/cc/fascinated/bat/service/EventService.java index 4e80322..77a104c 100644 --- a/src/main/java/cc/fascinated/bat/service/EventService.java +++ b/src/main/java/cc/fascinated/bat/service/EventService.java @@ -7,6 +7,8 @@ import lombok.NonNull; import lombok.extern.log4j.Log4j2; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; 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.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; @@ -22,16 +24,16 @@ import java.util.Set; /** * @author Fascinated (fascinated7) */ -@Service @Log4j2 +@Service +@Log4j2 @DependsOn("discordService") public class EventService extends ListenerAdapter { - private final GuildService guildService; - private final UserService userService; - /** * The list of listeners registered */ public static final Set LISTENERS = new HashSet<>(); + private final GuildService guildService; + private final UserService userService; @Autowired 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); } } + + @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); + } + } } diff --git a/src/main/java/cc/fascinated/bat/service/FeatureService.java b/src/main/java/cc/fascinated/bat/service/FeatureService.java index 2a82066..4eb7627 100644 --- a/src/main/java/cc/fascinated/bat/service/FeatureService.java +++ b/src/main/java/cc/fascinated/bat/service/FeatureService.java @@ -16,7 +16,9 @@ import java.util.List; /** * @author Fascinated (fascinated7) */ -@Service @Getter @Log4j2 +@Service +@Getter +@Log4j2 @DependsOn("commandService") public class FeatureService { /** diff --git a/src/main/java/cc/fascinated/bat/service/GuildService.java b/src/main/java/cc/fascinated/bat/service/GuildService.java index 2ae2132..e239341 100644 --- a/src/main/java/cc/fascinated/bat/service/GuildService.java +++ b/src/main/java/cc/fascinated/bat/service/GuildService.java @@ -20,7 +20,9 @@ import java.util.concurrent.TimeUnit; /** * @author Fascinated (fascinated7) */ -@Service @Log4j2 @Getter +@Service +@Log4j2 +@Getter @DependsOn("discordService") public class GuildService extends ListenerAdapter { /** diff --git a/src/main/java/cc/fascinated/bat/service/ScoreSaberService.java b/src/main/java/cc/fascinated/bat/service/ScoreSaberService.java index c4c1635..f2ea628 100644 --- a/src/main/java/cc/fascinated/bat/service/ScoreSaberService.java +++ b/src/main/java/cc/fascinated/bat/service/ScoreSaberService.java @@ -27,7 +27,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -@Service @Log4j2(topic = "ScoreSaber Service") +@Service +@Log4j2(topic = "ScoreSaber Service") 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"; @@ -50,7 +51,7 @@ public class ScoreSaberService extends TextWebSocketHandler { * * @param id The id of the account. * @return The account. - * @throws ResourceNotFoundException If the account is not found. + * @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) { @@ -77,7 +78,7 @@ public class ScoreSaberService extends TextWebSocketHandler { * Gets the scores for the account. * * @param profile The profile. - * @param page The page to get the scores from. + * @param page The page to get the scores from. * @return The scores. */ public ScoreSaberScoresPageToken getPageScores(UserScoreSaberProfile profile, int page) { @@ -131,7 +132,8 @@ public class ScoreSaberService extends TextWebSocketHandler { connectWebSocket(); // Reconnect to the WebSocket. } - @Override @SneakyThrows + @Override + @SneakyThrows protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) { // Ignore the connection message. if (message.getPayload().equals("Connected to the ScoreSaber WSS")) { diff --git a/src/main/java/cc/fascinated/bat/service/SpotifyService.java b/src/main/java/cc/fascinated/bat/service/SpotifyService.java new file mode 100644 index 0000000..6ae5544 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/service/SpotifyService.java @@ -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 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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/service/UserService.java b/src/main/java/cc/fascinated/bat/service/UserService.java index 1297c7d..1a66c7d 100644 --- a/src/main/java/cc/fascinated/bat/service/UserService.java +++ b/src/main/java/cc/fascinated/bat/service/UserService.java @@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Service; -import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -18,7 +17,9 @@ import java.util.concurrent.TimeUnit; /** * @author Fascinated (fascinated7) */ -@Service @Log4j2 @Getter +@Service +@Log4j2 +@Getter @DependsOn("discordService") public class UserService { /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a75ff70..dbae21a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,11 +1,14 @@ +# Discord Configuration discord: token: "oh my goodnesssssssssss" +# Spotify Configuration +spotify: + client-id: "spotify-client-id" + client-secret: "spotify-client-secret" + # Spring Configuration spring: - # Disable the Spring Web Server - main: - web-application-type: none data: # MongoDB Configuration mongodb: