# Conflicts:
#	src/main/java/cc/fascinated/bat/command/Category.java
This commit is contained in:
Nick 2024-07-03 18:44:09 -05:00
commit 285a0ca00a
41 changed files with 1480 additions and 71 deletions

@ -18,7 +18,6 @@ public enum Category {
SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false), SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false),
UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false), UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false),
MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false), MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false),
MOVIES_TV(Emoji.fromFormatted("U+1F37F"), "Movies & TV", false),
SNIPE(Emoji.fromFormatted("U+1F4A3"), "Snipe", false), SNIPE(Emoji.fromFormatted("U+1F4A3"), "Snipe", false),
LOGS(Emoji.fromFormatted("U+1F4D1"), "Logs", false), LOGS(Emoji.fromFormatted("U+1F4D1"), "Logs", false),
BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false), BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false),

@ -31,20 +31,26 @@ public class WebRequest {
* @return the response * @return the response
*/ */
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException { public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
ResponseEntity<T> responseEntity = CLIENT.get() try {
.uri(url) ResponseEntity<T> responseEntity = CLIENT.get()
.retrieve() .uri(url)
.onStatus(HttpStatusCode::isError, (request, response) -> { .retrieve()
}) // Don't throw exceptions on error .onStatus(HttpStatusCode::isError, (request, response) -> {
.toEntity(clazz); }) // Don't throw exceptions on error
.toEntity(clazz);
if (responseEntity.getStatusCode().isError()) { if (responseEntity.getStatusCode().isError()) {
return null;
}
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return responseEntity.getBody();
} catch (RateLimitException e) {
throw e;
} catch (Exception e) {
return null; return null;
} }
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return responseEntity.getBody();
} }
/** /**

@ -1,23 +0,0 @@
package cc.fascinated.bat.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
/**
* @author Fascinated (fascinated7)
*/
@Configuration
public class MongoConfig {
@Bean
public MappingMongoConverter mongoConverter(MongoDatabaseFactory mongoFactory, MongoMappingContext mongoMappingContext) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoFactory);
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
mongoConverter.setMapKeyDotReplacement("-DOT");
return mongoConverter;
}
}

@ -19,6 +19,7 @@ import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent; import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent;
import net.dv8tion.jda.api.events.guild.voice.GenericGuildVoiceEvent;
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;
@ -232,6 +233,15 @@ public interface EventListener {
default void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) { default void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) {
} }
/**
* Called when a user joins or leaves a voice channel
*
* @param guild the guild that the user joined or left the voice channel in
* @param user the user that joined or left the voice channel
*/
default void onGuildVoiceUpdate(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GenericGuildVoiceEvent event) {
}
/** /**
* Called when Spring is shutting down * Called when Spring is shutting down
*/ */

@ -49,7 +49,11 @@ public class AutoRoleListener implements EventListener {
event.getGuild().addRoleToMember(event.getMember(), role).queue(); event.getGuild().addRoleToMember(event.getMember(), role).queue();
} }
toRemove.forEach(profile::removeRole); toRemove.forEach(profile::removeRole);
log.info("Gave user \"{}\" {} auto roles{}", user.getId(), profile.getRoles().size(), toRemove.isEmpty() ? "" log.info("Gave user \"{}\" {} auto roles in guild \"{}\"{}",
: " and removed %s invalid roles".formatted(toRemove.size())); user.getId(),
profile.getRoles().size(),
guild.getName(),
toRemove.isEmpty() ? "" : " and removed %s invalid roles from the profile".formatted(toRemove.size())
);
} }
} }

@ -3,6 +3,7 @@ package cc.fascinated.bat.features.base;
import cc.fascinated.bat.command.Category; import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature; import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.commands.botadmin.premium.PremiumAdminCommand; import cc.fascinated.bat.features.base.commands.botadmin.premium.PremiumAdminCommand;
import cc.fascinated.bat.features.base.commands.fun.EightBallCommand;
import cc.fascinated.bat.features.base.commands.fun.image.ImageCommand; 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.*;
import cc.fascinated.bat.features.base.commands.general.avatar.AvatarCommand; import cc.fascinated.bat.features.base.commands.general.avatar.AvatarCommand;
@ -39,5 +40,6 @@ public class BaseFeature extends Feature {
super.registerCommand(commandService, context.getBean(AvatarCommand.class)); super.registerCommand(commandService, context.getBean(AvatarCommand.class));
super.registerCommand(commandService, context.getBean(ImageCommand.class)); super.registerCommand(commandService, context.getBean(ImageCommand.class));
super.registerCommand(commandService, context.getBean(FeatureCommand.class)); super.registerCommand(commandService, context.getBean(FeatureCommand.class));
super.registerCommand(commandService, context.getBean(EightBallCommand.class));
} }
} }

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.base.commands.fun;
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.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "8ball", description = "Ask the magic 8ball a question")
public class EightBallCommand extends BatCommand {
private final String[] responses = new String[]{
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes, definitely",
"You may rely on it",
"As I see it, yes",
"Most likely",
"Outlook good",
"Yes",
"Signs point to yes",
"Reply hazy, try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful"
};
public EightBallCommand() {
super.addOption(OptionType.STRING, "question", "The question you want to ask the 8ball", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping questionOption = event.getOption("question");
if (questionOption == null) {
return;
}
String question = questionOption.getAsString();
String response = responses[(int) (Math.random() * responses.length)];
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You asked: `%s`\n\n:8ball: The magic 8ball says: `%s`".formatted(question, response))
.build())
.queue();
}
}

@ -26,7 +26,7 @@ public class VoteCommand extends BatCommand {
@Override @Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) { public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Vote Links"); EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Vote Links");
description.appendLine("Vote for the bot on the following websites to support us!", true); description.appendLine("Vote for the bot on the following websites to support us!", false);
for (String link : VOTE_LINKS) { for (String link : VOTE_LINKS) {
description.appendLine(link, true); description.appendLine(link, true);
} }

