From 4e9372b4b36a376e3719cf2ca0cc296906ed386f Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 23 Nov 2024 17:31:00 +0100 Subject: [PATCH 1/5] native-image compilation, disable member cache --- .github/workflows/build.yml | 12 +- Dockerfile | 20 +- build.gradle.kts | 33 +- settings.gradle.kts | 2 +- .../javabot/RuntimeHintsConfiguration.java | 73 + .../net/discordjug/javabot/SpringConfig.java | 5 +- .../discordjug/javabot/api/TomcatConfig.java | 8 +- .../qotw/QOTWLeaderboardController.java | 6 +- .../JobChannelCloseOldPostsListener.java | 3 +- .../systems/help/HelpForumUpdater.java | 5 +- .../javabot/systems/help/HelpListener.java | 9 +- .../javabot/systems/help/HelpManager.java | 25 +- .../server_lock/ServerLockManager.java | 39 +- .../systems/qotw/QOTWPointsService.java | 21 +- .../qotw/dao/QOTWChampionRepository.java | 45 + .../systems/qotw/jobs/QOTWChampionJob.java | 30 +- .../RunScheduledTaskCommand.java | 4 +- .../commands/EditCustomTagSubcommand.java | 1 - .../user_commands/ServerInfoCommand.java | 16 +- .../ExperienceLeaderboardSubcommand.java | 2 +- .../ThanksLeaderboardSubcommand.java | 2 +- .../javabot/tasks/MetricsUpdater.java | 3 + .../discordjug/javabot/util/WebhookUtil.java | 23 +- .../net.discordjug/javabot/jni-config.json | 286 + src/main/resources/application.properties | 1 + .../11-11-2024_store_qotw_champion.sql | 5 + src/main/resources/database/schema.sql | 9 +- src/main/resources/logback.xml | 15 - src/main/resources/spamLinks.txt | 4736 ----------------- 29 files changed, 601 insertions(+), 4838 deletions(-) create mode 100644 src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java create mode 100644 src/main/java/net/discordjug/javabot/systems/qotw/dao/QOTWChampionRepository.java create mode 100644 src/main/resources/META-INF/native-image/net.discordjug/javabot/jni-config.json create mode 100644 src/main/resources/database/migrations/11-11-2024_store_qotw_champion.sql delete mode 100644 src/main/resources/spamLinks.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f480b1d74..477c95034 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,13 +27,13 @@ jobs: if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 + - name: Set up JDK 21 + uses: graalvm/setup-graalvm@v1 with: - java-version: '17' - distribution: 'temurin' - - name: Build JAR - run: ./gradlew shadowJar + java-version: '21' + distribution: 'graalvm-community' + - name: Build native-image + run: ./gradlew nativeCompile -Pprod - name: Build Docker image run: docker build -t javabot . - name: Tag docker image diff --git a/Dockerfile b/Dockerfile index cb92850f9..e5129ae84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,21 @@ -FROM eclipse-temurin:21-jre -RUN mkdir /work -COPY build/libs/JavaBot-1.0.0-SNAPSHOT-all.jar /work/bot.jar +FROM alpine:latest +RUN apk add --no-cache libsm libxrender libxext libxtst libxi gcompat ttf-dejavu + +COPY build/native/nativeCompile /work WORKDIR /work + RUN chown 1000:1000 /work +USER 1000 +ENV HOME=/work + +# https://github.com/openjdk/jdk/pull/20169 +# need to create fake JAVA_HOME +RUN mkdir -p /tmp/JAVA_HOME/conf/fonts +RUN mkdir /tmp/JAVA_HOME/lib + + VOLUME "/work/config" VOLUME "/work/logs" VOLUME "/work/db" VOLUME "/work/purgeArchives" -USER 1000 -ENTRYPOINT [ "java", "-jar", "bot.jar" ] \ No newline at end of file +ENTRYPOINT [ "./javabot", "-Djava.home=/tmp/JAVA_HOME" ] diff --git a/build.gradle.kts b/build.gradle.kts index 33b9ced43..b39382126 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,9 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.* plugins { java id("com.github.johnrengelman.shadow") version "7.1.2" - id("org.springframework.boot") version "3.2.0" - id("io.spring.dependency-management") version "1.0.15.RELEASE" + id("org.springframework.boot") version "3.3.5" + id("io.spring.dependency-management") version "1.1.6" + id("org.graalvm.buildtools.native") version "0.10.3" checkstyle } @@ -64,6 +65,9 @@ dependencies { // Spring implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + + //required for registering native hints + implementation("org.jetbrains.kotlin:kotlin-reflect") } configurations { @@ -101,4 +105,27 @@ tasks.withType { checkstyle { toolVersion = "9.1" configDirectory.set(File("checkstyle")) -} \ No newline at end of file +} + +tasks.withType() { + exclude("**/generated/**") +} + +tasks.checkstyleAot { + isEnabled = false +} +tasks.processTestAot { + isEnabled = false +} + +graalvmNative { + binaries { + named("main") { + if (hasProperty("prod")) { + buildArgs.add("-O3") + } else { + quickBuild.set(true) + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7c2c02a73..f22613145 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ -rootProject.name = "JavaBot" +rootProject.name = "javabot" diff --git a/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java b/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java new file mode 100644 index 000000000..7c9b6c974 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java @@ -0,0 +1,73 @@ +package net.discordjug.javabot; + +import java.nio.channels.Channel; + +import club.minnced.discord.webhook.send.WebhookEmbed; +import net.discordjug.javabot.data.config.BotConfig; +import net.discordjug.javabot.data.config.GuildConfig; +import net.discordjug.javabot.data.config.GuildConfigItem; +import net.discordjug.javabot.data.config.SystemsConfig; +import net.discordjug.javabot.data.config.guild.HelpConfig; +import net.discordjug.javabot.data.config.guild.MessageCacheConfig; +import net.discordjug.javabot.data.config.guild.MetricsConfig; +import net.discordjug.javabot.data.config.guild.ModerationConfig; +import net.discordjug.javabot.data.config.guild.QOTWConfig; +import net.discordjug.javabot.data.config.guild.ServerLockConfig; +import net.discordjug.javabot.data.config.guild.StarboardConfig; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.ScheduledEvent; +import net.dv8tion.jda.api.entities.ThreadMember; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.forums.ForumTag; +import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.sticker.GuildSticker; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.managers.AudioManager; +import net.dv8tion.jda.internal.entities.MemberPresenceImpl; +import net.dv8tion.jda.internal.requests.restaction.PermOverrideData; +import org.h2.server.TcpServer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.core.io.ClassPathResource; + +/** + * Configure classes and resources to be accessible from native-image. + */ +@RegisterReflectionForBinding({ + //register config classes for reflection + BotConfig.class, GuildConfig.class, GuildConfigItem.class,SystemsConfig.class, + HelpConfig.class, MessageCacheConfig.class, MetricsConfig.class, ModerationConfig.class, QOTWConfig.class, ServerLockConfig.class, StarboardConfig.class, + + //ensure JDA can create necessary caches + User[].class, Guild[].class, Member[].class, Role[].class, Channel[].class, AudioManager[].class, ScheduledEvent[].class, ThreadMember[].class, ForumTag[].class, RichCustomEmoji[].class, GuildSticker[].class, MemberPresenceImpl[].class, + //needs to be serialized for channel managers etc + PermOverrideData.class, + //ensure that webhook embed authors can be serialized + WebhookEmbed.EmbedAuthor.class + }) +public class RuntimeHintsConfiguration implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + + //ensure resources are available in native-image + hints.resources().registerPattern("assets/**"); + hints.resources().registerPattern("database/**"); + hints.resources().registerPattern("help_guidelines/**"); + hints.resources().registerPattern("help_overview/**"); + hints.resources().registerResource(new ClassPathResource("quartz.properties")); + + //allow H2 to create the TCP server (necessary for starting the DB) + hints.reflection().registerType(TcpServer.class, MemberCategory.INVOKE_PUBLIC_METHODS); + + // JDA needs to be able to access listener methods + hints.reflection().registerType(ListenerAdapter.class, MemberCategory.INVOKE_PUBLIC_METHODS); + + // caffeine + hints.reflection().registerTypeIfPresent(getClass().getClassLoader(), "com.github.benmanes.caffeine.cache.SSW", MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } +} diff --git a/src/main/java/net/discordjug/javabot/SpringConfig.java b/src/main/java/net/discordjug/javabot/SpringConfig.java index 36deb1489..fe07deebf 100644 --- a/src/main/java/net/discordjug/javabot/SpringConfig.java +++ b/src/main/java/net/discordjug/javabot/SpringConfig.java @@ -10,7 +10,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - +import org.springframework.context.annotation.ImportRuntimeHints; import xyz.dynxsty.dih4jda.DIH4JDA; import xyz.dynxsty.dih4jda.DIH4JDABuilder; import xyz.dynxsty.dih4jda.exceptions.DIH4JDAException; @@ -34,6 +34,7 @@ * This class holds all configuration settings and {@link Bean}s. */ @Configuration +@ImportRuntimeHints(RuntimeHintsConfiguration.class) @RequiredArgsConstructor public class SpringConfig { @Bean @@ -71,7 +72,7 @@ JDA jda(BotConfig botConfig, ApplicationContext ctx) { return JDABuilder.createDefault(botConfig.getSystems().getJdaBotToken()) .setStatus(OnlineStatus.DO_NOT_DISTURB) .setChunkingFilter(ChunkingFilter.ALL) - .setMemberCachePolicy(MemberCachePolicy.ALL) + .setMemberCachePolicy(MemberCachePolicy.NONE) .enableCache(CacheFlag.ACTIVITY) .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_PRESENCES, GatewayIntent.MESSAGE_CONTENT) .addEventListeners(listeners.toArray()) diff --git a/src/main/java/net/discordjug/javabot/api/TomcatConfig.java b/src/main/java/net/discordjug/javabot/api/TomcatConfig.java index 7283ffac0..18ce7649b 100644 --- a/src/main/java/net/discordjug/javabot/api/TomcatConfig.java +++ b/src/main/java/net/discordjug/javabot/api/TomcatConfig.java @@ -7,6 +7,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.net.InetAddress; + import net.discordjug.javabot.data.config.SystemsConfig; @@ -18,6 +20,7 @@ public class TomcatConfig { private final int ajpPort; + private final InetAddress ajpAddress; private final boolean tomcatAjpEnabled; private final SystemsConfig systemsConfig; @@ -26,11 +29,13 @@ public class TomcatConfig { * @param ajpPort The port to run AJP under * @param tomcatAjpEnabled true if AJP is enabled, else false * @param systemsConfig an object representing the configuration of various systems + * @param ajpAddress the listen address for AJP */ - public TomcatConfig(@Value("${tomcat.ajp.port}") int ajpPort, @Value("${tomcat.ajp.enabled}") boolean tomcatAjpEnabled, SystemsConfig systemsConfig) { + public TomcatConfig(@Value("${tomcat.ajp.port}") int ajpPort, @Value("${tomcat.ajp.enabled}") boolean tomcatAjpEnabled, @Value("${tomcat.ajp.address}") InetAddress ajpAddress, SystemsConfig systemsConfig) { this.ajpPort = ajpPort; this.tomcatAjpEnabled = tomcatAjpEnabled; this.systemsConfig = systemsConfig; + this.ajpAddress = ajpAddress; } /** @@ -46,6 +51,7 @@ TomcatServletWebServerFactory servletContainer() { Connector ajpConnector = new Connector("org.apache.coyote.ajp.AjpNioProtocol"); AjpNioProtocol protocol= (AjpNioProtocol) ajpConnector.getProtocolHandler(); protocol.setSecret(systemsConfig.getApiConfig().getAjpSecret()); + protocol.setAddress(ajpAddress); ajpConnector.setPort(ajpPort); ajpConnector.setSecure(true); tomcat.addAdditionalTomcatConnectors(ajpConnector); diff --git a/src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java b/src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java index 375c19ca2..915b94ce7 100644 --- a/src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java +++ b/src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java @@ -69,9 +69,9 @@ public ResponseEntity> getQOTWLeaderboard( if (members == null || members.isEmpty()) { List topAccounts = pointsService.getTopAccounts(PAGE_AMOUNT, page); members = topAccounts.stream() - .map(account -> new Pair<>(account, jda.retrieveUserById(account.getUserId()).complete())) - .filter(pair -> guild.isMember(pair.second())) - .map(pair -> createAPIAccount(pair.first(), pair.second(), topAccounts, page)) + .map(account -> new Pair<>(account, guild.retrieveMemberById(account.getUserId()).complete())) + .filter(pair -> pair.second() != null) + .map(pair -> createAPIAccount(pair.first(), pair.second().getUser(), topAccounts, page)) .toList(); getCache().put(new Pair<>(guild.getIdLong(), page), members); } diff --git a/src/main/java/net/discordjug/javabot/listener/JobChannelCloseOldPostsListener.java b/src/main/java/net/discordjug/javabot/listener/JobChannelCloseOldPostsListener.java index 22832902b..d61751653 100644 --- a/src/main/java/net/discordjug/javabot/listener/JobChannelCloseOldPostsListener.java +++ b/src/main/java/net/discordjug/javabot/listener/JobChannelCloseOldPostsListener.java @@ -9,6 +9,7 @@ import net.discordjug.javabot.data.config.guild.ModerationConfig; import net.discordjug.javabot.util.InteractionUtils; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.UserSnowflake; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.events.channel.ChannelCreateEvent; @@ -53,7 +54,7 @@ public void onChannelCreate(ChannelCreateEvent event) { .setTitle("Post closed") .setDescription("This post has been blocked because you have created other recent posts.\nPlease do not spam posts.") .build()) - .setContent(post.getOwner().getAsMention()) + .setContent(UserSnowflake.fromId(post.getOwnerIdLong()).getAsMention()) .flatMap(msg -> post.getManager().setArchived(true).setLocked(true)) .queue(); return; diff --git a/src/main/java/net/discordjug/javabot/systems/help/HelpForumUpdater.java b/src/main/java/net/discordjug/javabot/systems/help/HelpForumUpdater.java index 458e448a4..4d134a081 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/HelpForumUpdater.java +++ b/src/main/java/net/discordjug/javabot/systems/help/HelpForumUpdater.java @@ -81,9 +81,8 @@ private void checkForumPost(@NotNull ThreadChannel post, HelpConfig config) { private void sendDMDormantInfoIfEnabled(ThreadChannel post, HelpConfig config) { if(Boolean.parseBoolean(preferenceService.getOrCreate(post.getOwnerIdLong(), Preference.PRIVATE_DORMANT_NOTIFICATIONS).getState())) { post - .getOwner() - .getUser() - .openPrivateChannel() + .getJDA() + .openPrivateChannelById(post.getOwnerIdLong()) .flatMap(c -> c.sendMessageEmbeds(createDMDormantInfo(post, config))) .queue(); } diff --git a/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java b/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java index d15763205..638e0a2cc 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java +++ b/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java @@ -22,7 +22,6 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; -import net.dv8tion.jda.api.interactions.components.ActionComponent; import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.buttons.Button; @@ -254,7 +253,7 @@ private void handleHelpThanksInteraction(@NotNull ButtonInteractionEvent event, return; } switch (id[2]) { - case "done" -> handleThanksCloseButton(event, manager, post); + case "done" -> handleThanksCloseButton(event, manager, post, ""); case "cancel" -> event.deferEdit().flatMap(h -> event.getMessage().delete()).queue(); default -> { List