Skip to content

Commit 16fe84d

Browse files
committed
feat(cake-day): implement batch insert cake days routine
1 parent f809da4 commit 16fe84d

File tree

7 files changed

+169
-2
lines changed

7 files changed

+169
-2
lines changed

application/config.json.template

+4-1
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,8 @@
110110
"special": [
111111
]
112112
},
113-
"selectRolesChannelPattern": "select-your-roles"
113+
"selectRolesChannelPattern": "select-your-roles",
114+
"cakeDayConfig": {
115+
"rolePattern": "cakeDayRolePattern"
116+
}
114117
}

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

+4
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

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class Config {
4444
private final HelperPruneConfig helperPruneConfig;
4545
private final FeatureBlacklistConfig featureBlacklistConfig;
4646
private final String selectRolesChannelPattern;
47+
private final CakeDayConfig cakeDayConfig;
4748

4849
@SuppressWarnings("ConstructorWithTooManyParameters")
4950
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
@@ -91,7 +92,8 @@ private Config(@JsonProperty(value = "token", required = true) String token,
9192
@JsonProperty(value = "featureBlacklist",
9293
required = true) FeatureBlacklistConfig featureBlacklistConfig,
9394
@JsonProperty(value = "selectRolesChannelPattern",
94-
required = true) String selectRolesChannelPattern) {
95+
required = true) String selectRolesChannelPattern,
96+
@JsonProperty(value = "cakeDayConfig", required = true) CakeDayConfig cakeDayConfig) {
9597
this.token = Objects.requireNonNull(token);
9698
this.githubApiKey = Objects.requireNonNull(githubApiKey);
9799
this.databasePath = Objects.requireNonNull(databasePath);
@@ -123,6 +125,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
123125
this.helperPruneConfig = Objects.requireNonNull(helperPruneConfig);
124126
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
125127
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
128+
this.cakeDayConfig = cakeDayConfig;
126129
}
127130

128131
/**
@@ -405,4 +408,9 @@ public FeatureBlacklistConfig getFeatureBlacklistConfig() {
405408
public String getSelectRolesChannelPattern() {
406409
return selectRolesChannelPattern;
407410
}
411+
412+
// TODO: Add JavaDoc
413+
public CakeDayConfig getCakeDayConfig() {
414+
return cakeDayConfig;
415+
}
408416
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.togetherjava.tjbot.config.FeatureBlacklist;
99
import org.togetherjava.tjbot.config.FeatureBlacklistConfig;
1010
import org.togetherjava.tjbot.db.Database;
11+
import org.togetherjava.tjbot.features.basic.CakeDayRoutine;
1112
import org.togetherjava.tjbot.features.basic.PingCommand;
1213
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
1314
import org.togetherjava.tjbot.features.basic.SlashCommandEducator;
@@ -109,6 +110,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
109110
.add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database));
110111
features.add(new HelpThreadAutoArchiver(helpSystemHelper));
111112
features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem));
113+
features.add(new CakeDayRoutine(config, database));
112114

113115
// Message receivers
114116
features.add(new TopHelpersMessageListener(database, config));
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+
}
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)