@ -35,7 +35,9 @@ public enum LogType {
* Channel Events * Channel Events
*/ */
CHANNEL_CREATE(LogCategory.CHANNEL, "Channel Create"), CHANNEL_CREATE(LogCategory.CHANNEL, "Channel Create"),
CHANNEL_DELETE(LogCategory.CHANNEL, "Channel Delete"); CHANNEL_DELETE(LogCategory.CHANNEL, "Channel Delete"),
VOICE_CHANNEL_JOIN(LogCategory.CHANNEL, "Voice Channel Join"),
VOICE_CHANNEL_LEAVE(LogCategory.CHANNEL, "Voice Channel Leave");
/** /**
* The category of the log type * The category of the log type

@ -7,20 +7,33 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.logging.LogFeature; import cc.fascinated.bat.features.logging.LogFeature;
import cc.fascinated.bat.features.logging.LogType; import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.entities.channel.unions.ChannelUnion; import net.dv8tion.jda.api.entities.channel.unions.ChannelUnion;
import net.dv8tion.jda.api.events.channel.ChannelCreateEvent; import net.dv8tion.jda.api.events.channel.ChannelCreateEvent;
import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent; import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent;
import net.dv8tion.jda.api.events.guild.voice.GenericGuildVoiceEvent;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Component
@Log4j2
public class ChannelListener implements EventListener { public class ChannelListener implements EventListener {
/**
* A map of users and the last voice channel they were in
*/
private final Map<BatUser, VoiceChannel> lastVoiceChannel = new HashMap<>();
private final LogFeature logFeature; private final LogFeature logFeature;
@Autowired @Autowired
@ -30,6 +43,7 @@ public class ChannelListener implements EventListener {
@Override @Override
public void onChannelCreate(@NonNull BatGuild guild, @NonNull ChannelCreateEvent event) { public void onChannelCreate(@NonNull BatGuild guild, @NonNull ChannelCreateEvent event) {
log.info("Channel \"{}\" was created in guild \"{}\"", event.getChannel().getName(), guild.getName());
logFeature.sendLog(guild, LogType.CHANNEL_CREATE, EmbedUtils.successEmbed() logFeature.sendLog(guild, LogType.CHANNEL_CREATE, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("%s Channel Created".formatted(EnumUtils.getEnumName(event.getChannel().getType()))) .setDescription(new EmbedDescriptionBuilder("%s Channel Created".formatted(EnumUtils.getEnumName(event.getChannel().getType())))
.appendLine("Channel: %s".formatted(event.getChannel().getAsMention()), true) .appendLine("Channel: %s".formatted(event.getChannel().getAsMention()), true)
@ -40,6 +54,7 @@ public class ChannelListener implements EventListener {
@Override @Override
public void onChannelDelete(@NonNull BatGuild guild, @NonNull ChannelDeleteEvent event) { public void onChannelDelete(@NonNull BatGuild guild, @NonNull ChannelDeleteEvent event) {
log.info("Channel \"{}\" was deleted in guild \"{}\"", event.getChannel().getName(), guild.getName());
ChannelUnion channel = event.getChannel(); ChannelUnion channel = event.getChannel();
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("%s Channel Deleted".formatted(EnumUtils.getEnumName(channel.getType()))) EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("%s Channel Deleted".formatted(EnumUtils.getEnumName(channel.getType())))
.appendLine("Name: #%s".formatted(channel.getName()), true); .appendLine("Name: #%s".formatted(channel.getName()), true);
@ -49,4 +64,31 @@ public class ChannelListener implements EventListener {
} }
logFeature.sendLog(guild, LogType.CHANNEL_DELETE, EmbedUtils.errorEmbed().setDescription(description.build()).build()); logFeature.sendLog(guild, LogType.CHANNEL_DELETE, EmbedUtils.errorEmbed().setDescription(description.build()).build());
} }
@Override
public void onGuildVoiceUpdate(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GenericGuildVoiceEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();
if (channel != null) {
VoiceChannel voiceChannel = channel.asVoiceChannel();
lastVoiceChannel.put(user, voiceChannel);
}
VoiceChannel voiceChannel = lastVoiceChannel.get(user);
if (voiceChannel == null) {
return;
}
boolean joined = voiceChannel.getMembers().contains(event.getMember());
if (!joined) {
lastVoiceChannel.remove(user);
}
log.info("User \"{}\" {} voice channel \"{}\" in guild \"{}\"", user.getId(), joined ? "joined" : "left", voiceChannel.getName(), guild.getName());
String description = new EmbedDescriptionBuilder("User %s Voice Channel".formatted(joined ? "Joined" : "Left"))
.appendLine("User: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Channel: %s".formatted(voiceChannel.getAsMention()), true)
.build();
if (joined) {
logFeature.sendLog(guild, LogType.VOICE_CHANNEL_JOIN, EmbedUtils.successEmbed().setDescription(description).build());
return;
}
logFeature.sendLog(guild, LogType.VOICE_CHANNEL_LEAVE, EmbedUtils.errorEmbed().setDescription(description).build());
}
} }

@ -23,6 +23,8 @@ import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEv
import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent; import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent; import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent; import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -35,6 +37,7 @@ import java.util.List;
*/ */
@Component @Component
public class MemberListener implements EventListener { public class MemberListener implements EventListener {
private static final Logger log = LoggerFactory.getLogger(MemberListener.class);
private final LogFeature logFeature; private final LogFeature logFeature;
private final GuildService guildService; private final GuildService guildService;
@ -47,12 +50,13 @@ public class MemberListener implements EventListener {
@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) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" joined the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_JOIN, EmbedUtils.successEmbed() logFeature.sendLog(guild, LogType.MEMBER_JOIN, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Joined") .setDescription(new EmbedDescriptionBuilder("Member Joined")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true) .appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Username: %s".formatted(user.getDiscordUser().getName()), true) .appendLine("Username: %s".formatted(user.getDiscordUser().getName()), true)
.appendLine("Account Age: <t:%s:R>".formatted(user.getDiscordUser().getTimeCreated().toEpochSecond()), true) .appendLine("Joined Discord: <t:%s:R>".formatted(user.getDiscordUser().getTimeCreated().toEpochSecond()), true)
.build()) .build())
.setThumbnail(user.getDiscordUser().getEffectiveAvatarUrl()) .setThumbnail(user.getDiscordUser().getEffectiveAvatarUrl())
.build()); .build());
@ -60,12 +64,13 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberLeave(@NonNull BatGuild guild, BatUser user, @NonNull GuildMemberRemoveEvent event) { public void onGuildMemberLeave(@NonNull BatGuild guild, BatUser user, @NonNull GuildMemberRemoveEvent event) {
if (user.getDiscordUser().isBot()) return; if (user == null || user.getDiscordUser().isBot()) return;
log.info("User \"{}\" left the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_LEAVE, EmbedUtils.errorEmbed() logFeature.sendLog(guild, LogType.MEMBER_LEAVE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Left") .setDescription(new EmbedDescriptionBuilder("Member Left")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true) .appendLine("Member: <@%s>".formatted(user.getId()), true)
.appendLine("Username: %s".formatted(user.getDiscordUser().getName()), true) .appendLine("Username: %s".formatted(user.getName()), true)
.build()) .build())
.build()); .build());
} }
@ -73,6 +78,7 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) { public void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their nickname from \"{}\" to \"{}\" in the guild \"{}\"", user.getName(), oldName, newName, guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_NICKNAME_UPDATE, EmbedUtils.genericEmbed() logFeature.sendLog(guild, LogType.MEMBER_NICKNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Nickname Updated") .setDescription(new EmbedDescriptionBuilder("Member Nickname Updated")
@ -86,9 +92,12 @@ public class MemberListener implements EventListener {
@Override @Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) { public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their global name from \"{}\" to \"{}\"", user.getName(), oldName, newName);
for (Guild guild : DiscordService.JDA.getGuilds()) { for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId()); BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue; if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_GLOBAL_NAME_UPDATE, EmbedUtils.genericEmbed() logFeature.sendLog(batGuild, LogType.MEMBER_GLOBAL_NAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Name Updated") .setDescription(new EmbedDescriptionBuilder("Member Name Updated")
@ -103,9 +112,12 @@ public class MemberListener implements EventListener {
@Override @Override
public void onUserUpdateName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateNameEvent event) { public void onUserUpdateName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateNameEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their username from \"{}\" to \"{}\"", user.getName(), oldName, newName);
for (Guild guild : DiscordService.JDA.getGuilds()) { for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId()); BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue; if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed() logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Username Updated") .setDescription(new EmbedDescriptionBuilder("Member Username Updated")
@ -120,14 +132,17 @@ public class MemberListener implements EventListener {
@Override @Override
public void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) { public void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their avatar to \"{}\"", user.getName(), newAvatarUrl);
for (Guild guild : DiscordService.JDA.getGuilds()) { for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId()); BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue; if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed() logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Avatar Updated") .setDescription(new EmbedDescriptionBuilder("Member Avatar Updated")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true) .appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Old Avatar: [avatar](%s)".formatted(oldAvatarUrl), true) .appendLine("Old Avatar: %s".formatted(oldAvatarUrl == null ? "None" : "[avatar](%s)".formatted(oldAvatarUrl)), true)
.appendLine("New Avatar: [avatar](%s)".formatted(newAvatarUrl), true) .appendLine("New Avatar: [avatar](%s)".formatted(newAvatarUrl), true)
.build()) .build())
.build()); .build());
@ -137,6 +152,7 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberRoleAdd(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleAddEvent event) { public void onGuildMemberRoleAdd(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleAddEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was given {} roles in the guild \"{}\"", user.getName(), rolesAdded.size(), guild.getDiscordGuild().getName());
StringBuilder roles = new StringBuilder(); StringBuilder roles = new StringBuilder();
for (Role role : rolesAdded) { for (Role role : rolesAdded) {
@ -155,6 +171,7 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberRoleRemove(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleRemoveEvent event) { public void onGuildMemberRoleRemove(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleRemoveEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" had {} roles removed in the guild \"{}\"", user.getName(), rolesAdded.size(), guild.getDiscordGuild().getName());
StringBuilder roles = new StringBuilder(); StringBuilder roles = new StringBuilder();
for (Role role : rolesAdded) { for (Role role : rolesAdded) {
@ -173,6 +190,7 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberBan(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildBanEvent event) { public void onGuildMemberBan(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildBanEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was banned from the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_BAN, EmbedUtils.errorEmbed() logFeature.sendLog(guild, LogType.MEMBER_BAN, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Banned") .setDescription(new EmbedDescriptionBuilder("Member Banned")
@ -184,6 +202,7 @@ public class MemberListener implements EventListener {
@Override @Override
public void onGuildMemberUnban(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildUnbanEvent event) { public void onGuildMemberUnban(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildUnbanEvent event) {
if (user.getDiscordUser().isBot()) return; if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was unbanned from the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_UNBAN, EmbedUtils.successEmbed() logFeature.sendLog(guild, LogType.MEMBER_UNBAN, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Unbanned") .setDescription(new EmbedDescriptionBuilder("Member Unbanned")
@ -196,6 +215,7 @@ public class MemberListener implements EventListener {
public void onGuildMemberTimeout(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberUpdateTimeOutEvent event) { public void onGuildMemberTimeout(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberUpdateTimeOutEvent event) {
OffsetDateTime timeoutEnd = event.getNewTimeOutEnd(); OffsetDateTime timeoutEnd = event.getNewTimeOutEnd();
if (user.getDiscordUser().isBot() || timeoutEnd == null) return; if (user.getDiscordUser().isBot() || timeoutEnd == null) return;
log.info("User \"{}\" was timed out until \"{}\"", user.getName(), timeoutEnd);
long seconds = timeoutEnd.toInstant().getEpochSecond(); long seconds = timeoutEnd.toInstant().getEpochSecond();
logFeature.sendLog(guild, LogType.MEMBER_TIMEOUT, EmbedUtils.errorEmbed() logFeature.sendLog(guild, LogType.MEMBER_TIMEOUT, EmbedUtils.errorEmbed()

@ -9,6 +9,7 @@ import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage; import cc.fascinated.bat.model.DiscordMessage;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent; import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -19,6 +20,7 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Component @Component
@Log4j2
public class MessageListener implements EventListener { public class MessageListener implements EventListener {
private final LogFeature logFeature; private final LogFeature logFeature;
@ -29,7 +31,8 @@ public class MessageListener implements EventListener {
@Override @Override
public void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) { public void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) {
if (user.getDiscordUser().isBot() || message.getAuthor().isBot()) return; if (user == null || user.getDiscordUser().isBot() || message.getAuthor().isBot()) return;
log.info("User \"{}\" deleted a message in guild \"{}\"", user.getDiscordUser().getGlobalName(), guild.getName());
logFeature.sendLog(guild, LogType.MESSAGE_DELETE, EmbedUtils.errorEmbed() logFeature.sendLog(guild, LogType.MESSAGE_DELETE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Message Deleted") .setDescription(new EmbedDescriptionBuilder("Message Deleted")
@ -44,6 +47,7 @@ public class MessageListener implements EventListener {
public void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage, public void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage,
@NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) { @NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) {
if (user.getDiscordUser().isBot() || newMessage.getAuthor().isBot() || oldMessage == null) return; if (user.getDiscordUser().isBot() || newMessage.getAuthor().isBot() || oldMessage == null) return;
log.info("User \"{}\" edited a message in guild \"{}\"", user.getDiscordUser().getGlobalName(), guild.getName());
logFeature.sendLog(guild, LogType.MESSAGE_EDIT, EmbedUtils.genericEmbed() logFeature.sendLog(guild, LogType.MESSAGE_EDIT, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Message Edited") .setDescription(new EmbedDescriptionBuilder("Message Edited")

@ -30,7 +30,7 @@ public class MessageSnipeFeature extends Feature implements EventListener {
@Autowired @Autowired
public MessageSnipeFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { public MessageSnipeFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Message Snipe", false, Category.SNIPE); super("Message Snipe", false, Category.MESSAGES);
super.registerCommand(commandService, context.getBean(MessageSnipeCommand.class)); super.registerCommand(commandService, context.getBean(MessageSnipeCommand.class));
} }

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.moderation;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.moderation.command.PurgeCommand;
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 ModerationFeature extends Feature {
@Autowired
public ModerationFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Moderation", true,Category.MODERATION);
super.registerCommand(commandService, context.getBean(PurgeCommand.class));
}
}

@ -0,0 +1,67 @@
package cc.fascinated.bat.features.moderation.command;
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.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
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.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "purge", description = "Purge messages from a channel", requiredPermissions = Permission.MESSAGE_MANAGE)
public class PurgeCommand extends BatCommand {
private final long MESSAGE_DELETE_DELAY = TimeUnit.SECONDS.toMillis(10);
public PurgeCommand() {
super.addOption(OptionType.INTEGER, "amount", "The amount of messages to remove", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping amountOption = event.getOption("amount");
if (amountOption == null) {
return;
}
int amount = amountOption.getAsInt();
if (amount < 2 || amount > 100) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You can only purge between 2 and 100 messages")
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Purging `%s` messages...".formatted(amount))
.build()).queue(then -> {
TextChannel textChannel = (TextChannel) channel;
textChannel.getHistory().retrievePast(amount + 1).queue(messages -> {
// Filter out the command message
then.retrieveOriginal().queue(original -> {
if (original == null) return;
List<Message> toRemove = messages.stream().filter(message -> !original.getId().equals(message.getId())).toList();
textChannel.deleteMessages(toRemove).queue(done -> then.editOriginalEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully purged `%s` messages\n\n*This message will be removed <t:%s:R>*".formatted(
amount,
(System.currentTimeMillis() + MESSAGE_DELETE_DELAY) / 1000
))
.build()).queue(message -> message.delete().queueAfter(MESSAGE_DELETE_DELAY, TimeUnit.MILLISECONDS)));
});
});
});
}
}

@ -0,0 +1,47 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.service.DiscordService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor @Getter
public class Reminder {
/**
* What we should remind the user of
*/
private final String reminder;
/**
* The channel ID to send the reminder to
*/
private final String channelId;
/**
* The date the reminder should end
*/
private final Date endDate;
/**
* Check if the reminder is expired
*
* @return If the reminder is expired
*/
public boolean isExpired() {
return System.currentTimeMillis() >= endDate.getTime();
}
/**
* Get the channel to send the reminder to
*
* @return The channel
*/
public TextChannel getChannel() {
return DiscordService.JDA.getTextChannelById(channelId);
}
}

@ -0,0 +1,81 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.reminder.command.ReminderCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "Reminder Feature")
@DependsOn("discordService")
public class ReminderFeature extends Feature {
public static final int MAX_REMINDERS = 5; // 5 reminders
public static final long MAX_REMINDER_LENGTH = TimeUnit.DAYS.toMillis(30); // 1 month
private final GuildService guildService;
@Autowired
public ReminderFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Reminder", true, Category.GENERAL);
this.guildService = guildService;
super.registerCommand(commandService, context.getBean(ReminderCommand.class));
}
@Scheduled(cron = "*/30 * * * * *")
public void checkReminders() {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
ReminderProfile reminderProfile = batGuild.getProfile(ReminderProfile.class);
if (reminderProfile == null) {
continue;
}
for (Map.Entry<User, List<Reminder>> entry : reminderProfile.getReminders().entrySet()) {
User user = entry.getKey();
List<Reminder> toRemove = new ArrayList<>();
List<Reminder> reminders = entry.getValue();
for (Reminder reminder : reminders) {
if (!reminder.isExpired()) {
continue;
}
toRemove.add(reminder);
TextChannel channel = reminder.getChannel();
if (channel != null) {
channel.sendMessage("Hey %s! ⏰ It's time for your reminder: `%s`".formatted(
user.getAsMention(),
reminder.getReminder()
)).queue();
}
}
toRemove.forEach(reminder -> reminderProfile.removeReminder(user, reminder));
}
}
}
}

@ -0,0 +1,143 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.*;
/**
* @author Fascinated (fascinated7)
*/
@Getter
public class ReminderProfile extends Serializable {
/**
* The reminders in the guild
*/
private final Map<User, List<Reminder>> reminders = new HashMap<>();
/*
* Get the amount of reminders a user has
*
* @param user The user to get the reminders for
* @return The amount of reminders the user has
*/
public int getReminderCount(User user) {
List<Reminder> reminderList = reminders.get(user);
return reminderList == null ? 0 : reminderList.size();
}
/*
* Remove all reminders for a user
*
* @param user The user to remove the reminders for
*/
public void removeReminders(User user) {
reminders.remove(user);
}
/*
* Check if a user has reminders
*
* @param user The user to check for
* @return If the user has reminders
*/
public boolean hasReminders(User user) {
return reminders.containsKey(user);
}
/*
* Get the reminders for a user
*
* @param user The user to get the reminders for
* @return The reminders for the user
*/
public List<Reminder> getReminders(User user) {
return reminders.get(user);
}
/*
* Add a reminder for a user
*
* @param user The user to add the reminder for
* @param reminder The reminder to add
*/
public Reminder addReminder(User user, TextChannel channel, String reason, Date endDate) {
List<Reminder> reminderList = reminders.get(user);
if (reminderList == null) {
reminderList = new ArrayList<>();
}
Reminder reminder = new Reminder(reason, channel.getId(), endDate);
reminderList.add(reminder);
reminders.put(user, reminderList);
return reminder;
}
/*
* Remove a reminder for a user
*
* @param user The user to remove the reminder for
* @param reminder The reminder to remove
*/
public void removeReminder(User user, Reminder reminder) {
List<Reminder> reminderList = reminders.get(user);
if (reminderList == null) {
return;
}
reminderList.remove(reminder);
reminders.put(user, reminderList);
if (reminderList.isEmpty()) {
reminders.remove(user);
}
}
@Override
public void load(Document document, Gson gson) {
for (String key : document.keySet()) {
User user = DiscordService.JDA.getUserById(key);
if (user == null) {
continue;
}
List<Reminder> reminderList = new ArrayList<>();
for (Document reminderDocument : document.getList(key, Document.class)) {
reminderList.add(new Reminder(
reminderDocument.getString("reminder"),
reminderDocument.getString("channelId"),
reminderDocument.getDate("endDate")
));
}
reminders.put(user, reminderList);
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (Map.Entry<User, List<Reminder>> entry : reminders.entrySet()) {
List<Document> reminderDocuments = new ArrayList<>();
List<Reminder> value = entry.getValue();
if (value == null || value.isEmpty()) {
continue;
}
for (Reminder reminder : value) {
Document reminderDocument = new Document();
reminderDocument.append("reminder", reminder.getReminder());
reminderDocument.append("channelId", reminder.getChannelId());
reminderDocument.append("endDate", reminder.getEndDate());
reminderDocuments.add(reminderDocument);
}
document.append(entry.getKey().getId(), reminderDocuments);
}
return document;
}
@Override
public void reset() {
reminders.clear();
}
}

@ -0,0 +1,37 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.reminder.ReminderProfile;
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("reminder:clear.sub")
@CommandInfo(name = "clear", description = "Clear all your active reminders.")
public class ClearSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (!profile.hasReminders(user.getDiscordUser())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have any active reminders.")
.build()).queue();
return;
}
int reminderCount = profile.getReminderCount(user.getDiscordUser());
profile.removeReminders(user.getDiscordUser());
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully cleared %s reminders.".formatted(reminderCount))
.build()
).queue();
}
}

@ -0,0 +1,46 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.reminder.Reminder;
import cc.fascinated.bat.features.reminder.ReminderProfile;
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("reminder:list.sub")
@CommandInfo(name = "list", description = "View your active reminders.")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (!profile.hasReminders(user.getDiscordUser())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have any active reminders.")
.build()).queue();
return;
}
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Active Reminders");
for (Reminder reminder : profile.getReminders(user.getDiscordUser())) {
description.appendLine("%s - <t:%s:R> %s".formatted(
reminder.getReminder(),
reminder.getEndDate().toInstant().getEpochSecond(),
reminder.getChannel().getAsMention()
), true);
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(description.build())
.build()
).queue();
}
}

@ -0,0 +1,22 @@
package cc.fascinated.bat.features.reminder.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 = "reminder", description = "Set or view reminders.")
public class ReminderCommand extends BatCommand {
@Autowired
public ReminderCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(SetSubCommand.class));
super.addSubCommand(context.getBean(ListSubCommand.class));
super.addSubCommand(context.getBean(ClearSubCommand.class));
}
}

@ -0,0 +1,80 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TimeUtils;
import cc.fascinated.bat.features.reminder.Reminder;
import cc.fascinated.bat.features.reminder.ReminderFeature;
import cc.fascinated.bat.features.reminder.ReminderProfile;
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.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component("reminder:set.sub")
@CommandInfo(name = "set", description = "Set a reminder.")
public class SetSubCommand extends BatSubCommand {
public SetSubCommand() {
super.addOption(OptionType.STRING, "reminder", "The reminder to set.", true);
super.addOption(OptionType.STRING, "time", "After how long should the reminder be sent. (eg: 5m)", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (profile.getReminderCount(user.getDiscordUser()) >= ReminderFeature.MAX_REMINDERS) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have reached the maximum amount of reminders.")
.build()).queue();
return;
}
OptionMapping reminderOption = event.getOption("reminder");
if (reminderOption == null) {
return;
}
OptionMapping timeOption = event.getOption("time");
if (timeOption == null) {
return;
}
String reminderText = reminderOption.getAsString();
long time = TimeUtils.fromString(timeOption.getAsString());
if (time == 0) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid time format.")
.build()).queue();
return;
}
if (time < TimeUnit.MINUTES.toMillis(1)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The time must be at least 1 minute.")
.build()).queue();
return;
}
if (time > ReminderFeature.MAX_REMINDER_LENGTH) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The time must be at most %s.".formatted(TimeUtils.format(ReminderFeature.MAX_REMINDER_LENGTH)))
.build()).queue();
return;
}
Reminder reminder = profile.addReminder(user.getDiscordUser(), event.getChannel().asTextChannel(), reminderText, new Date(System.currentTimeMillis() + time));
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Reminder for `%s` set, you will be reminded <t:%s:R>".formatted(reminderText,
reminder.getEndDate().toInstant().getEpochSecond()))
.build()).queue();
}
}

@ -22,6 +22,7 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
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.util.ArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -40,7 +41,9 @@ public class CurrentSubCommand extends BatSubCommand implements EventListener {
@Override @Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) { public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.currentSong(spotifyService, user).build()).addComponents(createActions()).queue(); event.replyEmbeds(SpotifyFeature.currentSong(spotifyService, user).build()).addComponents(createActions()).queue(message -> {
message.editOriginalComponents(new ArrayList<>()).queueAfter(5, TimeUnit.MINUTES); // Remove the buttons after 5 minutes
});
} }
@Override @SneakyThrows @Override @SneakyThrows

@ -0,0 +1,50 @@
package cc.fascinated.bat.features.welcomer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import net.dv8tion.jda.api.EmbedBuilder;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class WelcomerEmbed {
/**
* The title of the embed
*/
private String title;
/**
* The description of the embed
*/
@NonNull private String description;
/**
* The color of the embed
*/
@NonNull private String color;
/**
* Should we ping the user before sending the message?
*/
private boolean pingBeforeSend;
/**
* Builds the embed and replaces the placeholders
*
* @return The built embed
*/
public EmbedBuilder buildEmbed(Object... replacements) {
EmbedBuilder embedBuilder = new EmbedBuilder();
if (title != null) {
embedBuilder.setTitle(WelcomerPlaceholders.replaceAllPlaceholders(title, replacements));
}
embedBuilder.setDescription(WelcomerPlaceholders.replaceAllPlaceholders(description, replacements));
embedBuilder.setColor(Integer.parseInt(color, 16));
return embedBuilder;
}
}

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.welcomer.command.WelcomerCommand;
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 WelcomerFeature extends Feature {
@Autowired
public WelcomerFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Welcomer", true,Category.SERVER);
super.registerCommand(commandService, context.getBean(WelcomerCommand.class));
}
}

@ -0,0 +1,32 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class WelcomerListener implements EventListener {
private final WelcomerFeature welcomerFeature;
@Autowired
public WelcomerListener(WelcomerFeature welcomerFeature) {
this.welcomerFeature = welcomerFeature;
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
if (guild.getFeatureProfile().isFeatureDisabled(welcomerFeature)) { // Check if the feature is disabled
return;
}
WelcomerProfile profile = guild.getWelcomerProfile();
profile.sendWelcomeMessage(guild, user);
}
}

@ -0,0 +1,27 @@
package cc.fascinated.bat.features.welcomer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class WelcomerMessage {
/**
* The message to send
*/
private String message;
/**
* Builds the message and replaces the placeholders
*
* @return The built message
*/
public String buildMessage(Object... replacements) {
return WelcomerPlaceholders.replaceAllPlaceholders(message, replacements);
}
}

@ -0,0 +1,82 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public enum WelcomerPlaceholders {
USER_MENTION("{user_mention}", BatUser.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatUser) object).getDiscordUser().getAsMention();
}
},
USER_NAME("{user_name}", BatUser.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatUser) object).getName();
}
},
GUILD_NAME("{guild_name}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatGuild) object).getName();
}
},
JOIN_DATE("{join_date}", null) {
@Override
public String replacePlaceholder(Object object) {
return "<t:%s>".formatted(new Date().toInstant().getEpochSecond());
}
};
/**
* The placeholder string that will get replaced
*/
private final String placeholder;
/**
* The class that the placeholder is associated with
*/
private final Class<?> clazz;
/**
* Replaces the placeholder with the string based on the overridden method
*
* @param object The object to replace the placeholder with
* @return The string with the placeholder replaced
*/
public String replacePlaceholder(Object object) {
if (clazz != null && !clazz.isInstance(object)) {
throw new IllegalArgumentException("Object is not an instance of " + clazz.getName());
}
return null;
}
/**
* Replaces all placeholders in a message with the objects provided
*
* @param message The message to replace the placeholders in
* @param objects The objects to replace the placeholders with
* @return The message with the placeholders replaced
*/
public static String replaceAllPlaceholders(String message, Object... objects) {
for (WelcomerPlaceholders placeholder : values()) {
for (Object object : objects) {
if (placeholder.getClazz() != null && !placeholder.getClazz().isInstance(object)) {
continue;
}
message = message.replace(placeholder.getPlaceholder(), placeholder.replacePlaceholder(object));
}
}
return message;
}
}

