Compare commits

...

41 Commits

Author SHA1 Message Date
Lee
87bf0b9f67 Merge pull request 'Configure Renovate' (#3) from renovate/configure into master
Reviewed-on: Fascinated/Bat#3
2024-07-01 15:01:23 +00:00
419bdf1fea Add renovate.json 2024-07-01 15:00:36 +00:00
f2b2dbc794 rename interaction to event 2024-07-01 15:27:39 +01:00
8361f3c784 update member count command to look similar to botstats command 2024-07-01 15:23:19 +01:00
52349a17c3 cleanup imports 2024-07-01 15:21:09 +01:00
c1f9bfec6a don't remove birthdays for members that have left 2024-07-01 15:20:49 +01:00
be7f8a9057 only check guilds that we're in 2024-07-01 15:16:06 +01:00
c93e112ebf might fix, who knows 2024-07-01 15:14:42 +01:00
7485bd2ec8 fix premium admin command 2024-07-01 01:46:56 +01:00
ed83175a39 add env for admin guild 2024-07-01 01:44:22 +01:00
a001f2dd4c add valid guild and user id check 2024-07-01 01:41:40 +01:00
b1785ce373 impl pre shutdown saving 2024-07-01 01:33:52 +01:00
b1f5db9b2d maybe fix this? 2024-07-01 01:26:10 +01:00
d372c41c98 big ass refactor to handle loading guilds and users without spring to make it more futureproof 2024-07-01 01:12:32 +01:00
f566c3bcb5 add emojis to feature states 2024-06-30 08:34:59 +01:00
6403c57db5 add checks for some events to see if the feature is enabled and more cleanup 2024-06-30 08:24:14 +01:00
22d4558d84 use an enum for feature states 2024-06-30 08:10:49 +01:00
ea546f02ca cleanup features and move all misc commands into a base feature 2024-06-30 08:00:03 +01:00
5b1ddb145f make feature command less ugly and update feature disabled message 2024-06-30 07:41:31 +01:00
5ce5ef6898 format numbers on botstats command 2024-06-30 07:34:49 +01:00
729e0b482b update number formatter 2024-06-30 07:34:03 +01:00
5aa56c2955 rename feature command 2024-06-30 05:16:00 +01:00
ee6456e4d8 add feature toggling 2024-06-30 05:15:37 +01:00
93350f1506 remove useless config option 2024-06-30 04:16:49 +01:00
702aead53a cleanup dev mode 2024-06-30 04:13:54 +01:00
b7f2b6a3d7 fix birthday date validation 2024-06-30 03:47:34 +01:00
b66114503c fix npe 2024-06-30 03:39:38 +01:00
50391e5344 add name history tracking 2024-06-30 03:36:00 +01:00
91ecc9882c cleanup 2024-06-30 02:36:17 +01:00
06a2584e63 fix migrations 2024-06-30 01:03:10 +01:00
29affe2f12 update birthdays to have a view command and a private command to be able to hide your birthday 2024-06-30 00:35:52 +01:00
86c7afac42 many changes 2024-06-29 22:38:53 +01:00
b0949d17e6 add skip spotify command and show song when pausing and resuming the song 2024-06-29 21:36:45 +01:00
df44ae90b9 update member count command 2024-06-29 18:35:53 +01:00
4821e2a4fa add emojis to the spotify commands 2024-06-29 16:54:39 +01:00
4cb34fbb9a add duck image command 2024-06-29 13:23:12 +01:00
d824f957fe add vote command 2024-06-29 13:23:05 +01:00
d2d898a5b8 disable spotify linking (until they accept out application) 2024-06-29 13:08:13 +01:00
320eab34a3 Merge remote-tracking branch 'origin/master' 2024-06-29 12:45:35 +01:00
433dfb4693 make linking and unlinking only viewable to the user for spotify commands 2024-06-29 12:45:16 +01:00
Lee
71158fd477 Merge pull request 'Fix NPE caused by default avatars + make MemberCountCommand message consistent' (#2) from okNick/Bat:master into master
Reviewed-on: Fascinated/Bat#2
2024-06-28 20:38:26 +00:00
125 changed files with 3054 additions and 1055 deletions

23
pom.xml

@ -85,6 +85,29 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.10.0</version>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-bom</artifactId>
<version>5.2.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-springboot-v3</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongodb-springdata-v4-driver</artifactId>
<version>5.2.4</version>
</dependency>
<!-- Libraries -->
<dependency>

6
renovate.json Normal file

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>Fascinated/renovate-config"
]
}

@ -1,7 +1,12 @@
package cc.fascinated.bat;
import cc.fascinated.bat.config.Config;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.service.EventService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.mongock.runner.springboot.EnableMongock;
import jakarta.annotation.PreDestroy;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
@ -15,7 +20,8 @@ import java.nio.file.StandardCopyOption;
import java.util.Objects;
@EnableScheduling
@SpringBootApplication
@SpringBootApplication(scanBasePackages = "cc.fascinated.bat")
@EnableMongock
@Log4j2(topic = "Bat")
public class BatApplication {
public static Gson GSON = new GsonBuilder().create();
@ -35,5 +41,15 @@ public class BatApplication {
// Start the app
SpringApplication.run(BatApplication.class, args);
log.info("APP IS RUNNING IN %s MODE!!!!!!!!!".formatted(Config.isProduction() ? "PRODUCTION" : "DEVELOPMENT"));
}
@PreDestroy
public void onShutdown() {
log.info("Shutting down...");
for (EventListener listener : EventService.LISTENERS) {
listener.onSpringShutdown();
}
}
}

@ -8,4 +8,10 @@ public class Consts {
public static final String SUPPORT_INVITE_URL = "https://discord.gg/invite/yjj2U3ctEG";
public static String BOT_OWNER = "474221560031608833";
public static String ADMIN_GUILD = "1203163422498361404";
static {
if (System.getenv("ADMIN_GUILD") != null) {
ADMIN_GUILD = System.getenv("ADMIN_GUILD");
}
}
}

@ -0,0 +1,45 @@
package cc.fascinated.bat;
import cc.fascinated.bat.service.DiscordService;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.emoji.Emoji;
/**
* @author Fascinated (fascinated7)
*/
@Log4j2
public class Emojis {
public static final Emoji SPOTIFY_EMOJI;
public static final Emoji CHECK_MARK_EMOJI;
public static final Emoji CROSS_MARK_EMOJI;
public static final Emoji SAD_FACE_EMOJI;
public static final Emoji PAUSE_EMOJI;
public static final Emoji PLAY_EMOJI;
public static final Emoji SKIP_EMOJI;
/**
* Presence Status Emojis
*/
public static final Emoji ONLINE_EMOJI;
public static final Emoji IDLE_EMOJI;
public static final Emoji DND_EMOJI;
public static final Emoji OFFLINE_EMOJI;
static {
log.info("Loading emojis...");
JDA jda = DiscordService.JDA;
SPOTIFY_EMOJI = jda.getEmojiById("1256629771975266479");
CHECK_MARK_EMOJI = jda.getEmojiById("1256633734065557676");
CROSS_MARK_EMOJI = jda.getEmojiById("1256634487429922897");
SAD_FACE_EMOJI = jda.getEmojiById("1256636078258131055");
ONLINE_EMOJI = jda.getEmojiById("1256662465668710430");
IDLE_EMOJI = jda.getEmojiById("1256662632203685991");
DND_EMOJI = jda.getEmojiById("1256662572933845032");
OFFLINE_EMOJI = jda.getEmojiById("1256662679402053662");
PAUSE_EMOJI = Emoji.fromUnicode("");
PLAY_EMOJI = Emoji.fromUnicode("");
SKIP_EMOJI = Emoji.fromUnicode("");
log.info("Loaded emojis!");
}
}

@ -1,5 +1,6 @@
package cc.fascinated.bat.command;
import cc.fascinated.bat.features.Feature;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@ -38,6 +39,11 @@ public abstract class BatCommand implements BatCommandExecutor {
*/
private Category category;
/**
* The feature that the command belongs to
*/
private Feature feature;
/**
* Whether the command can only be used by the bot owner
*/

@ -12,20 +12,20 @@ import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
*/
public interface BatCommandExecutor {
/**
* Executes the command using a slash command interaction.
* Executes the command using a slash command event.
*
* @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
* @param event the slash command event
*/
default void execute(
BatGuild guild,
@NonNull BatUser user,
@NonNull MessageChannel channel,
Member member,
@NonNull SlashCommandInteraction interaction
@NonNull SlashCommandInteraction event
) {
}
}

@ -1,29 +0,0 @@
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.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
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.stereotype.Component;
/**
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "membercount", description = "View the member count of the server!")
public class MemberCountCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Member Count");
Guild discordGuild = guild.getDiscordGuild();
embed.setDescription("**%s** has a total of %s members.".formatted(discordGuild.getName(), discordGuild.getMembers().size()));
interaction.replyEmbeds(embed.build()).queue();
}
}

@ -0,0 +1,83 @@
package cc.fascinated.bat.common;
import java.text.DecimalFormat;
import java.util.Locale;
/**
* @author Fascinated (fascinated7)
*/
public class NumberFormatter {
/**
* The suffixes for the numbers
*/
private static final String[] SUFFIXES = new String[] { "K", "M", "B", "T", "Q", "QT", "S", "SP", "O", "N", "D", "UD", "DD", "TD" };
private static final DecimalFormat FORMAT = new DecimalFormat("###.##");
/**
* Format the provided double
*
* @param input the value to format
* @return the formatted double, in the format of xx.xx[suffix]
*/
public static String format(double input) {
if (Double.isNaN(input)) {
return "ERROR";
}
if (Double.isInfinite(input) || input == Double.MAX_VALUE) {
return "";
}
if (1000 > input) {
return FORMAT.format(input);
}
double power = (int) Math.log10(input);
int index = (int) Math.floor(power / 3) - 1;
double factor = input / Math.pow(10, 3 + index * 3);
if (index >= SUFFIXES.length) {
return "ERROR";
}
return FORMAT.format(factor) + SUFFIXES[index];
}
/**
* Format the provided double with commas
*
* @param input the value to format
* @return the formatted double, in the format of xx,xxx,xxx
*/
public static String formatCommas(double input) {
return String.format("%,.0f", input);
}
/**
* Turns a provided string into a double, for example 1M -> 1000000.00
* Accepts decimal and negative values and is not case-sensitive
*
* @param input the string to convert
* @return the value the string represents
*/
public static double fromString(String input) {
if ((input = input.trim()).isEmpty()) {
return -1D;
}
try {
double value = Double.parseDouble(input); // parse pure numbers
if (Double.isNaN(value) || Double.isInfinite(value)) {
return -1;
}
return value;
} catch (NumberFormatException ignored) {
input = input.toUpperCase(Locale.UK);
for (int i = SUFFIXES.length - 1; i > 0; i--) {
String suffix = SUFFIXES[i];
if (!input.endsWith(suffix)) {
continue;
}
String amount = input.substring(0, input.length() - suffix.length());
if (!amount.isEmpty()) {
return Double.parseDouble(amount) * Math.pow(10, 3 + i * 3);
}
}
}
return -1;
}
}

@ -1,27 +0,0 @@
package cc.fascinated.bat.common;
import lombok.experimental.UtilityClass;
import java.text.NumberFormat;
/**
* @author Fascinated (fascinated7)
*/
@UtilityClass
public class NumberUtils {
/**
* Formats a number with commas.
* <p>
* Example: 1000 -> 1,000 | Example: 1000.5 -> 1,000.5
* </p>
*
* @param number the number to format
* @return the formatted number
*/
public static String formatNumberCommas(double number) {
NumberFormat format = NumberFormat.getNumberInstance();
format.setGroupingUsed(true);
format.setMaximumFractionDigits(2);
return format.format(number);
}
}

@ -1,26 +0,0 @@
package cc.fascinated.bat.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public abstract class Profile {
/**
* The key of the profile.
*/
private String profileKey;
public Profile() {
}
/**
* Resets the profile
*/
public abstract void reset();
}

@ -1,6 +1,9 @@
package cc.fascinated.bat.common;
import cc.fascinated.bat.BatApplication;
import lombok.Getter;
import lombok.SneakyThrows;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
@ -9,11 +12,11 @@ import java.util.Map;
* @author Fascinated (fascinated7)
*/
@Getter
public class ProfileHolder {
public abstract class ProfileHolder {
/**
* The profiles for the holder
*/
private Map<String, Profile> profiles;
private final Map<String, Serializable> profiles = new HashMap<>();
/**
* Gets a profile for the holder
@ -22,19 +25,25 @@ public class ProfileHolder {
* @param <T> The type of the profile
* @return The profile
*/
public <T extends Profile> T getProfile(Class<T> clazz) {
if (profiles == null) {
profiles = new HashMap<>();
}
public abstract <T extends Serializable> T getProfile(Class<T> clazz);
Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null);
/**
* Gets the profiles for the holder
* using the provided document
*
* @return the profiles
*/
@SneakyThrows
protected <T extends Serializable> T getProfileFromDocument(Class<T> clazz, Document document) {
Serializable profile = getProfiles().get(clazz.getSimpleName());
if (profile == null) {
try {
profile = clazz.newInstance();
profiles.put(profile.getProfileKey(), profile);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
T newProfile = clazz.cast(clazz.getDeclaredConstructors()[0].newInstance());
org.bson.Document profiles = document.get("profiles", new org.bson.Document());
org.bson.Document profileDocument = profiles.isEmpty() ? new org.bson.Document() : profiles.get(clazz.getSimpleName(), new org.bson.Document());
newProfile.load(profileDocument, BatApplication.GSON);
getProfiles().put(clazz.getSimpleName(), newProfile);
return newProfile;
}
return clazz.cast(profile);
}

@ -0,0 +1,36 @@
package cc.fascinated.bat.common;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Setter
@Getter
@NoArgsConstructor
public abstract class Serializable {
/**
* Load data from the provided document into this profile
*
* @param document the document to load data from
* @param gson the GSON instance to use
*/
public abstract void load(Document document, Gson gson);
/**
* Serialize this profile into a Bson document
*
* @param gson the GSON instance to use
* @return the serialized document
*/
public abstract Document serialize(Gson gson);
/**
* Resets the profile to its default state
*/
public abstract void reset();
}

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

@ -0,0 +1,20 @@
package cc.fascinated.bat.config;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
public class Config {
/**
* Is the app running in a production environment?
*/
@Getter
private static final boolean production;
static {
// Are we running on production?
String appEnv = System.getenv("APP_ENV");
production = appEnv != null && (appEnv.equals("production"));
}
}

@ -27,7 +27,7 @@ public class SpotifyController {
* @return the response entity
*/
@GetMapping(value = "/callback")
public ResponseEntity<String> authorizationCallback(@RequestParam String code) {
public ResponseEntity<String> authorizationCallback(@RequestParam(required = false) String code) {
return ResponseEntity.ok(spotifyService.authorize(code));
}
}

@ -8,10 +8,12 @@ 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.guild.member.update.GuildMemberUpdateNicknameEvent;
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.events.user.update.UserUpdateGlobalNameEvent;
/**
* @author Fascinated (fascinated7)
@ -81,4 +83,31 @@ public interface EventListener {
*/
default void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) {
}
/**
* Called when a user updates their global name
*
* @param user the user that updated their global name
* @param oldName the old global name
* @param newName the new global name
*/
default void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
}
/**
* Called when a user updates their nickname in a guild
*
* @param guild the guild that the user updated their nickname in
* @param user the user that updated their nickname
* @param oldName the old nickname
* @param newName the new nickname
*/
default void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
}
/**
* Called when Spring is shutting down
*/
default void onSpringShutdown() {
}
}

@ -0,0 +1,10 @@
package cc.fascinated.bat.exception;
/**
* @author Fascinated (fascinated7)
*/
public class BatException extends Exception {
public BatException(String message) {
super(message);
}
}

@ -5,7 +5,6 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}

@ -0,0 +1,13 @@
package cc.fascinated.bat.exception.spotify;
import lombok.experimental.StandardException;
/**
* @author Fascinated (fascinated7)
*/
@StandardException
public class SpotifyTokenRefreshException extends RuntimeException {
public SpotifyTokenRefreshException(String message) {
super(message);
}
}

@ -20,6 +20,11 @@ public abstract class Feature {
*/
private final String name;
/**
* The description of the feature
*/
public final boolean canBeDisabled;
/**
* The category of the feature
*/
@ -32,7 +37,11 @@ public abstract class Feature {
* @param command The command to register
*/
public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) {
command.setCategory(category);
// If the command using the default category then set the category to the feature's category
if (command.getCategory() == Category.GENERAL) {
command.setCategory(this.category);
}
command.setFeature(this);
commandService.registerCommand(command);
}
}

@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
@Component
public class AfkFeature extends Feature {
public AfkFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AFK", Category.GENERAL);
super("AFK", true, Category.GENERAL);
registerCommand(commandService, context.getBean(AfkCommand.class));
}

@ -4,10 +4,8 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -15,13 +13,6 @@ import org.springframework.stereotype.Component;
*/
@Component
public class AfkReturnListener implements EventListener {
private final GuildService guildService;
@Autowired
public AfkReturnListener(@NonNull GuildService guildService) {
this.guildService = guildService;
}
@Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
AfkProfile profile = guild.getProfile(AfkProfile.class);
@ -29,7 +20,6 @@ public class AfkReturnListener implements EventListener {
return;
}
profile.removeAfkUser(guild, user.getId());
guildService.saveGuild(guild);
event.getMessage().reply("Welcome back, %s! You are no longer AFK.".formatted(user.getDiscordUser().getAsMention())).queue();
}
}

