From 4e6fa50387062055795e12f94e73b6beb2940d1d Mon Sep 17 00:00:00 2001 From: christolis Date: Sun, 3 Mar 2024 01:59:49 +0200 Subject: [PATCH 01/16] feat(cake-day): implement batch insert cake days routine --- application/config.json.template | 3 + .../org/togetherjava/tjbot/Application.java | 4 + .../tjbot/config/CakeDayConfig.java | 7 + .../org/togetherjava/tjbot/config/Config.java | 16 ++- .../togetherjava/tjbot/features/Features.java | 2 + .../tjbot/features/basic/CakeDayRoutine.java | 133 ++++++++++++++++++ .../main/resources/db/V16__Add_Cake_Days.sql | 10 ++ 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java create mode 100644 application/src/main/resources/db/V16__Add_Cake_Days.sql diff --git a/application/config.json.template b/application/config.json.template index 5cfe9ac38e..95ca849e61 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -200,4 +200,7 @@ "assignmentChannelPattern": "community-commands", "announcementChannelPattern": "hall-of-fame" } + "cakeDayConfig": { + "rolePattern": "cakeDayRolePattern" + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 4c228cb02a..f5135d665d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -5,6 +5,8 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.ChunkingFilter; +import net.dv8tion.jda.api.utils.MemberCachePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,6 +85,8 @@ public static void runBot(Config config) { Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath()); JDA jda = JDABuilder.createDefault(config.getToken()) + .setChunkingFilter(ChunkingFilter.ALL) + .setMemberCachePolicy(MemberCachePolicy.ALL) .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) .build(); diff --git a/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java new file mode 100644 index 0000000000..4596841055 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java @@ -0,0 +1,7 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record CakeDayConfig( + @JsonProperty(value = "rolePattern", required = true) String rolePattern) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 60e6622cbc..e70fb4f7d0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -49,6 +49,7 @@ public final class Config { private final String selectRolesChannelPattern; private final String memberCountCategoryPattern; private final TopHelpersConfig topHelpers; + private final CakeDayConfig cakeDayConfig; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -102,7 +103,10 @@ private Config(@JsonProperty(value = "token", required = true) String token, @JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig, @JsonProperty(value = "selectRolesChannelPattern", required = true) String selectRolesChannelPattern, - @JsonProperty(value = "topHelpers", required = true) TopHelpersConfig topHelpers) { + @JsonProperty(value = "topHelpers", + required = true) TopHelpersConfig topHelpers, + @JsonProperty(value = "cakeDayConfig", + required = true) CakeDayConfig cakeDayConfig) { this.token = Objects.requireNonNull(token); this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath); @@ -138,6 +142,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig); this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern); this.topHelpers = Objects.requireNonNull(topHelpers); + this.cakeDayConfig = cakeDayConfig; } /** @@ -431,6 +436,15 @@ public String getSelectRolesChannelPattern() { return selectRolesChannelPattern; } + /** + * Gets the cake day configuration file. + * + * @return the cake day configuration file + */ + public CakeDayConfig getCakeDayConfig() { + return cakeDayConfig; + } + /** * Gets the pattern matching the category that is used to display the total member count. * diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 463c3b5248..17306883f8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,6 +6,7 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.features.basic.CakeDayRoutine; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; @@ -144,6 +145,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem)); features.add(new MarkHelpThreadCloseInDBRoutine(database, helpThreadLifecycleListener)); features.add(new MemberCountDisplayRoutine(config)); + features.add(new CakeDayRoutine(config, database)); features.add(new RSSHandlerRoutine(config, database)); features.add(topHelpersAssignmentRoutine); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java new file mode 100644 index 0000000000..52617123a9 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java @@ -0,0 +1,133 @@ +package org.togetherjava.tjbot.features.basic; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import org.jooq.Query; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.CakeDayConfig; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.records.CakeDaysRecord; +import org.togetherjava.tjbot.features.Routine; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS; + +public class CakeDayRoutine implements Routine { + + private static final Logger logger = LoggerFactory.getLogger(CakeDayRoutine.class); + private static final DateTimeFormatter MONTH_DAY_FORMATTER = + DateTimeFormatter.ofPattern("MM-dd"); + private static final int BULK_INSERT_SIZE = 500; + private final CakeDayConfig config; + private final Database database; + + public CakeDayRoutine(Config config, Database database) { + this.config = config.getCakeDayConfig(); + this.database = database; + } + + /** + * Retrieves the schedule of this routine. Called by the core system once during the startup in + * order to execute the routine accordingly. + *

+ * Changes on the schedule returned by this method afterwards will not be picked up. + * + * @return the schedule of this routine + */ + @Override + public Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.DAYS); + } + + /** + * Triggered by the core system on the schedule defined by {@link #createSchedule()}. + * + * @param jda the JDA instance the bot is operating with + */ + @Override + public void runRoutine(JDA jda) { + if (getCakeDayCount(this.database) == 0) { + int guildsCount = jda.getGuilds().size(); + + logger.info("Found empty cake_days table. Populating from guild count: {}", + guildsCount); + CompletableFuture.runAsync(() -> populateAllGuildCakeDays(jda)) + .handle((result, exception) -> { + if (exception != null) { + logger.error("populateAllGuildCakeDays failed. Message: {}", + exception.getMessage()); + } else { + logger.info("populateAllGuildCakeDays completed."); + } + + return result; + }); + } + } + + private int getCakeDayCount(Database database) { + return database.read(context -> context.fetchCount(CAKE_DAYS)); + } + + private void populateAllGuildCakeDays(JDA jda) { + jda.getGuilds().forEach(this::batchPopulateGuildCakeDays); + } + + private void batchPopulateGuildCakeDays(Guild guild) { + final List queriesBuffer = new ArrayList<>(); + + guild.getMembers().stream().filter(Member::hasTimeJoined).forEach(member -> { + if (queriesBuffer.size() == BULK_INSERT_SIZE) { + database.write(context -> context.batch(queriesBuffer).execute()); + queriesBuffer.clear(); + return; + } + + Optional query = createMemberCakeDayQuery(member, guild.getIdLong()); + query.ifPresent(queriesBuffer::add); + }); + + // Flush the queries buffer so that the remaining ones get written + if (!queriesBuffer.isEmpty()) { + database.write(context -> context.batch(queriesBuffer).execute()); + } + } + + private Optional createMemberCakeDayQuery(Member member, long guildId) { + if (!member.hasTimeJoined()) { + return Optional.empty(); + } + + OffsetDateTime cakeDay = member.getTimeJoined(); + String joinedMonthDay = cakeDay.format(MONTH_DAY_FORMATTER); + + return Optional.of(DSL.insertInto(CAKE_DAYS) + .set(CAKE_DAYS.JOINED_MONTH_DAY, joinedMonthDay) + .set(CAKE_DAYS.JOINED_YEAR, cakeDay.getYear()) + .set(CAKE_DAYS.GUILD_ID, guildId) + .set(CAKE_DAYS.USER_ID, member.getIdLong())); + } + + private List findCakeDaysTodayFromDatabase() { + String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); + + return database + .read(context -> context.selectFrom(CAKE_DAYS) + .where(CAKE_DAYS.JOINED_MONTH_DAY.eq(todayMonthDay)) + .fetch()) + .collect(Collectors.toList()); + } +} diff --git a/application/src/main/resources/db/V16__Add_Cake_Days.sql b/application/src/main/resources/db/V16__Add_Cake_Days.sql new file mode 100644 index 0000000000..c78a39568c --- /dev/null +++ b/application/src/main/resources/db/V16__Add_Cake_Days.sql @@ -0,0 +1,10 @@ +CREATE TABLE cake_days +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + joined_month_day TEXT NOT NULL, + joined_year INT NOT NULL, + guild_id BIGINT NOT NULL, + user_id BIGINT NOT NULL +); + +CREATE INDEX cake_day_idx ON cake_days(joined_month_day); \ No newline at end of file From affca1971457ff67d6a26169edc64df439e759ee Mon Sep 17 00:00:00 2001 From: christolis Date: Sun, 3 Mar 2024 14:50:13 +0200 Subject: [PATCH 02/16] feat(cake-day): implement actual routine logic --- .../tjbot/features/basic/CakeDayRoutine.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java index 52617123a9..86f91a1ef8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java @@ -3,6 +3,8 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.UserSnowflake; import org.jooq.Query; import org.jooq.impl.DSL; import org.slf4j.Logger; @@ -21,6 +23,8 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS; @@ -31,12 +35,15 @@ public class CakeDayRoutine implements Routine { private static final DateTimeFormatter MONTH_DAY_FORMATTER = DateTimeFormatter.ofPattern("MM-dd"); private static final int BULK_INSERT_SIZE = 500; + private final Predicate cakeDayRolePredicate; private final CakeDayConfig config; private final Database database; public CakeDayRoutine(Config config, Database database) { this.config = config.getCakeDayConfig(); this.database = database; + + this.cakeDayRolePredicate = Pattern.compile(this.config.rolePattern()).asPredicate(); } /** @@ -75,7 +82,49 @@ public void runRoutine(JDA jda) { return result; }); + + return; } + + jda.getGuilds().forEach(this::reassignCakeDayRole); + } + + private void reassignCakeDayRole(Guild guild) { + Role cakeDayRole = getCakeDayRoleFromGuild(guild).orElse(null); + + if (cakeDayRole == null) { + logger.warn("Cake day role with pattern {} not found for guild: {}", + config.rolePattern(), guild.getName()); + return; + } + + removeMembersCakeDayRole(cakeDayRole, guild) + .thenCompose(result -> addTodayMembersCakeDayRole(cakeDayRole, guild)) + .join(); + } + + private CompletableFuture addTodayMembersCakeDayRole(Role cakeDayRole, Guild guild) { + return CompletableFuture + .runAsync(() -> findCakeDaysTodayFromDatabase().forEach(cakeDayRecord -> { + UserSnowflake snowflake = UserSnowflake.fromId(cakeDayRecord.getUserId()); + + int anniversary = OffsetDateTime.now().getYear() - cakeDayRecord.getJoinedYear(); + if (anniversary > 0) { + guild.addRoleToMember(snowflake, cakeDayRole).complete(); + } + })); + } + + private CompletableFuture removeMembersCakeDayRole(Role cakeDayRole, Guild guild) { + return CompletableFuture.runAsync(() -> guild.findMembersWithRoles(cakeDayRole) + .onSuccess(members -> removeRoleFromMembers(guild, cakeDayRole, members))); + } + + private void removeRoleFromMembers(Guild guild, Role role, List members) { + members.forEach(member -> { + UserSnowflake snowflake = UserSnowflake.fromId(member.getIdLong()); + guild.removeRoleFromMember(snowflake, role).complete(); + }); } private int getCakeDayCount(Database database) { @@ -121,6 +170,13 @@ private Optional createMemberCakeDayQuery(Member member, long guildId) { .set(CAKE_DAYS.USER_ID, member.getIdLong())); } + private Optional getCakeDayRoleFromGuild(Guild guild) { + return guild.getRoles() + .stream() + .filter(role -> cakeDayRolePredicate.test(role.getName())) + .findFirst(); + } + private List findCakeDaysTodayFromDatabase() { String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); From e709cbfb423eb97e04a6eb56e5e261cc6f796d68 Mon Sep 17 00:00:00 2001 From: christolis Date: Sun, 3 Mar 2024 15:21:10 +0200 Subject: [PATCH 03/16] docs: add JavaDocs --- .../tjbot/config/CakeDayConfig.java | 3 + .../org/togetherjava/tjbot/config/Config.java | 4 +- .../tjbot/features/basic/CakeDayRoutine.java | 107 +++++++++++++++--- 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java index 4596841055..e3f35818c7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Configuration record for the Cake Day feature. + */ public record CakeDayConfig( @JsonProperty(value = "rolePattern", required = true) String rolePattern) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index e70fb4f7d0..502f1b6dba 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -437,9 +437,9 @@ public String getSelectRolesChannelPattern() { } /** - * Gets the cake day configuration file. + * Retrieves the Cake Day configuration. * - * @return the cake day configuration file + * @return the cake-day feature configuration */ public CakeDayConfig getCakeDayConfig() { return cakeDayConfig; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java index 86f91a1ef8..5702a0e821 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java @@ -29,6 +29,12 @@ import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS; +/** + * Represents a routine for managing cake day celebrations. + *

+ * This routine handles the assignment and removal of a designated cake day role to guild members + * based on their anniversary of joining the guild. + */ public class CakeDayRoutine implements Routine { private static final Logger logger = LoggerFactory.getLogger(CakeDayRoutine.class); @@ -39,6 +45,12 @@ public class CakeDayRoutine implements Routine { private final CakeDayConfig config; private final Database database; + /** + * Constructs a new {@link CakeDayRoutine} instance. + * + * @param config the configuration for cake day routines + * @param database the database for accessing cake day data + */ public CakeDayRoutine(Config config, Database database) { this.config = config.getCakeDayConfig(); this.database = database; @@ -46,24 +58,11 @@ public CakeDayRoutine(Config config, Database database) { this.cakeDayRolePredicate = Pattern.compile(this.config.rolePattern()).asPredicate(); } - /** - * Retrieves the schedule of this routine. Called by the core system once during the startup in - * order to execute the routine accordingly. - *

- * Changes on the schedule returned by this method afterwards will not be picked up. - * - * @return the schedule of this routine - */ @Override public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.DAYS); } - /** - * Triggered by the core system on the schedule defined by {@link #createSchedule()}. - * - * @param jda the JDA instance the bot is operating with - */ @Override public void runRoutine(JDA jda) { if (getCakeDayCount(this.database) == 0) { @@ -89,6 +88,14 @@ public void runRoutine(JDA jda) { jda.getGuilds().forEach(this::reassignCakeDayRole); } + /** + * Reassigns the cake day role for all members of the given guild. + *

+ * If the cake day role is not found based on the configured pattern, a warning message is + * logged, and no action is taken. + * + * @param guild the guild for which to reassign the cake day role + */ private void reassignCakeDayRole(Guild guild) { Role cakeDayRole = getCakeDayRoleFromGuild(guild).orElse(null); @@ -103,6 +110,16 @@ private void reassignCakeDayRole(Guild guild) { .join(); } + /** + * Asynchronously adds the specified cake day role to guild members who are celebrating their + * cake day today. + *

+ * The cake day role is added to members who have been in the guild for at least one year. + * + * @param cakeDayRole the cake day role to add to qualifying members + * @param guild the guild in which to add the cake day role to members + * @return a {@link CompletableFuture} representing the asynchronous operation + */ private CompletableFuture addTodayMembersCakeDayRole(Role cakeDayRole, Guild guild) { return CompletableFuture .runAsync(() -> findCakeDaysTodayFromDatabase().forEach(cakeDayRecord -> { @@ -115,11 +132,27 @@ private CompletableFuture addTodayMembersCakeDayRole(Role cakeDayRole, Gui })); } + /** + * Removes the specified cake day role from all members who possess it in the given guild + * asynchronously. + * + * @param cakeDayRole the cake day role to be removed from members + * @param guild the guild from which to remove the cake day role + * @return a {@link CompletableFuture} representing the asynchronous operation + */ private CompletableFuture removeMembersCakeDayRole(Role cakeDayRole, Guild guild) { return CompletableFuture.runAsync(() -> guild.findMembersWithRoles(cakeDayRole) .onSuccess(members -> removeRoleFromMembers(guild, cakeDayRole, members))); } + + /** + * Removes a specified role from a list of members in a guild. + * + * @param guild the guild from which to remove the role from members + * @param role the role to be removed from the members + * @param members the list of members from which the role will be removed + */ private void removeRoleFromMembers(Guild guild, Role role, List members) { members.forEach(member -> { UserSnowflake snowflake = UserSnowflake.fromId(member.getIdLong()); @@ -127,14 +160,39 @@ private void removeRoleFromMembers(Guild guild, Role role, List members) }); } + /** + * Retrieves the count of cake days from the provided database. + *

+ * This uses the table cake_days to find the answer. + * + * @param database the database from which to retrieve the count of cake days + * @return the count of cake days stored in the database + */ private int getCakeDayCount(Database database) { return database.read(context -> context.fetchCount(CAKE_DAYS)); } + /** + * Populates cake days for all guilds in the provided JDA instance. + *

+ * This method iterates through all guilds in the provided JDA instance and populates cake days + * for each guild. It is primarily used for batch populating the cake_days table once it + * is found to be empty. + * + * @param jda the JDA instance containing the guilds to populate cake days for + */ private void populateAllGuildCakeDays(JDA jda) { jda.getGuilds().forEach(this::batchPopulateGuildCakeDays); } + /** + * Batch populates guild cake days for the given guild. + *

+ * Uses a buffer for all the queries it makes and its size is determined by the + * {@code BULK_INSERT_SIZE} option. + * + * @param guild the guild for which to populate cake days + */ private void batchPopulateGuildCakeDays(Guild guild) { final List queriesBuffer = new ArrayList<>(); @@ -155,6 +213,18 @@ private void batchPopulateGuildCakeDays(Guild guild) { } } + /** + * Creates a query to insert a member's cake day information into the database. + *

+ * Primarily used for manually constructing queries for members' cake days which are called from + * {@link CakeDayRoutine#batchPopulateGuildCakeDays(Guild)} and added in a batch to be sent to + * the database. + * + * @param member the member whose cake day information is to be inserted + * @param guildId the ID of the guild to which the member belongs + * @return an Optional containing the query to insert cake day information if the member has a + * join time; empty Optional otherwise + */ private Optional createMemberCakeDayQuery(Member member, long guildId) { if (!member.hasTimeJoined()) { return Optional.empty(); @@ -170,6 +240,12 @@ private Optional createMemberCakeDayQuery(Member member, long guildId) { .set(CAKE_DAYS.USER_ID, member.getIdLong())); } + /** + * Retrieves the cake day {@link Role} from the specified guild. + * + * @param guild the guild from which to retrieve the cake day role + * @return an optional containing the cake day role if found, otherwise empty + */ private Optional getCakeDayRoleFromGuild(Guild guild) { return guild.getRoles() .stream() @@ -177,6 +253,11 @@ private Optional getCakeDayRoleFromGuild(Guild guild) { .findFirst(); } + /** + * Finds cake days records for today from the database. + * + * @return a list of {@link CakeDaysRecord} objects representing cake days for today + */ private List findCakeDaysTodayFromDatabase() { String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); From 35806eee761f6c3398e87a8249d0ba56dec63c7e Mon Sep 17 00:00:00 2001 From: christolis Date: Mon, 4 Mar 2024 13:29:48 +0200 Subject: [PATCH 04/16] refactor: move cake day feature to its own directory --- .../src/main/java/org/togetherjava/tjbot/features/Features.java | 2 +- .../tjbot/features/{basic => cakeday}/CakeDayRoutine.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename application/src/main/java/org/togetherjava/tjbot/features/{basic => cakeday}/CakeDayRoutine.java (99%) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 17306883f8..4b79d3d585 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,7 +6,6 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.features.basic.CakeDayRoutine; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; @@ -16,6 +15,7 @@ import org.togetherjava.tjbot.features.bookmarks.BookmarksSystem; import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksCleanupRoutine; import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksListener; +import org.togetherjava.tjbot.features.cakeday.CakeDayRoutine; import org.togetherjava.tjbot.features.chatgpt.ChatGptCommand; import org.togetherjava.tjbot.features.chatgpt.ChatGptService; import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java similarity index 99% rename from application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java rename to application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java index 5702a0e821..ed13eca92f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/CakeDayRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java @@ -1,4 +1,4 @@ -package org.togetherjava.tjbot.features.basic; +package org.togetherjava.tjbot.features.cakeday; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; From d9afbb2a27669022f1a6fbff9eec22c664693154 Mon Sep 17 00:00:00 2001 From: christolis Date: Mon, 4 Mar 2024 18:18:39 +0200 Subject: [PATCH 05/16] fix: improve role refresh handling and make it work --- .../org/togetherjava/tjbot/Application.java | 4 - .../togetherjava/tjbot/features/Features.java | 6 +- .../features/cakeday/CakeDayListener.java | 84 +++++ .../features/cakeday/CakeDayRoutine.java | 247 +-------------- .../features/cakeday/CakeDayService.java | 298 ++++++++++++++++++ 5 files changed, 395 insertions(+), 244 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayListener.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index f5135d665d..4c228cb02a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -5,8 +5,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.requests.GatewayIntent; -import net.dv8tion.jda.api.utils.ChunkingFilter; -import net.dv8tion.jda.api.utils.MemberCachePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,8 +83,6 @@ public static void runBot(Config config) { Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath()); JDA jda = JDABuilder.createDefault(config.getToken()) - .setChunkingFilter(ChunkingFilter.ALL) - .setMemberCachePolicy(MemberCachePolicy.ALL) .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) .build(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 4b79d3d585..3f1af1f2f5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -15,7 +15,9 @@ import org.togetherjava.tjbot.features.bookmarks.BookmarksSystem; import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksCleanupRoutine; import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksListener; +import org.togetherjava.tjbot.features.cakeday.CakeDayListener; import org.togetherjava.tjbot.features.cakeday.CakeDayRoutine; +import org.togetherjava.tjbot.features.cakeday.CakeDayService; import org.togetherjava.tjbot.features.chatgpt.ChatGptCommand; import org.togetherjava.tjbot.features.chatgpt.ChatGptService; import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection; @@ -120,6 +122,7 @@ public static Collection createFeatures(JDA jda, Database database, Con new CodeMessageHandler(blacklistConfig.special(), jshellEval); ChatGptService chatGptService = new ChatGptService(config); HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService); + CakeDayService cakeDayService = new CakeDayService(config, database); HelpThreadLifecycleListener helpThreadLifecycleListener = new HelpThreadLifecycleListener(helpSystemHelper, database); TopHelpersService topHelpersService = new TopHelpersService(database); @@ -145,7 +148,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem)); features.add(new MarkHelpThreadCloseInDBRoutine(database, helpThreadLifecycleListener)); features.add(new MemberCountDisplayRoutine(config)); - features.add(new CakeDayRoutine(config, database)); + features.add(new CakeDayRoutine(cakeDayService)); features.add(new RSSHandlerRoutine(config, database)); features.add(topHelpersAssignmentRoutine); @@ -168,6 +171,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new GuildLeaveCloseThreadListener(config)); features.add(new LeftoverBookmarksListener(bookmarksSystem)); features.add(new HelpThreadCreatedListener(helpSystemHelper)); + features.add(new CakeDayListener(cakeDayService)); features.add(new HelpThreadLifecycleListener(helpSystemHelper, database)); features.add(new ProjectsThreadCreatedListener(config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayListener.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayListener.java new file mode 100644 index 0000000000..3dd0d5d43a --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayListener.java @@ -0,0 +1,84 @@ +package org.togetherjava.tjbot.features.cakeday; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +import org.togetherjava.tjbot.db.generated.tables.records.CakeDaysRecord; +import org.togetherjava.tjbot.features.EventReceiver; + +import java.util.Optional; + +/** + * A listener class responsible for handling cake day related events. + */ +public class CakeDayListener extends ListenerAdapter implements EventReceiver { + + private final CakeDayService cakeDayService; + + /** + * Constructs a new CakeDayListener with the given {@link CakeDayService}. + * + * @param cakeDayService the {@link CakeDayService} to be used by this listener + */ + public CakeDayListener(CakeDayService cakeDayService) { + this.cakeDayService = cakeDayService; + } + + /** + * Handles the event of a message being received in a guild. + *

+ * It caches the user's cake day and inserts the member's cake day into the database if not + * already present. + * + * @param event the {@link MessageReceivedEvent} representing the message received + */ + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + User author = event.getAuthor(); + Member member = event.getMember(); + long authorId = author.getIdLong(); + long guildId = event.getGuild().getIdLong(); + + if (member == null || author.isBot() || author.isSystem()) { + return; + } + + + if (cakeDayService.hasMemberCakeDayToday(member)) { + cakeDayService.addCakeDayRole(member); + return; + } + + if (cakeDayService.isUserCached(author)) { + return; + } + + cakeDayService.addToCache(author); + Optional cakeDaysRecord = + cakeDayService.findUserCakeDayFromDatabase(authorId); + if (cakeDaysRecord.isPresent()) { + return; + } + + cakeDayService.insertMemberCakeDayToDatabase(member, guildId); + } + + /** + * Handles the event of a guild member being removed from the guild. It removes the user's cake + * day information from the database if present. + * + * @param event the {@link GuildMemberRemoveEvent} representing the member removal event + */ + @Override + public void onGuildMemberRemove(GuildMemberRemoveEvent event) { + User user = event.getUser(); + Guild guild = event.getGuild(); + + cakeDayService.handleUserLeft(user, guild); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java index ed13eca92f..6bfa1b935c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayRoutine.java @@ -1,33 +1,11 @@ package org.togetherjava.tjbot.features.cakeday; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.entities.UserSnowflake; -import org.jooq.Query; -import org.jooq.impl.DSL; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.NotNull; -import org.togetherjava.tjbot.config.CakeDayConfig; -import org.togetherjava.tjbot.config.Config; -import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.db.generated.tables.records.CakeDaysRecord; import org.togetherjava.tjbot.features.Routine; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS; /** * Represents a routine for managing cake day celebrations. @@ -37,234 +15,25 @@ */ public class CakeDayRoutine implements Routine { - private static final Logger logger = LoggerFactory.getLogger(CakeDayRoutine.class); - private static final DateTimeFormatter MONTH_DAY_FORMATTER = - DateTimeFormatter.ofPattern("MM-dd"); - private static final int BULK_INSERT_SIZE = 500; - private final Predicate cakeDayRolePredicate; - private final CakeDayConfig config; - private final Database database; + private final CakeDayService cakeDayService; /** * Constructs a new {@link CakeDayRoutine} instance. * - * @param config the configuration for cake day routines - * @param database the database for accessing cake day data + * @param cakeDayService an instance of the cake day service */ - public CakeDayRoutine(Config config, Database database) { - this.config = config.getCakeDayConfig(); - this.database = database; - - this.cakeDayRolePredicate = Pattern.compile(this.config.rolePattern()).asPredicate(); + public CakeDayRoutine(CakeDayService cakeDayService) { + this.cakeDayService = cakeDayService; } @Override + @NotNull public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.DAYS); } @Override - public void runRoutine(JDA jda) { - if (getCakeDayCount(this.database) == 0) { - int guildsCount = jda.getGuilds().size(); - - logger.info("Found empty cake_days table. Populating from guild count: {}", - guildsCount); - CompletableFuture.runAsync(() -> populateAllGuildCakeDays(jda)) - .handle((result, exception) -> { - if (exception != null) { - logger.error("populateAllGuildCakeDays failed. Message: {}", - exception.getMessage()); - } else { - logger.info("populateAllGuildCakeDays completed."); - } - - return result; - }); - - return; - } - - jda.getGuilds().forEach(this::reassignCakeDayRole); - } - - /** - * Reassigns the cake day role for all members of the given guild. - *

- * If the cake day role is not found based on the configured pattern, a warning message is - * logged, and no action is taken. - * - * @param guild the guild for which to reassign the cake day role - */ - private void reassignCakeDayRole(Guild guild) { - Role cakeDayRole = getCakeDayRoleFromGuild(guild).orElse(null); - - if (cakeDayRole == null) { - logger.warn("Cake day role with pattern {} not found for guild: {}", - config.rolePattern(), guild.getName()); - return; - } - - removeMembersCakeDayRole(cakeDayRole, guild) - .thenCompose(result -> addTodayMembersCakeDayRole(cakeDayRole, guild)) - .join(); - } - - /** - * Asynchronously adds the specified cake day role to guild members who are celebrating their - * cake day today. - *

- * The cake day role is added to members who have been in the guild for at least one year. - * - * @param cakeDayRole the cake day role to add to qualifying members - * @param guild the guild in which to add the cake day role to members - * @return a {@link CompletableFuture} representing the asynchronous operation - */ - private CompletableFuture addTodayMembersCakeDayRole(Role cakeDayRole, Guild guild) { - return CompletableFuture - .runAsync(() -> findCakeDaysTodayFromDatabase().forEach(cakeDayRecord -> { - UserSnowflake snowflake = UserSnowflake.fromId(cakeDayRecord.getUserId()); - - int anniversary = OffsetDateTime.now().getYear() - cakeDayRecord.getJoinedYear(); - if (anniversary > 0) { - guild.addRoleToMember(snowflake, cakeDayRole).complete(); - } - })); - } - - /** - * Removes the specified cake day role from all members who possess it in the given guild - * asynchronously. - * - * @param cakeDayRole the cake day role to be removed from members - * @param guild the guild from which to remove the cake day role - * @return a {@link CompletableFuture} representing the asynchronous operation - */ - private CompletableFuture removeMembersCakeDayRole(Role cakeDayRole, Guild guild) { - return CompletableFuture.runAsync(() -> guild.findMembersWithRoles(cakeDayRole) - .onSuccess(members -> removeRoleFromMembers(guild, cakeDayRole, members))); - } - - - /** - * Removes a specified role from a list of members in a guild. - * - * @param guild the guild from which to remove the role from members - * @param role the role to be removed from the members - * @param members the list of members from which the role will be removed - */ - private void removeRoleFromMembers(Guild guild, Role role, List members) { - members.forEach(member -> { - UserSnowflake snowflake = UserSnowflake.fromId(member.getIdLong()); - guild.removeRoleFromMember(snowflake, role).complete(); - }); - } - - /** - * Retrieves the count of cake days from the provided database. - *

- * This uses the table cake_days to find the answer. - * - * @param database the database from which to retrieve the count of cake days - * @return the count of cake days stored in the database - */ - private int getCakeDayCount(Database database) { - return database.read(context -> context.fetchCount(CAKE_DAYS)); - } - - /** - * Populates cake days for all guilds in the provided JDA instance. - *

- * This method iterates through all guilds in the provided JDA instance and populates cake days - * for each guild. It is primarily used for batch populating the cake_days table once it - * is found to be empty. - * - * @param jda the JDA instance containing the guilds to populate cake days for - */ - private void populateAllGuildCakeDays(JDA jda) { - jda.getGuilds().forEach(this::batchPopulateGuildCakeDays); - } - - /** - * Batch populates guild cake days for the given guild. - *

- * Uses a buffer for all the queries it makes and its size is determined by the - * {@code BULK_INSERT_SIZE} option. - * - * @param guild the guild for which to populate cake days - */ - private void batchPopulateGuildCakeDays(Guild guild) { - final List queriesBuffer = new ArrayList<>(); - - guild.getMembers().stream().filter(Member::hasTimeJoined).forEach(member -> { - if (queriesBuffer.size() == BULK_INSERT_SIZE) { - database.write(context -> context.batch(queriesBuffer).execute()); - queriesBuffer.clear(); - return; - } - - Optional query = createMemberCakeDayQuery(member, guild.getIdLong()); - query.ifPresent(queriesBuffer::add); - }); - - // Flush the queries buffer so that the remaining ones get written - if (!queriesBuffer.isEmpty()) { - database.write(context -> context.batch(queriesBuffer).execute()); - } - } - - /** - * Creates a query to insert a member's cake day information into the database. - *

- * Primarily used for manually constructing queries for members' cake days which are called from - * {@link CakeDayRoutine#batchPopulateGuildCakeDays(Guild)} and added in a batch to be sent to - * the database. - * - * @param member the member whose cake day information is to be inserted - * @param guildId the ID of the guild to which the member belongs - * @return an Optional containing the query to insert cake day information if the member has a - * join time; empty Optional otherwise - */ - private Optional createMemberCakeDayQuery(Member member, long guildId) { - if (!member.hasTimeJoined()) { - return Optional.empty(); - } - - OffsetDateTime cakeDay = member.getTimeJoined(); - String joinedMonthDay = cakeDay.format(MONTH_DAY_FORMATTER); - - return Optional.of(DSL.insertInto(CAKE_DAYS) - .set(CAKE_DAYS.JOINED_MONTH_DAY, joinedMonthDay) - .set(CAKE_DAYS.JOINED_YEAR, cakeDay.getYear()) - .set(CAKE_DAYS.GUILD_ID, guildId) - .set(CAKE_DAYS.USER_ID, member.getIdLong())); - } - - /** - * Retrieves the cake day {@link Role} from the specified guild. - * - * @param guild the guild from which to retrieve the cake day role - * @return an optional containing the cake day role if found, otherwise empty - */ - private Optional getCakeDayRoleFromGuild(Guild guild) { - return guild.getRoles() - .stream() - .filter(role -> cakeDayRolePredicate.test(role.getName())) - .findFirst(); - } - - /** - * Finds cake days records for today from the database. - * - * @return a list of {@link CakeDaysRecord} objects representing cake days for today - */ - private List findCakeDaysTodayFromDatabase() { - String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); - - return database - .read(context -> context.selectFrom(CAKE_DAYS) - .where(CAKE_DAYS.JOINED_MONTH_DAY.eq(todayMonthDay)) - .fetch()) - .collect(Collectors.toList()); + public void runRoutine(@NotNull JDA jda) { + jda.getGuilds().forEach(cakeDayService::reassignCakeDayRole); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java new file mode 100644 index 0000000000..faa12aecde --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -0,0 +1,298 @@ +package org.togetherjava.tjbot.features.cakeday; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.UserSnowflake; +import org.jooq.Query; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.CakeDayConfig; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.records.CakeDaysRecord; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS; + +/** + * Service for managing the Cake Day feature. + */ +public class CakeDayService { + private static final Logger logger = LoggerFactory.getLogger(CakeDayService.class); + private static final DateTimeFormatter MONTH_DAY_FORMATTER = + DateTimeFormatter.ofPattern("MM-dd"); + private final Set cakeDaysCache = new HashSet<>(); + private final Predicate cakeDayRolePredicate; + private final CakeDayConfig config; + private final Database database; + + /** + * Constructs a {@link CakeDayService} with the given configuration and database. + * + * @param config the configuration for cake day management + * @param database the database for storing cake day information + */ + public CakeDayService(Config config, Database database) { + this.config = config.getCakeDayConfig(); + this.database = database; + + this.cakeDayRolePredicate = Pattern.compile(this.config.rolePattern()).asPredicate(); + } + + private Optional getCakeDayRole(Guild guild) { + Role cakeDayRole = getCakeDayRoleFromGuild(guild).orElse(null); + + if (cakeDayRole == null) { + logger.warn("Cake day role with pattern {} not found for guild: {}", + config.rolePattern(), guild.getName()); + return Optional.empty(); + } + + return Optional.of(cakeDayRole); + } + + /** + * Reassigns the cake day role for all members of the given guild. + *

+ * If the cake day role is not found based on the configured pattern, a warning message is + * logged, and no action is taken. + * + * @param guild the guild for which to reassign the cake day role + */ + protected void reassignCakeDayRole(Guild guild) { + Role cakeDayRole = getCakeDayRole(guild).orElse(null); + + if (cakeDayRole == null) { + return; + } + + refreshMembersCakeDayRoles(cakeDayRole, guild); + } + + private void refreshMembersCakeDayRoles(Role cakeDayRole, Guild guild) { + guild.findMembersWithRoles(cakeDayRole).onSuccess(members -> { + removeRoleFromMembers(guild, cakeDayRole, members); + addTodayMembersCakeDayRole(guild); + }); + } + + /** + * Asynchronously adds the specified cake day role to guild members who are celebrating their + * cake day today. + *

+ * The cake day role is added to members who have been in the guild for at least one year. + * + * @param guild the guild in which to add the cake day role to members + */ + private void addTodayMembersCakeDayRole(Guild guild) { + findCakeDaysTodayFromDatabase().forEach(cakeDayRecord -> { + UserSnowflake userSnowflake = UserSnowflake.fromId(cakeDayRecord.getUserId()); + + int anniversary = OffsetDateTime.now().getYear() - cakeDayRecord.getJoinedYear(); + if (anniversary > 0) { + addCakeDayRole(userSnowflake, guild); + } + }); + } + + protected void addCakeDayRole(UserSnowflake snowflake, Guild guild) { + Role cakeDayRole = getCakeDayRole(guild).orElse(null); + + if (cakeDayRole == null) { + return; + } + + guild.addRoleToMember(snowflake, cakeDayRole).complete(); + } + + protected void addCakeDayRole(Member member) { + Guild guild = member.getGuild(); + UserSnowflake snowflake = UserSnowflake.fromId(member.getId()); + Role cakeDayRole = getCakeDayRole(guild).orElse(null); + + if (cakeDayRole == null) { + return; + } + + guild.addRoleToMember(snowflake, cakeDayRole).complete(); + } + + /** + * Removes the specified cake day role from all members who possess it in the given guild + * asynchronously. + * + * @param cakeDayRole the cake day role to be removed from members + * @param guild the guild from which to remove the cake day role + */ + private synchronized void removeMembersCakeDayRole(Role cakeDayRole, Guild guild) { + guild.findMembersWithRoles(cakeDayRole) + .onSuccess(members -> removeRoleFromMembers(guild, cakeDayRole, members)); + } + + + /** + * Removes a specified role from a list of members in a guild. + * + * @param guild the guild from which to remove the role from members + * @param role the role to be removed from the members + * @param members the list of members from which the role will be removed + */ + private void removeRoleFromMembers(Guild guild, Role role, List members) { + members.forEach(member -> { + UserSnowflake snowflake = UserSnowflake.fromId(member.getIdLong()); + guild.removeRoleFromMember(snowflake, role).complete(); + }); + } + + /** + * Creates a query to insert a member's cake day information into the database. + * + * @param member the member whose cake day information is to be inserted + * @param guildId the ID of the guild to which the member belongs + * @return an Optional containing the query to insert cake day information if the member has a + * join time; empty Optional otherwise + */ + private Optional createMemberCakeDayQuery(Member member, long guildId) { + if (!member.hasTimeJoined()) { + return Optional.empty(); + } + + OffsetDateTime cakeDay = member.getTimeJoined(); + String joinedMonthDay = cakeDay.format(MONTH_DAY_FORMATTER); + + return Optional.of(DSL.insertInto(CAKE_DAYS) + .set(CAKE_DAYS.JOINED_MONTH_DAY, joinedMonthDay) + .set(CAKE_DAYS.JOINED_YEAR, cakeDay.getYear()) + .set(CAKE_DAYS.GUILD_ID, guildId) + .set(CAKE_DAYS.USER_ID, member.getIdLong())); + } + + /** + * Inserts the cake day of a member into the database. + *

+ * If the member has no join date, nothing happens. + * + * @param member the member whose cake day is to be inserted into the database + * @param guildId the ID of the guild to which the member belongs + */ + protected void insertMemberCakeDayToDatabase(Member member, long guildId) { + Query insertQuery = createMemberCakeDayQuery(member, guildId).orElse(null); + + if (insertQuery == null) { + logger.warn("Tried to add member {} to database but found no time joined", + member.getId()); + } + + database.write(context -> context.batch(insertQuery).execute()); + } + + /** + * Removes the member's cake day record from the database. + * + * @param userId the ID of the user whose cake day information is to be removed + * @param guildId the ID of the guild where the user belongs + */ + protected void removeMemberCakeDayFromDatabase(long userId, long guildId) { + database.write(context -> context.deleteFrom(CAKE_DAYS) + .where(CAKE_DAYS.USER_ID.eq(userId)) + .and(CAKE_DAYS.GUILD_ID.eq(guildId)) + .execute()); + } + + /** + * Retrieves the cake day {@link Role} from the specified guild. + * + * @param guild the {@link Guild} from which to retrieve the cake day role + * @return an {@link Optional} containing the cake day role if found, otherwise empty + */ + private Optional getCakeDayRoleFromGuild(Guild guild) { + return guild.getRoles() + .stream() + .filter(role -> cakeDayRolePredicate.test(role.getName())) + .findFirst(); + } + + /** + * Removes the cake day information of the specified user from the database and clears the cache + * for the guild. + * + * @param user the {@link User} who left the guild + * @param guild the {@link Guild} from which the user left + */ + protected void handleUserLeft(User user, Guild guild) { + removeMemberCakeDayFromDatabase(user.getIdLong(), guild.getIdLong()); + cakeDaysCache.remove(guild.getId()); + } + + /** + * Finds cake days records for today from the database. + * + * @return a list of {@link CakeDaysRecord} objects representing cake days for today + */ + private List findCakeDaysTodayFromDatabase() { + String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); + + return database + .read(context -> context.selectFrom(CAKE_DAYS) + .where(CAKE_DAYS.JOINED_MONTH_DAY.eq(todayMonthDay)) + .fetch()) + .collect(Collectors.toList()); + } + + /** + * Searches for the {@link CakeDaysRecord} of a user in the database. + * + * @param userId the user ID of the user whose cake day record is to be retrieved + * @return an {@link Optional} containing the cake day record of the user, or an empty + * {@link Optional} if no record is found + */ + protected Optional findUserCakeDayFromDatabase(long userId) { + return database + .read(context -> context.selectFrom(CAKE_DAYS) + .where(CAKE_DAYS.USER_ID.eq(userId)) + .fetch()) + .collect(Collectors.toList()) + .stream() + .findFirst(); + } + + /** + * Checks if the provided user is cached in the cake day stores cache. + * + * @param user the user to check if cached + * @return true if the user is cached, false otherwise + */ + protected boolean isUserCached(User user) { + return cakeDaysCache.contains(user.getId()); + } + + protected boolean hasMemberCakeDayToday(Member member) { + OffsetDateTime now = OffsetDateTime.now(); + OffsetDateTime joinMonthDate = member.getTimeJoined(); + + return now.getMonth() == joinMonthDate.getMonth() + && now.getDayOfMonth() == joinMonthDate.getDayOfMonth(); + } + + /** + * Adds the provided user to the cake day stores cache. + * + * @param user the user to add to the cache + */ + protected void addToCache(User user) { + cakeDaysCache.add(user.getId()); + } +} From 7a7cb94dca29022e6fdcb81ff87787b00e23f516 Mon Sep 17 00:00:00 2001 From: christolis Date: Mon, 4 Mar 2024 23:44:59 +0200 Subject: [PATCH 06/16] refactor: remove unused method --- .../tjbot/features/cakeday/CakeDayService.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index faa12aecde..e5c7cbde43 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -130,19 +130,6 @@ protected void addCakeDayRole(Member member) { guild.addRoleToMember(snowflake, cakeDayRole).complete(); } - /** - * Removes the specified cake day role from all members who possess it in the given guild - * asynchronously. - * - * @param cakeDayRole the cake day role to be removed from members - * @param guild the guild from which to remove the cake day role - */ - private synchronized void removeMembersCakeDayRole(Role cakeDayRole, Guild guild) { - guild.findMembersWithRoles(cakeDayRole) - .onSuccess(members -> removeRoleFromMembers(guild, cakeDayRole, members)); - } - - /** * Removes a specified role from a list of members in a guild. * From caddb0c5286a4a4d4c98bfc9255afda5f9c1bdc0 Mon Sep 17 00:00:00 2001 From: christolis Date: Mon, 4 Mar 2024 23:45:41 +0200 Subject: [PATCH 07/16] docs: add JavaDocs --- .../features/cakeday/CakeDayService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index e5c7cbde43..d1a0dd8e1d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -82,6 +82,12 @@ protected void reassignCakeDayRole(Guild guild) { refreshMembersCakeDayRoles(cakeDayRole, guild); } + /** + * Refreshes the Cake Day roles for members in the specified guild. + * + * @param cakeDayRole the Cake Day role to refresh + * @param guild the guild in which to refresh Cake Day roles + */ private void refreshMembersCakeDayRoles(Role cakeDayRole, Guild guild) { guild.findMembersWithRoles(cakeDayRole).onSuccess(members -> { removeRoleFromMembers(guild, cakeDayRole, members); @@ -108,6 +114,13 @@ private void addTodayMembersCakeDayRole(Guild guild) { }); } + + /** + * Adds the cake day role to the specified user in the given guild, if available. + * + * @param snowflake the snowflake ID of the user to whom the cake day role will be added + * @param guild the guild in which the cake day role will be added to the user + */ protected void addCakeDayRole(UserSnowflake snowflake, Guild guild) { Role cakeDayRole = getCakeDayRole(guild).orElse(null); @@ -118,6 +131,11 @@ protected void addCakeDayRole(UserSnowflake snowflake, Guild guild) { guild.addRoleToMember(snowflake, cakeDayRole).complete(); } + /** + * Adds the cake day role to the specified member if the cake day role exists in the guild. + * + * @param member the {@link Member} to whom the cake day role will be added + */ protected void addCakeDayRole(Member member) { Guild guild = member.getGuild(); UserSnowflake snowflake = UserSnowflake.fromId(member.getId()); @@ -266,6 +284,13 @@ protected boolean isUserCached(User user) { return cakeDaysCache.contains(user.getId()); } + + /** + * Checks if the provided {@link Member} has their "cake day" today. + * + * @param member the {@link Member} whose cake day is to be checked + * @return true if the member has their cake day today; otherwise, false + */ protected boolean hasMemberCakeDayToday(Member member) { OffsetDateTime now = OffsetDateTime.now(); OffsetDateTime joinMonthDate = member.getTimeJoined(); From 56e5e1dee5034b98dd5fb0157eee3f7e57455848 Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 5 Mar 2024 14:37:46 +0200 Subject: [PATCH 08/16] fix: include anniversary logic for cake day Co-authored-by: TheCodeMr <151576372+TheCodeMr@users.noreply.github.com> Co-authored-by: cab <161495905+cabagbe@users.noreply.github.com> Co-authored-by: Devansh Tiwari <65783463+devloves@users.noreply.github.com> --- .../togetherjava/tjbot/features/cakeday/CakeDayService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index d1a0dd8e1d..d0807200c5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -294,8 +294,9 @@ protected boolean isUserCached(User user) { protected boolean hasMemberCakeDayToday(Member member) { OffsetDateTime now = OffsetDateTime.now(); OffsetDateTime joinMonthDate = member.getTimeJoined(); + int anniversary = now.getYear() - joinMonthDate.getYear(); - return now.getMonth() == joinMonthDate.getMonth() + return anniversary > 0 && now.getMonth() == joinMonthDate.getMonth() && now.getDayOfMonth() == joinMonthDate.getDayOfMonth(); } From a1864278145398aa152772f97c063883cc6ff819 Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 5 Mar 2024 20:04:37 +0200 Subject: [PATCH 09/16] fix: add Objects#requireNonNull() on cakeDayConfig --- .../src/main/java/org/togetherjava/tjbot/config/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 502f1b6dba..0fc377a762 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -142,7 +142,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig); this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern); this.topHelpers = Objects.requireNonNull(topHelpers); - this.cakeDayConfig = cakeDayConfig; + this.cakeDayConfig = Objects.requireNonNull(cakeDayConfig); } /** From 2c67ed5f68a4d223fff6c23349ece9b9d3e14cb2 Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 5 Mar 2024 20:06:02 +0200 Subject: [PATCH 10/16] feat(config): add proper default value for key --- application/config.json.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config.json.template b/application/config.json.template index 95ca849e61..85cb64d575 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -201,6 +201,6 @@ "announcementChannelPattern": "hall-of-fame" } "cakeDayConfig": { - "rolePattern": "cakeDayRolePattern" + "rolePattern": "Cake Day" } } From fe711e748d459573173ee6c3936631d42ba9ad50 Mon Sep 17 00:00:00 2001 From: christolis Date: Wed, 6 Mar 2024 23:31:29 +0200 Subject: [PATCH 11/16] feat(cake-day): improve removeRoleFromMembers() method --- .../features/cakeday/CakeDayService.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index d0807200c5..f5f626b515 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -5,6 +5,8 @@ import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.UserSnowflake; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.Result; import org.jooq.Query; import org.jooq.impl.DSL; import org.slf4j.Logger; @@ -149,17 +151,22 @@ protected void addCakeDayRole(Member member) { } /** - * Removes a specified role from a list of members in a guild. + * Removes a specified role from a list of members in a {@link Guild}. * - * @param guild the guild from which to remove the role from members - * @param role the role to be removed from the members - * @param members the list of members from which the role will be removed + * @param guild the {@link Guild} from which to remove the role from members + * @param role the {@link Role} to be removed from the members + * @param members the {@link List} of members from which the {@link Role} will be removed */ private void removeRoleFromMembers(Guild guild, Role role, List members) { - members.forEach(member -> { - UserSnowflake snowflake = UserSnowflake.fromId(member.getIdLong()); - guild.removeRoleFromMember(snowflake, role).complete(); - }); + List>> chain = members.stream() + .map(member -> guild.removeRoleFromMember(member, role).mapToResult()) + .toList(); + + if (chain.isEmpty()) { + return; + } + + RestAction.allOf(chain).queue(); } /** From ab83944a1f62f481be022e2cb50bacae99dec280 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 7 Mar 2024 00:24:28 +0200 Subject: [PATCH 12/16] feat: add CakeDayConfig record constructor --- .../org/togetherjava/tjbot/config/CakeDayConfig.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java index e3f35818c7..859578c921 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/CakeDayConfig.java @@ -2,9 +2,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + /** * Configuration record for the Cake Day feature. */ public record CakeDayConfig( @JsonProperty(value = "rolePattern", required = true) String rolePattern) { + + /** + * Configuration constructor for the Cake Day feature. + */ + public CakeDayConfig { + Objects.requireNonNull(rolePattern); + } } From a7f45b6b1bd8506a5df06042ee71c771359841e6 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 7 Mar 2024 00:33:28 +0200 Subject: [PATCH 13/16] feat: use Optional properly --- .../togetherjava/tjbot/features/cakeday/CakeDayService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index f5f626b515..b796500c2d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -75,13 +75,13 @@ private Optional getCakeDayRole(Guild guild) { * @param guild the guild for which to reassign the cake day role */ protected void reassignCakeDayRole(Guild guild) { - Role cakeDayRole = getCakeDayRole(guild).orElse(null); + Optional cakeDayRole = getCakeDayRole(guild); - if (cakeDayRole == null) { + if (cakeDayRole.isEmpty()) { return; } - refreshMembersCakeDayRoles(cakeDayRole, guild); + refreshMembersCakeDayRoles(cakeDayRole.get(), guild); } /** From e12f8d97ef491f34f0bb6e8d661f2527027c8125 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 7 Mar 2024 01:13:25 +0200 Subject: [PATCH 14/16] fix(CakeDayService): consider month and day for cake day role This commit aims to fix a bug where the CakeDayService#addTodayMembersCakeDayRole() method would add the cake day role to all members who have been at least one year into the server, disregarding the month and date in which they joined. The documentation has also been made more clean and concise, while the CakeDayService#addCakeDayRole() which required a UserSnowflake as one of its inputs has been removed and now the other version of this function is used which only requires a Member instance. Passing the Guild would be unnecessary as it could be easily acquired from the Member instance, and additionally it helps make sure that the right Member and Guild are used to call this method. Finally, this commit adds an extra condition in the select-from query found in CakeDayService#findCakeDaysTodayFromDatabase() which makes sure that we get all the cake days for the right guild, instead of getting them all unconditionally. --- .../features/cakeday/CakeDayService.java | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index b796500c2d..adf93fa860 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -98,39 +98,30 @@ private void refreshMembersCakeDayRoles(Role cakeDayRole, Guild guild) { } /** - * Asynchronously adds the specified cake day role to guild members who are celebrating their - * cake day today. + * Assigns a special role to members whose cake day (anniversary of joining) is today, but only + * if they have been a member for at least one year. *

- * The cake day role is added to members who have been in the guild for at least one year. + * This method checks the current date against the cake day records in the database for each + * member of the given guild. If the member's cake day is today, and they have been a member for + * at least one year, the method assigns them a special role. * - * @param guild the guild in which to add the cake day role to members + * @param guild the guild to check for members celebrating their cake day today */ private void addTodayMembersCakeDayRole(Guild guild) { - findCakeDaysTodayFromDatabase().forEach(cakeDayRecord -> { - UserSnowflake userSnowflake = UserSnowflake.fromId(cakeDayRecord.getUserId()); + findCakeDaysTodayFromDatabase(guild).forEach(cakeDayRecord -> { + Member member = guild.getMemberById(cakeDayRecord.getUserId()); - int anniversary = OffsetDateTime.now().getYear() - cakeDayRecord.getJoinedYear(); - if (anniversary > 0) { - addCakeDayRole(userSnowflake, guild); + if (member == null) { + return; } - }); - } - - - /** - * Adds the cake day role to the specified user in the given guild, if available. - * - * @param snowflake the snowflake ID of the user to whom the cake day role will be added - * @param guild the guild in which the cake day role will be added to the user - */ - protected void addCakeDayRole(UserSnowflake snowflake, Guild guild) { - Role cakeDayRole = getCakeDayRole(guild).orElse(null); - if (cakeDayRole == null) { - return; - } + boolean isAnniversaryDay = hasMemberCakeDayToday(member); + int yearsSinceJoin = OffsetDateTime.now().getYear() - cakeDayRecord.getJoinedYear(); - guild.addRoleToMember(snowflake, cakeDayRole).complete(); + if (yearsSinceJoin > 0 && isAnniversaryDay) { + addCakeDayRole(member); + } + }); } /** @@ -254,12 +245,13 @@ protected void handleUserLeft(User user, Guild guild) { * * @return a list of {@link CakeDaysRecord} objects representing cake days for today */ - private List findCakeDaysTodayFromDatabase() { + private List findCakeDaysTodayFromDatabase(Guild guild) { String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER); return database .read(context -> context.selectFrom(CAKE_DAYS) .where(CAKE_DAYS.JOINED_MONTH_DAY.eq(todayMonthDay)) + .and(CAKE_DAYS.GUILD_ID.eq(guild.getIdLong())) .fetch()) .collect(Collectors.toList()); } From 8b38313874c8b418c7e50cf419bc4e1ca7d7fa91 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 7 Mar 2024 01:24:28 +0200 Subject: [PATCH 15/16] refactor: better use of Optional --- .../tjbot/features/cakeday/CakeDayService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java index adf93fa860..5cc4c829ec 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/cakeday/CakeDayService.java @@ -55,15 +55,14 @@ public CakeDayService(Config config, Database database) { } private Optional getCakeDayRole(Guild guild) { - Role cakeDayRole = getCakeDayRoleFromGuild(guild).orElse(null); + Optional cakeDayRole = getCakeDayRoleFromGuild(guild); - if (cakeDayRole == null) { + if (cakeDayRole.isEmpty()) { logger.warn("Cake day role with pattern {} not found for guild: {}", config.rolePattern(), guild.getName()); - return Optional.empty(); } - return Optional.of(cakeDayRole); + return cakeDayRole; } /** @@ -132,13 +131,13 @@ private void addTodayMembersCakeDayRole(Guild guild) { protected void addCakeDayRole(Member member) { Guild guild = member.getGuild(); UserSnowflake snowflake = UserSnowflake.fromId(member.getId()); - Role cakeDayRole = getCakeDayRole(guild).orElse(null); + Optional cakeDayRole = getCakeDayRole(guild); - if (cakeDayRole == null) { + if (cakeDayRole.isEmpty()) { return; } - guild.addRoleToMember(snowflake, cakeDayRole).complete(); + guild.addRoleToMember(snowflake, cakeDayRole.get()).complete(); } /** From 42ffceb413495691805c80f51d631b0d378a9434 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Sun, 21 Dec 2025 13:50:27 +0300 Subject: [PATCH 16/16] style: apply Spotless Signed-off-by: Chris Sdogkos --- .../src/main/java/org/togetherjava/tjbot/config/Config.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 0fc377a762..b66c8093ad 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -103,10 +103,8 @@ private Config(@JsonProperty(value = "token", required = true) String token, @JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig, @JsonProperty(value = "selectRolesChannelPattern", required = true) String selectRolesChannelPattern, - @JsonProperty(value = "topHelpers", - required = true) TopHelpersConfig topHelpers, - @JsonProperty(value = "cakeDayConfig", - required = true) CakeDayConfig cakeDayConfig) { + @JsonProperty(value = "topHelpers", required = true) TopHelpersConfig topHelpers, + @JsonProperty(value = "cakeDayConfig", required = true) CakeDayConfig cakeDayConfig) { this.token = Objects.requireNonNull(token); this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath);