Skip to content

Helper prune config #889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,12 @@
"baseUrl": "<put_jshell_rest_api_url_here>",
"rateLimitWindowSeconds": 10,
"rateLimitRequestsInWindow": 3
},
"helperPruneConfig": {
"roleFullLimit": 100,
"roleFullThreshold": 95,
"pruneMemberAmount": 7,
"inactivateAfterDays": 90,
"recentlyJoinedDays": 4
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public final class Config {
private final String openaiApiKey;
private final String sourceCodeBaseUrl;
private final JShellConfig jshell;
private final HelperPruneConfig helperPruneConfig;

@SuppressWarnings("ConstructorWithTooManyParameters")
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
Expand Down Expand Up @@ -76,7 +77,9 @@ private Config(@JsonProperty(value = "token", required = true) String token,
required = true) String logErrorChannelWebhook,
@JsonProperty(value = "openaiApiKey", required = true) String openaiApiKey,
@JsonProperty(value = "sourceCodeBaseUrl", required = true) String sourceCodeBaseUrl,
@JsonProperty(value = "jshell", required = true) JShellConfig jshell) {
@JsonProperty(value = "jshell", required = true) JShellConfig jshell,
@JsonProperty(value = "helperPruneConfig",
required = true) HelperPruneConfig helperPruneConfig) {
this.token = Objects.requireNonNull(token);
this.gistApiKey = Objects.requireNonNull(gistApiKey);
this.databasePath = Objects.requireNonNull(databasePath);
Expand All @@ -102,6 +105,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
this.openaiApiKey = Objects.requireNonNull(openaiApiKey);
this.sourceCodeBaseUrl = Objects.requireNonNull(sourceCodeBaseUrl);
this.jshell = Objects.requireNonNull(jshell);
this.helperPruneConfig = Objects.requireNonNull(helperPruneConfig);
}

/**
Expand Down Expand Up @@ -342,4 +346,13 @@ public String getSourceCodeBaseUrl() {
public JShellConfig getJshell() {
return jshell;
}

/**
* Gets the config for automatic pruning of helper roles.
*
* @return the configuration
*/
public HelperPruneConfig getHelperPruneConfig() {
return helperPruneConfig;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.togetherjava.tjbot.config;


/**
* Config for automatic pruning of helper roles, see
* {@link org.togetherjava.tjbot.features.help.AutoPruneHelperRoutine}.
*
* @param roleFullLimit if a helper role contains that many users, it is considered full and pruning
* must occur
* @param roleFullThreshold if a helper role contains that many users, pruning will start to occur
* to prevent reaching the limit
* @param pruneMemberAmount amount of users to remove from helper roles during a prune
* @param inactivateAfterDays after how many days of inactivity a user is eligible for pruning
* @param recentlyJoinedDays if a user is with the server for just this amount of days, they are
* protected from pruning
*/
public record HelperPruneConfig(int roleFullLimit, int roleFullThreshold, int pruneMemberAmount,
int inactivateAfterDays, int recentlyJoinedDays) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.slf4j.LoggerFactory;

import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.config.HelperPruneConfig;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.Routine;
import org.togetherjava.tjbot.features.moderation.audit.ModAuditLogWriter;
Expand All @@ -31,11 +32,11 @@
public final class AutoPruneHelperRoutine implements Routine {
private static final Logger logger = LoggerFactory.getLogger(AutoPruneHelperRoutine.class);

private static final int ROLE_FULL_LIMIT = 100;
private static final int ROLE_FULL_THRESHOLD = 95;
private static final int PRUNE_MEMBER_AMOUNT = 7;
private static final Period INACTIVE_AFTER = Period.ofDays(90);
private static final int RECENTLY_JOINED_DAYS = 4;
private final int roleFullLimit;
private final int roleFullThreshold;
private final int pruneMemberAmount;
private final Period inactiveAfter;
private final int recentlyJoinedDays;

private final HelpSystemHelper helper;
private final ModAuditLogWriter modAuditLogWriter;
Expand All @@ -56,6 +57,13 @@ public AutoPruneHelperRoutine(Config config, HelpSystemHelper helper,
this.helper = helper;
this.modAuditLogWriter = modAuditLogWriter;
this.database = database;

HelperPruneConfig helperPruneConfig = config.getHelperPruneConfig();
roleFullLimit = helperPruneConfig.roleFullLimit();
roleFullThreshold = helperPruneConfig.roleFullThreshold();
pruneMemberAmount = helperPruneConfig.pruneMemberAmount();
inactiveAfter = Period.ofDays(helperPruneConfig.inactivateAfterDays());
recentlyJoinedDays = helperPruneConfig.recentlyJoinedDays();
}

@Override
Expand Down Expand Up @@ -93,7 +101,7 @@ private void pruneRoleIfFull(Role role, ForumChannel helpForum, Instant when) {
}

private boolean isRoleFull(Collection<?> members) {
return members.size() >= ROLE_FULL_THRESHOLD;
return members.size() >= roleFullThreshold;
}

private void pruneRole(Role role, List<? extends Member> members, ForumChannel helpForum,
Expand All @@ -103,18 +111,18 @@ private void pruneRole(Role role, List<? extends Member> members, ForumChannel h

List<Member> membersToPrune = membersShuffled.stream()
.filter(member -> isMemberInactive(member, when))
.limit(PRUNE_MEMBER_AMOUNT)
.limit(pruneMemberAmount)
.toList();
if (membersToPrune.size() < PRUNE_MEMBER_AMOUNT) {
if (membersToPrune.size() < pruneMemberAmount) {
warnModsAbout(
"Attempting to prune helpers from role **%s** (%d members), but only found %d inactive users. That is less than expected, the category might eventually grow beyond the limit."
.formatted(role.getName(), members.size(), membersToPrune.size()),
role.getGuild());
}
if (members.size() - membersToPrune.size() >= ROLE_FULL_LIMIT) {
if (members.size() - membersToPrune.size() >= roleFullLimit) {
warnModsAbout(
"The helper role **%s** went beyond its member limit (%d), despite automatic pruning. It will not function correctly anymore. Please manually prune some users."
.formatted(role.getName(), ROLE_FULL_LIMIT),
.formatted(role.getName(), roleFullLimit),
role.getGuild());
}

Expand All @@ -126,14 +134,14 @@ private void pruneRole(Role role, List<? extends Member> members, ForumChannel h
private boolean isMemberInactive(Member member, Instant when) {
if (member.hasTimeJoined()) {
Instant memberJoined = member.getTimeJoined().toInstant();
if (Duration.between(memberJoined, when).toDays() <= RECENTLY_JOINED_DAYS) {
if (Duration.between(memberJoined, when).toDays() <= recentlyJoinedDays) {
// New users are protected from purging to not immediately kick them out of the role
// again
return false;
}
}

Instant latestActiveMoment = when.minus(INACTIVE_AFTER);
Instant latestActiveMoment = when.minus(inactiveAfter);

// Has no recent help message
return database.read(context -> context.fetchCount(HELP_CHANNEL_MESSAGES,
Expand Down