Skip to content

Add routine support #346

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 3 commits into from
Feb 2, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ public enum Features {
Collection<Feature> features = new ArrayList<>();

// Routines
// TODO This should be moved into some proper command system instead (see GH issue #235
// which adds support for routines)
new ModAuditLogRoutine(jda, database).start();
new TemporaryModerationRoutine(jda, actionsStore).start();
features.add(new ModAuditLogRoutine(database));
features.add(new TemporaryModerationRoutine(jda, actionsStore));

// Message receivers

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.togetherjava.tjbot.commands;

import net.dv8tion.jda.api.JDA;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.TimeUnit;

/**
* Routines are executed on a reoccurring schedule by the core system.
* <p>
* All routines have to implement this interface. A new routine can then be registered by adding it
* to {@link Features}.
* <p>
* <p>
* After registration, the system will automatically start and execute {@link #runRoutine(JDA)} on
* the schedule defined by {@link #createSchedule()}.
*/
public interface Routine extends Feature {
/**
* Triggered by the core system on the schedule defined by {@link #createSchedule()}.
*
* @param jda the JDA instance the bot is operating with
*/
void runRoutine(@NotNull JDA jda);

/**
* Retrieves the schedule of this routine. Called by the core system once during the startup in
* order to execute the routine accordingly.
* <p>
* Changes on the schedule returned by this method afterwards will not be picked up.
*
* @return the schedule of this routine
*/
@NotNull
Schedule createSchedule();

/**
* The schedule of routines.
*
* @param mode whether subsequent executions are executed at a fixed rate or are delayed,
* influences how {@link #duration} is interpreted
* @param initialDuration the time which the first execution of the routine is delayed
* @param duration the time all subsequent executions of the routine are delayed. Either
* measured before execution ({@link ScheduleMode#FIXED_RATE}) or after execution has
* finished ({@link ScheduleMode#FIXED_DELAY}).
* @param unit the time unit for both, {@link #initialDuration} and {@link #duration}, e.g.
* seconds
*/
record Schedule(@NotNull ScheduleMode mode, long initialDuration, long duration,
@NotNull TimeUnit unit) {
}


/**
* Whether subsequent executions of a routine are executed at a fixed rate or are delayed.
*/
enum ScheduleMode {
/**
* Executions are scheduled for a fixed rate, the time duration between executions is
* measured between their starting time.
*/
FIXED_RATE,
/**
* Executions are scheduled for a fixed delay, the time duration between executions is
* measured between after they have finished.
*/
FIXED_DELAY
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togetherjava.tjbot.commands.Routine;
import org.togetherjava.tjbot.commands.moderation.ActionRecord;
import org.togetherjava.tjbot.commands.moderation.ModerationAction;
import org.togetherjava.tjbot.commands.moderation.ModerationActionsStore;
Expand All @@ -15,8 +16,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand All @@ -26,19 +25,15 @@
* Routine that revokes temporary moderation actions, such as temporary bans, as listed by
* {@link ModerationActionsStore}.
* <p>
* The routine is started by using {@link #start()} and then automatically executes on a schedule.
* <p>
* Revoked actions are compatible with {@link ModerationActionsStore} and commands such as
* {@link org.togetherjava.tjbot.commands.moderation.UnbanCommand} and
* {@link org.togetherjava.tjbot.commands.moderation.AuditCommand}.
*/
public final class TemporaryModerationRoutine {
public final class TemporaryModerationRoutine implements Routine {
private static final Logger logger = LoggerFactory.getLogger(TemporaryModerationRoutine.class);

private final ModerationActionsStore actionsStore;
private final JDA jda;
private final ScheduledExecutorService checkExpiredActionsService =
Executors.newSingleThreadScheduledExecutor();
private final Map<ModerationAction, RevocableModerationAction> typeToRevocableAction;

/**
Expand All @@ -57,6 +52,16 @@ public TemporaryModerationRoutine(@NotNull JDA jda,
Collectors.toMap(RevocableModerationAction::getApplyType, Function.identity()));
}

@Override
public void runRoutine(@NotNull JDA jda) {
checkExpiredActions();
}

@Override
public @NotNull Schedule createSchedule() {
return new Schedule(ScheduleMode.FIXED_DELAY, 5, 5, TimeUnit.MINUTES);
}

private void checkExpiredActions() {
logger.debug("Checking expired temporary moderation actions to revoke...");

Expand Down Expand Up @@ -146,19 +151,6 @@ private void handleFailure(@NotNull Throwable failure,
"Action type is not revocable: " + type);
}

/**
* Starts the routine, automatically checking expired temporary moderation actions on a
* schedule.
*/
public void start() {
// TODO This should be registered at some sort of routine system instead (see GH issue #235
// which adds support for routines)
// TODO The initial run has to be delayed until after the guild cache has been updated
// (during CommandSystem startup)
checkExpiredActionsService.scheduleWithFixedDelay(this::checkExpiredActions, 5, 5,
TimeUnit.MINUTES);
}

private record RevocationGroupIdentifier(long guildId, long targetId,
@NotNull ModerationAction type) {
static RevocationGroupIdentifier of(@NotNull ActionRecord actionRecord) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand All @@ -51,6 +52,8 @@ public final class BotCore extends ListenerAdapter implements SlashCommandProvid
private static final Logger logger = LoggerFactory.getLogger(BotCore.class);
private static final String RELOAD_COMMAND = "reload";
private static final ExecutorService COMMAND_SERVICE = Executors.newCachedThreadPool();
private static final ScheduledExecutorService ROUTINE_SERVICE =
Executors.newScheduledThreadPool(5);
private final Map<String, SlashCommand> nameToSlashCommands;
private final ComponentIdParser componentIdParser;
private final ComponentIdStore componentIdStore;
Expand Down Expand Up @@ -81,6 +84,23 @@ public BotCore(@NotNull JDA jda, @NotNull Database database) {
.map(EventReceiver.class::cast)
.forEach(jda::addEventListener);

// Routines
features.stream()
.filter(Routine.class::isInstance)
.map(Routine.class::cast)
.forEach(routine -> {
Routine.Schedule schedule = routine.createSchedule();
switch (schedule.mode()) {
case FIXED_RATE -> ROUTINE_SERVICE.scheduleAtFixedRate(
() -> routine.runRoutine(jda), schedule.initialDuration(),
schedule.duration(), schedule.unit());
case FIXED_DELAY -> ROUTINE_SERVICE.scheduleWithFixedDelay(
() -> routine.runRoutine(jda), schedule.initialDuration(),
schedule.duration(), schedule.unit());
default -> throw new AssertionError("Unsupported schedule mode");
}
});

// Slash commands
nameToSlashCommands = features.stream()
.filter(SlashCommand.class::isInstance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togetherjava.tjbot.commands.Routine;
import org.togetherjava.tjbot.commands.moderation.ModerationUtils;
import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.db.Database;
Expand All @@ -26,7 +27,6 @@
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
Expand All @@ -36,13 +36,13 @@

/**
* Routine that automatically checks moderator actions on a schedule and logs them to dedicated
* channels. Use {@link #start()} to trigger automatic execution of the routine.
* channels.
* <p>
* The routine is executed periodically, for example three times per day. When it runs, it checks
* all moderator actions, such as user bans, kicks, muting or message deletion. Actions are then
* logged to a dedicated channel, given by {@link Config#getModAuditLogChannelPattern()}.
*/
public final class ModAuditLogRoutine {
public final class ModAuditLogRoutine implements Routine {
private static final Logger logger = LoggerFactory.getLogger(ModAuditLogRoutine.class);
private static final int CHECK_AUDIT_LOG_START_HOUR = 4;
private static final int CHECK_AUDIT_LOG_EVERY_HOURS = 8;
Expand All @@ -51,24 +51,19 @@ public final class ModAuditLogRoutine {

private final Predicate<TextChannel> isAuditLogChannel;
private final Database database;
private final JDA jda;
private final ScheduledExecutorService checkAuditLogService =
Executors.newSingleThreadScheduledExecutor();

/**
* Creates a new instance.
*
* @param jda the JDA instance to use to send messages and retrieve information
* @param database the database for memorizing audit log dates
*/
public ModAuditLogRoutine(@NotNull JDA jda, @NotNull Database database) {
public ModAuditLogRoutine(@NotNull Database database) {
Predicate<String> isAuditLogChannelName =
Pattern.compile(Config.getInstance().getModAuditLogChannelPattern())
.asMatchPredicate();
isAuditLogChannel = channel -> isAuditLogChannelName.test(channel.getName());

this.database = database;
this.jda = jda;
}

private static @NotNull RestAction<MessageEmbed> handleAction(@NotNull Action action,
Expand Down Expand Up @@ -105,7 +100,7 @@ private static boolean isSnowflakeAfter(@NotNull ISnowflake snowflake,
}

/**
* Schedules the given task for execution at a fixed rate (see
* Creates a schedule for execution at a fixed rate (see
* {@link ScheduledExecutorService#scheduleAtFixedRate(Runnable, long, long, TimeUnit)}). The
* initial first execution will be delayed to the next fixed time that matches the given period,
* effectively making execution stable at fixed times of a day - regardless of when this method
Expand All @@ -119,14 +114,11 @@ private static boolean isSnowflakeAfter(@NotNull ISnowflake snowflake,
* Execution will also correctly roll over to the next day, for example if the method is
* triggered at 21:30, the next execution will be at 4:00 the following day.
*
* @param service the scheduler to use
* @param command the command to schedule
* @param periodStartHour the hour of the day that marks the start of this period
* @param periodHours the scheduling period in hours
* @return the instant when the command will be executed the first time
* @return the according schedule representing the planned execution
*/
private static @NotNull Instant scheduleAtFixedRateFromNextFixedTime(
@NotNull ScheduledExecutorService service, @NotNull Runnable command,
private static @NotNull Schedule scheduleAtFixedRateFromNextFixedTime(
@SuppressWarnings("SameParameterValue") int periodStartHour,
@SuppressWarnings("SameParameterValue") int periodHours) {
// NOTE This scheduler could be improved, for example supporting arbitrary periods (not just
Expand All @@ -152,9 +144,8 @@ private static boolean isSnowflakeAfter(@NotNull ISnowflake snowflake,
Instant now = Instant.now();
Instant nextFixedTime =
computeClosestNextScheduleDate(now, fixedScheduleHours, periodHours);
service.scheduleAtFixedRate(command, ChronoUnit.SECONDS.between(now, nextFixedTime),
return new Schedule(ScheduleMode.FIXED_RATE, ChronoUnit.SECONDS.between(now, nextFixedTime),
TimeUnit.HOURS.toSeconds(periodHours), TimeUnit.SECONDS);
return nextFixedTime;
}

private static @NotNull Instant computeClosestNextScheduleDate(@NotNull Instant instant,
Expand Down Expand Up @@ -212,19 +203,21 @@ private static boolean isSnowflakeAfter(@NotNull ISnowflake snowflake,
return Optional.of(handleAction(Action.MESSAGE_DELETION, entry));
}

/**
* Starts the routine, automatically checking the audit logs on a schedule.
*/
public void start() {
// TODO This should be registered at some sort of routine system instead (see GH issue #235
// which adds support for routines)
Instant startInstant = scheduleAtFixedRateFromNextFixedTime(checkAuditLogService,
this::checkAuditLogsRoutine, CHECK_AUDIT_LOG_START_HOUR,
@Override
public void runRoutine(@NotNull JDA jda) {
checkAuditLogsRoutine(jda);
}

@Override
public @NotNull Schedule createSchedule() {
Schedule schedule = scheduleAtFixedRateFromNextFixedTime(CHECK_AUDIT_LOG_START_HOUR,
CHECK_AUDIT_LOG_EVERY_HOURS);
logger.info("Checking audit logs is scheduled for {}.", startInstant);
logger.info("Checking audit logs is scheduled for {}.",
Instant.now().plus(schedule.initialDuration(), schedule.unit().toChronoUnit()));
return schedule;
}

private void checkAuditLogsRoutine() {
private void checkAuditLogsRoutine(@NotNull JDA jda) {
logger.info("Checking audit logs of all guilds...");

jda.getGuildCache().forEach(guild -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* This package contains most routines of the bot. Routines can also be created in different
* modules, if desired.
* <p>
* Routines are actions that are executed periodically on a schedule. They are added and started
* manually in {@link org.togetherjava.tjbot.Application}.
* Routines are actions that are executed periodically on a schedule. They are added to the system
* in {@link org.togetherjava.tjbot.commands.Features}.
*/
// TODO GH issue #235 will introduce a proper routine system
package org.togetherjava.tjbot.routines;