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>
<version>0.5.11</version>
</dependency>
<dependency>
<groupId>se.michaelthelin.spotify</groupId>
<artifactId>spotify-web-api-java</artifactId>
<version>8.4.0</version>
</dependency>
<!-- Test Dependencies -->
<dependency>

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

@ -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

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

@ -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

@ -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);

@ -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()) +
"➜ 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())
"➜ 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();
}
}

@ -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;

@ -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,8 +25,8 @@ 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", "<t:%d>".formatted(premium.getActivatedAt().toInstant().toEpochMilli()/1000), true);
embed.addField("Expires At", premium.isInfinite() ? "Never" : "<t:%d>"
embed.addField("Started", "<t:%d>".formatted(premium.getActivatedAt().toInstant().toEpochMilli() / 1000), true);
embed.addField("Expires", premium.isInfinite() ? "Never" : "<t:%d>"
.formatted(premium.getExpiresAt().toInstant().toEpochMilli() / 1000), true);
} else {
embed.setDescription("The guild does not have premium");

@ -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

@ -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();

@ -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
*/
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
* @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);
}
@ -42,7 +42,7 @@ public final class TimeUtils {
* @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);
}
@ -55,14 +55,14 @@ public final class TimeUtils {
* @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;
}
@ -89,14 +89,14 @@ 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();
}
@ -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;
}

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

@ -8,4 +8,5 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@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 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;
@ -23,7 +25,8 @@ public interface EventListener {
* @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
@ -31,7 +34,8 @@ public interface EventListener {
* @param guild the guild the user joined
* @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
@ -39,7 +43,8 @@ public interface EventListener {
* @param guild the guild the user left
* @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
@ -47,7 +52,8 @@ public interface EventListener {
* @param guild the guild that the message was sent in
* @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
@ -55,5 +61,24 @@ public interface EventListener {
* @param guild the guild that the string was selected in
* @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
@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
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException { }
public class ResourceNotFoundException extends RuntimeException {
}

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

@ -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();

@ -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;
}
}

@ -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;
}
}

@ -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;

@ -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;

@ -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;
}

@ -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)))

@ -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);
}
}

@ -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;
}

@ -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

@ -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

@ -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

@ -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.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

@ -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
*

@ -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.

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

@ -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.

@ -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.

@ -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.

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

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

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

@ -6,4 +6,5 @@ import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @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)
*/
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)
*/
@Service @Getter
@Service
@Getter
public class DiscordService {
/**
* The JDA instance

@ -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<EventListener> 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);
}
}
}

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

@ -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 {
/**

@ -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";
@ -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")) {

@ -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.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 {
/**

@ -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: