Skip to content

Commit 454ac2b

Browse files
committed
feat(cake-day): implement batch insert cake days routine
1 parent 817e044 commit 454ac2b

File tree

7 files changed

+169
-2
lines changed

7 files changed

+169
-2
lines changed

application/config.json.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,8 @@
115115
"fallbackChannelPattern": "java-news-and-changes",
116116
"pollIntervalInMinutes": 10
117117
},
118-
"memberCountCategoryPattern": "Info"
118+
"memberCountCategoryPattern": "Info",
119+
"cakeDayConfig": {
120+
"rolePattern": "cakeDayRolePattern"
121+
}
119122
}

application/src/main/java/org/togetherjava/tjbot/Application.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
66
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
77
import net.dv8tion.jda.api.requests.GatewayIntent;
8+
import net.dv8tion.jda.api.utils.ChunkingFilter;
9+
import net.dv8tion.jda.api.utils.MemberCachePolicy;
810
import org.slf4j.Logger;
911
import org.slf4j.LoggerFactory;
1012

@@ -83,6 +85,8 @@ public static void runBot(Config config) {
8385
Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath());
8486

8587
JDA jda = JDABuilder.createDefault(config.getToken())
88+
.setChunkingFilter(ChunkingFilter.ALL)
89+
.setMemberCachePolicy(MemberCachePolicy.ALL)
8690
.enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT)
8791
.build();
8892

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.togetherjava.tjbot.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public record CakeDayConfig(
6+
@JsonProperty(value = "rolePattern", required = true) String rolePattern) {
7+
}

application/src/main/java/org/togetherjava/tjbot/config/Config.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public final class Config {
4646
private final RSSFeedsConfig rssFeedsConfig;
4747
private final String selectRolesChannelPattern;
4848
private final String memberCountCategoryPattern;
49+
private final CakeDayConfig cakeDayConfig;
4950

5051
@SuppressWarnings("ConstructorWithTooManyParameters")
5152
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
@@ -94,7 +95,8 @@ private Config(@JsonProperty(value = "token", required = true) String token,
9495
required = true) FeatureBlacklistConfig featureBlacklistConfig,
9596
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
9697
@JsonProperty(value = "selectRolesChannelPattern",
97-
required = true) String selectRolesChannelPattern) {
98+
required = true) String selectRolesChannelPattern,
99+
@JsonProperty(value = "cakeDayConfig", required = true) CakeDayConfig cakeDayConfig) {
98100
this.token = Objects.requireNonNull(token);
99101
this.githubApiKey = Objects.requireNonNull(githubApiKey);
100102
this.databasePath = Objects.requireNonNull(databasePath);
@@ -127,6 +129,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
127129
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
128130
this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig);
129131
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
132+
this.cakeDayConfig = cakeDayConfig;
130133
}
131134

