From 048d2856f9b6df95ec431642cef96e8024c6bba0 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 3 Jul 2024 00:10:02 +0100 Subject: [PATCH] impl reminders --- .../bat/features/reminder/Reminder.java | 47 ++++++ .../features/reminder/ReminderFeature.java | 81 ++++++++++ .../features/reminder/ReminderProfile.java | 143 ++++++++++++++++++ .../reminder/command/ClearSubCommand.java | 37 +++++ .../reminder/command/ListSubCommand.java | 46 ++++++ .../reminder/command/ReminderCommand.java | 22 +++ .../reminder/command/SetSubCommand.java | 80 ++++++++++ .../spotify/command/CurrentSubCommand.java | 5 +- .../cc/fascinated/bat/model/BatGuild.java | 10 ++ 9 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/Reminder.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/ReminderFeature.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/ReminderProfile.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/command/ClearSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/command/ListSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/command/ReminderCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/reminder/command/SetSubCommand.java diff --git a/src/main/java/cc/fascinated/bat/features/reminder/Reminder.java b/src/main/java/cc/fascinated/bat/features/reminder/Reminder.java new file mode 100644 index 0000000..5a10194 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/Reminder.java @@ -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); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/ReminderFeature.java b/src/main/java/cc/fascinated/bat/features/reminder/ReminderFeature.java new file mode 100644 index 0000000..0243bd8 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/ReminderFeature.java @@ -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.toMicros(7); // 7 days + + 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> entry : reminderProfile.getReminders().entrySet()) { + User user = entry.getKey(); + List toRemove = new ArrayList<>(); + List 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)); + } + } + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/ReminderProfile.java b/src/main/java/cc/fascinated/bat/features/reminder/ReminderProfile.java new file mode 100644 index 0000000..001153e --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/ReminderProfile.java @@ -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> 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 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 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 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 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 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> entry : reminders.entrySet()) { + List reminderDocuments = new ArrayList<>(); + List 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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/command/ClearSubCommand.java b/src/main/java/cc/fascinated/bat/features/reminder/command/ClearSubCommand.java new file mode 100644 index 0000000..4977bf3 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/command/ClearSubCommand.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/command/ListSubCommand.java b/src/main/java/cc/fascinated/bat/features/reminder/command/ListSubCommand.java new file mode 100644 index 0000000..76f76c2 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/command/ListSubCommand.java @@ -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 - %s".formatted( + reminder.getReminder(), + reminder.getEndDate().toInstant().getEpochSecond(), + reminder.getChannel().getAsMention() + ), true); + } + event.replyEmbeds(EmbedUtils.genericEmbed() + .setDescription(description.build()) + .build() + ).queue(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/command/ReminderCommand.java b/src/main/java/cc/fascinated/bat/features/reminder/command/ReminderCommand.java new file mode 100644 index 0000000..ba51e30 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/command/ReminderCommand.java @@ -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)); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/reminder/command/SetSubCommand.java b/src/main/java/cc/fascinated/bat/features/reminder/command/SetSubCommand.java new file mode 100644 index 0000000..7fca81c --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/reminder/command/SetSubCommand.java @@ -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 in ".formatted(reminderText, + reminder.getEndDate().toInstant().getEpochSecond())) + .build()).queue(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java index f9267d9..69518a2 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/CurrentSubCommand.java @@ -22,6 +22,7 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.concurrent.TimeUnit; /** @@ -40,7 +41,9 @@ public class CurrentSubCommand extends BatSubCommand implements EventListener { @Override 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 diff --git a/src/main/java/cc/fascinated/bat/model/BatGuild.java b/src/main/java/cc/fascinated/bat/model/BatGuild.java index f3f7726..26ebe8f 100644 --- a/src/main/java/cc/fascinated/bat/model/BatGuild.java +++ b/src/main/java/cc/fascinated/bat/model/BatGuild.java @@ -7,6 +7,7 @@ import cc.fascinated.bat.features.base.profile.FeatureProfile; import cc.fascinated.bat.features.birthday.profile.BirthdayProfile; import cc.fascinated.bat.features.logging.LogProfile; import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile; +import cc.fascinated.bat.features.reminder.ReminderProfile; import cc.fascinated.bat.premium.PremiumProfile; import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.MongoService; @@ -119,6 +120,15 @@ public class BatGuild extends ProfileHolder { return getProfile(LogProfile.class); } + /** + * Gets the reminder profile + * + * @return the reminder profile + */ + public ReminderProfile getReminderProfile() { + return getProfile(ReminderProfile.class); + } + /** * Saves the user */