@ -6,14 +6,12 @@ import cc.fascinated.bat.common.MemberUtils;
import cc.fascinated.bat.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -22,26 +20,21 @@ import org.springframework.stereotype.Component;
@Component
@CommandInfo(name = "afk", description = "Sets your AFK status")
public class AfkCommand extends BatCommand {
private final GuildService guildService;
@Autowired
public AfkCommand(@NonNull GuildService guildService) {
this.guildService = guildService;
public AfkCommand() {
super.addOption(OptionType.STRING, "reason", "The reason for being AFK", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AfkProfile profile = guild.getProfile(AfkProfile.class);
String reason = null;
OptionMapping reasonOption = interaction.getOption("reason");
OptionMapping reasonOption = event.getOption("reason");
if (reasonOption != null) {
reason = reasonOption.getAsString();
}
profile.addAfkUser(guild, member.getId(), reason);
guildService.saveGuild(guild);
interaction.reply("You are now AFK: %s%s".formatted(
event.reply("You are now AFK: %s%s".formatted(
profile.getAfkReason(member.getId()),
MemberUtils.hasPermissionToEdit(guild, user) ? "" :
"\n\n*I do not have enough permissions to edit your user, and therefore cannot update your nickname*"

@ -1,9 +1,12 @@
package cc.fascinated.bat.features.afk.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import com.google.gson.Gson;
import lombok.NoArgsConstructor;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import org.bson.Document;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@ -13,7 +16,8 @@ import java.util.Map;
* @author Fascinated (fascinated7)
*/
@Component
public class AfkProfile extends Profile {
@NoArgsConstructor
public class AfkProfile extends Serializable {
private static final String DEFAULT_REASON = "Away";
/**
@ -98,4 +102,21 @@ public class AfkProfile extends Profile {
public void reset() {
afkUsers = new HashMap<>();
}
@Override
public void load(Document document, Gson gson) {
afkUsers = new HashMap<>();
for (String key : document.keySet()) {
afkUsers.put(key, document.getString(key));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : afkUsers.keySet()) {
document.put(key, afkUsers.get(key));
}
return document;
}
}

@ -16,7 +16,7 @@ import org.springframework.stereotype.Component;
public class AutoRoleFeature extends Feature {
@Autowired
public AutoRoleFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AutoRole", Category.SERVER);
super("Auto Role",true, Category.SERVER);
registerCommand(commandService, context.getBean(AutoRoleCommand.class));
}

@ -4,10 +4,12 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
@ -19,8 +21,20 @@ import java.util.List;
@Component
@Log4j2
public class AutoRoleListener implements EventListener {
private final FeatureService featureService;
@Autowired
public AutoRoleListener(@NonNull FeatureService featureService) {
this.featureService = featureService;
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
AutoRoleFeature autoRoleFeature = featureService.getFeature(AutoRoleFeature.class);
if (!guild.getFeatureProfile().isFeatureEnabled(autoRoleFeature)) { // Check if the feature is enabled
return;
}
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) {
return;

@ -7,7 +7,6 @@ import cc.fascinated.bat.common.RoleUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
@ -24,30 +23,27 @@ import org.springframework.stereotype.Component;
@Component("autoroles:add.sub")
@CommandInfo(name = "add", description = "Adds a role to the auto roles list")
public class AddSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public AddSubCommand(@NonNull GuildService guildService) {
public AddSubCommand() {
super.addOption(OptionType.ROLE, "role", "The role to add", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
// Check if the guild has reached the maximum auto roles count
int maxRoleSlots = AutoRoleProfile.getMaxRoleSlots(guild);
if (profile.getRoleSlotsInUse() >= maxRoleSlots) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The guild can only have a maximum of %d auto roles"
.formatted(maxRoleSlots))
.build()).queue();
return;
}
OptionMapping option = interaction.getOption("role");
OptionMapping option = event.getOption("role");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a role to add")
.build()).queue();
return;
@ -56,7 +52,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the role is already in the auto roles list
if (profile.hasRole(role.getId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The role %s is already in the auto roles list".formatted(role.getAsMention()))
.build()).queue();
return;
@ -64,7 +60,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the bot has permission to give the role
if (!RoleUtils.hasPermissionToGiveRole(guild, guild.getDiscordGuild().getSelfMember(), role)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("I do not have permission to give the role %s".formatted(role.getAsMention()))
.build()).queue();
return;
@ -72,7 +68,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the role is higher than the user adding the role
if (!RoleUtils.hasPermissionToGiveRole(guild, member, role)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You cannot add a role that is higher than you")
.build()).queue();
return;
@ -80,8 +76,7 @@ public class AddSubCommand extends BatSubCommand {
// Add the role to the auto roles list
profile.addRole(role.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have added %s to the auto roles list".formatted(role.getAsMention()))
.build()).queue();
}

@ -6,12 +6,10 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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;
/**
@ -20,20 +18,12 @@ import org.springframework.stereotype.Component;
@Component("autoroles:clear.sub")
@CommandInfo(name = "clear", description = "Clears all auto roles")
public class ClearSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ClearSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
profile.reset();
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully cleared all auto roles")
.build()).queue();
}

@ -20,10 +20,10 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "list", description = "Lists all auto roles")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no auto roles set")
.build()).queue();
return;
@ -41,6 +41,6 @@ public class ListSubCommand extends BatSubCommand {
EmbedBuilder embed = EmbedUtils.genericEmbed();
embed.setAuthor("Auto Role List");
embed.setDescription(roles.toString());
interaction.replyEmbeds(embed.build()).queue();
event.replyEmbeds(embed.build()).queue();
}
}

@ -6,7 +6,6 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
@ -23,20 +22,17 @@ import org.springframework.stereotype.Component;
@Component("autoroles:remove.sub")
@CommandInfo(name = "remove", description = "Removes a role from the auto roles list")
public class RemoveSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public RemoveSubCommand(GuildService guildService) {
public RemoveSubCommand() {
super.addOption(OptionType.ROLE, "role", "The role to remove", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
OptionMapping option = interaction.getOption("role");
OptionMapping option = event.getOption("role");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a role to remove")
.build()).queue();
return;
@ -44,15 +40,14 @@ public class RemoveSubCommand extends BatSubCommand {
Role role = option.getAsRole();
if (!profile.hasRole(role.getId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The role %s is not in the auto roles list".formatted(role.getAsMention()))
.build()).queue();
return;
}
profile.removeRole(role.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the role %s from the auto roles list".formatted(role.getAsMention()))
.build()).queue();
}

@ -1,11 +1,14 @@
package cc.fascinated.bat.features.autorole.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Role;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
@ -15,7 +18,8 @@ import java.util.List;
*/
@Setter
@Getter
public class AutoRoleProfile extends Profile {
@NoArgsConstructor
public class AutoRoleProfile extends Serializable {
private static final int DEFAULT_MAX_ROLES = 10;
private static final int PREMIUM_MAX_ROLES = 25;
@ -24,10 +28,6 @@ public class AutoRoleProfile extends Profile {
*/
private List<String> roleIds;
public AutoRoleProfile() {
super("auto-role");
}
/**
* Gets the maximum amount of roles that can be set in the guild
*
@ -35,7 +35,7 @@ public class AutoRoleProfile extends Profile {
* @return the amount of role slots
*/
public static int getMaxRoleSlots(BatGuild guild) {
if (guild.getPremium().hasPremium()) {
if (guild.getPremiumProfile().hasPremium()) {
return PREMIUM_MAX_ROLES;
}
return DEFAULT_MAX_ROLES;
@ -110,4 +110,16 @@ public class AutoRoleProfile extends Profile {
public void reset() {
roleIds.clear();
}
@Override
public void load(Document document, Gson gson) {
roleIds = document.getList("roleIds", String.class);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("roleIds", roleIds);
return document;
}
}

@ -0,0 +1,43 @@
package cc.fascinated.bat.features.base;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.commands.botadmin.premium.PremiumAdminCommand;
import cc.fascinated.bat.features.base.commands.fun.image.ImageCommand;
import cc.fascinated.bat.features.base.commands.general.*;
import cc.fascinated.bat.features.base.commands.general.avatar.AvatarCommand;
import cc.fascinated.bat.features.base.commands.general.banner.BannerCommand;
import cc.fascinated.bat.features.base.commands.server.MemberCountCommand;
import cc.fascinated.bat.features.base.commands.server.PremiumCommand;
import cc.fascinated.bat.features.base.commands.server.channel.ChannelCommand;
import cc.fascinated.bat.features.base.commands.server.feature.FeatureCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class BaseFeature extends Feature {
@Autowired
public BaseFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Base", false, Category.GENERAL);
super.registerCommand(commandService, context.getBean(PremiumCommand.class));
super.registerCommand(commandService, context.getBean(PremiumAdminCommand.class));
super.registerCommand(commandService, context.getBean(MemberCountCommand.class));
super.registerCommand(commandService, context.getBean(ChannelCommand.class));
super.registerCommand(commandService, context.getBean(VoteCommand.class));
super.registerCommand(commandService, context.getBean(PingCommand.class));
super.registerCommand(commandService, context.getBean(InviteCommand.class));
super.registerCommand(commandService, context.getBean(HelpCommand.class));
super.registerCommand(commandService, context.getBean(BotStatsCommand.class));
super.registerCommand(commandService, context.getBean(BannerCommand.class));
super.registerCommand(commandService, context.getBean(AvatarCommand.class));
super.registerCommand(commandService, context.getBean(ImageCommand.class));
super.registerCommand(commandService, context.getBean(FeatureCommand.class));
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;

@ -1,9 +1,10 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
@ -29,26 +30,25 @@ public class RemoveSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping guildOption = interaction.getOption("guild");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) {
interaction.reply("Please provide a guild id").queue();
event.reply("Please provide a guild id").queue();
return;
}
String guildId = guildOption.getAsString();
BatGuild batGuild = guildService.getGuild(guildId);
if (batGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
BatGuild targetGuild = guildService.getGuild(guildId);
if (targetGuild == null) {
event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return;
}
BatGuild.Premium premium = batGuild.getPremium();
PremiumProfile premium = targetGuild.getPremiumProfile();
if (!premium.hasPremium()) {
interaction.reply("The guild does not have premium").queue();
event.reply("The guild does not have premium").queue();
return;
}
premium.removePremium();
guildService.saveGuild(batGuild);
interaction.reply("The guild **%s** has had its premium removed".formatted(guild.getName())).queue();
event.reply("The guild **%s** has had its premium removed".formatted(targetGuild.getName())).queue();
}
}

@ -1,9 +1,10 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
@ -23,43 +24,42 @@ public class SetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public SetSubCommand(GuildService guildService) {
public SetSubCommand(@NonNull GuildService guildService) {
this.guildService = guildService;
super.addOption(OptionType.STRING, "guild", "The guild id to set as premium", true);
super.addOption(OptionType.BOOLEAN, "infinite", "Whether the premium length should be infinite", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping guildOption = interaction.getOption("guild");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) {
interaction.reply("Please provide a guild id").queue();
event.reply("Please provide a guild id").queue();
return;
}
String guildId = guildOption.getAsString();
OptionMapping infiniteOption = interaction.getOption("infinite");
OptionMapping infiniteOption = event.getOption("infinite");
if (infiniteOption == null) {
interaction.reply("Please provide whether the premium length should be infinite").queue();
event.reply("Please provide whether the premium length should be infinite").queue();
return;
}
boolean infinite = infiniteOption.getAsBoolean();
BatGuild batGuild = guildService.getGuild(guildId);
if (batGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
BatGuild targetGuild = guildService.getGuild(guildId);
if (targetGuild == null) {
event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return;
}
BatGuild.Premium premium = batGuild.getPremium();
PremiumProfile premium = targetGuild.getPremiumProfile();
if (!infinite) {
premium.addTime();
} else {
premium.addInfiniteTime();
}
guildService.saveGuild(batGuild);
if (!infinite) {
interaction.reply("The guild **%s** has been set as premium until <t:%s>".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
event.reply("The guild **%s** has been set as premium until <t:%s>".formatted(targetGuild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
} else {
interaction.reply("The guild **%s** has been set as premium indefinitely".formatted(guild.getName())).queue();
event.reply("The guild **%s** has been set as premium indefinitely".formatted(targetGuild.getName())).queue();
}
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.fun.image;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "cat", description = "Get a random cat image")
public class CatSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
CatImageToken[] responseEntity = WebRequest.getAsEntity("https://api.thecatapi.com/v1/images/search", CatImageToken[].class);
if (responseEntity == null || responseEntity.length == 0) {
interaction.reply("Failed to get a cat image!").queue();
event.reply("Failed to get a cat image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random cat image!")
.setImage(responseEntity[0].getUrl())
.build()).queue();

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.fun.image;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "dog", description = "Get a random dog image")
public class DogSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomImage responseEntity = WebRequest.getAsEntity("https://dog.ceo/api/breeds/image/random", RandomImage.class);
if (responseEntity == null) {
interaction.reply("Failed to get a dog image!").queue();
event.reply("Failed to get a dog image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random dog image!")
.setImage(responseEntity.getMessage())
.build()).queue();

@ -0,0 +1,35 @@
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.WebRequest;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.randomd.RandomDuck;
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.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "duck", description = "Get a random duck image")
public class DuckSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomDuck responseEntity = WebRequest.getAsEntity("https://random-d.uk/api/v2/random", RandomDuck.class);
if (responseEntity == null) {
event.reply("Failed to get a duck image!").queue();
return;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random duck image!")
.setImage(responseEntity.getUrl())
.build()).queue();
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.fun.image;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "fox", description = "Get a random fox image")
public class FoxSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomFoxToken responseEntity = WebRequest.getAsEntity("https://randomfox.ca/floof/", RandomFoxToken.class);
if (responseEntity == null) {
interaction.reply("Failed to get a fox image!").queue();
event.reply("Failed to get a fox image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random fox image!")
.setImage(responseEntity.getImage())
.build()).queue();

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.fun.image;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
@ -19,5 +19,6 @@ public class ImageCommand extends BatCommand {
super.addSubCommand(context.getBean(CatSubCommand.class));
super.addSubCommand(context.getBean(DogSubCommand.class));
super.addSubCommand(context.getBean(FoxSubCommand.class));
super.addSubCommand(context.getBean(DuckSubCommand.class));
}
}

@ -1,8 +1,9 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.TimeUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
@ -37,19 +38,19 @@ public class BotStatsCommand extends BatCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
JDA jda = DiscordService.JDA;
interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
event.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
"**Bot Statistics**\n" +
"➜ Guilds: **%s**\n".formatted(jda.getGuilds().size()) +
"➜ Users: **%s**\n".formatted(jda.getUsers().size()) +
"➜ Guilds: **%s**\n".formatted(NumberFormatter.format(jda.getGuilds().size())) +
"➜ Users: **%s**\n".formatted(NumberFormatter.format(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())
"➜ Cached Guilds: **%s**\n".formatted(NumberFormatter.format(guildService.getGuilds().size())) +
"➜ Cached Users: **%s**".formatted(NumberFormatter.format(userService.getUsers().size()))
).build()).queue();
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.Consts;
import cc.fascinated.bat.command.BatCommand;
@ -45,8 +45,8 @@ public class HelpCommand extends BatCommand implements EventListener {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
interaction.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue();
}
@Override

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.Consts;
import cc.fascinated.bat.command.BatCommand;
@ -19,8 +19,8 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "invite", description = "Invite the bot to your server!", guildOnly = false)
public class InviteCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You can invite the bot to your server by clicking [here](%s)".formatted(Consts.INVITE_URL))
.build())
.setEphemeral(true)

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -18,9 +18,9 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "ping", description = "Gets the ping of the bot", guildOnly = false)
public class PingCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
long time = System.currentTimeMillis();
interaction.reply("Pinging...").queue(response -> {
event.reply("Pinging...").queue(response -> {
response.editOriginal("Gateway response time: `%sms`\nAPI response time `%sms`".formatted(
DiscordService.JDA.getGatewayPing(),
System.currentTimeMillis() - time

@ -0,0 +1,36 @@
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import 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.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "vote", description = "Vote for the bot", guildOnly = false)
public class VoteCommand extends BatCommand {
private static final String[] VOTE_LINKS = new String[]{
"https://top.gg/bot/1254161119975833652/vote"
};
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
StringBuilder builder = new StringBuilder();
builder.append("You can vote for the bot by clicking the following links:\n\n");
for (String link : VOTE_LINKS) {
builder.append("%s\n".formatted(link));
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(builder.toString())
.build()
).queue();
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -19,18 +19,18 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "guild", description = "View the avatar of the guild")
public class GuildSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ImageProxy icon = guild.getDiscordGuild().getIcon();
if (icon == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have an avatar!".formatted(guild.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(guild.getName()), null, guild.getDiscordGuild().getIconUrl())
.setImage(icon.getUrl(4096))
.build()

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the avatar of!")
.build())
.queue();
@ -36,7 +36,7 @@ public class UserSubCommand extends BatSubCommand {
}
User target = userOption.getAsUser();
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(target.getName()), null, target.getEffectiveAvatarUrl())
.setImage(target.getEffectiveAvatarUrl())
.build()

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -19,17 +19,17 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "guild", description = "View the banner of the guild")
public class GuildSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ImageProxy banner = guild.getDiscordGuild().getBanner();
if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(guild.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(guild.getName()))
.setImage(banner.getUrl(512))
.build()

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the banner of!")
.build())
.queue();
@ -39,14 +39,14 @@ public class UserSubCommand extends BatSubCommand {
User target = userOption.getAsUser();
ImageProxy banner = target.retrieveProfile().complete().getBanner();
if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(target.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(target.getName()))
.setImage(banner.getUrl(512))
.build()

@ -0,0 +1,69 @@
package cc.fascinated.bat.features.base.commands.server;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
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.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;
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.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "membercount", description = "View the member count of the server!")
public class MemberCountCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Guild discordGuild = guild.getDiscordGuild();
int totalMembers = 0, totalUsers = 0, totalBots = 0;
Map<OnlineStatus, Integer> memberCounts = new HashMap<>();
for (Member guildMember : discordGuild.getMembers()) {
OnlineStatus status = guildMember.getOnlineStatus();
memberCounts.put(status, memberCounts.getOrDefault(status, 0) + 1);
if (guildMember.getUser().isBot()) {
totalBots++;
} else {
totalUsers++;
}
totalMembers++;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("""
**Member Count**
Total Members: `%s`
Total Users: `%s`
Total Bots: `%s`
\s
**Member Presence**
%s Online: `%s`
%s Idle: `%s`
%s Do Not Disturb: `%s`
%s Offline: `%s`""".formatted(
NumberFormatter.format(totalMembers),
NumberFormatter.format(totalUsers),
NumberFormatter.format(totalBots),
Emojis.ONLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.ONLINE, 0)),
Emojis.IDLE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.IDLE, 0)),
Emojis.DND_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.DO_NOT_DISTURB, 0)),
Emojis.OFFLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.OFFLINE, 0))))
.build()).queue();
}
}

@ -1,10 +1,11 @@
package cc.fascinated.bat.command.impl.server;
package cc.fascinated.bat.features.base.commands.server;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
@ -20,8 +21,8 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "premium", description = "View the premium information for the guild", requiredPermissions = Permission.ADMINISTRATOR)
public class PremiumCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BatGuild.Premium premium = guild.getPremium();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
PremiumProfile premium = guild.getPremiumProfile();
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information");
if (premium.hasPremium()) {
embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true);
@ -31,6 +32,6 @@ public class PremiumCommand extends BatCommand {
} else {
embed.setDescription("The guild does not have premium");
}
interaction.replyEmbeds(embed.build()).queue();
event.replyEmbeds(embed.build()).queue();
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
@ -37,7 +37,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
if (textChannel.getTopic() == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build())
.queue();
@ -45,7 +45,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
textChannel.getManager().setTopic(null).queue();
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the topic of <#%s>".formatted(textChannel.getId()))
.build()
).queue();

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -27,19 +27,19 @@ public class SetTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
return;
}
String topic = interaction.getOption("topic").getAsString();
String topic = event.getOption("topic").getAsString();
if (topic.length() > 1024) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The topic must be 1024 characters or less!")
.build())
.queue();
@ -47,7 +47,7 @@ public class SetTopicSubCommand extends BatSubCommand {
}
textChannel.getManager().setTopic(topic).queue();
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the topic of <#%s> to: \"%s\"".formatted(textChannel.getId(), topic))
.build()
).queue();

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class ViewTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
@ -37,14 +37,14 @@ public class ViewTopicSubCommand extends BatSubCommand {
String topic = textChannel.getTopic();
if (topic == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The topic of <#%s> is: \"%s\"".formatted(textChannel.getId(), topic))
.build()
).queue();

@ -0,0 +1,61 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("feature:disable.sub")
@CommandInfo(name = "disable", description = "Disables a feature")
public class DisableSubCommand extends BatSubCommand {
@Autowired
public DisableSubCommand() {
super.addOption(OptionType.STRING, "feature", "The feature to disable", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
FeatureProfile featureProfile = guild.getFeatureProfile();
OptionMapping featureOption = event.getOption("feature");
if (featureOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a feature to enabled")
.build()).queue();
return;
}
String featureName = featureOption.getAsString();
if (!FeatureService.INSTANCE.isFeature(featureName)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("That feature does not exist")
.build()).queue();
return;
}
Feature feature = FeatureService.INSTANCE.getFeature(featureName);
if (featureProfile.isFeatureDisabled(feature)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The feature `%s` is already disabled".formatted(feature.getName()))
.build()).queue();
return;
}
featureProfile.disableFeature(feature);
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Successfully disabled the `%s` feature".formatted(feature.getName()))
.build()).queue();
}
}