@ -0,0 +1,146 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Getter @Setter
public class WelcomerProfile extends Serializable {
/**
* The welcomer message, null if we're using an embed
*/
private WelcomerMessage welcomerMessage;
/**
* The welcomer embed, null if we're using a message
*/
private WelcomerEmbed welcomerEmbed;
/**
* The channel to send the welcomer messages to
*/
private TextChannel channel;
/**
* Gets the welcomer message
*
* @return The welcomer message, false if we're using an embed
*/
public boolean isEmbed() {
return welcomerEmbed != null;
}
/**
* Gets the welcomer message
*
* @return The welcomer message, false if we're using an embed
*/
public boolean isMessage() {
return welcomerMessage != null;
}
/**
* Sets the welcomer message
* <p>
* This will disable the embed if it's enabled
* </p>
*/
public void setMessage(String message) {
welcomerMessage = new WelcomerMessage(message);
welcomerEmbed = null;
}
/**
* Sets the welcomer embed
* <p>
* This will disable the message if it's enabled
* <p>
*/
public void setEmbed(String title, String description, String color, boolean pingBeforeSend) {
welcomerEmbed = new WelcomerEmbed(title, description, color, pingBeforeSend);
welcomerMessage = null;
}
/**
* Sends the welcome message to the user
*
* @param guild The guild to send the message in
* @param user The user to send the message to
*/
public void sendWelcomeMessage(BatGuild guild, BatUser user) {
if (this.channel == null || (!this.isMessage() && !this.isEmbed())) {
return;
}
if (welcomerEmbed != null) {
if (welcomerEmbed.isPingBeforeSend()) { // Ping the user before sending the message
this.channel.sendMessage(user.getDiscordUser().getAsMention()).queue();
}
this.channel.sendMessageEmbeds(welcomerEmbed.buildEmbed(guild, user).build()).queue();
return;
}
if (welcomerMessage != null) {
this.channel.sendMessage(welcomerMessage.buildMessage(guild, user)).queue();
}
}
@Override
public void load(Document document, Gson gson) {
Document welcomerMessageDocument = document.get("welcomerMessage", Document.class);
if (welcomerMessageDocument != null) {
welcomerMessage = new WelcomerMessage(
welcomerMessageDocument.getString("message")
);
}
Document welcomerEmbedDocument = document.get("welcomerEmbed", Document.class);
if (welcomerEmbedDocument != null) {
welcomerEmbed = new WelcomerEmbed(
welcomerEmbedDocument.getString("title"),
welcomerEmbedDocument.getString("description"),
welcomerEmbedDocument.getString("color"),
welcomerEmbedDocument.getBoolean("pingBeforeSend")
);
}
String channelId = document.getString("channelId");
if (channelId != null) {
TextChannel textChannel = DiscordService.JDA.getTextChannelById(channelId);
if (textChannel != null) {
channel = textChannel;
}
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
if (welcomerMessage != null) {
document.put("welcomerMessage", new Document("message", welcomerMessage.getMessage()));
}
if (welcomerEmbed != null) {
document.put("welcomerEmbed", new Document()
.append("title", welcomerEmbed.getTitle())
.append("description", welcomerEmbed.getDescription())
.append("color", welcomerEmbed.getColor())
.append("pingBeforeSend", welcomerEmbed.isPingBeforeSend())
);
}
if (channel != null) {
document.put("channelId", channel.getId());
}
return document;
}
@Override
public void reset() {
welcomerMessage = null;
welcomerEmbed = null;
}
}

@ -0,0 +1,41 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
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.concrete.TextChannel;
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.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:channel.sub")
@CommandInfo(name = "channel", description = "Set the welcomer channel")
public class ChannelSubCommand extends BatSubCommand {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel to send the welcomer messages to", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping channelOption = event.getOption("channel");
if (channelOption == null) {
return;
}
TextChannel textChannel = channelOption.getAsChannel().asTextChannel();
profile.setChannel(textChannel);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The welcomer channel has been set to %s".formatted(textChannel.getAsMention()))
.build()).queue();
}
}

