Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/code-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -95,4 +95,4 @@ jobs:
./gradlew build

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
23 changes: 23 additions & 0 deletions .github/workflows/wiki-sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Wiki Sync

on:
push:
branches:
- 'develop'
paths:
- wiki/**
- .github/workflows/wiki-sync.yaml

concurrency:
group: sync-wiki
cancel-in-progress: true

permissions:
contents: write

jobs:
sync-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/[email protected]
2 changes: 1 addition & 1 deletion application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.14.0'
implementation 'com.apptasticsoftware:rssreader:3.10.0'

testImplementation 'org.mockito:mockito-core:5.18.0'
testImplementation 'org.mockito:mockito-core:5.19.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
13 changes: 9 additions & 4 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"mutedRolePattern": "Muted",
"heavyModerationRolePattern": "Moderator",
"softModerationRolePattern": "Moderator|Community Ambassador",
"tagManageRolePattern": "Moderator|Community Ambassador|Top Helpers .+",
"excludeCodeAutoDetectionRolePattern": "Top Helpers .+|Moderator|Community Ambassador|Expert",
"tagManageRolePattern": "Moderator|Community Ambassador|Top Helper.*",
"excludeCodeAutoDetectionRolePattern": "Top Helper.*|Moderator|Community Ambassador|Expert",
"suggestions": {
"channelPattern": "tj-suggestions",
"upVoteEmoteName": "peepo_yes",
Expand All @@ -22,7 +22,7 @@
"mode": "AUTO_DELETE_BUT_APPROVE_QUARANTINE",
"reportChannelPattern": "commands",
"botTrapChannelPattern": "bot-trap",
"trustedUserRolePattern": "Top Helpers .+|Moderator|Community Ambassador|Expert",
"trustedUserRolePattern": "Top Helper.*|Moderator|Community Ambassador|Expert",
"suspiciousKeywords": [
"nitro",
"boob",
Expand Down Expand Up @@ -189,5 +189,10 @@
"fallbackChannelPattern": "java-news-and-changes",
"pollIntervalInMinutes": 10
},
"memberCountCategoryPattern": "Info"
"memberCountCategoryPattern": "Info",
"topHelpers": {
"rolePattern": "Top Helper.*",
"assignmentChannelPattern": "community-commands",
"announcementChannelPattern": "hall-of-fame"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public final class Config {
private final RSSFeedsConfig rssFeedsConfig;
private final String selectRolesChannelPattern;
private final String memberCountCategoryPattern;
private final TopHelpersConfig topHelpers;

@SuppressWarnings("ConstructorWithTooManyParameters")
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
Expand Down Expand Up @@ -100,7 +101,8 @@ private Config(@JsonProperty(value = "token", required = true) String token,
required = true) FeatureBlacklistConfig featureBlacklistConfig,
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
@JsonProperty(value = "selectRolesChannelPattern",
required = true) String selectRolesChannelPattern) {
required = true) String selectRolesChannelPattern,
@JsonProperty(value = "topHelpers", required = true) TopHelpersConfig topHelpers) {
this.token = Objects.requireNonNull(token);
this.githubApiKey = Objects.requireNonNull(githubApiKey);
this.databasePath = Objects.requireNonNull(databasePath);
Expand Down Expand Up @@ -135,6 +137,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig);
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
this.topHelpers = Objects.requireNonNull(topHelpers);
}

/**
Expand Down Expand Up @@ -445,4 +448,13 @@ public String getMemberCountCategoryPattern() {
public RSSFeedsConfig getRSSFeedsConfig() {
return rssFeedsConfig;
}

/**
* Gets the config for the Top Helpers system.
*
* @return the configuration
*/
public TopHelpersConfig getTopHelpers() {
return topHelpers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,25 @@

/**
* Configuration of the feature blacklist, any feature present here will be disabled.
* <p>
* The argument {@code "normal"} expects a list of fully qualified class names of
* {@link org.togetherjava.tjbot.features.Feature}s, for example:
*
* <pre>
* {@code
* "normal": [
* "org.togetherjava.tjbot.features.basic.PingCommand",
* "org.togetherjava.tjbot.features.tophelper.TopHelpersAssignmentRoutine"
* ]}
* </pre>
* <p>
* The argument {@code "special"} is a special set of predefined strings that disable specific
* features. Currently available are:
* <ul>
* <li>{@code "org.togetherjava.tjbot.features.code.FormatCodeCommand"}</li>
* <li>{@code "org.togetherjava.tjbot.features.code.EvalCodeCommand"}</li>
* </ul>
*
* @param normal the normal features, which are present in
* {@link org.togetherjava.tjbot.features.Features}
* @param special the special features, which require special code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.togetherjava.tjbot.config;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;

import java.util.Objects;

/**
* Configuration for the top helper system, see
* {@link org.togetherjava.tjbot.features.tophelper.TopHelpersCommand}.
*/
@JsonRootName("topHelpers")
public final class TopHelpersConfig {
private final String rolePattern;
private final String assignmentChannelPattern;
private final String announcementChannelPattern;

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
private TopHelpersConfig(
@JsonProperty(value = "rolePattern", required = true) String rolePattern,
@JsonProperty(value = "assignmentChannelPattern",
required = true) String assignmentChannelPattern,
@JsonProperty(value = "announcementChannelPattern",
required = true) String announcementChannelPattern) {
this.rolePattern = Objects.requireNonNull(rolePattern);
this.assignmentChannelPattern = Objects.requireNonNull(assignmentChannelPattern);
this.announcementChannelPattern = Objects.requireNonNull(announcementChannelPattern);
}

/**
* Gets the REGEX pattern matching the role used to represent Top Helpers.
*
* @return the role name pattern
*/
public String getRolePattern() {
return rolePattern;
}

/**
* Gets the REGEX pattern used to identify the channel where Top Helper assignments are
* automatically executed.
*
* @return the channel name pattern
*/
public String getAssignmentChannelPattern() {
return assignmentChannelPattern;
}

/**
* Gets the REGEX pattern used to identify the channel where Top Helper announcements are send.
*
* @return the channel name pattern
*/
public String getAnnouncementChannelPattern() {
return announcementChannelPattern;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@
import org.togetherjava.tjbot.features.tags.TagManageCommand;
import org.togetherjava.tjbot.features.tags.TagSystem;
import org.togetherjava.tjbot.features.tags.TagsCommand;
import org.togetherjava.tjbot.features.tophelper.TopHelpersAssignmentRoutine;
import org.togetherjava.tjbot.features.tophelper.TopHelpersCommand;
import org.togetherjava.tjbot.features.tophelper.TopHelpersMessageListener;
import org.togetherjava.tjbot.features.tophelper.TopHelpersPurgeMessagesRoutine;
import org.togetherjava.tjbot.features.tophelper.TopHelpersService;

import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -119,6 +121,9 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService);
HelpThreadLifecycleListener helpThreadLifecycleListener =
new HelpThreadLifecycleListener(helpSystemHelper, database);
TopHelpersService topHelpersService = new TopHelpersService(database);
TopHelpersAssignmentRoutine topHelpersAssignmentRoutine =
new TopHelpersAssignmentRoutine(config, topHelpersService);

// NOTE The system can add special system relevant commands also by itself,
// hence this list may not necessarily represent the full list of all commands actually
Expand All @@ -140,6 +145,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new MarkHelpThreadCloseInDBRoutine(database, helpThreadLifecycleListener));
features.add(new MemberCountDisplayRoutine(config));
features.add(new RSSHandlerRoutine(config, database));
features.add(topHelpersAssignmentRoutine);

// Message receivers
features.add(new TopHelpersMessageListener(database, config));
Expand Down Expand Up @@ -182,7 +188,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new AuditCommand(actionsStore));
features.add(new MuteCommand(actionsStore, config));
features.add(new UnmuteCommand(actionsStore, config));
features.add(new TopHelpersCommand(database));
features.add(new TopHelpersCommand(topHelpersService, topHelpersAssignmentRoutine));
features.add(new RoleSelectCommand());
features.add(new NoteCommand(actionsStore));
features.add(new ReminderCommand(database));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@