@ -0,0 +1,61 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("feature:enable.sub")
@CommandInfo(name = "enable", description = "Enables a feature")
public class EnableSubCommand extends BatSubCommand {
@Autowired
public EnableSubCommand() {
super.addOption(OptionType.STRING, "feature", "The feature to enable", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
FeatureProfile featureProfile = guild.getFeatureProfile();
OptionMapping featureOption = event.getOption("feature");
if (featureOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a feature to enabled")
.build()).queue();
return;
}
String featureName = featureOption.getAsString();
if (!FeatureService.INSTANCE.isFeature(featureName)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("That feature does not exist")
.build()).queue();
return;
}
Feature feature = FeatureService.INSTANCE.getFeature(featureName);
if (featureProfile.isFeatureEnabled(feature)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The feature `%s` is already enabled".formatted(feature.getName()))
.build()).queue();
return;
}
featureProfile.enableFeature(feature);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully enabled the `%s` feature".formatted(feature.getName()))
.build()).queue();
}
}

@ -0,0 +1,24 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "feature", description = "Configure features in your guild", requiredPermissions = Permission.ADMINISTRATOR, category = Category.SERVER)
public class FeatureCommand extends BatCommand {
@Autowired
public FeatureCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(EnableSubCommand.class));
super.addSubCommand(context.getBean(DisableSubCommand.class));
super.addSubCommand(context.getBean(ListSubCommand.class));
}
}

@ -0,0 +1,39 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("feature:list.sub")
@CommandInfo(name = "list", description = "Lists the features and their states")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
StringBuilder featureStates = new StringBuilder();
for (Feature feature : FeatureService.INSTANCE.getFeaturesSorted()) {
FeatureProfile featureProfile = guild.getFeatureProfile();
featureStates.append("%s `%s`\n".formatted(
featureProfile.getFeatureState(feature).getEmoji(),
feature.getName()
));
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setTitle("Feature List")
.setDescription(featureStates.toString())
.build()
).queue();
}
}

@ -0,0 +1,120 @@
package cc.fascinated.bat.features.base.profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.Feature;
import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@NoArgsConstructor
public class FeatureProfile extends Serializable {
private static final FeatureState DEFAULT_STATE = FeatureState.ENABLED;
/**
* The feature states
*/
private Map<String, FeatureState> featureStates;
/**
* Gets the feature states
*
* @return the feature states
*/
public FeatureState getFeatureState(Feature feature) {
if (feature == null) {
return DEFAULT_STATE;
}
String featureName = feature.getName().toUpperCase();
if (!this.featureStates.containsKey(featureName)) {
this.featureStates.put(featureName, DEFAULT_STATE);
}
return this.featureStates.get(featureName);
}
/**
* Gets whether the feature is enabled
*
* @return the feature state
*/
public boolean isFeatureEnabled(Feature feature) {
return this.getFeatureState(feature) == FeatureState.ENABLED;
}
/**
* Gets whether the feature is disabled
*
* @return the feature state
*/
public boolean isFeatureDisabled(Feature feature) {
return this.getFeatureState(feature) == FeatureState.DISABLED;
}
/**
* Enables the feature
*
* @param feature the feature to enable
*/
public void enableFeature(Feature feature) {
this.setFeatureState(feature, FeatureState.ENABLED);
}
/**
* Disables the feature
*
* @param feature the feature to disable
*/
public void disableFeature(Feature feature) {
this.setFeatureState(feature, FeatureState.DISABLED);
}
/**
* Sets the feature state
*
* @param feature the feature to set the state for
* @param state the state to set
*/
public void setFeatureState(Feature feature, FeatureState state) {
this.featureStates.put(feature.getName().toUpperCase(), state);
}
@Override
public void reset() {
this.featureStates = null;
}
@Override
public void load(Document document, Gson gson) {
this.featureStates = new HashMap<>();
for (String key : document.keySet()) {
this.featureStates.put(key, FeatureState.valueOf(document.getString(key)));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : this.featureStates.keySet()) {
document.put(key, this.featureStates.get(key).name());
}
return document;
}
@AllArgsConstructor @Getter
public enum FeatureState {
ENABLED(":white_check_mark:"),
DISABLED(":x:");
/**
* The emoji for the feature state
*/
private final String emoji;
}
}