@ -0,0 +1,64 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerPlaceholders;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
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("welcomer:current.sub")
@CommandInfo(name = "current", description = "View the current welcomer configuration")
public class CurrentSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
if (!profile.isEmbed() && !profile.isMessage()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The welcomer is not configured.\n\n" + getPlaceholders(guild, user))
.build()).queue();
return;
}
if (profile.isEmbed()) {
event.replyEmbeds(profile.getWelcomerEmbed().buildEmbed(guild, user)
.appendDescription("\n\n" + getPlaceholders(guild, user))
.build()).queue();
return;
}
if (profile.isMessage()) {
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("**Preview:** %s\n*note: the real message won't be an embed*\n\n%s".formatted(
profile.getWelcomerMessage().buildMessage(guild, user),
getPlaceholders(guild, user)
))
.build()).queue();
}
}
/**
* Get the placeholders that the user can use
*
* @param replacements What to replace the placeholders using
* @return The placeholders
*/
public String getPlaceholders(Object... replacements) {
StringBuilder builder = new StringBuilder();
builder.append("**Available Placeholders:**\n");
for (WelcomerPlaceholders placeholder : WelcomerPlaceholders.values()) {
String placeholderString = placeholder.getPlaceholder();
builder.append("`").append(placeholderString).append("` - ")
.append(WelcomerPlaceholders.replaceAllPlaceholders(placeholderString, replacements)).append("\n");
}
return builder.toString();
}
}

@ -0,0 +1,98 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
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.stereotype.Component;
import java.awt.*;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:embed.sub")
@CommandInfo(name = "embed", description = "Set the welcomer embed (this will remove the welcomer plain message if set)")
public class EmbedSubCommand extends BatSubCommand {
public EmbedSubCommand() {
super.addOption(OptionType.BOOLEAN, "ping-before-send", "Should we ping the user before sending the message?", true);
super.addOption(OptionType.STRING, "description", "The description of the embed", true);
super.addOption(OptionType.STRING, "color", "The color of the embed", true);
super.addOption(OptionType.STRING, "title", "The title of the embed (only set if you want a title)", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping titleOption = event.getOption("title");
OptionMapping descriptionOption = event.getOption("description");
OptionMapping colorOption = event.getOption("color");
OptionMapping pingBeforeSendOption = event.getOption("ping-before-send");
if (descriptionOption == null || colorOption == null || pingBeforeSendOption == null) {
return;
}
String title = titleOption == null ? null : titleOption.getAsString();
String description = descriptionOption.getAsString();
String color = colorOption.getAsString();
boolean pingBeforeSend = pingBeforeSendOption.getAsBoolean();
// Remove # if the user added it
color = color.replace("#", "");
// Validate the input
if (color.length() != 6 || Color.decode("#" + color).getRGB() == -1){
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The color must be a valid hex color code\n" +
"You can use this website to get a hex color code: https://htmlcolorcodes.com")
.build()).queue();
return;
}
if (title != null && title.length() > 128) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The title must be less than 128 characters")
.build()).queue();
return;
}
if (description.length() > 512) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The description must be less than 512 characters")
.build()).queue();
return;
}
boolean isMessageEnabled = profile.isMessage();
profile.setEmbed(title, description, color, pingBeforeSend);
EmbedDescriptionBuilder successDescription = new EmbedDescriptionBuilder("Welcomer Embed")
.appendLine("%s Successfully set the welcomer embed!".formatted(Emojis.CHECK_MARK_EMOJI), false);
if (isMessageEnabled) {
successDescription.appendLine("*This has removed the plain message welcomer*", false);
}
successDescription.emptyLine();
successDescription.appendLine("**Configuration:**", false);
if (title != null) {
successDescription.appendLine("Title: `%s`".formatted(title), true);
}
successDescription.appendLine("Description: `%s`".formatted(description), true);
successDescription.appendLine("Color: `#%s`".formatted(color), true);
successDescription.appendLine("Ping Before Send: %s".formatted(pingBeforeSend ? Emojis.CHECK_MARK_EMOJI.getFormatted() + " *(Preview won't ping you)*" : Emojis.CROSS_MARK_EMOJI), true);
successDescription.emptyLine();
successDescription.appendLine("**Preview Below:**", false);
event.replyEmbeds(
EmbedUtils.successEmbed()
.setDescription(successDescription.build())
.build(),
profile.getWelcomerEmbed().buildEmbed(guild, user).build()
).queue();
}
}

@ -0,0 +1,52 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerPlaceholders;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
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.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:message.sub")
@CommandInfo(name = "message", description = "Set the welcomer message (this will remove the welcomer embed if set)")
public class MessageSubCommand extends BatSubCommand {
public MessageSubCommand() {
super.addOption(OptionType.STRING, "message", "The message to send", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping messageOption = event.getOption("message");
if (messageOption == null) {
return;
}
String message = messageOption.getAsString();
boolean isEmbedEnabled = profile.isEmbed();
profile.setMessage(message);
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Welcomer Message")
.appendLine("%s Set the message to `%s`".formatted(Emojis.CHECK_MARK_EMOJI, message), false);
if (isEmbedEnabled) {
description.appendLine("*This has removed the embed welcomer*", false);
}
description.emptyLine();
description.appendLine("**Preview:** %s".formatted(WelcomerPlaceholders.replaceAllPlaceholders(message, guild, user)), false);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
}
}

@ -0,0 +1,36 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
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("welcomer:reset.sub")
@CommandInfo(name = "reset", description = "Clear the welcomer configuration")
public class ResetSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
if (!profile.isEmbed() && !profile.isMessage()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The welcomer is not configured")
.build()).queue();
return;
}
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The welcomer configuration has been reset")
.build()).queue();
}
}

@ -0,0 +1,25 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatCommand;
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 = "welcomer", description = "Configure the welcomer on your server", requiredPermissions = Permission.MANAGE_SERVER)
public class WelcomerCommand extends BatCommand {
@Autowired
public WelcomerCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(MessageSubCommand.class));
super.addSubCommand(context.getBean(EmbedSubCommand.class));
super.addSubCommand(context.getBean(CurrentSubCommand.class));
super.addSubCommand(context.getBean(ChannelSubCommand.class));
super.addSubCommand(context.getBean(ResetSubCommand.class));
}
}