import net.dv8tion.jda.api.JDA;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
* Routines are executed on a reoccurring schedule by the core system.
Expand Down Expand Up @@ -45,6 +57,97 @@ public interface Routine extends Feature {
* seconds
*/
record Schedule(ScheduleMode mode, long initialDuration, long duration, TimeUnit unit) {

private static final int HOURS_OF_DAY = 24;

/**
* Creates a schedule for execution at a fixed hour of the day. The initial first execution
* will be delayed to the next fixed time that matches the given hour of the day,
* effectively making execution stable at that fixed hour - regardless of when this method
* was originally triggered.
* <p>
* For example, if the given hour is 12 o'clock, this leads to the fixed execution times of
* only 12:00 each day. The first execution is then delayed to the closest time in that
* schedule. For example, if triggered at 7:00, execution will happen at 12:00 and then
* follow the schedule.
* <p>
* 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 12:00 the following day.
*
* @param hourOfDay the hour of the day that marks the start of this period
* @return the according schedule representing the planned execution
*/
public static Schedule atFixedHour(int hourOfDay) {
return atFixedRateFromNextFixedTime(hourOfDay, HOURS_OF_DAY);
}

/**
* 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 was originally triggered.
* <p>
* For example, if the given period is 8 hours with a start hour of 4 o'clock, this leads to
* the fixed execution times of 4:00, 12:00 and 20:00 each day. The first execution is then
* delayed to the closest time in that schedule. For example, if triggered at 7:00,
* execution will happen at 12:00 and then follow the schedule.
* <p>
* 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 periodStartHour the hour of the day that marks the start of this period
* @param periodHours the scheduling period in hours
* @return the according schedule representing the planned execution
*/
public static Schedule atFixedRateFromNextFixedTime(int periodStartHour, int periodHours) {
// NOTE This scheduler could be improved, for example supporting arbitrary periods (not
// just hour-based). Also, it probably does not correctly handle all date/time-quirks,
// for example if a schedule would hit a time that does not exist for a specific date
// due to DST or similar issues. Those are minor though and can be ignored for now.
if (periodStartHour < 0 || periodStartHour >= HOURS_OF_DAY) {
throw new IllegalArgumentException(
"Schedule period start hour must be a valid hour of a day (0-23)");
}
if (periodHours <= 0 || periodHours > HOURS_OF_DAY) {
throw new IllegalArgumentException(
"Schedule period must not be zero and must fit into a single day (0-24)");
}

// Compute fixed schedule hours
List<Integer> fixedScheduleHours = new ArrayList<>();

for (int hour = periodStartHour; hour < HOURS_OF_DAY; hour += periodHours) {
fixedScheduleHours.add(hour);
}

Instant now = Instant.now();
Instant nextFixedTime =
computeClosestNextScheduleDate(now, fixedScheduleHours, periodHours);
return new Schedule(ScheduleMode.FIXED_RATE,
ChronoUnit.SECONDS.between(now, nextFixedTime),
TimeUnit.HOURS.toSeconds(periodHours), TimeUnit.SECONDS);
}

private static Instant computeClosestNextScheduleDate(Instant instant,
List<Integer> scheduleHours, int periodHours) {
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.UTC);
BiFunction<OffsetDateTime, Integer, Instant> dateAtTime =
(date, hour) -> date.with(LocalTime.of(hour, 0)).toInstant();

// The instant is either before the given hours, in between, or after.
// For latter, we roll the schedule over once to the next day
List<Instant> scheduleDates = scheduleHours.stream()
.map(hour -> dateAtTime.apply(offsetDateTime, hour))
.collect(Collectors.toCollection(ArrayList::new));
int rolloverHour = (scheduleHours.getLast() + periodHours) % HOURS_OF_DAY;
scheduleDates.add(dateAtTime.apply(offsetDateTime.plusDays(1), rolloverHour));

return scheduleDates.stream()
.filter(instant::isBefore)
.min(Comparator.comparing(scheduleDate -> Duration.between(instant, scheduleDate)))
.orElseThrow();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
Expand All @@ -21,6 +20,7 @@
import org.togetherjava.tjbot.features.UserInteractor;
import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator;
import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor;
import org.togetherjava.tjbot.features.utils.Guilds;

import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -99,8 +99,7 @@ public void onMessageReceived(MessageReceivedEvent event) {
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
Member interactionUser = event.getMember();
String gistAuthorId = args.getFirst();
boolean hasSoftModPermissions =
interactionUser.getRoles().stream().map(Role::getName).anyMatch(isSoftModRole);
boolean hasSoftModPermissions = Guilds.hasMemberRole(interactionUser, isSoftModRole);

if (!gistAuthorId.equals(interactionUser.getId()) && !hasSoftModPermissions) {
event.reply("You do not have permission for this action.").setEphemeral(true).queue();
Expand Down
Loading
Loading