@ -1,13 +1,16 @@
package cc.fascinated.bat.features.birthday;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.birthday.command.BirthdayCommand;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@ -16,11 +19,11 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7)
*/
@Component
public class BirthdayFeature extends Feature {
public class BirthdayFeature extends Feature implements EventListener {
private final GuildService guildService;
public BirthdayFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Birthday", Category.UTILITY);
super("Birthday", true, Category.UTILITY);
this.guildService = guildService;
registerCommand(commandService, context.getBean(BirthdayCommand.class));
@ -29,11 +32,15 @@ public class BirthdayFeature extends Feature {
/**
* Check birthdays every day at midnight
*/
@Scheduled(cron = "0 0 0 * * *")
@Scheduled(cron = "0 1 0 * * *")
private void checkBirthdays() {
for (BatGuild guild : guildService.getAllGuilds()) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
profile.checkBirthdays(guild);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
BirthdayProfile profile = batGuild.getBirthdayProfile();
profile.checkBirthdays(batGuild);
}
}
}

@ -0,0 +1,64 @@
package cc.fascinated.bat.features.birthday;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import org.bson.Document;
import java.util.Calendar;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
public class UserBirthday extends Serializable {
/**
* The user's birthday
*/
private Date birthday;
/**
* If the birthday should be hidden
*/
private boolean hidden;
/**
* Calculates the age of the user
*
* @return the age of the user
*/
public int calculateAge() {
Calendar birthdayCalendar = Calendar.getInstance();
birthdayCalendar.setTime(this.getBirthday());
Calendar today = Calendar.getInstance();
int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR);
// Check if the birthday hasn't occurred yet this year
if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
@Override
public void load(Document document, Gson gson) {
this.birthday = document.getDate("birthday");
this.hidden = document.getBoolean("hidden", false);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("birthday", this.birthday);
document.put("hidden", this.hidden);
return document;
}
@Override
public void reset() {
}
}

@ -19,5 +19,7 @@ public class BirthdayCommand extends BatCommand {
super.addSubCommand(context.getBean(RemoveSubCommand.class));
super.addSubCommand(context.getBean(ChannelSubCommand.class));
super.addSubCommand(context.getBean(MessageSubCommand.class));
super.addSubCommand(context.getBean(ViewSubCommand.class));
super.addSubCommand(context.getBean(PrivateSubCommand.class));
}
}

@ -7,7 +7,6 @@ import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
@ -26,26 +25,23 @@ import org.springframework.stereotype.Component;
@Component("birthday:channel.sub")
@CommandInfo(name = "channel", description = "Sets the birthday notification channel", requiredPermissions = Permission.MANAGE_SERVER)
public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ChannelSubCommand(GuildService guildService) {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel birthdays will be sent in", false);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
OptionMapping option = interaction.getOption("channel");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for birthday notifications. Please provide a channel to set the birthday channel to")
.build()).queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current birthday channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
@ -53,16 +49,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the birthday channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}

@ -6,7 +6,6 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
@ -17,10 +16,6 @@ import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
@ -28,22 +23,17 @@ import java.util.Date;
@Component("birthday:message.sub")
@CommandInfo(name = "message", description = "Changes the message that is sent when it is a user's birthday", requiredPermissions = Permission.MANAGE_SERVER)
public class MessageSubCommand extends BatSubCommand {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy");
private final GuildService guildService;
@Autowired
public MessageSubCommand(GuildService guildService) {
public MessageSubCommand() {
super.addOption(OptionType.STRING, "message", "The message that is sent. (Placeholders: {user}, {age})", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
OptionMapping messageOption = interaction.getOption("message");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping messageOption = event.getOption("message");
if (messageOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a message")
.build()).queue();
return;
@ -51,37 +41,21 @@ public class MessageSubCommand extends BatSubCommand {
String message = messageOption.getAsString();
if (message.length() > 2000) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The message must be less than 2000 characters")
.build()).queue();
return;
}
if (!message.contains("{user}") || !message.contains("{age}")) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The message must contain the placeholders {user} and {age}")
.build()).queue();
return;
}
profile.setMessage(message);
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have updated the birthday message!\n\n**Message:** %s".formatted(profile.getBirthdayMessage(user.getDiscordUser())))
.build()).queue();
}
/**
* Parses a birthday from the string
*
* @param birthday the date to parse
* @return the birthday
*/
private Date parseBirthday(String birthday) {
try {
return FORMATTER.parse(birthday);
} catch (ParseException ignored) {
}
return null;
}
}

@ -0,0 +1,56 @@
package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("birthday:private.sub")
@CommandInfo(name = "private", description = "Changes whether your birthday is private or not")
public class PrivateSubCommand extends BatSubCommand {
@Autowired
public PrivateSubCommand() {
super.addOption(OptionType.BOOLEAN, "enabled", "Whether your birthday is private or not", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping enabledOption = event.getOption("enabled");
if (enabledOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide whether your birthday is private or not")
.build()).queue();
return;
}
boolean enabled = enabledOption.getAsBoolean();
UserBirthday birthday = profile.getBirthday(user.getId());
if (birthday == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have not set your birthday yet")
.build()).queue();
return;
}
birthday.setHidden(enabled);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Your birthday privacy settings have been updated\n\n**Private:** " + (enabled ? "Yes" : "No"))
.build()).queue();
}
}

@ -6,12 +6,10 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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;
@ -21,21 +19,12 @@ import org.springframework.stereotype.Component;
@Component("birthday:remove.sub")
@CommandInfo(name = "remove", description = "Remove your birthday from this guild")
public class RemoveSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public RemoveSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
profile.removeBirthday(user.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Your birthday has been removed from this guild")
.build()).queue();
}

@ -3,10 +3,10 @@ package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
@ -28,28 +28,25 @@ import java.util.Date;
@CommandInfo(name = "set", description = "Add your birthday to this guild")
public class SetSubCommand extends BatSubCommand {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy");
private final GuildService guildService;
@Autowired
public SetSubCommand(GuildService guildService) {
public SetSubCommand() {
super.addOption(OptionType.STRING, "birthday", "Your birthday (format: DAY/MONTH/YEAR - 01/05/2004)", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
if (!profile.hasChannelSetup()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Birthdays have not been enabled in this guild. Please ask an administrator to enable them.")
.build()).queue();
return;
}
OptionMapping birthdayOption = interaction.getOption("birthday");
OptionMapping birthdayOption = event.getOption("birthday");
if (birthdayOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a birthday")
.build()).queue();
return;
@ -57,16 +54,20 @@ public class SetSubCommand extends BatSubCommand {
String birthdayString = birthdayOption.getAsString();
Date birthday = parseBirthday(birthdayString);
if (birthday == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid birthday format. Please use the format: DAY/MONTH/YEAR - 01/05/2004")
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("""
Invalid birthday format. Please use the following format:
DAY/MONTH/YEAR - 01/05/2004
""")
.build()).queue();
return;
}
profile.addBirthday(member.getId(), birthday);
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
UserBirthday userBirthday = new UserBirthday();
userBirthday.setBirthday(birthday);
userBirthday.setHidden(false);
profile.addBirthday(member.getId(), userBirthday);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have updated your birthday!")
.build()).queue();
}
@ -79,9 +80,16 @@ public class SetSubCommand extends BatSubCommand {
*/
private Date parseBirthday(String birthday) {
try {
return FORMATTER.parse(birthday);
} catch (ParseException ignored) {
Date date = FORMATTER.parse(birthday);
if (date.after(new Date())) {
return null;
}
if (date.toInstant().toEpochMilli() < 0) {
return null;
}
return date;
} catch (ParseException ignored) {
return null;
}
}
}

@ -0,0 +1,74 @@
package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("birthday:view.sub")
@CommandInfo(name = "view", description = "Add your birthday to this guild")
public class ViewSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public ViewSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the birthday of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
if (!profile.hasChannelSetup()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Birthdays have not been enabled in this guild. Please ask an administrator to enable them.")
.build()).queue();
return;
}
OptionMapping birthdayOption = event.getOption("user");
BatUser target = birthdayOption == null ? user : userService.getUser(birthdayOption.getAsUser().getId());
if (target == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a valid user")
.build()).queue();
return;
}
UserBirthday birthday = profile.getBirthday(target.getId());
if (birthday == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a birthday set".formatted(target.getDiscordUser().getAsMention()))
.build()).queue();
return;
}
if (birthday.isHidden() && !user.getId().equals(target.getId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s has their birthday set to private".formatted(target.getDiscordUser().getAsMention()))
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s was born on <t:%s:D> they are `%s` years old!".formatted(
target.getDiscordUser().getAsMention(), birthday.getBirthday().toInstant().toEpochMilli()/1000,
birthday.calculateAge()
))
.build()).queue();
}
}