132135
/**
@@ -401,6 +404,11 @@ public String getSelectRolesChannelPattern() {
401404
return selectRolesChannelPattern;
402405
}
403406

407+
// TODO: Add JavaDoc
408+
public CakeDayConfig getCakeDayConfig() {
409+
return cakeDayConfig;
410+
}
411+
404412
/**
405413
* Gets the pattern matching the category that is used to display the total member count.
406414
*

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.togetherjava.tjbot.config.FeatureBlacklist;
77
import org.togetherjava.tjbot.config.FeatureBlacklistConfig;
88
import org.togetherjava.tjbot.db.Database;
9+
import org.togetherjava.tjbot.features.basic.CakeDayRoutine;
910
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
1011
import org.togetherjava.tjbot.features.basic.PingCommand;
1112
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
@@ -135,6 +136,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
135136
features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem));
136137
features.add(new MarkHelpThreadCloseInDBRoutine(database, helpThreadLifecycleListener));
137138
features.add(new MemberCountDisplayRoutine(config));
139+
features.add(new CakeDayRoutine(config, database));
138140
features.add(new RSSHandlerRoutine(config, database));
139141

140142
// Message receivers
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.togetherjava.tjbot.features.basic;
2+
3+
import net.dv8tion.jda.api.JDA;
4+
import net.dv8tion.jda.api.entities.Guild;
5+
import net.dv8tion.jda.api.entities.Member;
6+
import org.jooq.Query;
7+
import org.jooq.impl.DSL;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
import org.togetherjava.tjbot.config.CakeDayConfig;
12+
import org.togetherjava.tjbot.config.Config;
13+
import org.togetherjava.tjbot.db.Database;
14+
import org.togetherjava.tjbot.db.generated.tables.records.CakeDaysRecord;
15+
import org.togetherjava.tjbot.features.Routine;
16+
17+
import java.time.OffsetDateTime;
18+
import java.time.format.DateTimeFormatter;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Optional;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.stream.Collectors;
25+
26+
import static org.togetherjava.tjbot.db.generated.tables.CakeDays.CAKE_DAYS;
27+
28+
public class CakeDayRoutine implements Routine {
29+
30+
private static final Logger logger = LoggerFactory.getLogger(CakeDayRoutine.class);
31+
private static final DateTimeFormatter MONTH_DAY_FORMATTER =
32+
DateTimeFormatter.ofPattern("MM-dd");
33+
private static final int BULK_INSERT_SIZE = 500;
34+
private final CakeDayConfig config;
35+
private final Database database;
36+
37+
public CakeDayRoutine(Config config, Database database) {
38+
this.config = config.getCakeDayConfig();
39+
this.database = database;
40+
}
41+
42+
/**
43+
* Retrieves the schedule of this routine. Called by the core system once during the startup in
44+
* order to execute the routine accordingly.
45+
* <p>
46+
* Changes on the schedule returned by this method afterwards will not be picked up.
47+
*
48+
* @return the schedule of this routine
49+
*/
50+
@Override
51+
public Schedule createSchedule() {
52+
return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.DAYS);
53+
}
54+
55+
/**
56+
* Triggered by the core system on the schedule defined by {@link #createSchedule()}.
57+
*
58+
* @param jda the JDA instance the bot is operating with
59+
*/
60+
@Override
61+
public void runRoutine(JDA jda) {
62+
if (getCakeDayCount(this.database) == 0) {
63+
int guildsCount = jda.getGuilds().size();
64+
65+
logger.info("Found empty cake_days table. Populating from guild count: {}",
66+
guildsCount);
67+
CompletableFuture.runAsync(() -> populateAllGuildCakeDays(jda))
68+
.handle((result, exception) -> {
69+
if (exception != null) {
70+
logger.error("populateAllGuildCakeDays failed. Message: {}",
71+
exception.getMessage());
72+
} else {
73+
logger.info("populateAllGuildCakeDays completed.");
74+
}
75+
76+
return result;
77+
});
78+
}
79+
}
80+
81+
private int getCakeDayCount(Database database) {
82+
return database.read(context -> context.fetchCount(CAKE_DAYS));
83+
}
84+
85+
private void populateAllGuildCakeDays(JDA jda) {
86+
jda.getGuilds().forEach(this::batchPopulateGuildCakeDays);
87+
}
88+
89+
private void batchPopulateGuildCakeDays(Guild guild) {
90+
final List<Query> queriesBuffer = new ArrayList<>();
91+
92+
guild.getMembers().stream().filter(Member::hasTimeJoined).forEach(member -> {
93+
if (queriesBuffer.size() == BULK_INSERT_SIZE) {
94+
database.write(context -> context.batch(queriesBuffer).execute());
95+
queriesBuffer.clear();
96+
return;
97+
}
98+
99+
Optional<Query> query = createMemberCakeDayQuery(member, guild.getIdLong());
100+
query.ifPresent(queriesBuffer::add);
101+
});
102+
103+
// Flush the queries buffer so that the remaining ones get written
104+
if (!queriesBuffer.isEmpty()) {
105+
database.write(context -> context.batch(queriesBuffer).execute());
106+
}
107+
}
108+
109+
private Optional<Query> createMemberCakeDayQuery(Member member, long guildId) {
110+
if (!member.hasTimeJoined()) {
111+
return Optional.empty();
112+
}
113+
114+
OffsetDateTime cakeDay = member.getTimeJoined();
115+
String joinedMonthDay = cakeDay.format(MONTH_DAY_FORMATTER);
116+
117+
return Optional.of(DSL.insertInto(CAKE_DAYS)
118+
.set(CAKE_DAYS.JOINED_MONTH_DAY, joinedMonthDay)
119+
.set(CAKE_DAYS.JOINED_YEAR, cakeDay.getYear())
120+
.set(CAKE_DAYS.GUILD_ID, guildId)
121+
.set(CAKE_DAYS.USER_ID, member.getIdLong()));
122+
}
123+
124+
private List<CakeDaysRecord> findCakeDaysTodayFromDatabase() {
125+
String todayMonthDay = OffsetDateTime.now().format(MONTH_DAY_FORMATTER);
126+
127+
return database
128+
.read(context -> context.selectFrom(CAKE_DAYS)
129+
.where(CAKE_DAYS.JOINED_MONTH_DAY.eq(todayMonthDay))
130+
.fetch())
131+
.collect(Collectors.toList());
132+
}
133+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE cake_days
2+
(
3+
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4+
joined_month_day TEXT NOT NULL,
5+
joined_year INT NOT NULL,
6+
guild_id BIGINT NOT NULL,
7+
user_id BIGINT NOT NULL
8+
);
9+
10+
CREATE INDEX cake_day_idx ON cake_days(joined_month_day);

0 commit comments

Comments
 (0)