@ -7,6 +7,8 @@ import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile; import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.features.logging.LogProfile; import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile; import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.premium.PremiumProfile; import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.MongoService; import cc.fascinated.bat.service.MongoService;
@ -18,7 +20,6 @@ import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -29,7 +30,6 @@ import java.util.Map;
*/ */
@Getter @Getter
@Setter @Setter
@Document(collection = "guilds")
public class BatGuild extends ProfileHolder { public class BatGuild extends ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(BatGuild.class); private static final Logger log = LoggerFactory.getLogger(BatGuild.class);
/** /**
@ -49,11 +49,21 @@ public class BatGuild extends ProfileHolder {
*/ */
private Date createdAt; private Date createdAt;
/**
* The guild as the JDA Guild
*/
private Guild guild;
public BatGuild(@NonNull String id, @NonNull org.bson.Document document) { public BatGuild(@NonNull String id, @NonNull org.bson.Document document) {
this.id = id; this.id = id;
this.document = document; this.document = document;
boolean newAccount = this.document.isEmpty(); boolean newAccount = this.document.isEmpty();
this.createdAt = newAccount ? new Date() : document.getDate("createdAt"); this.createdAt = newAccount ? new Date() : document.getDate("createdAt");
Guild guild = DiscordService.JDA.getGuildById(id);
if (guild != null) {
this.guild = guild;
}
} }
/** /**
@ -71,7 +81,10 @@ public class BatGuild extends ProfileHolder {
* @return the guild * @return the guild
*/ */
public Guild getDiscordGuild() { public Guild getDiscordGuild() {
return DiscordService.JDA.getGuildById(id); if (guild == null) {
guild = DiscordService.JDA.getGuildById(id);
}
return guild;
} }
/** /**
@ -119,6 +132,24 @@ public class BatGuild extends ProfileHolder {
return getProfile(LogProfile.class); return getProfile(LogProfile.class);
} }
/**
* Gets the reminder profile
*
* @return the reminder profile
*/
public ReminderProfile getReminderProfile() {
return getProfile(ReminderProfile.class);
}
/**
* Gets the welcomer profile
*
* @return the welcomer profile
*/
public WelcomerProfile getWelcomerProfile() {
return getProfile(WelcomerProfile.class);
}
/** /**
* Saves the user * Saves the user
*/ */

@ -13,10 +13,7 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -28,9 +25,7 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
@Getter @Getter
@Setter @Setter
@Document(collection = "users")
public class BatUser extends ProfileHolder { public class BatUser extends ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(BatUser.class);
/** /**
* The document that belongs to this user * The document that belongs to this user
*/ */
@ -53,6 +48,11 @@ public class BatUser extends ProfileHolder {
*/ */
private Date createdAt; private Date createdAt;
/**
* The discord user associated with this user
*/
private User user;
public BatUser(@NonNull String id, @NonNull org.bson.Document document) { public BatUser(@NonNull String id, @NonNull org.bson.Document document) {
this.id = id; this.id = id;
this.document = document; this.document = document;
@ -61,6 +61,7 @@ public class BatUser extends ProfileHolder {
User user = DiscordService.JDA.getUserById(id); User user = DiscordService.JDA.getUserById(id);
if (user != null) { if (user != null) {
this.user = user;
this.globalName = user.getGlobalName(); this.globalName = user.getGlobalName();
} }
} }
@ -78,7 +79,10 @@ public class BatUser extends ProfileHolder {
* @return the guild * @return the guild
*/ */
public User getDiscordUser() { public User getDiscordUser() {
return DiscordService.JDA.getUserById(id); if (user == null) {
user = DiscordService.JDA.getUserById(id);
}
return user;
} }
/** /**

@ -138,6 +138,7 @@ public class CommandService extends ListenerAdapter {
@Override @Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
long before = System.currentTimeMillis();
Guild discordGuild = event.getGuild(); Guild discordGuild = event.getGuild();
if (event.getUser().isBot()) { if (event.getUser().isBot()) {
return; return;
@ -230,9 +231,10 @@ public class CommandService extends ListenerAdapter {
} }
} }
log.info("Executing command \"{}\" for user \"{}\"", commandName, user.getDiscordUser().getName());
executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(), executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(),
event.getMember(), event.getInteraction()); event.getMember(), event.getInteraction());
log.info("Executed command \"{}\" for user \"{}\" (took: {}ms)", commandName, user.getDiscordUser().getName(),
System.currentTimeMillis() - before);
} catch (Exception ex) { } catch (Exception ex) {
log.error("An error occurred while executing command \"{}\"", commandName, ex); log.error("An error occurred while executing command \"{}\"", commandName, ex);
event.replyEmbeds(EmbedUtils.errorEmbed() event.replyEmbeds(EmbedUtils.errorEmbed()

@ -16,6 +16,7 @@ import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent; import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
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;
@ -290,4 +291,17 @@ public class EventService extends ListenerAdapter {
listener.onUserUpdateAvatar(user, event.getOldAvatarUrl(), event.getNewAvatarUrl(), event); listener.onUserUpdateAvatar(user, event.getOldAvatarUrl(), event.getNewAvatarUrl(), event);
} }
} }
@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
if (event.getEntity().getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getEntity().getGuild().getId());
BatUser user = userService.getUser(event.getEntity().getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildVoiceUpdate(guild, user, event);
}
}
} }

@ -12,12 +12,12 @@ import org.springframework.stereotype.Service;
@Service @Service
public class MongoService { public class MongoService {
public static MongoService INSTANCE; public static MongoService INSTANCE;
private final MongoTemplate mongo; private final MongoTemplate mongoTemplate;
@Autowired @Autowired
public MongoService(MongoTemplate mongo) { public MongoService(MongoTemplate mongo) {
INSTANCE = this; INSTANCE = this;
this.mongo = mongo; this.mongoTemplate = mongo;
} }
/** /**
@ -26,7 +26,7 @@ public class MongoService {
* @return The guilds collection * @return The guilds collection
*/ */
public MongoCollection<Document> getGuildsCollection() { public MongoCollection<Document> getGuildsCollection() {
return mongo.getCollection("guilds"); return mongoTemplate.getCollection("guilds");
} }
/** /**
@ -35,6 +35,6 @@ public class MongoService {
* @return The users collection * @return The users collection
*/ */
public MongoCollection<Document> getUsersCollection() { public MongoCollection<Document> getUsersCollection() {
return mongo.getCollection("users"); return mongoTemplate.getCollection("users");
} }
} }

@ -8,6 +8,7 @@ import com.mongodb.client.model.Filters;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent; import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import org.bson.Document; import org.bson.Document;
@ -57,23 +58,28 @@ public class UserService implements EventListener {
if (users.containsKey(id)) { if (users.containsKey(id)) {
return users.get(id); return users.get(id);
} }
if (DiscordService.JDA.getUserById(id) == null) { User user = DiscordService.JDA.getUserById(id);
log.warn("Attempted to get user with ID \"{}\" but it does not exist", id); if (user == null) {
log.warn("Attempted to get user with ID \"{}\" but they do not exist", id);
return null;
}
if (user.isBot()) {
log.warn("Attempted to get user with ID \"{}\" but they are a bot", id);
return null; return null;
} }
// User is not cached // User is not cached
Document document = MongoService.INSTANCE.getUsersCollection().find(Filters.eq("_id", id)).first(); Document document = MongoService.INSTANCE.getUsersCollection().find(Filters.eq("_id", id)).first();
if (document != null) { if (document != null) {
BatUser user = new BatUser(id, document); BatUser batUser = new BatUser(id, document);
users.put(id, user); users.put(id, batUser);
log.info("Loaded user \"{}\" in {}ms", user.getName(),System.currentTimeMillis() - before); log.info("Loaded user \"{}\" in {}ms", batUser.getName(),System.currentTimeMillis() - before);
return user; return batUser;
} }
// New user // New user
BatUser user = new BatUser(id, new Document()); BatUser batUser = new BatUser(id, new Document());
users.put(id, user); users.put(id, batUser);
log.info("Created user \"{}\" - \"{}\"", user.getName(), user.getId()); log.info("Created user \"{}\" - \"{}\"", user.getName(), user.getId());
return user; return batUser;
} }
@Override @Override
@ -92,7 +98,6 @@ public class UserService implements EventListener {
@Override @Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) { public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
log.info("User \"{}\" changed their name from \"{}\" to \"{}\"", user.getName(), oldName, newName);
user.setGlobalName(newName); user.setGlobalName(newName);
} }
} }