@ -1,28 +1,36 @@
package cc.fascinated.bat.features.birthday.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.model.BatGuild;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.*;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
public class BirthdayProfile extends Profile {
@NoArgsConstructor
public class BirthdayProfile extends Serializable {
private static final String DEFAULT_MESSAGE = "Happy Birthday {user} :tada: :birthday: You are now {age} years old!";
/**
* The list of birthdays that are being tracked
*/
private Map<String, Date> birthdays;
private Map<String, UserBirthday> birthdays;
/**
* The channel ID of the birthday feed
@ -34,17 +42,13 @@ public class BirthdayProfile extends Profile {
*/
private String message = DEFAULT_MESSAGE;
public BirthdayProfile() {
super("birthday");
}
/**
* Adds a birthday to be tracked
*
* @param userId the id of the user to track
* @param birthday the birthday of the user
*/
public void addBirthday(String userId, Date birthday) {
public void addBirthday(String userId, UserBirthday birthday) {
if (birthdays == null) {
birthdays = new HashMap<>();
}
@ -69,7 +73,7 @@ public class BirthdayProfile extends Profile {
* @param userId the id of the user
* @return the birthday of the user
*/
public Date getBirthday(String userId) {
public UserBirthday getBirthday(String userId) {
if (birthdays == null) {
birthdays = new HashMap<>();
}
@ -85,33 +89,6 @@ public class BirthdayProfile extends Profile {
return channelId != null;
}
/**
* Calculates the age of a user
*
* @param userId the id of the user
* @return the age of the user
*/
public int calculateAge(String userId) {
Date birthday = getBirthday(userId);
if (birthday == null) {
return 0; // or throw an exception
}
Calendar birthdayCalendar = Calendar.getInstance();
birthdayCalendar.setTime(birthday);
Calendar today = Calendar.getInstance();
int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR);
// Check if the birthday hasn't occurred yet this year
if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
/**
* Validates the profiles configuration
*
@ -122,33 +99,10 @@ public class BirthdayProfile extends Profile {
if (birthdays == null) {
birthdays = new HashMap<>();
}
List<String> toRemove = new ArrayList<>();
Guild discordGuild = guild.getDiscordGuild();
for (Map.Entry<String, Date> entry : birthdays.entrySet()) {
String userId = entry.getKey();
Date birthday = entry.getValue();
if (userId == null || birthday == null) { // this should never happen
continue;
}
// Check if the user is still in the guild, if not remove them
Member member = discordGuild.getMemberById(userId);
if (member == null) {
toRemove.add(userId);
}
}
for (String userId : toRemove) {
birthdays.remove(userId);
}
if (channelId == null) {
return false;
}
if (discordGuild.getTextChannelById(channelId) == null) {
if (guild.getDiscordGuild().getTextChannelById(channelId) == null) {
channelId = null;
return false;
}
@ -170,13 +124,10 @@ public class BirthdayProfile extends Profile {
int todayDay = today.get(Calendar.DAY_OF_MONTH);
int todayMonth = today.get(Calendar.MONTH); // Note: January is 0
Iterator<Map.Entry<String, Date>> iterator = birthdays.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Date> entry = iterator.next();
Date birthday = entry.getValue();
for (Map.Entry<String, UserBirthday> entry : birthdays.entrySet()) {
Date birthday = entry.getValue().getBirthday();
if (birthday == null) {
iterator.remove();
continue;
}
@ -223,7 +174,7 @@ public class BirthdayProfile extends Profile {
public String getBirthdayMessage(User user) {
return message
.replace("{user}", user.getAsMention())
.replace("{age}", String.valueOf(calculateAge(user.getId())));
.replace("{age}", String.valueOf(birthdays.get(user.getId()).calculateAge()));
}
@Override
@ -231,4 +182,27 @@ public class BirthdayProfile extends Profile {
birthdays.clear();
channelId = null;
}
@Override
public void load(Document document, Gson gson) {
birthdays = new HashMap<>();
for (String key : document.keySet()) {
UserBirthday userBirthday = new UserBirthday();
userBirthday.load((Document) document.get(key), gson);
birthdays.put(key, userBirthday);
}
channelId = document.getString("channelId");
message = (String) document.getOrDefault("message", DEFAULT_MESSAGE);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : birthdays.keySet()) {
document.put(key, birthdays.get(key).serialize(gson));
}
document.put("channelId", channelId);
document.put("message", message);
return document;
}
}

@ -0,0 +1,25 @@
package cc.fascinated.bat.features.namehistory;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.namehistory.command.NameHistoryCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class NameHistoryFeature extends Feature {
public static final int NAME_HISTORY_SIZE = 25;
@Autowired
public NameHistoryFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Name History", true,Category.UTILITY);
super.registerCommand(commandService, context.getBean(NameHistoryCommand.class));
}
}

@ -0,0 +1,46 @@
package cc.fascinated.bat.features.namehistory;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class NameHistoryListener implements EventListener {
private final GuildService guildService;
private final FeatureService featureService;
@Autowired
public NameHistoryListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService;
this.featureService = featureService;
}
@Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
NameHistoryProfile profile = user.getNameHistoryProfile();
profile.addName(newName);
}
@Override
public void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName,
@NonNull GuildMemberUpdateNicknameEvent event) {
NameHistoryFeature nameHistoryFeature = featureService.getFeature(NameHistoryFeature.class);
if (!guild.getFeatureProfile().isFeatureEnabled(nameHistoryFeature)) { // Check if the feature is enabled
return;
}
cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile profile = guild.getNameHistoryProfile();
profile.addName(user, newName);
}
}

@ -0,0 +1,42 @@
package cc.fascinated.bat.features.namehistory;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import org.bson.Document;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@Getter @Setter
public class TrackedName extends Serializable {
/**
* The new name of the user
*/
private String name;
/**
* The date the name was changed
*/
private Date changedDate;
@Override
public void load(Document document, Gson gson) {
this.name = document.getString("name");
this.changedDate = document.getDate("changedDate");
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("name", this.name);
document.put("changedDate", this.changedDate);
return document;
}
@Override
public void reset() {}
}

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.namehistory.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("namehistory:guild.sub")
@CommandInfo(name = "guild", description = "View the guild nickname history of a user")
public class GuildSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public GuildSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the name history of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
BatUser target = userOption == null ? user : userService.getUser(userOption.getAsUser().getId());
if (target == null) {
channel.sendMessage("User not found").queue();
return;
}
NameHistoryProfile profile = guild.getNameHistoryProfile();
StringBuilder builder = new StringBuilder();
if (profile.getNameHistory(target).isEmpty()) {
builder.append("%s has no name history".formatted(target.getDiscordUser().getAsMention()));
} else {
for (TrackedName trackedName : profile.getNameHistorySorted(target)) {
builder.append("%s - <t:%s>\n".formatted(
trackedName.getName() == null ? "Removed Nickname" : "`%s`".formatted(trackedName.getName()),
trackedName.getChangedDate().toInstant().toEpochMilli()/1000
));
}
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Nickname History in %s".formatted(target.getName(), guild.getName()))
.setDescription(builder.toString())
.build()
).queue();
}
}

@ -0,0 +1,21 @@
package cc.fascinated.bat.features.namehistory.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 = "namehistory", description = "View the name history of a user")
public class NameHistoryCommand extends BatCommand {
@Autowired
public NameHistoryCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(UserSubCommand.class));
super.addSubCommand(context.getBean(GuildSubCommand.class));
}
}

@ -0,0 +1,59 @@
package cc.fascinated.bat.features.namehistory.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("namehistory:user.sub")
@CommandInfo(name = "user", description = "View the global name history of a user", guildOnly = false)
public class UserSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public UserSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the name history of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
BatUser target = userOption == null ? user : userService.getUser(userOption.getAsUser().getId());
if (target == null) {
channel.sendMessage("User not found").queue();
return;
}
NameHistoryProfile profile = target.getNameHistoryProfile();
StringBuilder builder = new StringBuilder();
if (profile.getNameHistory().isEmpty()) {
builder.append("%s has no name history".formatted(target.getDiscordUser().getAsMention()));
} else {
for (TrackedName trackedName : profile.getNameHistorySorted()) {
builder.append("`%s` - <t:%s>\n".formatted(trackedName.getName(), trackedName.getChangedDate().toInstant().toEpochMilli()/1000));
}
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Global Name History".formatted(target.getName()))
.setDescription(builder.toString())
.build()
).queue();
}
}

@ -0,0 +1,111 @@
package cc.fascinated.bat.features.namehistory.profile.guild;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.namehistory.NameHistoryFeature;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.model.BatUser;
import com.google.gson.Gson;
import lombok.NoArgsConstructor;
import org.bson.Document;
import java.util.*;
/**
* @author Fascinated (fascinated7)
*/
@NoArgsConstructor
public class NameHistoryProfile extends Serializable {
/**
* The name history of the user
*/
private Map<String, List<TrackedName>> nameHistory;
/**
* Gets the name history of the user
*
* @param user the user to get the name history of
* @return the name history of the user
*/
public List<TrackedName> getNameHistory(BatUser user) {
if (this.nameHistory == null) {
this.nameHistory = new HashMap<>();
}
if (!this.nameHistory.containsKey(user.getId())) {
this.nameHistory.put(user.getId(), new LinkedList<>());
}
return this.nameHistory.get(user.getId());
}
/**
* Gets the name history of the user sorted
*
* @param user the user to get the name history of
* @return the name history of the user sorted
*/
public List<TrackedName> getNameHistorySorted(BatUser user) {
return getNameHistory(user).stream().sorted((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate())).toList();
}
/**
* Adds a name to the name history
*
* @param user the user to add the name to
* @param name the name to add
*/
public void addName(BatUser user, String name) {
TrackedName trackedName = new TrackedName();
trackedName.setName(name);
trackedName.setChangedDate(new Date());
getNameHistory(user).add(trackedName);
cleanup();
}
/**
* Cleans up the name history
* <p>
* This will remove any names that are not within
* the size limit of the name history
* </p>
*/
private void cleanup() {
for (String userId : this.nameHistory.keySet()) {
List<TrackedName> trackedNames = this.nameHistory.get(userId);
if (trackedNames.size() > NameHistoryFeature.NAME_HISTORY_SIZE) {
trackedNames.sort((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate()));
trackedNames.subList(NameHistoryFeature.NAME_HISTORY_SIZE, trackedNames.size()).clear();
}
}
}
@Override
public void reset() {
this.nameHistory = null;
}
@Override
public void load(Document document, Gson gson) {
this.nameHistory = new HashMap<>();
for (String key : document.keySet()) {
List<TrackedName> trackedNames = new LinkedList<>();
for (Document trackedNameDocument : (List<Document>) document.get(key)) {
TrackedName trackedName = new TrackedName();
trackedName.load(trackedNameDocument, gson);
trackedNames.add(trackedName);
}
this.nameHistory.put(key, trackedNames);
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : this.nameHistory.keySet()) {
List<Document> trackedNames = new LinkedList<>();
for (TrackedName trackedName : this.nameHistory.get(key)) {
trackedNames.add(trackedName.serialize(gson));
}
document.put(key, trackedNames);
}
return document;
}
}

