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> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
</dependency> </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 --> <!-- Libraries -->
<dependency> <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; 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.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import io.mongock.runner.springboot.EnableMongock;
import jakarta.annotation.PreDestroy;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@ -15,7 +20,8 @@ import java.nio.file.StandardCopyOption;
import java.util.Objects; import java.util.Objects;
@EnableScheduling @EnableScheduling
@SpringBootApplication @SpringBootApplication(scanBasePackages = "cc.fascinated.bat")
@EnableMongock
@Log4j2(topic = "Bat") @Log4j2(topic = "Bat")
public class BatApplication { public class BatApplication {
public static Gson GSON = new GsonBuilder().create(); public static Gson GSON = new GsonBuilder().create();
@ -35,5 +41,15 @@ public class BatApplication {
// Start the app // Start the app
SpringApplication.run(BatApplication.class, args); 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 final String SUPPORT_INVITE_URL = "https://discord.gg/invite/yjj2U3ctEG";
public static String BOT_OWNER = "474221560031608833"; public static String BOT_OWNER = "474221560031608833";
public static String ADMIN_GUILD = "1203163422498361404"; 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; package cc.fascinated.bat.command;
import cc.fascinated.bat.features.Feature;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
@ -38,6 +39,11 @@ public abstract class BatCommand implements BatCommandExecutor {
*/ */
private Category category; private Category category;
/**
* The feature that the command belongs to
*/
private Feature feature;
/** /**
* Whether the command can only be used by the bot owner * 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 { 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 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 user the bat user that executed the command
* @param channel the channel the command was executed in * @param channel the channel the command was executed in
* @param member the member that executed the command * @param member the member that executed the command
* @param interaction the slash command interaction * @param event the slash command event
*/ */
default void execute( default void execute(
BatGuild guild, BatGuild guild,
@NonNull BatUser user, @NonNull BatUser user,
@NonNull MessageChannel channel, @NonNull MessageChannel channel,
Member member, 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; package cc.fascinated.bat.common;
import cc.fascinated.bat.BatApplication;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows;
import org.bson.Document;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -9,11 +12,11 @@ import java.util.Map;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Getter @Getter
public class ProfileHolder { public abstract class ProfileHolder {
/** /**
* The profiles for the holder * The profiles for the holder
*/ */
private Map<String, Profile> profiles; private final Map<String, Serializable> profiles = new HashMap<>();
/** /**
* Gets a profile for the holder * Gets a profile for the holder
@ -22,19 +25,25 @@ public class ProfileHolder {
* @param <T> The type of the profile * @param <T> The type of the profile
* @return The profile * @return The profile
*/ */
public <T extends Profile> T getProfile(Class<T> clazz) { public abstract <T extends Serializable> T getProfile(Class<T> clazz);
if (profiles == null) {
profiles = new HashMap<>();
}
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) { if (profile == null) {
try { T newProfile = clazz.cast(clazz.getDeclaredConstructors()[0].newInstance());
profile = clazz.newInstance(); org.bson.Document profiles = document.get("profiles", new org.bson.Document());
profiles.put(profile.getProfileKey(), profile); org.bson.Document profileDocument = profiles.isEmpty() ? new org.bson.Document() : profiles.get(clazz.getSimpleName(), new org.bson.Document());
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace(); newProfile.load(profileDocument, BatApplication.GSON);
} getProfiles().put(clazz.getSimpleName(), newProfile);
return newProfile;
} }
return clazz.cast(profile); 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 * @return the response entity
*/ */
@GetMapping(value = "/callback") @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)); return ResponseEntity.ok(spotifyService.authorize(code));
} }
} }

@ -8,10 +8,12 @@ import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; 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.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
@ -81,4 +83,31 @@ public interface EventListener {
*/ */
default void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) { 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) @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public class RateLimitException extends RuntimeException { public class RateLimitException extends RuntimeException {
public RateLimitException(String message) { public RateLimitException(String message) {
super(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; private final String name;
/**
* The description of the feature
*/
public final boolean canBeDisabled;
/** /**
* The category of the feature * The category of the feature
*/ */
@ -32,7 +37,11 @@ public abstract class Feature {
* @param command The command to register * @param command The command to register
*/ */
public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) { 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); commandService.registerCommand(command);
} }
} }

@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AfkFeature extends Feature { public class AfkFeature extends Feature {
public AfkFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { public AfkFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AFK", Category.GENERAL); super("AFK", true, Category.GENERAL);
registerCommand(commandService, context.getBean(AfkCommand.class)); 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.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -15,13 +13,6 @@ import org.springframework.stereotype.Component;
*/ */
@Component @Component
public class AfkReturnListener implements EventListener { public class AfkReturnListener implements EventListener {
private final GuildService guildService;
@Autowired
public AfkReturnListener(@NonNull GuildService guildService) {
this.guildService = guildService;
}
@Override @Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) { public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
AfkProfile profile = guild.getProfile(AfkProfile.class); AfkProfile profile = guild.getProfile(AfkProfile.class);
@ -29,7 +20,6 @@ public class AfkReturnListener implements EventListener {
return; return;
} }
profile.removeAfkUser(guild, user.getId()); profile.removeAfkUser(guild, user.getId());
guildService.saveGuild(guild);
event.getMessage().reply("Welcome back, %s! You are no longer AFK.".formatted(user.getDiscordUser().getAsMention())).queue(); 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.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -22,26 +20,21 @@ import org.springframework.stereotype.Component;
@Component @Component
@CommandInfo(name = "afk", description = "Sets your AFK status") @CommandInfo(name = "afk", description = "Sets your AFK status")
public class AfkCommand extends BatCommand { public class AfkCommand extends BatCommand {
private final GuildService guildService; public AfkCommand() {
@Autowired
public AfkCommand(@NonNull GuildService guildService) {
this.guildService = guildService;
super.addOption(OptionType.STRING, "reason", "The reason for being AFK", false); super.addOption(OptionType.STRING, "reason", "The reason for being AFK", false);
} }
@Override @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); AfkProfile profile = guild.getProfile(AfkProfile.class);
String reason = null; String reason = null;
OptionMapping reasonOption = interaction.getOption("reason"); OptionMapping reasonOption = event.getOption("reason");
if (reasonOption != null) { if (reasonOption != null) {
reason = reasonOption.getAsString(); reason = reasonOption.getAsString();
} }
profile.addAfkUser(guild, member.getId(), reason); profile.addAfkUser(guild, member.getId(), reason);
guildService.saveGuild(guild); event.reply("You are now AFK: %s%s".formatted(
interaction.reply("You are now AFK: %s%s".formatted(
profile.getAfkReason(member.getId()), profile.getAfkReason(member.getId()),
MemberUtils.hasPermissionToEdit(guild, user) ? "" : MemberUtils.hasPermissionToEdit(guild, user) ? "" :
"\n\n*I do not have enough permissions to edit your user, and therefore cannot update your nickname*" "\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; 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 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.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import org.bson.Document;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
@ -13,7 +16,8 @@ import java.util.Map;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Component
public class AfkProfile extends Profile { @NoArgsConstructor
public class AfkProfile extends Serializable {
private static final String DEFAULT_REASON = "Away"; private static final String DEFAULT_REASON = "Away";
/** /**
@ -98,4 +102,21 @@ public class AfkProfile extends Profile {
public void reset() { public void reset() {
afkUsers = new HashMap<>(); 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 { public class AutoRoleFeature extends Feature {
@Autowired @Autowired
public AutoRoleFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { public AutoRoleFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AutoRole", Category.SERVER); super("Auto Role",true, Category.SERVER);
registerCommand(commandService, context.getBean(AutoRoleCommand.class)); 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.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,8 +21,20 @@ import java.util.List;
@Component @Component
@Log4j2 @Log4j2
public class AutoRoleListener implements EventListener { public class AutoRoleListener implements EventListener {
private final FeatureService featureService;
@Autowired
public AutoRoleListener(@NonNull FeatureService featureService) {
this.featureService = featureService;
}
@Override @Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) { 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); AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) { if (profile.getRoles().isEmpty()) {
return; return;

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

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

@ -20,10 +20,10 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "list", description = "Lists all auto roles") @CommandInfo(name = "list", description = "Lists all auto roles")
public class ListSubCommand extends BatSubCommand { public class ListSubCommand extends BatSubCommand {
@Override @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); AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) { if (profile.getRoles().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no auto roles set") .setDescription("There are no auto roles set")
.build()).queue(); .build()).queue();
return; return;
@ -41,6 +41,6 @@ public class ListSubCommand extends BatSubCommand {
EmbedBuilder embed = EmbedUtils.genericEmbed(); EmbedBuilder embed = EmbedUtils.genericEmbed();
embed.setAuthor("Auto Role List"); embed.setAuthor("Auto Role List");
embed.setDescription(roles.toString()); 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.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
@ -23,20 +22,17 @@ import org.springframework.stereotype.Component;
@Component("autoroles:remove.sub") @Component("autoroles:remove.sub")
@CommandInfo(name = "remove", description = "Removes a role from the auto roles list") @CommandInfo(name = "remove", description = "Removes a role from the auto roles list")
public class RemoveSubCommand extends BatSubCommand { public class RemoveSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired @Autowired
public RemoveSubCommand(GuildService guildService) { public RemoveSubCommand() {
super.addOption(OptionType.ROLE, "role", "The role to remove", true); super.addOption(OptionType.ROLE, "role", "The role to remove", true);
this.guildService = guildService;
} }
@Override @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); AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
OptionMapping option = interaction.getOption("role"); OptionMapping option = event.getOption("role");
if (option == null) { if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a role to remove") .setDescription("Please provide a role to remove")
.build()).queue(); .build()).queue();
return; return;
@ -44,15 +40,14 @@ public class RemoveSubCommand extends BatSubCommand {
Role role = option.getAsRole(); Role role = option.getAsRole();
if (!profile.hasRole(role.getId())) { 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())) .setDescription("The role %s is not in the auto roles list".formatted(role.getAsMention()))
.build()).queue(); .build()).queue();
return; return;
} }
profile.removeRole(role.getId()); profile.removeRole(role.getId());
guildService.saveGuild(guild); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the role %s from the auto roles list".formatted(role.getAsMention())) .setDescription("Successfully removed the role %s from the auto roles list".formatted(role.getAsMention()))
.build()).queue(); .build()).queue();
} }

@ -1,11 +1,14 @@
package cc.fascinated.bat.features.autorole.profile; 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.model.BatGuild;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
import org.bson.Document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -15,7 +18,8 @@ import java.util.List;
*/ */
@Setter @Setter
@Getter @Getter
public class AutoRoleProfile extends Profile { @NoArgsConstructor
public class AutoRoleProfile extends Serializable {
private static final int DEFAULT_MAX_ROLES = 10; private static final int DEFAULT_MAX_ROLES = 10;
private static final int PREMIUM_MAX_ROLES = 25; private static final int PREMIUM_MAX_ROLES = 25;
@ -24,10 +28,6 @@ public class AutoRoleProfile extends Profile {
*/ */
private List<String> roleIds; private List<String> roleIds;
public AutoRoleProfile() {
super("auto-role");
}
/** /**
* Gets the maximum amount of roles that can be set in the guild * 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 * @return the amount of role slots
*/ */
public static int getMaxRoleSlots(BatGuild guild) { public static int getMaxRoleSlots(BatGuild guild) {
if (guild.getPremium().hasPremium()) { if (guild.getPremiumProfile().hasPremium()) {
return PREMIUM_MAX_ROLES; return PREMIUM_MAX_ROLES;
} }
return DEFAULT_MAX_ROLES; return DEFAULT_MAX_ROLES;
@ -110,4 +110,16 @@ public class AutoRoleProfile extends Profile {
public void reset() { public void reset() {
roleIds.clear(); 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.BatCommand;
import cc.fascinated.bat.command.Category; 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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@ -29,26 +30,25 @@ public class RemoveSubCommand extends BatSubCommand {
} }
@Override @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) {
OptionMapping guildOption = interaction.getOption("guild"); OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) { if (guildOption == null) {
interaction.reply("Please provide a guild id").queue(); event.reply("Please provide a guild id").queue();
return; return;
} }
String guildId = guildOption.getAsString(); String guildId = guildOption.getAsString();
BatGuild batGuild = guildService.getGuild(guildId); BatGuild targetGuild = guildService.getGuild(guildId);
if (batGuild == null) { if (targetGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue(); event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return; return;
} }
BatGuild.Premium premium = batGuild.getPremium(); PremiumProfile premium = targetGuild.getPremiumProfile();
if (!premium.hasPremium()) { if (!premium.hasPremium()) {
interaction.reply("The guild does not have premium").queue(); event.reply("The guild does not have premium").queue();
return; return;
} }
premium.removePremium(); premium.removePremium();
guildService.saveGuild(batGuild); event.reply("The guild **%s** has had its premium removed".formatted(targetGuild.getName())).queue();
interaction.reply("The guild **%s** has had its premium removed".formatted(guild.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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@ -23,43 +24,42 @@ public class SetSubCommand extends BatSubCommand {
private final GuildService guildService; private final GuildService guildService;
@Autowired @Autowired
public SetSubCommand(GuildService guildService) { public SetSubCommand(@NonNull GuildService guildService) {
this.guildService = guildService; this.guildService = guildService;
super.addOption(OptionType.STRING, "guild", "The guild id to set as premium", true); 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); super.addOption(OptionType.BOOLEAN, "infinite", "Whether the premium length should be infinite", true);
} }
@Override @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) {
OptionMapping guildOption = interaction.getOption("guild"); OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) { if (guildOption == null) {
interaction.reply("Please provide a guild id").queue(); event.reply("Please provide a guild id").queue();
return; return;
} }
String guildId = guildOption.getAsString(); String guildId = guildOption.getAsString();
OptionMapping infiniteOption = interaction.getOption("infinite"); OptionMapping infiniteOption = event.getOption("infinite");
if (infiniteOption == null) { 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; return;
} }
boolean infinite = infiniteOption.getAsBoolean(); boolean infinite = infiniteOption.getAsBoolean();
BatGuild batGuild = guildService.getGuild(guildId); BatGuild targetGuild = guildService.getGuild(guildId);
if (batGuild == null) { if (targetGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue(); event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return; return;
} }
BatGuild.Premium premium = batGuild.getPremium(); PremiumProfile premium = targetGuild.getPremiumProfile();
if (!infinite) { if (!infinite) {
premium.addTime(); premium.addTime();
} else { } else {
premium.addInfiniteTime(); premium.addInfiniteTime();
} }
guildService.saveGuild(batGuild);
if (!infinite) { if (!infinite) {
interaction.reply("The guild **%s** has been set as premium until <t:%s>".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue(); event.reply("The guild **%s** has been set as premium until <t:%s>".formatted(targetGuild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
} else { } 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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "cat", description = "Get a random cat image") @CommandInfo(name = "cat", description = "Get a random cat image")
public class CatSubCommand extends BatSubCommand { public class CatSubCommand extends BatSubCommand {
@Override @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); CatImageToken[] responseEntity = WebRequest.getAsEntity("https://api.thecatapi.com/v1/images/search", CatImageToken[].class);
if (responseEntity == null || responseEntity.length == 0) { 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; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random cat image!") .setAuthor("Here's a random cat image!")
.setImage(responseEntity[0].getUrl()) .setImage(responseEntity[0].getUrl())
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "dog", description = "Get a random dog image") @CommandInfo(name = "dog", description = "Get a random dog image")
public class DogSubCommand extends BatSubCommand { public class DogSubCommand extends BatSubCommand {
@Override @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); RandomImage responseEntity = WebRequest.getAsEntity("https://dog.ceo/api/breeds/image/random", RandomImage.class);
if (responseEntity == null) { if (responseEntity == null) {
interaction.reply("Failed to get a dog image!").queue(); event.reply("Failed to get a dog image!").queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random dog image!") .setAuthor("Here's a random dog image!")
.setImage(responseEntity.getMessage()) .setImage(responseEntity.getMessage())
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -20,14 +20,14 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "fox", description = "Get a random fox image") @CommandInfo(name = "fox", description = "Get a random fox image")
public class FoxSubCommand extends BatSubCommand { public class FoxSubCommand extends BatSubCommand {
@Override @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); RandomFoxToken responseEntity = WebRequest.getAsEntity("https://randomfox.ca/floof/", RandomFoxToken.class);
if (responseEntity == null) { if (responseEntity == null) {
interaction.reply("Failed to get a fox image!").queue(); event.reply("Failed to get a fox image!").queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random fox image!") .setAuthor("Here's a random fox image!")
.setImage(responseEntity.getImage()) .setImage(responseEntity.getImage())
.build()).queue(); .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.BatCommand;
import cc.fascinated.bat.command.Category; 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(CatSubCommand.class));
super.addSubCommand(context.getBean(DogSubCommand.class)); super.addSubCommand(context.getBean(DogSubCommand.class));
super.addSubCommand(context.getBean(FoxSubCommand.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.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.TimeUtils; import cc.fascinated.bat.common.TimeUtils;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
@ -37,19 +38,19 @@ public class BotStatsCommand extends BatCommand {
} }
@Override @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; JDA jda = DiscordService.JDA;
interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription( event.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
"**Bot Statistics**\n" + "**Bot Statistics**\n" +
"➜ Guilds: **%s**\n".formatted(jda.getGuilds().size()) + "➜ Guilds: **%s**\n".formatted(NumberFormatter.format(jda.getGuilds().size())) +
"➜ Users: **%s**\n".formatted(jda.getUsers().size()) + "➜ Users: **%s**\n".formatted(NumberFormatter.format(jda.getUsers().size())) +
"➜ Gateway Ping: **%sms**\n".formatted(jda.getGatewayPing()) + "➜ Gateway Ping: **%sms**\n".formatted(jda.getGatewayPing()) +
"\n" + "\n" +
"**Bat Statistics**\n" + "**Bat Statistics**\n" +
"➜ Uptime: **%s**\n".formatted(TimeUtils.format(bean.getUptime())) + "➜ Uptime: **%s**\n".formatted(TimeUtils.format(bean.getUptime())) +
"➜ Cached Guilds: **%s**\n".formatted(guildService.getGuilds().size()) + "➜ Cached Guilds: **%s**\n".formatted(NumberFormatter.format(guildService.getGuilds().size())) +
"➜ Cached Users: **%s**".formatted(userService.getUsers().size()) "➜ Cached Users: **%s**".formatted(NumberFormatter.format(userService.getUsers().size()))
).build()).queue(); ).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.Consts;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
@ -45,8 +45,8 @@ public class HelpCommand extends BatCommand implements EventListener {
} }
@Override @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) {
interaction.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue(); event.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue();
} }
@Override @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.Consts;
import cc.fascinated.bat.command.BatCommand; 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) @CommandInfo(name = "invite", description = "Invite the bot to your server!", guildOnly = false)
public class InviteCommand extends BatCommand { public class InviteCommand extends BatCommand {
@Override @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) {
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You can invite the bot to your server by clicking [here](%s)".formatted(Consts.INVITE_URL)) .setDescription("You can invite the bot to your server by clicking [here](%s)".formatted(Consts.INVITE_URL))
.build()) .build())
.setEphemeral(true) .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.BatCommand;
import cc.fascinated.bat.command.CommandInfo; 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) @CommandInfo(name = "ping", description = "Gets the ping of the bot", guildOnly = false)
public class PingCommand extends BatCommand { public class PingCommand extends BatCommand {
@Override @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(); 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( response.editOriginal("Gateway response time: `%sms`\nAPI response time `%sms`".formatted(
DiscordService.JDA.getGatewayPing(), DiscordService.JDA.getGatewayPing(),
System.currentTimeMillis() - time 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.BatCommand;
import cc.fascinated.bat.command.CommandInfo; 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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; 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") @CommandInfo(name = "guild", description = "View the avatar of the guild")
public class GuildSubCommand extends BatSubCommand { public class GuildSubCommand extends BatSubCommand {
@Override @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(); ImageProxy icon = guild.getDiscordGuild().getIcon();
if (icon == null) { if (icon == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have an avatar!".formatted(guild.getName())) .setDescription("**%s** does not have an avatar!".formatted(guild.getName()))
.build()) .build())
.queue(); .queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(guild.getName()), null, guild.getDiscordGuild().getIconUrl()) .setAuthor("%s's Avatar".formatted(guild.getName()), null, guild.getDiscordGuild().getIconUrl())
.setImage(icon.getUrl(4096)) .setImage(icon.getUrl(4096))
.build() .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class UserSubCommand extends BatSubCommand {
} }
@Override @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) {
OptionMapping userOption = interaction.getOption("user"); OptionMapping userOption = event.getOption("user");
if (userOption == null) { if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the avatar of!") .setDescription("You must provide a user to view the avatar of!")
.build()) .build())
.queue(); .queue();
@ -36,7 +36,7 @@ public class UserSubCommand extends BatSubCommand {
} }
User target = userOption.getAsUser(); User target = userOption.getAsUser();
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(target.getName()), null, target.getEffectiveAvatarUrl()) .setAuthor("%s's Avatar".formatted(target.getName()), null, target.getEffectiveAvatarUrl())
.setImage(target.getEffectiveAvatarUrl()) .setImage(target.getEffectiveAvatarUrl())
.build() .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.BatCommand;
import cc.fascinated.bat.command.CommandInfo; 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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; 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") @CommandInfo(name = "guild", description = "View the banner of the guild")
public class GuildSubCommand extends BatSubCommand { public class GuildSubCommand extends BatSubCommand {
@Override @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(); ImageProxy banner = guild.getDiscordGuild().getBanner();
if (banner == null) { if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(guild.getName())) .setDescription("**%s** does not have a banner!".formatted(guild.getName()))
.build()) .build())
.queue(); .queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(guild.getName())) .setAuthor("%s's Banner".formatted(guild.getName()))
.setImage(banner.getUrl(512)) .setImage(banner.getUrl(512))
.build() .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class UserSubCommand extends BatSubCommand {
} }
@Override @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) {
OptionMapping userOption = interaction.getOption("user"); OptionMapping userOption = event.getOption("user");
if (userOption == null) { if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the banner of!") .setDescription("You must provide a user to view the banner of!")
.build()) .build())
.queue(); .queue();
@ -39,14 +39,14 @@ public class UserSubCommand extends BatSubCommand {
User target = userOption.getAsUser(); User target = userOption.getAsUser();
ImageProxy banner = target.retrieveProfile().complete().getBanner(); ImageProxy banner = target.retrieveProfile().complete().getBanner();
if (banner == null) { if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(target.getName())) .setDescription("**%s** does not have a banner!".formatted(target.getName()))
.build()) .build())
.queue(); .queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(target.getName())) .setAuthor("%s's Banner".formatted(target.getName()))
.setImage(banner.getUrl(512)) .setImage(banner.getUrl(512))
.build() .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.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
@ -20,8 +21,8 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "premium", description = "View the premium information for the guild", requiredPermissions = Permission.ADMINISTRATOR) @CommandInfo(name = "premium", description = "View the premium information for the guild", requiredPermissions = Permission.ADMINISTRATOR)
public class PremiumCommand extends BatCommand { public class PremiumCommand extends BatCommand {
@Override @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) {
BatGuild.Premium premium = guild.getPremium(); PremiumProfile premium = guild.getPremiumProfile();
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information"); EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information");
if (premium.hasPremium()) { if (premium.hasPremium()) {
embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true); embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true);
@ -31,6 +32,6 @@ public class PremiumCommand extends BatCommand {
} else { } else {
embed.setDescription("The guild does not have premium"); 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.BatCommand;
import cc.fascinated.bat.command.Category; 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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class RemoveTopicSubCommand extends BatSubCommand {
} }
@Override @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) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel(); Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) { if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId())) .setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build()) .build())
.queue(); .queue();
@ -37,7 +37,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
} }
if (textChannel.getTopic() == null) { if (textChannel.getTopic() == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId())) .setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build()) .build())
.queue(); .queue();
@ -45,7 +45,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
} }
textChannel.getManager().setTopic(null).queue(); textChannel.getManager().setTopic(null).queue();
interaction.replyEmbeds(EmbedUtils.successEmbed() event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the topic of <#%s>".formatted(textChannel.getId())) .setDescription("Successfully removed the topic of <#%s>".formatted(textChannel.getId()))
.build() .build()
).queue(); ).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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -27,19 +27,19 @@ public class SetTopicSubCommand extends BatSubCommand {
} }
@Override @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) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel(); Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) { if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId())) .setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build()) .build())
.queue(); .queue();
return; return;
} }
String topic = interaction.getOption("topic").getAsString(); String topic = event.getOption("topic").getAsString();
if (topic.length() > 1024) { if (topic.length() > 1024) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The topic must be 1024 characters or less!") .setDescription("The topic must be 1024 characters or less!")
.build()) .build())
.queue(); .queue();
@ -47,7 +47,7 @@ public class SetTopicSubCommand extends BatSubCommand {
} }
textChannel.getManager().setTopic(topic).queue(); 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)) .setDescription("Successfully set the topic of <#%s> to: \"%s\"".formatted(textChannel.getId(), topic))
.build() .build()
).queue(); ).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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class ViewTopicSubCommand extends BatSubCommand {
} }
@Override @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) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel(); Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) { if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId())) .setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build()) .build())
.queue(); .queue();
@ -37,14 +37,14 @@ public class ViewTopicSubCommand extends BatSubCommand {
String topic = textChannel.getTopic(); String topic = textChannel.getTopic();
if (topic == null) { if (topic == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId())) .setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build()) .build())
.queue(); .queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The topic of <#%s> is: \"%s\"".formatted(textChannel.getId(), topic)) .setDescription("The topic of <#%s> is: \"%s\"".formatted(textChannel.getId(), topic))
.build() .build()
).queue(); ).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; package cc.fascinated.bat.features.birthday;
import cc.fascinated.bat.command.Category; import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.Feature; import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.birthday.command.BirthdayCommand; import cc.fascinated.bat.features.birthday.command.BirthdayCommand;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile; import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService; import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -16,11 +19,11 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Component
public class BirthdayFeature extends Feature { public class BirthdayFeature extends Feature implements EventListener {
private final GuildService guildService; private final GuildService guildService;
public BirthdayFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull 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; this.guildService = guildService;
registerCommand(commandService, context.getBean(BirthdayCommand.class)); registerCommand(commandService, context.getBean(BirthdayCommand.class));
@ -29,11 +32,15 @@ public class BirthdayFeature extends Feature {
/** /**
* Check birthdays every day at midnight * Check birthdays every day at midnight
*/ */
@Scheduled(cron = "0 0 0 * * *") @Scheduled(cron = "0 1 0 * * *")
private void checkBirthdays() { private void checkBirthdays() {
for (BatGuild guild : guildService.getAllGuilds()) { for (Guild guild : DiscordService.JDA.getGuilds()) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class); BatGuild batGuild = guildService.getGuild(guild.getId());
profile.checkBirthdays(guild); 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(RemoveSubCommand.class));
super.addSubCommand(context.getBean(ChannelSubCommand.class)); super.addSubCommand(context.getBean(ChannelSubCommand.class));
super.addSubCommand(context.getBean(MessageSubCommand.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.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@ -26,26 +25,23 @@ import org.springframework.stereotype.Component;
@Component("birthday:channel.sub") @Component("birthday:channel.sub")
@CommandInfo(name = "channel", description = "Sets the birthday notification channel", requiredPermissions = Permission.MANAGE_SERVER) @CommandInfo(name = "channel", description = "Sets the birthday notification channel", requiredPermissions = Permission.MANAGE_SERVER)
public class ChannelSubCommand extends BatSubCommand { public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired @Autowired
public ChannelSubCommand(GuildService guildService) { public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel birthdays will be sent in", false); super.addOption(OptionType.CHANNEL, "channel", "The channel birthdays will be sent in", false);
this.guildService = guildService;
} }
@Override @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) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class); BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping option = interaction.getOption("channel"); OptionMapping option = event.getOption("channel");
if (option == null) { if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { 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") .setDescription("There is no channel set for birthday notifications. Please provide a channel to set the birthday channel to")
.build()).queue(); .build()).queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current birthday channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId()))) .setDescription("The current birthday channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue(); .build()).queue();
return; return;
@ -53,16 +49,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel(); GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) { if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel") .setDescription("Invalid channel type, please provide a text channel")
.build()).queue(); .build()).queue();
return; return;
} }
profile.setChannelId(targetChannel.getId()); profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the birthday channel to %s".formatted(targetChannel.asTextChannel().getAsMention())) .setDescription("Successfully set the birthday channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue(); .build()).queue();
} }

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

@ -3,10 +3,10 @@ package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand; import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile; import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 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") @CommandInfo(name = "set", description = "Add your birthday to this guild")
public class SetSubCommand extends BatSubCommand { public class SetSubCommand extends BatSubCommand {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy"); private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy");
private final GuildService guildService;
@Autowired @Autowired
public SetSubCommand(GuildService guildService) { public SetSubCommand() {
super.addOption(OptionType.STRING, "birthday", "Your birthday (format: DAY/MONTH/YEAR - 01/05/2004)", true); super.addOption(OptionType.STRING, "birthday", "Your birthday (format: DAY/MONTH/YEAR - 01/05/2004)", true);
this.guildService = guildService;
} }
@Override @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) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class); BirthdayProfile profile = guild.getBirthdayProfile();
if (!profile.hasChannelSetup()) { 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.") .setDescription("Birthdays have not been enabled in this guild. Please ask an administrator to enable them.")
.build()).queue(); .build()).queue();
return; return;
} }
OptionMapping birthdayOption = interaction.getOption("birthday"); OptionMapping birthdayOption = event.getOption("birthday");
if (birthdayOption == null) { if (birthdayOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a birthday") .setDescription("You must provide a birthday")
.build()).queue(); .build()).queue();
return; return;
@ -57,16 +54,20 @@ public class SetSubCommand extends BatSubCommand {
String birthdayString = birthdayOption.getAsString(); String birthdayString = birthdayOption.getAsString();
Date birthday = parseBirthday(birthdayString); Date birthday = parseBirthday(birthdayString);
if (birthday == null) { if (birthday == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid birthday format. Please use the format: DAY/MONTH/YEAR - 01/05/2004") .setDescription("""
Invalid birthday format. Please use the following format:
DAY/MONTH/YEAR - 01/05/2004
""")
.build()).queue(); .build()).queue();
return; return;
} }
profile.addBirthday(member.getId(), birthday); UserBirthday userBirthday = new UserBirthday();
guildService.saveGuild(guild); userBirthday.setBirthday(birthday);
userBirthday.setHidden(false);
interaction.replyEmbeds(EmbedUtils.successEmbed() profile.addBirthday(member.getId(), userBirthday);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have updated your birthday!") .setDescription("You have updated your birthday!")
.build()).queue(); .build()).queue();
} }
@ -79,9 +80,16 @@ public class SetSubCommand extends BatSubCommand {
*/ */
private Date parseBirthday(String birthday) { private Date parseBirthday(String birthday) {
try { try {
return FORMATTER.parse(birthday); Date date = FORMATTER.parse(birthday);
} catch (ParseException ignored) { if (date.after(new Date())) {
return null;
} }
if (date.toInstant().toEpochMilli() < 0) {
return null;
}
return date;
} catch (ParseException ignored) {
return null; 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; 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 cc.fascinated.bat.model.BatGuild;
import com.google.gson.Gson;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 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) * @author Fascinated (fascinated7)
*/ */
@Getter @Getter
@Setter @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!"; 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 * 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 * The channel ID of the birthday feed
@ -34,17 +42,13 @@ public class BirthdayProfile extends Profile {
*/ */
private String message = DEFAULT_MESSAGE; private String message = DEFAULT_MESSAGE;
public BirthdayProfile() {
super("birthday");
}
/** /**
* Adds a birthday to be tracked * Adds a birthday to be tracked
* *
* @param userId the id of the user to track * @param userId the id of the user to track
* @param birthday the birthday of the user * @param birthday the birthday of the user
*/ */
public void addBirthday(String userId, Date birthday) { public void addBirthday(String userId, UserBirthday birthday) {
if (birthdays == null) { if (birthdays == null) {
birthdays = new HashMap<>(); birthdays = new HashMap<>();
} }
@ -69,7 +73,7 @@ public class BirthdayProfile extends Profile {
* @param userId the id of the user * @param userId the id of the user
* @return the birthday of the user * @return the birthday of the user
*/ */
public Date getBirthday(String userId) { public UserBirthday getBirthday(String userId) {
if (birthdays == null) { if (birthdays == null) {
birthdays = new HashMap<>(); birthdays = new HashMap<>();
} }
@ -85,33 +89,6 @@ public class BirthdayProfile extends Profile {
return channelId != null; 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 * Validates the profiles configuration
* *
@ -122,33 +99,10 @@ public class BirthdayProfile extends Profile {
if (birthdays == null) { if (birthdays == null) {
birthdays = new HashMap<>(); 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) { if (channelId == null) {
return false; return false;
} }
if (guild.getDiscordGuild().getTextChannelById(channelId) == null) {
if (discordGuild.getTextChannelById(channelId) == null) {
channelId = null; channelId = null;
return false; return false;
} }
@ -170,13 +124,10 @@ public class BirthdayProfile extends Profile {
int todayDay = today.get(Calendar.DAY_OF_MONTH); int todayDay = today.get(Calendar.DAY_OF_MONTH);
int todayMonth = today.get(Calendar.MONTH); // Note: January is 0 int todayMonth = today.get(Calendar.MONTH); // Note: January is 0
Iterator<Map.Entry<String, Date>> iterator = birthdays.entrySet().iterator(); for (Map.Entry<String, UserBirthday> entry : birthdays.entrySet()) {
while (iterator.hasNext()) { Date birthday = entry.getValue().getBirthday();
Map.Entry<String, Date> entry = iterator.next();
Date birthday = entry.getValue();
if (birthday == null) { if (birthday == null) {
iterator.remove();
continue; continue;
} }
@ -223,7 +174,7 @@ public class BirthdayProfile extends Profile {
public String getBirthdayMessage(User user) { public String getBirthdayMessage(User user) {
return message return message
.replace("{user}", user.getAsMention()) .replace("{user}", user.getAsMention())
.replace("{age}", String.valueOf(calculateAge(user.getId()))); .replace("{age}", String.valueOf(birthdays.get(user.getId()).calculateAge()));
} }
@Override @Override
@ -231,4 +182,27 @@ public class BirthdayProfile extends Profile {
birthdays.clear(); birthdays.clear();
channelId = null; 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; 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.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.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken; 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.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken; import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@ -23,10 +25,12 @@ import org.springframework.stereotype.Component;
@Log4j2 @Log4j2
public class NumberOneScoreFeedListener implements EventListener { public class NumberOneScoreFeedListener implements EventListener {
private final GuildService guildService; private final GuildService guildService;
private final FeatureService featureService;
@Autowired @Autowired
public NumberOneScoreFeedListener(GuildService guildService) { public NumberOneScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService; this.guildService = guildService;
this.featureService = featureService;
} }
@Override @Override
@ -41,7 +45,7 @@ public class NumberOneScoreFeedListener implements EventListener {
log.info("A new #1 score has been set by {} on {} ({})!", log.info("A new #1 score has been set by {} on {} ({})!",
player.getName(), player.getName(),
leaderboard.getSongName(), leaderboard.getSongName(),
"%s⭐".formatted(NumberUtils.formatNumberCommas(leaderboard.getStars())) "%s⭐".formatted(NumberFormatter.formatCommas(leaderboard.getStars()))
); );
for (Guild guild : DiscordService.JDA.getGuilds()) { for (Guild guild : DiscordService.JDA.getGuilds()) {
@ -49,16 +53,20 @@ public class NumberOneScoreFeedListener implements EventListener {
if (batGuild == null) { if (batGuild == null) {
continue; 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) { if (profile == null || profile.getChannelId() == null) {
continue; continue;
} }
TextChannel channel = profile.getAsTextChannel(); TextChannel channel = profile.getTextChannel();
if (channel == null) { if (channel == null) {
log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId()); log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId());
profile.setChannelId(null); profile.setChannelId(null);
guildService.saveGuild(batGuild);
continue; continue;
} }
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue(); 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.command.Category;
import cc.fascinated.bat.common.DateUtils; import cc.fascinated.bat.common.DateUtils;
import cc.fascinated.bat.common.EmbedUtils; 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.common.ScoreSaberUtils;
import cc.fascinated.bat.features.Feature; import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.scoresaber.command.numberone.NumberOneFeedCommand; import cc.fascinated.bat.features.scoresaber.command.numberone.NumberOneFeedCommand;
@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
public class ScoreSaberFeature extends Feature { public class ScoreSaberFeature extends Feature {
@Autowired @Autowired
public ScoreSaberFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { 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(ScoreSaberCommand.class));
registerCommand(commandService, context.getBean(UserFeedCommand.class)); registerCommand(commandService, context.getBean(UserFeedCommand.class));
@ -55,10 +55,10 @@ public class ScoreSaberFeature extends Feature {
); );
String accuracy = leaderboardToken.getMaxScore() == 0 ? "N/A" : 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 rawPp = scoreToken.getPp() == 0 ? "Unranked" : NumberFormatter.formatCommas(scoreToken.getPp());
String rank = String.format("#%s", NumberUtils.formatNumberCommas(scoreToken.getRank())); String rank = String.format("#%s", NumberFormatter.formatCommas(scoreToken.getRank()));
String misses = String.format("%s", scoreToken.getMissedNotes()); String misses = String.format("%s", scoreToken.getMissedNotes());
String badCuts = String.format("%s", scoreToken.getBadCuts()); String badCuts = String.format("%s", scoreToken.getBadCuts());
String maxCombo = String.format("%s %s", String maxCombo = String.format("%s %s",

@ -1,13 +1,15 @@
package cc.fascinated.bat.features.scoresaber; package cc.fascinated.bat.features.scoresaber;
import cc.fascinated.bat.event.EventListener; 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.BatGuild;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken; 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.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken; import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.FeatureService;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@ -22,10 +24,12 @@ import org.springframework.stereotype.Component;
@Log4j2 @Log4j2
public class UserScoreFeedListener implements EventListener { public class UserScoreFeedListener implements EventListener {
private final GuildService guildService; private final GuildService guildService;
private final FeatureService featureService;
@Autowired @Autowired
public UserScoreFeedListener(GuildService guildService) { public UserScoreFeedListener(@NonNull GuildService guildService, @NonNull FeatureService featureService) {
this.guildService = guildService; this.guildService = guildService;
this.featureService = featureService;
} }
@Override @Override
@ -36,16 +40,19 @@ public class UserScoreFeedListener implements EventListener {
if (batGuild == null) { if (batGuild == null) {
continue; 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())) { if (profile == null || profile.getChannelId() == null || !profile.getTrackedUsers().contains(player.getId())) {
continue; continue;
} }
TextChannel channel = profile.getAsTextChannel(); TextChannel channel = profile.getTextChannel();
if (channel == null) { if (channel == null) {
log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId()); log.error("Scoresaber user feed channel is null for guild {}, removing the stored channel.", guild.getId());
profile.setChannelId(null); profile.setChannelId(null);
guildService.saveGuild(batGuild);
continue; continue;
} }
channel.sendMessageEmbeds(ScoreSaberFeature.buildScoreEmbed(score)).queue(); 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.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils; 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.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.ChannelType;
@ -25,26 +24,23 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-number-one-feed:channel.sub") @Component("scoresaber-number-one-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel") @CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatSubCommand { public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired @Autowired
public ChannelSubCommand(GuildService guildService) { public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false); super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false);
this.guildService = guildService;
} }
@Override @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) {
GuildNumberOneScoreFeedProfile profile = guild.getProfile(GuildNumberOneScoreFeedProfile.class); NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
OptionMapping option = interaction.getOption("channel"); OptionMapping option = event.getOption("channel");
if (option == null) { if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.") .setDescription("There is no channel set for the feed notifications.")
.build()).queue(); .build()).queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId()))) .setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue(); .build()).queue();
return; return;
@ -52,16 +48,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel(); GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) { if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel") .setDescription("Invalid channel type, please provide a text channel")
.build()).queue(); .build()).queue();
return; return;
} }
profile.setChannelId(targetChannel.getId()); profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention())) .setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildNumberOneScoreFeedProfile; import cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -20,20 +18,12 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-number-one-feed:reset.sub") @Component("scoresaber-number-one-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings") @CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatSubCommand { public class ResetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ResetSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override @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) {
GuildNumberOneScoreFeedProfile profile = guild.getProfile(GuildNumberOneScoreFeedProfile.class); NumberOneScoreFeedProfile profile = guild.getProfile(NumberOneScoreFeedProfile.class);
profile.reset(); profile.reset();
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed() event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.") .setDescription("Successfully reset the settings.")
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.UserScoreSaberProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken; import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberAccountToken;
import cc.fascinated.bat.service.ScoreSaberService; import cc.fascinated.bat.service.ScoreSaberService;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 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") @CommandInfo(name = "link", description = "Links your ScoreSaber profile")
public class LinkSubCommand extends BatSubCommand { public class LinkSubCommand extends BatSubCommand {
private final ScoreSaberService scoreSaberService; private final ScoreSaberService scoreSaberService;
private final UserService userService;
@Autowired @Autowired
public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService) { public LinkSubCommand(@NonNull ScoreSaberService scoreSaberService) {
super.addOption(OptionType.STRING, "link", "Link your ScoreSaber profile", true); super.addOption(OptionType.STRING, "link", "Link your ScoreSaber profile", true);
this.scoreSaberService = scoreSaberService; this.scoreSaberService = scoreSaberService;
this.userService = userService;
} }
@Override @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) {
OptionMapping option = interaction.getOption("link"); OptionMapping option = event.getOption("link");
if (option == null) { if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a ScoreSaber profile link") .setDescription("Please provide a ScoreSaber profile link")
.build()).queue(); .build()).queue();
return; return;
@ -46,7 +42,7 @@ public class LinkSubCommand extends BatSubCommand {
String link = option.getAsString(); String link = option.getAsString();
if (!link.contains("scoresaber.com/u/")) { if (!link.contains("scoresaber.com/u/")) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link") .setDescription("Invalid ScoreSaber profile link")
.build()).queue(); .build()).queue();
return; return;
@ -59,15 +55,14 @@ public class LinkSubCommand extends BatSubCommand {
ScoreSaberAccountToken account = scoreSaberService.getAccount(id); ScoreSaberAccountToken account = scoreSaberService.getAccount(id);
if (account == null) { if (account == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid ScoreSaber profile link") .setDescription("Invalid ScoreSaber profile link")
.build()).queue(); .build()).queue();
return; return;
} }
user.getProfile(UserScoreSaberProfile.class).setSteamId(id); user.getScoreSaberProfile().setAccountId(id);
userService.saveUser(user); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id))) .setDescription("Successfully linked your [ScoreSaber](%s) profile".formatted("https://scoresaber.com/u/%s".formatted(id)))
.build()).queue(); .build()).queue();
} }

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

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

@ -33,17 +33,17 @@ public class UserSubCommand extends BatSubCommand {
} }
@Override @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) {
OptionMapping option = interaction.getOption("user"); OptionMapping option = event.getOption("user");
if (option == null) { if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a user to view the ScoreSaber profile of") .setDescription("Please provide a user to view the ScoreSaber profile of")
.build()).queue(); .build()).queue();
return; return;
} }
if (option.getAsUser().isBot()) { if (option.getAsUser().isBot()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You cannot view the ScoreSaber profile for a Bot") .setDescription("You cannot view the ScoreSaber profile for a Bot")
.build()).queue(); .build()).queue();
return; return;
@ -51,11 +51,11 @@ public class UserSubCommand extends BatSubCommand {
BatUser target = userService.getUser(option.getAsUser().getId()); BatUser target = userService.getUser(option.getAsUser().getId());
if (target == null) { if (target == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Unknown user") .setDescription("Unknown user")
.build()).queue(); .build()).queue();
return; 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.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TextChannelUtils; 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.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.ChannelType;
@ -25,26 +24,23 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-user-feed:channel.sub") @Component("scoresaber-user-feed:channel.sub")
@CommandInfo(name = "channel", description = "Sets the feed channel") @CommandInfo(name = "channel", description = "Sets the feed channel")
public class ChannelSubCommand extends BatSubCommand { public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired @Autowired
public ChannelSubCommand(GuildService guildService) { public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false); super.addOption(OptionType.CHANNEL, "channel", "The channel scores are sent in", false);
this.guildService = guildService;
} }
@Override @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) {
GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class); UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = interaction.getOption("channel"); OptionMapping option = event.getOption("channel");
if (option == null) { if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) { if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for the feed notifications.") .setDescription("There is no channel set for the feed notifications.")
.build()).queue(); .build()).queue();
return; return;
} }
interaction.replyEmbeds(EmbedUtils.genericEmbed() event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId()))) .setDescription("The current feed channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue(); .build()).queue();
return; return;
@ -52,16 +48,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel(); GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) { if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel") .setDescription("Invalid channel type, please provide a text channel")
.build()).queue(); .build()).queue();
return; return;
} }
profile.setChannelId(targetChannel.getId()); profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention())) .setDescription("Successfully set the feed channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile; import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -20,20 +18,11 @@ import org.springframework.stereotype.Component;
@Component("scoresaber-user-feed:reset.sub") @Component("scoresaber-user-feed:reset.sub")
@CommandInfo(name = "reset", description = "Resets the settings") @CommandInfo(name = "reset", description = "Resets the settings")
public class ResetSubCommand extends BatSubCommand { public class ResetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ResetSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override @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) {
GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class); UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
profile.reset(); profile.reset();
guildService.saveGuild(guild); event.replyEmbeds(EmbedUtils.successEmbed()
interaction.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully reset the settings.") .setDescription("Successfully reset the settings.")
.build()).queue(); .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.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.scoresaber.profile.GuildUserScoreFeedProfile; import cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile;
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.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService; import cc.fascinated.bat.service.GuildService;
@ -36,12 +36,12 @@ public class UserSubCommand extends BatSubCommand {
} }
@Override @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) {
GuildUserScoreFeedProfile profile = guild.getProfile(GuildUserScoreFeedProfile.class); UserScoreFeedProfile profile = guild.getProfile(UserScoreFeedProfile.class);
OptionMapping option = interaction.getOption("user"); OptionMapping option = event.getOption("user");
if (option == null) { if (option == null) {
if (profile.getTrackedUsers().isEmpty()) { if (profile.getTrackedUsers().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no users being tracked in the feed") .setDescription("There are no users being tracked in the feed")
.build()).queue(); .build()).queue();
return; return;
@ -53,7 +53,7 @@ public class UserSubCommand extends BatSubCommand {
"https://scoresaber.com/u/%s".formatted(accountId) "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())) .setDescription("The current users being tracked in the feed are:\n%s".formatted(stringBuilder.toString()))
.build()).queue(); .build()).queue();
return; return;
@ -61,25 +61,27 @@ public class UserSubCommand extends BatSubCommand {
User target = option.getAsUser(); User target = option.getAsUser();
BatUser targetUser = userService.getUser(target.getId()); BatUser targetUser = userService.getUser(target.getId());
UserScoreSaberProfile targetProfile = targetUser.getProfile(UserScoreSaberProfile.class); ScoreSaberProfile targetProfile = targetUser.getScoreSaberProfile();
if (targetProfile.getSteamId() == null) { if (targetProfile.getAccountId() == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The user you are trying to track does not have a linked ScoreSaber profile") .setDescription("The user you are trying to track does not have a linked ScoreSaber profile")
.build()).queue(); .build()).queue();
return; return;
} }
if (profile.getTrackedUsers().contains(targetProfile.getSteamId())) { boolean added = false;
profile.getTrackedUsers().remove(targetProfile.getSteamId()); if (profile.isUserTracked(targetProfile.getAccountId())) {
interaction.replyEmbeds(EmbedUtils.successEmbed() profile.removeTrackedUser(targetProfile.getAccountId());
.setDescription("Successfully removed %s from the feed".formatted(target.getAsMention()))
.build()).queue();
} else { } else {
profile.getTrackedUsers().add(targetProfile.getSteamId()); profile.addTrackedUser(targetProfile.getAccountId());
interaction.replyEmbeds(EmbedUtils.successEmbed() added = true;
.setDescription("Successfully added %s to the feed".formatted(target.getAsMention())) }
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully %s %s from the feed".formatted(
added ? "added" : "removed",
target.getAsMention()
))
.build()).queue(); .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 cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -14,7 +17,8 @@ import java.util.List;
*/ */
@Getter @Getter
@Setter @Setter
public class GuildUserScoreFeedProfile extends Profile { @NoArgsConstructor
public class UserScoreFeedProfile extends Serializable {
/** /**
* The channel ID of the score feed * The channel ID of the score feed
*/ */
@ -25,10 +29,6 @@ public class GuildUserScoreFeedProfile extends Profile {
*/ */
private List<String> trackedUsers; private List<String> trackedUsers;
public GuildUserScoreFeedProfile() {
super("scoresaber-user-score-feed");
}
/** /**
* Gets the tracked users * Gets the tracked users
* *
@ -38,7 +38,20 @@ public class GuildUserScoreFeedProfile extends Profile {
if (this.trackedUsers == null) { if (this.trackedUsers == null) {
this.trackedUsers = new ArrayList<>(); 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 * @return the channel as a TextChannel
*/ */
public TextChannel getAsTextChannel() { public TextChannel getTextChannel() {
return DiscordService.JDA.getTextChannelById(channelId); return DiscordService.JDA.getTextChannelById(channelId);
} }
@ -79,4 +92,18 @@ public class GuildUserScoreFeedProfile extends Profile {
this.channelId = null; this.channelId = null;
this.trackedUsers = 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; package cc.fascinated.bat.features.spotify;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.Category; import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.EmbedUtils; 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 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.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; 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) * @author Fascinated (fascinated7)
@ -13,18 +28,153 @@ import org.springframework.stereotype.Component;
@Component @Component
public class SpotifyFeature extends Feature { public class SpotifyFeature extends Feature {
@Autowired @Autowired
public SpotifyFeature() { public SpotifyFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Spotify", Category.MUSIC); 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() return EmbedUtils.genericEmbed()
.setDescription("You need to link your Spotify account before you can use this command.") .setAuthor("Listening to %s | %s".formatted(track.getName(), track.getArtists()[0].getName()), trackUrl)
.build(); .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