diff --git a/src/main/java/de/slimecloud/slimeball/config/GuildConfig.java b/src/main/java/de/slimecloud/slimeball/config/GuildConfig.java index 0d5b1350..884a9531 100644 --- a/src/main/java/de/slimecloud/slimeball/config/GuildConfig.java +++ b/src/main/java/de/slimecloud/slimeball/config/GuildConfig.java @@ -23,6 +23,7 @@ import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -69,6 +70,10 @@ public static GuildConfig readFromFile(@NotNull SlimeBot bot, long guild) { @ConfigField(name = "Team", command = "team", description = "Die Team-Rolle", type = ConfigFieldType.ROLE) private Long teamRole; + @Setter + @ConfigField(name = "TeamChannel", command = "teamChannel", description = "Der Team-Channel", type = ConfigFieldType.MESSAGE_CHANNEL) + private Long teamChannel; + @Getter @ConfigField(name = "Beitritts Rollen", command = "autorole", description = "Rollen, die Mitgliedern beim Beitreten gegeben werden", type = ConfigFieldType.ROLE) private final List joinRoles = new ArrayList<>(); @@ -199,6 +204,11 @@ public Optional getTeamRole() { return Optional.ofNullable(teamRole).map(bot.getJda()::getRoleById); } + @NotNull + public Optional getTeamChannel() { + return Optional.ofNullable(teamChannel).map(channel -> getGuild().getChannelById(MessageChannel.class, channel)); + } + @NotNull public Optional getGreetingsChannel() { return Optional.ofNullable(greetingsChannel).map(id -> bot.getJda().getChannelById(GuildMessageChannel.class, id)); diff --git a/src/main/java/de/slimecloud/slimeball/features/reminder/RemindCommand.java b/src/main/java/de/slimecloud/slimeball/features/reminder/RemindCommand.java new file mode 100644 index 00000000..5a8af248 --- /dev/null +++ b/src/main/java/de/slimecloud/slimeball/features/reminder/RemindCommand.java @@ -0,0 +1,110 @@ +package de.slimecloud.slimeball.features.reminder; + +import de.mineking.discordutils.commands.ApplicationCommand; +import de.mineking.discordutils.commands.ApplicationCommandMethod; +import de.mineking.discordutils.commands.Command; +import de.mineking.discordutils.commands.Setup; +import de.mineking.discordutils.commands.context.ICommandContext; +import de.mineking.discordutils.commands.option.Option; +import de.mineking.discordutils.list.ListManager; +import de.slimecloud.slimeball.main.CommandPermission; +import de.slimecloud.slimeball.main.Main; +import de.slimecloud.slimeball.main.SlimeBot; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.utils.TimeFormat; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.List; + +@ApplicationCommand(name = "remind", description = "Setzt einen Reminder") +public class RemindCommand { + @Setup + public static void setup(@NotNull SlimeBot bot, @NotNull ListManager manager, @NotNull Command command) { + command.addSubcommand(manager.createCommand(s -> bot.getReminder()).withDescription("Zeigt alle deine Reminder an")); + } + + public static void createReminder(@NotNull SlimeBot bot, @NotNull SlashCommandInteractionEvent event, + @Nullable Role role, + @NotNull String time, + @NotNull String message + ) throws DateTimeParseException { + if (message.length() > 1024) event.reply("Deine Nachricht darf maximal nur 1024 Zeichen lang sein!").setEphemeral(true).queue(); + Instant timestamp = convertTime(time); + + if (timestamp.isBefore(Instant.now())) event.reply("Deine angegebene Zeit ist schon vergangen!").setEphemeral(true).queue(); + + try { + bot.getReminder().createReminder(event.getMember(), role, timestamp, Instant.now(), message); + event.reply("Reminder wurde gesetzt! Löst aus " + TimeFormat.RELATIVE.format(timestamp)).setEphemeral(true).queue(); + } catch (DateTimeParseException e) { + event.reply("Falsches Zeitformat! Versuche etwas wie \"14:45\" oder \"09:04 04.05.2024\"").setEphemeral(true).queue(); + } + } + + public static Instant convertTime(String time) throws DateTimeParseException { + LocalDateTime now = LocalDateTime.now(Main.timezone); + + return LocalDateTime.parse(time, new DateTimeFormatterBuilder().appendPattern("HH:mm[ dd.MM.yyyy]") + .parseDefaulting(ChronoField.DAY_OF_MONTH, now.getDayOfMonth()) + .parseDefaulting(ChronoField.MONTH_OF_YEAR, now.getMonthValue()) + .parseDefaulting(ChronoField.YEAR, now.getYear()) + .toFormatter() + ).toInstant(Main.timezone); + } + + @ApplicationCommand(name = "me", description = "Setze einen Reminder") + public static class MeCommand { + @ApplicationCommandMethod + public void performCommand(@NotNull SlimeBot bot, @NotNull SlashCommandInteractionEvent event, + @Option(description = "Die Zeit an welcher du erinnert werden möchtest, beispielsweise 13:30") String time, + @Option(description = "Die Sache an die du erinnert werden möchtest") String message + ) { + createReminder(bot, event, null, time, message); + } + } + + @ApplicationCommand(name = "role", description = "Setze einen Reminder") + public static class RoleCommand { + public final CommandPermission permission = CommandPermission.TEAM; + + @ApplicationCommandMethod + public void performCommand(@NotNull SlimeBot bot, @NotNull SlashCommandInteractionEvent event, + @Option(description = "Rolle die erwähnt werden soll") Role role, + @Option(description = "Die Zeit an welcher du erinnern möchtest, beispielsweise 13:30") String time, + @Option(description = "Die Sache an die du erinnern möchtest") String message + ) { + createReminder(bot, event, role, time, message); + } + } + + @ApplicationCommand(name = "delete", description = "Lösche einen aktiven Reminder") + public static class DeleteCommand { + @ApplicationCommandMethod + public void performCommand(@NotNull SlimeBot bot, @NotNull SlashCommandInteractionEvent event, + @Option(description = "Nummer des Reminders") int number + ) { + List reminders = bot.getReminder().getByMember(event.getMember()); + if (reminders.isEmpty()) { + event.reply("Du hast keine aktiven Reminder auf diesem Server!").setEphemeral(true).queue(); + return; + } + + if (number < 1 || number > reminders.size()) { + event.reply("Diesen Reminder gibt es nicht oder ist bereits ausgelaufen!").setEphemeral(true).queue(); + return; + } + + reminders.get(number - 1).delete(); + bot.getRemindManager().scheduleNextReminder(); + + event.reply("Reminder gelöscht!").setEphemeral(true).queue(); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/slimecloud/slimeball/features/reminder/RemindManager.java b/src/main/java/de/slimecloud/slimeball/features/reminder/RemindManager.java new file mode 100644 index 00000000..9ca87d29 --- /dev/null +++ b/src/main/java/de/slimecloud/slimeball/features/reminder/RemindManager.java @@ -0,0 +1,20 @@ +package de.slimecloud.slimeball.features.reminder; + +import de.slimecloud.slimeball.main.SlimeBot; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ScheduledFuture; + +public class RemindManager { + private final ReminderTable table; + private ScheduledFuture scheduledFuture = null; + + public RemindManager(@NotNull SlimeBot bot) { + this.table = bot.getReminder(); + } + + public void scheduleNextReminder() { + if (scheduledFuture != null) scheduledFuture.cancel(true); + table.getNext().flatMap(Reminder::schedule).ifPresent(f -> scheduledFuture = f); + } +} \ No newline at end of file diff --git a/src/main/java/de/slimecloud/slimeball/features/reminder/Reminder.java b/src/main/java/de/slimecloud/slimeball/features/reminder/Reminder.java new file mode 100644 index 00000000..ca2de212 --- /dev/null +++ b/src/main/java/de/slimecloud/slimeball/features/reminder/Reminder.java @@ -0,0 +1,111 @@ +package de.slimecloud.slimeball.features.reminder; + +import de.mineking.discordutils.list.ListContext; +import de.mineking.discordutils.list.ListEntry; +import de.mineking.javautils.database.Column; +import de.mineking.javautils.database.DataClass; +import de.mineking.javautils.database.Table; +import de.slimecloud.slimeball.main.SlimeBot; +import de.slimecloud.slimeball.main.SlimeEmoji; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.UserSnowflake; +import net.dv8tion.jda.api.utils.TimeFormat; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Getter +@AllArgsConstructor +public class Reminder implements DataClass, Comparable, Runnable, ListEntry { + private final SlimeBot bot; + + @Column(autoincrement = true, key = true) + private final int id; + + @Column + private final Guild guild; + @Column + private final UserSnowflake user; + + @Column + @Nullable + private final Role role; + + @Column + private final Instant time; + @Column + private final Instant timeSet; + + @Column + private final String message; + + public Reminder(@NotNull SlimeBot bot) { + this(bot, 0, null, null, null, null, null, null); + } + + @NotNull + @Override + public Table getTable() { + return bot.getReminder(); + } + + @Override + public int compareTo(@NotNull Reminder o) { + return this.getTime().compareTo(o.getTime()); + } + + @Override + public void run() { + if (role == null) { + // Send Private Reminder + EmbedBuilder embedBuilder = new EmbedBuilder() + .setAuthor(guild.getName(), null, guild.getIconUrl()) + .setTitle(SlimeEmoji.EXCLAMATION.toString(guild) + " Reminder!") + .setColor(bot.getColor(guild)) + .setDescription(message + " \n \n" + "(Reminder vor " + TimeFormat.RELATIVE.format(timeSet) + ")"); + + bot.getJda().openPrivateChannelById(user.getIdLong()) + .flatMap(channel -> channel.sendMessageEmbeds(embedBuilder.build())) + .queue(); + } else { + // Send Role Reminder + guild.retrieveMember(user).queue(member -> { + EmbedBuilder embedBuilder = new EmbedBuilder() + .setAuthor(member.getEffectiveName(), null, member.getEffectiveAvatarUrl()) + .setTitle(SlimeEmoji.EXCLAMATION.toString(guild) + " Reminder!") + .setColor(bot.getColor(guild)) + .setDescription(message + " \n \n" + "(Reminder vor " + TimeFormat.RELATIVE.format(timeSet) + ")"); + + bot.loadGuild(guild.getIdLong()).getTeamChannel().ifPresent(channel -> { + channel.sendMessage(role.getAsMention()).setEmbeds(embedBuilder.build()).queue(); + }); + }); + } + + delete(); + bot.getRemindManager().scheduleNextReminder(); + } + + public Optional> schedule() { + long delay = time.toEpochMilli() - System.currentTimeMillis(); + if (delay <= 0) { + run(); + return Optional.empty(); + } + return Optional.of(bot.getExecutor().schedule(this, delay / 1000, TimeUnit.SECONDS)); + } + + @NotNull + @Override + public String build(int index, @NotNull ListContext context) { + return (index + 1) + ". " + TimeFormat.RELATIVE.format(time) + ": " + message; + } +} \ No newline at end of file diff --git a/src/main/java/de/slimecloud/slimeball/features/reminder/ReminderTable.java b/src/main/java/de/slimecloud/slimeball/features/reminder/ReminderTable.java new file mode 100644 index 00000000..42de09a6 --- /dev/null +++ b/src/main/java/de/slimecloud/slimeball/features/reminder/ReminderTable.java @@ -0,0 +1,77 @@ +package de.slimecloud.slimeball.features.reminder; + +import de.mineking.discordutils.list.ListContext; +import de.mineking.discordutils.list.Listable; +import de.mineking.discordutils.ui.MessageMenu; +import de.mineking.discordutils.ui.state.DataState; +import de.mineking.javautils.database.Order; +import de.mineking.javautils.database.Table; +import de.mineking.javautils.database.Where; +import de.slimecloud.slimeball.main.SlimeBot; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public interface ReminderTable extends Table, Listable { + @NotNull + default Optional getNext() { + return selectAll(Order.ascendingBy("time").limit(1)).stream().findFirst(); + } + + default Reminder createReminder(@NotNull Member member, Role role, @NotNull Instant time, @NotNull Instant timeSet, @NotNull String message) { + SlimeBot bot = getManager().getData("bot"); + + Reminder result = insert(new Reminder(bot, 0, member.getGuild(), member, role, time, timeSet, message)); + bot.getRemindManager().scheduleNextReminder(); + return result; + } + + @NotNull + default List getByMember(@NotNull Member member) { + return selectMany(Where.allOf( + Where.equals("user", member.getUser()), + Where.equals("guild", member.getGuild()), + Where.equals("role", null) + ), Order.ascendingBy("time")); + } + + @NotNull + default List getByGuild(@NotNull Guild guild) { + return selectMany(Where.equals("guild", guild)); + } + + /* + * Listable implementation + */ + + @NotNull + @Override + default EmbedBuilder createEmbed(@NotNull DataState state, @NotNull ListContext context) { + EmbedBuilder builder = new EmbedBuilder() + .setTitle("Reminder auf **" + state.getEvent().getGuild().getName() + "**") + .setColor(getManager().getData("bot").getColor(state.getEvent().getGuild())); + + if (context.entries().isEmpty()) builder.setDescription("*Du hast keine aktiven Reminder auf diesem Server*"); + else builder.setFooter("Insgesamt " + context.entries().size() + " Reminder"); + + return builder; + } + + @Override + default void finalizeEmbed(@NotNull EmbedBuilder builder, @NotNull DataState state, @NotNull ListContext context) { + if(context.entries().isEmpty()) return; + builder.appendDescription("\n\nLösche einen Reminder mit " + context.manager().getManager().getCommandManager().getCommand(RemindCommand.DeleteCommand.class).getAsMention()); + } + + @NotNull + @Override + default List getEntries(@NotNull DataState state, @NotNull ListContext context) { + return getByMember(context.event().getMember()); + } +} diff --git a/src/main/java/de/slimecloud/slimeball/main/Main.java b/src/main/java/de/slimecloud/slimeball/main/Main.java index 53adfb81..31ece6b9 100644 --- a/src/main/java/de/slimecloud/slimeball/main/Main.java +++ b/src/main/java/de/slimecloud/slimeball/main/Main.java @@ -11,10 +11,10 @@ @Slf4j public class Main { - public final static Random random = new Random(); - public final static ZoneOffset timezone = ZoneOffset.ofHours(1); + public final static Random random = new Random(); + public final static Gson json = new Gson(); public final static Gson formattedJson = new GsonBuilder() .setPrettyPrinting() diff --git a/src/main/java/de/slimecloud/slimeball/main/SlimeBot.java b/src/main/java/de/slimecloud/slimeball/main/SlimeBot.java index d1953707..f82862d0 100644 --- a/src/main/java/de/slimecloud/slimeball/main/SlimeBot.java +++ b/src/main/java/de/slimecloud/slimeball/main/SlimeBot.java @@ -40,6 +40,10 @@ import de.slimecloud.slimeball.features.poll.PollTable; import de.slimecloud.slimeball.features.quote.QuoteCommand; import de.slimecloud.slimeball.features.quote.QuoteMessageCommand; +import de.slimecloud.slimeball.features.reminder.RemindCommand; +import de.slimecloud.slimeball.features.reminder.RemindManager; +import de.slimecloud.slimeball.features.reminder.Reminder; +import de.slimecloud.slimeball.features.reminder.ReminderTable; import de.slimecloud.slimeball.features.report.Report; import de.slimecloud.slimeball.features.report.ReportBlock; import de.slimecloud.slimeball.features.report.ReportBlockTable; @@ -111,6 +115,9 @@ public class SlimeBot extends ListenerAdapter { private final PollTable polls; + private final ReminderTable reminder; + private RemindManager remindManager; + private final LevelTable level; private final CardDataTable profileData; private final GuildCardTable cardProfiles; @@ -150,6 +157,8 @@ public SlimeBot(@NotNull Config config, @NotNull Dotenv credentials) throws IOEx polls = (PollTable) database.getTable(PollTable.class, Poll.class, () -> new Poll(this), "polls").createTable(); + reminder = (ReminderTable) database.getTable(ReminderTable.class, Reminder.class, () -> new Reminder(this), "reminders").createTable(); + level = (LevelTable) database.getTable(LevelTable.class, Level.class, () -> new Level(this), "levels").createTable(); profileData = (CardDataTable) database.getTable(CardDataTable.class, CardProfileData.class, () -> new CardProfileData(this), "card_data").createTable(); cardProfiles = (GuildCardTable) database.getTable(GuildCardTable.class, GuildCardProfile.class, () -> new GuildCardProfile(this), "guild_card_profiles").createTable(); @@ -165,6 +174,7 @@ public SlimeBot(@NotNull Config config, @NotNull Dotenv credentials) throws IOEx reports = null; reportBlocks = null; polls = null; + reminder = null; level = null; profileData = null; cardProfiles = null; @@ -242,6 +252,11 @@ public void performCommand(@NotNull ICommandContext context) throws Exception { manager.registerCommand(FdmdsCommand.class); + //Register remind commands + if (reminder != null) { + manager.registerCommand(RemindCommand.class); + } else logger.warn("Reminders disabled due to missing database"); + //Register report commands if (reports != null) { manager.registerCommand(ReportCommand.class); @@ -314,6 +329,12 @@ public void onReady(@NotNull ReadyEvent event) { } else logger.warn("Spotify alerts disabled due to missing configuration"); } + // Initialize RemindMe manger + if (reminder != null) { + remindManager = new RemindManager(this); + remindManager.scheduleNextReminder(); + } + new HolidayAlert(this); new BirthdayAlert(this); new BirthdayListener(this); diff --git a/src/main/java/de/slimecloud/slimeball/main/SlimeEmoji.java b/src/main/java/de/slimecloud/slimeball/main/SlimeEmoji.java index 37aead2c..8c3505a2 100644 --- a/src/main/java/de/slimecloud/slimeball/main/SlimeEmoji.java +++ b/src/main/java/de/slimecloud/slimeball/main/SlimeEmoji.java @@ -10,6 +10,7 @@ public enum SlimeEmoji { UP("slimesymvup"), DOWN("slimesymvdw"), + EXCLAMATION("slimesymex"), NUMBER_0("slime0"), NUMBER_1("slime1"),