@ -0,0 +1,106 @@
package cc.fascinated.bat.features.namehistory.profile.user;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.namehistory.NameHistoryFeature;
import cc.fascinated.bat.features.namehistory.TrackedName;
import com.google.gson.Gson;
import lombok.NoArgsConstructor;
import org.bson.Document;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@NoArgsConstructor
public class NameHistoryProfile extends Serializable {
/**
* The name history of the user
*/
private List<TrackedName> nameHistory;
/**
* Gets the name history of the user
*
* @return the name history of the user
*/
public List<TrackedName> getNameHistory() {
if (this.nameHistory == null) {
this.nameHistory = new LinkedList<>();
}
return nameHistory;
}
/**
* Gets the name history of the user sorted
*
* @return the name history of the user sorted
*/
public List<TrackedName> getNameHistorySorted() {
return getNameHistory().stream().sorted((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate())).toList();
}
/**
* Adds a name to the name history
*
* @param name the name to add
*/
public void addName(String name) {
if (this.nameHistory == null) {
this.nameHistory = new LinkedList<>();
}
TrackedName trackedName = new TrackedName();
trackedName.setName(name);
trackedName.setChangedDate(new Date());
this.nameHistory.add(trackedName);
cleanup();
}
/**
* Cleans up the name history
* <p>
* This will remove any names that are not within
* the size limit of the name history
* </p>
*/
private void cleanup() {
if (this.nameHistory.size() > NameHistoryFeature.NAME_HISTORY_SIZE) {
List<TrackedName> trackedNames = new ArrayList<>(this.nameHistory);
trackedNames.sort((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate()));
this.nameHistory = trackedNames.subList(0, NameHistoryFeature.NAME_HISTORY_SIZE);
}
}
@Override
public void reset() {
this.nameHistory = null;
}
@Override
public void load(Document document, Gson gson) {
this.nameHistory = new LinkedList<>();
for (Document trackedNameDocument : document.getList("nameHistory", Document.class, new ArrayList<>())) {
TrackedName trackedName = new TrackedName();
trackedName.load(trackedNameDocument, gson);
this.nameHistory.add(trackedName);
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
List<Document> trackedNames = new ArrayList<>();
for (TrackedName trackedName : this.nameHistory) {
Document trackedNameDocument = new Document();
trackedNameDocument.put("name", trackedName.getName());
trackedNameDocument.put("changedDate", trackedName.getChangedDate());
trackedNames.add(trackedNameDocument);
}
document.put("nameHistory", trackedNames);
return document;
}
}

@ -1,14 +1,16 @@
package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.common.NumberUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.scoresaber.profile.GuildNumberOneScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@ -23,10 +25,12 @@ import org.springframework.stereotype.Component;
@Log4j2
public class NumberOneScoreFeedListener implements EventListener {
private final GuildService guildService;
private final FeatureService featureService;
@Autowired
public NumberOneScoreFeedListener(GuildService guildService) {
public NumberOneScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService;
this.featureService = featureService;
}
@Override
@ -41,7 +45,7 @@ public class NumberOneScoreFeedListener implements EventListener {
log.info("A new #1 score has been set by {} on {} ({})!",
player.getName(),
leaderboard.getSongName(),
"%s⭐".formatted(NumberUtils.formatNumberCommas(leaderboard.getStars()))
"%s⭐".formatted(NumberFormatter.formatCommas(leaderboard.getStars()))
);
for (Guild guild : DiscordService.JDA.getGuilds()) {
@ -49,16 +53,20 @@ public class NumberOneScoreFeedListener implements EventListener {
if (batGuild == null) {
continue;
}
GuildNumberOneScoreFeedProfile profile = batGuild.getProfile(GuildNumberOneScoreFeedProfile.class);
ScoreSaberFeature scoreSaberFeature = featureService.getFeature(ScoreSaberFeature.class);
if (!batGuild.getFeatureProfile().isFeatureEnabled(scoreSaberFeature)) { // Check if the feature is enabled
return;
}
NumberOneScoreFeedProfile profile = batGuild.getProfile(NumberOneScoreFeedProfile.class);
if (profile == null || profile.getChannelId() == null) {
continue;
}
TextChannel channel = profile.getAsTextChannel();
TextChannel channel = profile.getTextChannel();
if (channel == null) {
log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId());
profile.setChannelId(null);
guildService.saveGuild(batGuild);
continue;
}
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue();

@ -3,7 +3,7 @@ package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.DateUtils;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.ScoreSaberUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.scoresaber.command.numberone.NumberOneFeedCommand;
@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
public class ScoreSaberFeature extends Feature {
@Autowired
public ScoreSaberFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("ScoreSaber", Category.BEAT_SABER);
super("ScoreSaber", true, Category.BEAT_SABER);
registerCommand(commandService, context.getBean(ScoreSaberCommand.class));
registerCommand(commandService, context.getBean(UserFeedCommand.class));
@ -55,10 +55,10 @@ public class ScoreSaberFeature extends Feature {
);
String accuracy = leaderboardToken.getMaxScore() == 0 ? "N/A" :
String.format("%s%%", NumberUtils.formatNumberCommas(((double) scoreToken.getBaseScore() / leaderboardToken.getMaxScore()) * 100));
String.format("%s%%", NumberFormatter.formatCommas(((double) scoreToken.getBaseScore() / leaderboardToken.getMaxScore()) * 100));
String rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberUtils.formatNumberCommas(scoreToken.getPp());
String rank = String.format("#%s", NumberUtils.formatNumberCommas(scoreToken.getRank()));
String rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberFormatter.formatCommas(scoreToken.getPp());
String rank = String.format("#%s", NumberFormatter.formatCommas(scoreToken.getRank()));
String misses = String.format("%s", scoreToken.getMissedNotes());
String badCuts = String.format("%s", scoreToken.getBadCuts());
String maxCombo = String.format("%s %s",

@ -1,13 +1,15 @@
package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@ -22,10 +24,12 @@ import org.springframework.stereotype.Component;
@Log4j2
public class UserScoreFeedListener implements EventListener {
private final GuildService guildService;
private final FeatureService featureService;
@Autowired
public UserScoreFeedListener(GuildService guildService) {
public UserScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService;
this.featureService = featureService;
}
@Override
@ -36,16 +40,19 @@ public class UserScoreFeedListener implements EventListener {
if (batGuild == null) {
continue;
}
GuildUserScoreFeedProfile profile = batGuild.getProfile(GuildUserScoreFeedProfile.class);
ScoreSaberFeature scoreSaberFeature = featureService.getFeature(ScoreSaberFeature.class);
if (!batGuild.getFeatureProfile().isFeatureEnabled(scoreSaberFeature)) { // Check if the feature is enabled
return;
}
UserScoreFeedProfile profile = batGuild.getProfile(UserScoreFeedProfile.class);
if (profile == null || profile.getChannelId() == null || !profile.getTrackedUsers().contains(player.getId())) {
continue;
}
TextChannel channel = profile.getAsTextChannel();
TextChannel channel = profile.getTextChannel();
if (channel == null) {
log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId());
profile.setChannelId(null);
guildService.saveGuild(batGuild);
continue;
}
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue();

@ -4,10 +4,9 @@ import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildNumberOneScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
@ -25,26 +24,23 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-number-one-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ChannelSubCommand(GuildService guildService) {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
GuildNumberOneScoreFeedProfile profile = guild.getProfile(GuildNumberOneScoreFeedProfile.class);
OptionMapping option = interaction.getOption("channel");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.")
.build()).queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
@ -52,16 +48,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}

@ -3,15 +3,13 @@ package cc.fascinated.bat.features.scoresaber.command.numberone;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildNumberOneScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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;
/**
@ -20,20 +18,12 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-number-one-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ResetSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
GuildNumberOneScoreFeedProfile profile = guild.getProfile(GuildNumberOneScoreFeedProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
profile.reset();
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.")
.build()).queue();
}

@ -3,12 +3,10 @@ package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.UserScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
import cc.fascinated.bat.service.ScoreSaberService;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
@ -25,20 +23,18 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "link", description = "Links your ScoreSaber profile")
public class LinkSubCommand extends BatSubCommand {
private final ScoreSaberService scoreSaberService;
private final UserService userService;
@Autowired
public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService) {
public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService) {
super.addOption(OptionType.STRING, "link", "Link your ScoreSaber profile", true);
this.scoreSaberService = scoreSaberService;
this.userService = userService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping option = interaction.getOption("link");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping option = event.getOption("link");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a ScoreSaber profile link")
.build()).queue();
return;
@ -46,7 +42,7 @@ public class LinkSubCommand extends BatSubCommand {
String link = option.getAsString();
if (!link.contains("scoresaber.com/u/")) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link")
.build()).queue();
return;
@ -59,15 +55,14 @@ public class LinkSubCommand extends BatSubCommand {
ScoreSaberAccountToken account = scoreSaberService.getAccount(id);
if (account == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link")
.build()).queue();
return;
}
user.getProfile(UserScoreSaberProfile.class).setSteamId(id);
userService.saveUser(user);
interaction.replyEmbeds(EmbedUtils.successEmbed()
user.getScoreSaberProfile().setAccountId(id);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id)))
.build()).queue();
}

@ -26,7 +26,7 @@ public class MeSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
ScoreSaberCommand.sendProfileEmbed(true, user, scoreSaberService, interaction);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ScoreSaberCommand.sendProfileEmbed(true, user, scoreSaberService, event);
}
}

@ -3,15 +3,13 @@ package cc.fascinated.bat.features.scoresaber.command.scoresaber;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.UserScoreSaberProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -20,20 +18,12 @@ import org.springframework.stereotype.Component;
@Component("scoresaber:reset.sub")
@CommandInfo(name = "reset", description = "Reset your settings")
public class ResetSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public ResetSubCommand(@NonNull UserService userService) {
this.userService = userService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
UserScoreSaberProfile profile = guild.getProfile(UserScoreSaberProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ScoreSaberProfile profile = user.getScoreSaberProfile();
profile.reset();
userService.saveUser(user);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset your settings.")
.build()).queue();
}

@ -5,9 +5,9 @@ import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.Colors;
import cc.fascinated.bat.common.DateUtils;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.exception.RateLimitException;
import cc.fascinated.bat.features.scoresaber.profile.UserScoreSaberProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
@ -49,8 +49,8 @@ public class ScoreSaberCommand extends BatCommand {
* @param interaction The interaction
*/
public static void sendProfileEmbed(boolean isSelf, BatUser user, ScoreSaberService scoreSaberService, SlashCommandInteraction interaction) {
UserScoreSaberProfile profile = user.getProfile(UserScoreSaberProfile.class);
if (profile.getSteamId() == null) {
ScoreSaberProfile profile = user.getScoreSaberProfile();
if (profile.getAccountId() == null) {
if (!isSelf) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a linked ScoreSaber account".formatted(user.getDiscordUser().getAsMention()))
@ -64,7 +64,7 @@ public class ScoreSaberCommand extends BatCommand {
try {
long before = System.currentTimeMillis();
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getSteamId());
ScoreSaberAccountToken account = scoreSaberService.getAccount(profile.getAccountId());
if (account == null) {
if (!isSelf) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
@ -84,9 +84,9 @@ public class ScoreSaberCommand extends BatCommand {
"https://cdn.scoresaber.com/avatars/%s.jpg".formatted(account.getId()))
.addField("Name", account.getName(), true)
.addField("Country", account.getCountry(), true)
.addField("Rank", "#" + NumberUtils.formatNumberCommas(account.getRank()), true)
.addField("Country Rank", "#" + NumberUtils.formatNumberCommas(account.getCountryRank()), true)
.addField("PP", NumberUtils.formatNumberCommas(account.getPp()), true)
.addField("Rank", "#" + NumberFormatter.formatCommas(account.getRank()), true)
.addField("Country Rank", "#" + NumberFormatter.formatCommas(account.getCountryRank()), true)
.addField("PP", NumberFormatter.formatCommas(account.getPp()), true)
.addField("Joined", "<t:%s>".formatted(DateUtils.getDateFromString(account.getFirstSeen()).toInstant().toEpochMilli() / 1000), true)
.setTimestamp(LocalDateTime.now())
.setFooter(fetchTime > 3 ? "Fetched in %sms".formatted(fetchTime) : "Cached", "https://flagcdn.com/h120/%s.png".formatted(account.getCountry().toLowerCase()))
@ -100,7 +100,7 @@ public class ScoreSaberCommand extends BatCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
sendProfileEmbed(true, user, scoreSaberService, interaction);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
sendProfileEmbed(true, user, scoreSaberService, event);
}
}

@ -33,17 +33,17 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping option = interaction.getOption("user");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping option = event.getOption("user");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a user to view the ScoreSaber profile of")
.build()).queue();
return;
}
if (option.getAsUser().isBot()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You cannot view the ScoreSaber profile for a Bot")
.build()).queue();
return;
@ -51,11 +51,11 @@ public class UserSubCommand extends BatSubCommand {
BatUser target = userService.getUser(option.getAsUser().getId());
if (target == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Unknown user")
.build()).queue();
return;
}
ScoreSaberCommand.sendProfileEmbed(false, target, scoreSaberService, interaction);
ScoreSaberCommand.sendProfileEmbed(false, target, scoreSaberService, event);
}
}

@ -4,10 +4,9 @@ import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
@ -25,26 +24,23 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-user-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ChannelSubCommand(GuildService guildService) {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false);
this.guildService = guildService;
}
@Override
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("channel");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.")
.build()).queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
@ -52,16 +48,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}

@ -3,15 +3,13 @@ package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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;
/**
@ -20,20 +18,11 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-user-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ResetSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
profile.reset();
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.")
.build()).queue();
}

@ -3,8 +3,8 @@ package cc.fascinated.bat.features.scoresaber.command.userfeed;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.UserScoreSaberProfile;
import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
@ -36,12 +36,12 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
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");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = event.getOption("user");
if (option == null) {
if (profile.getTrackedUsers().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no users being tracked in the feed")
.build()).queue();
return;
@ -53,7 +53,7 @@ public class UserSubCommand extends BatSubCommand {
"https://scoresaber.com/u/%s".formatted(accountId)
));
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current users being tracked in the feed are:\n%s".formatted(stringBuilder.toString()))
.build()).queue();
return;
@ -61,25 +61,27 @@ public class UserSubCommand extends BatSubCommand {
User target = option.getAsUser();
BatUser targetUser = userService.getUser(target.getId());
UserScoreSaberProfile targetProfile = targetUser.getProfile(UserScoreSaberProfile.class);
if (targetProfile.getSteamId() == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
ScoreSaberProfile targetProfile = targetUser.getScoreSaberProfile();
if (targetProfile.getAccountId() == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The user you are trying to track does not have a linked ScoreSaber profile")
.build()).queue();
return;
}
if (profile.getTrackedUsers().contains(targetProfile.getSteamId())) {
profile.getTrackedUsers().remove(targetProfile.getSteamId());
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed %s from the feed".formatted(target.getAsMention()))
.build()).queue();
boolean added = false;
if (profile.isUserTracked(targetProfile.getAccountId())) {
profile.removeTrackedUser(targetProfile.getAccountId());
} else {
profile.getTrackedUsers().add(targetProfile.getSteamId());
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully added %s to the feed".formatted(target.getAsMention()))
profile.addTrackedUser(targetProfile.getAccountId());
added = true;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully %s %s from the feed".formatted(
added ? "added" : "removed",
target.getAsMention()
))
.build()).queue();
}
guildService.saveGuild(guild);
}
}

@ -1,37 +0,0 @@
package cc.fascinated.bat.features.scoresaber.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.service.DiscordService;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
public class GuildNumberOneScoreFeedProfile extends Profile {
/**
* The channel ID of the score feed
*/
private String channelId;
public GuildNumberOneScoreFeedProfile() {
super("scoresaber-number-one-score-feed");
}
/**
* Gets the channel as a TextChannel
*
* @return the channel as a TextChannel
*/
public TextChannel getAsTextChannel() {
return DiscordService.JDA.getTextChannelById(channelId);
}
@Override
public void reset() {
this.channelId = null;
}
}

@ -1,26 +0,0 @@
package cc.fascinated.bat.features.scoresaber.profile;
import cc.fascinated.bat.common.Profile;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@Setter
@Getter
public class UserScoreSaberProfile extends Profile {
/**
* The Account ID of the ScoreSaber profile
*/
private String steamId;
public UserScoreSaberProfile() {
super("scoresaber");
}
@Override
public void reset() {
this.steamId = null;
}
}

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

@ -1,10 +1,13 @@
package cc.fascinated.bat.features.scoresaber.profile;
package cc.fascinated.bat.features.scoresaber.profile.guild;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
@ -14,7 +17,8 @@ import java.util.List;
*/
@Getter
@Setter
public class GuildUserScoreFeedProfile extends Profile {
@NoArgsConstructor
public class UserScoreFeedProfile extends Serializable {
/**
* The channel ID of the score feed
*/
@ -25,10 +29,6 @@ public class GuildUserScoreFeedProfile extends Profile {
*/
private List<String> trackedUsers;
public GuildUserScoreFeedProfile() {
super("scoresaber-user-score-feed");
}
/**
* Gets the tracked users
*
@ -38,7 +38,20 @@ public class GuildUserScoreFeedProfile extends Profile {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
return this.trackedUsers;
return trackedUsers;
}
/**
* Checks if a user is being tracked
*
* @param userId the user ID to check
* @return if the user is being tracked
*/
public boolean isUserTracked(String userId) {
if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>();
}
return trackedUsers.contains(userId);
}
/**
@ -70,7 +83,7 @@ public class GuildUserScoreFeedProfile extends Profile {
*
* @return the channel as a TextChannel
*/
public TextChannel getAsTextChannel() {
public TextChannel getTextChannel() {
return DiscordService.JDA.getTextChannelById(channelId);
}
@ -79,4 +92,18 @@ public class GuildUserScoreFeedProfile extends Profile {
this.channelId = null;
this.trackedUsers = null;
}
@Override
public void load(Document document, Gson gson) {
this.channelId = document.getString("channelId");
this.trackedUsers = document.getList("trackedUsers", String.class, new ArrayList<>());
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("channelId", this.channelId);
document.put("trackedUsers", this.trackedUsers);
return document;
}
}

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

@ -1,11 +1,26 @@
package cc.fascinated.bat.features.spotify;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.SpotifyUtils;
import cc.fascinated.bat.exception.BatException;
import cc.fascinated.bat.features.Feature;
import net.dv8tion.jda.api.entities.MessageEmbed;
import cc.fascinated.bat.features.spotify.command.SpotifyCommand;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.SneakyThrows;
import net.dv8tion.jda.api.EmbedBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import se.michaelthelin.spotify.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)
@ -13,18 +28,153 @@ import org.springframework.stereotype.Component;
@Component
public class SpotifyFeature extends Feature {
@Autowired
public SpotifyFeature() {
super("Spotify", Category.MUSIC);
public SpotifyFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Spotify", true,Category.MUSIC);
super.registerCommand(commandService, context.getBean(SpotifyCommand.class));
}
/**
* The embed for when a user needs to link their Spotify account.
* Gets the currently playing song.
*
* @return The embed.
* @param spotifyService The Spotify service.
* @param user The user.
*/
public static MessageEmbed linkAccountEmbed() {
@SneakyThrows
public static EmbedBuilder currentSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
throw new BatException("%s You need to link your Spotify account before you can use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
if (!spotifyService.hasTrackPlaying(user)) {
throw new BatException("%s You are not currently playing a track.".formatted(Emojis.CROSS_MARK_EMOJI));
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
AlbumSimplified album = track.getAlbum();
String trackUrl = SpotifyUtils.getTrackUrl(currentlyPlaying);
String albumUrl = "https://open.spotify.com/album/" + album.getId();
StringBuilder artists = new StringBuilder();
for (int i = 0; i < track.getArtists().length; i++) {
artists.append("**[%s](%s)**".formatted(track.getArtists()[i].getName(), "https://open.spotify.com/artist/" + track.getArtists()[i].getId()));
if (i != track.getArtists().length - 1) {
artists.append(", ");
}
}
Image albumCover = album.getImages()[0];
return EmbedUtils.genericEmbed()
.setDescription("You need to link your Spotify account before you can use this command.")
.build();
.setAuthor("Listening to %s | %s".formatted(track.getName(), track.getArtists()[0].getName()), trackUrl)
.setThumbnail(albumCover.getUrl())
.setDescription("""
Song: **[%s](%s)**
Album: **[%s](%s)**
Artist%s: %s
Position: %s
""".formatted(
track.getName(), trackUrl,
album.getName(), albumUrl,
track.getArtists().length > 1 ? "s" : "", artists,
SpotifyUtils.getFormattedTime(currentlyPlaying)
));
}
/**
* Skips the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder skipSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
throw new BatException("%s You need to link your Spotify account before you can use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
if (!spotifyService.hasTrackPlaying(user)) {
throw new BatException("%s You are not currently playing a track.".formatted(Emojis.CROSS_MARK_EMOJI));
}
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
String trackName = track.getName();
spotifyService.skipTrack(user);
CurrentlyPlaying newCurrentlyPlaying = SpotifyUtils.getNewTrack(spotifyService, user, trackName);
if (newCurrentlyPlaying == null) {
return EmbedUtils.errorEmbed()
.setDescription("%s There are no more tracks in the queue.".formatted(Emojis.CROSS_MARK_EMOJI));
}
Track newTrack = (Track) newCurrentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription("""
:track_next: Skipped the track: **[%s | %s](%s)**
%s New Track: **[%s | %s](%s)**
""".formatted(
trackName, track.getArtists()[0].getName(), SpotifyUtils.getTrackUrl(currentlyPlaying),
Emojis.CHECK_MARK_EMOJI, newTrack.getName(), newTrack.getArtists()[0].getName(), SpotifyUtils.getTrackUrl(newCurrentlyPlaying)
));
}
/**
* Pauses the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder pauseSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
throw new BatException("%s You need to link your Spotify account before you can use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
if (!spotifyService.hasTrackPlaying(user)) {
throw new BatException("%s You are not currently playing a track.".formatted(Emojis.CROSS_MARK_EMOJI));
}
boolean didPause = spotifyService.pausePlayback(user);
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription(didPause ? ":pause_button: Paused the track **[%s | %s](%s)**".formatted(
track.getName(),
track.getArtists()[0].getName(),
SpotifyUtils.getTrackUrl(currentlyPlaying))
: "%s The current track is already paused.".formatted(Emojis.CROSS_MARK_EMOJI));
}
/**
* Resumes the current song.
*
* @param spotifyService The Spotify service.
* @param user The user.
*/
@SneakyThrows
public static EmbedBuilder resumeSong(@NonNull SpotifyService spotifyService, @NonNull BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
if (!profile.hasLinkedAccount()) {
throw new BatException("%s You need to link your Spotify account before you can use this command.".formatted(Emojis.CROSS_MARK_EMOJI));
}
if (!spotifyService.hasTrackPlaying(user)) {
throw new BatException("%s You are not currently playing a track.".formatted(Emojis.CROSS_MARK_EMOJI));
}
boolean didResume = spotifyService.resumePlayback(user);
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
return EmbedUtils.successEmbed()
.setDescription(didResume ? ":play_pause: Resumed the track **[%s | %s](%s)**".formatted(
track.getName(),
track.getArtists()[0].getName(),
SpotifyUtils.getTrackUrl(currentlyPlaying))
: "%s The current track is already playing.".formatted(Emojis.CROSS_MARK_EMOJI));
}
}

Some files were not shown because too many files have changed in this diff Show More