From 9d702ef8d90dad56e40102ce8895d72ad2495c98 Mon Sep 17 00:00:00 2001 From: Vankka Date: Sun, 10 Nov 2024 19:50:24 +0200 Subject: [PATCH] Initial for console rotation --- .../discordsrv/common/AbstractDiscordSRV.java | 9 ++ .../com/discordsrv/common/DiscordSRV.java | 2 + .../common/config/main/ConsoleConfig.java | 5 +- .../main/generic/DestinationConfig.java | 8 ++ .../connection/jda/JDAConnectionManager.java | 10 +- .../feature/console/SingleConsoleHandler.java | 8 +- .../helper/DestinationLookupHelper.java | 32 +++--- .../common/helper/TemporaryLocalData.java | 97 +++++++++++++++++++ .../com/discordsrv/common/helper/Timeout.java | 6 +- 9 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 634fc9ef..75896739 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -81,6 +81,7 @@ import com.discordsrv.common.feature.profile.ProfileManager; import com.discordsrv.common.feature.update.UpdateChecker; import com.discordsrv.common.helper.ChannelConfigHelper; import com.discordsrv.common.helper.DestinationLookupHelper; +import com.discordsrv.common.helper.TemporaryLocalData; import com.discordsrv.common.logging.adapter.DependencyLoggerAdapter; import com.discordsrv.common.util.ApiInstanceUtil; import com.discordsrv.common.util.UUIDUtil; @@ -154,6 +155,7 @@ public abstract class AbstractDiscordSRV< private JDAConnectionManager discordConnectionManager; private ChannelConfigHelper channelConfig; private DestinationLookupHelper destinationLookupHelper; + private TemporaryLocalData temporaryLocalData; private Storage storage; private LinkProvider linkProvider; @@ -191,6 +193,7 @@ public abstract class AbstractDiscordSRV< this.discordConnectionManager = new JDAConnectionManager(this); this.channelConfig = new ChannelConfigHelper(this); this.destinationLookupHelper = new DestinationLookupHelper(this); + this.temporaryLocalData = new TemporaryLocalData(this); this.updateChecker = new UpdateChecker(this); readManifest(); @@ -363,6 +366,11 @@ public abstract class AbstractDiscordSRV< return storage; } + @Override + public TemporaryLocalData temporaryLocalData() { + return temporaryLocalData; + } + @Override public final LinkProvider linkProvider() { return linkProvider; @@ -707,6 +715,7 @@ public abstract class AbstractDiscordSRV< } catch (Throwable t) { logger().error("Failed to close storage connection", t); } + temporaryLocalData.save(); this.status.set(Status.SHUTDOWN); } diff --git a/common/src/main/java/com/discordsrv/common/DiscordSRV.java b/common/src/main/java/com/discordsrv/common/DiscordSRV.java index b54ebf0c..01a6edb7 100644 --- a/common/src/main/java/com/discordsrv/common/DiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/DiscordSRV.java @@ -53,6 +53,7 @@ import com.discordsrv.common.feature.linking.LinkProvider; import com.discordsrv.common.feature.profile.ProfileManager; import com.discordsrv.common.helper.ChannelConfigHelper; import com.discordsrv.common.helper.DestinationLookupHelper; +import com.discordsrv.common.helper.TemporaryLocalData; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Caffeine; import okhttp3.OkHttpClient; @@ -110,6 +111,7 @@ public interface DiscordSRV extends DiscordSRVApi { // Storage Storage storage(); + TemporaryLocalData temporaryLocalData(); // Link Provider LinkProvider linkProvider(); diff --git a/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java b/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java index 542f4a1f..dfbe204b 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java @@ -32,7 +32,10 @@ import java.util.List; public class ConsoleConfig { @Comment("The console channel or thread") - public DestinationConfig.Single channel = new DestinationConfig.Single(); + public DestinationConfig.Single channel = new DestinationConfig.Single("DiscordSRV Console #%date:'w'%", true); + + @Comment("The amount of threads to keep. Rotation interval is based on placeholders in the thread name") + public int threadsToKeepInRotation = 3; public Appender appender = new Appender(); diff --git a/common/src/main/java/com/discordsrv/common/config/main/generic/DestinationConfig.java b/common/src/main/java/com/discordsrv/common/config/main/generic/DestinationConfig.java index ed931ed4..626193ad 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/generic/DestinationConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/generic/DestinationConfig.java @@ -51,6 +51,14 @@ public class DestinationConfig { @Setting(nodeFromParent = true) public ThreadConfig thread = new ThreadConfig(""); + @SuppressWarnings("unused") // Configurate + public Single() {} + + public Single(String threadName, boolean privateThread) { + this.thread.threadName = threadName; + this.thread.privateThread = privateThread; + } + public DestinationConfig asDestination() { DestinationConfig config = new DestinationConfig(); if (thread == null || StringUtils.isEmpty(thread.threadName)) { diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index b70aa39d..4896c3af 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -94,12 +94,12 @@ public class JDAConnectionManager implements DiscordConnectionManager { private final Set memberCachePolicies = new HashSet<>(); // Bot owner details - private final Timeout botOwnerTimeout = new Timeout(5, TimeUnit.MINUTES); + private final Timeout botOwnerTimeout = new Timeout(Duration.ofMinutes(5)); private final AtomicReference> botOwnerRequest = new AtomicReference<>(); // Logging timeouts - private final Timeout mfaTimeout = new Timeout(30, TimeUnit.SECONDS); - private final Timeout serverErrorTimeout = new Timeout(20, TimeUnit.SECONDS); + private final Timeout mfaTimeout = new Timeout(Duration.ofSeconds(30)); + private final Timeout serverErrorTimeout = new Timeout(Duration.ofSeconds(20)); public JDAConnectionManager(DiscordSRV discordSRV) { this.discordSRV = discordSRV; @@ -293,7 +293,9 @@ public class JDAConnectionManager implements DiscordConnectionManager { throw new IllegalStateException("Cannot reconnect, still active"); } - return connectionFuture = discordSRV.scheduler().execute(this::connectInternal); + this.connectInternal(); + return CompletableFuture.completedFuture(null); + //return connectionFuture = discordSRV.scheduler().execute(this::connectInternal); } private void connectInternal() { diff --git a/common/src/main/java/com/discordsrv/common/feature/console/SingleConsoleHandler.java b/common/src/main/java/com/discordsrv/common/feature/console/SingleConsoleHandler.java index fd1ff4aa..ef7140c0 100644 --- a/common/src/main/java/com/discordsrv/common/feature/console/SingleConsoleHandler.java +++ b/common/src/main/java/com/discordsrv/common/feature/console/SingleConsoleHandler.java @@ -45,6 +45,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.tuple.Pair; import java.time.Duration; +import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; @@ -511,7 +512,12 @@ public class SingleConsoleHandler { } sendFuture = sendFuture - .thenCompose(__ -> discordSRV.destinations().lookupDestination(config.channel.asDestination(), true, true)) + .thenCompose(__ -> discordSRV.destinations().lookupDestination( + config.channel.asDestination(), + true, + true, + OffsetDateTime.now()) + ) .thenCompose(channels -> { if (channels.isEmpty()) { // Nowhere to send to diff --git a/common/src/main/java/com/discordsrv/common/helper/DestinationLookupHelper.java b/common/src/main/java/com/discordsrv/common/helper/DestinationLookupHelper.java index 0c9b3345..9a7b98b9 100644 --- a/common/src/main/java/com/discordsrv/common/helper/DestinationLookupHelper.java +++ b/common/src/main/java/com/discordsrv/common/helper/DestinationLookupHelper.java @@ -48,7 +48,8 @@ public class DestinationLookupHelper { public CompletableFuture> lookupDestination( DestinationConfig config, boolean allowRequests, - boolean logFailures + boolean logFailures, + Object... threadNameContext ) { List> futures = new ArrayList<>(); @@ -90,7 +91,9 @@ public class DestinationLookupHelper { continue; } - DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadConfig); + String threadName = discordSRV.placeholderService().replacePlaceholders(threadConfig.threadName, threadNameContext); + + DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadName); if (existingThread != null && !existingThread.isArchived()) { futures.add(CompletableFuture.completedFuture(existingThread)); continue; @@ -100,7 +103,7 @@ public class DestinationLookupHelper { continue; } - String threadKey = Long.toUnsignedString(channelId) + ":" + threadConfig.threadName + "/" + threadConfig.privateThread; + String threadKey = Long.toUnsignedString(channelId) + ":" + threadName + "/" + threadConfig.privateThread; CompletableFuture future; synchronized (threadActions) { @@ -110,7 +113,7 @@ public class DestinationLookupHelper { future = existingFuture; } else if (!threadConfig.unarchiveExisting) { // Unarchiving not allowed, create new - future = createThread(threadContainer, threadConfig, logFailures); + future = createThread(threadContainer, threadName, threadConfig.privateThread, logFailures); } else if (existingThread != null) { // Unarchive existing thread future = unarchiveThread(existingThread, logFailures); @@ -122,14 +125,14 @@ public class DestinationLookupHelper { : threadContainer.retrieveArchivedPublicThreads(); future = threads.thenCompose(archivedThreads -> { - DiscordThreadChannel archivedThread = findThread(archivedThreads, threadConfig); + DiscordThreadChannel archivedThread = findThread(archivedThreads, threadName); if (archivedThread != null) { // Unarchive existing thread return unarchiveThread(archivedThread, logFailures); } // Create thread - return createThread(threadContainer, threadConfig, logFailures); + return createThread(threadContainer, threadName, threadConfig.privateThread, logFailures); }).exceptionally(t -> { if (logFailures) { logger.error("Failed to lookup threads in channel #" + threadContainer.getName(), t); @@ -164,9 +167,9 @@ public class DestinationLookupHelper { }); } - private DiscordThreadChannel findThread(Collection threads, ThreadConfig config) { + private DiscordThreadChannel findThread(Collection threads, String threadName) { for (DiscordThreadChannel thread : threads) { - if (thread.getName().equals(config.threadName)) { + if (thread.getName().equals(threadName)) { return thread; } } @@ -175,11 +178,12 @@ public class DestinationLookupHelper { private CompletableFuture createThread( DiscordThreadContainer threadContainer, - ThreadConfig threadConfig, + String threadName, + boolean privateThread, boolean logFailures ) { boolean forum = threadContainer instanceof DiscordForumChannel; - boolean privateThread = !forum && threadConfig.privateThread; + if (forum) privateThread = false; Permission createPermission; if (forum) { @@ -196,7 +200,7 @@ public class DestinationLookupHelper { ); if (missingPermissions != null) { if (logFailures) { - logger.error("Failed to create thread \"" + threadConfig.threadName + "\" " + logger.error("Failed to create thread \"" + threadName + "\" " + "in channel #" + threadContainer.getName() + ": " + missingPermissions); } return CompletableFuture.completedFuture(null); @@ -205,15 +209,15 @@ public class DestinationLookupHelper { CompletableFuture future; if (forum) { future = ((DiscordForumChannel) threadContainer).createPost( - threadConfig.threadName, + threadName, SendableDiscordMessage.builder().setContent("\u200B").build() // zero-width-space ); } else { - future = threadContainer.createThread(threadConfig.threadName, privateThread); + future = threadContainer.createThread(threadName, privateThread); } return future.exceptionally(t -> { if (logFailures) { - logger.error("Failed to create thread \"" + threadConfig.threadName + "\" " + logger.error("Failed to create thread \"" + threadName + "\" " + "in channel #" + threadContainer.getName(), t); } return null; diff --git a/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java b/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java new file mode 100644 index 00000000..3871e0a8 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java @@ -0,0 +1,97 @@ +package com.discordsrv.common.helper; + +import com.discordsrv.common.DiscordSRV; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NonBlocking; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; + +/** + * Data that may or may not actually ever persist. + * @see Model + * @see #get() + * @see #save() + */ +public class TemporaryLocalData { + + private final DiscordSRV discordSRV; + private final Path file; + private Model model; + private Future saveFuture; + + public TemporaryLocalData(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + this.file = discordSRV.dataDirectory().resolve(".temporary-local-data.json"); + } + + @Blocking + public Model get() { + if (model != null) { + return model; + } + + synchronized (this) { + return model = resolve(); + } + } + + @NonBlocking + public void saveLater() { + synchronized (this) { + if (saveFuture != null && !saveFuture.isDone()) { + return; + } + saveFuture = discordSRV.scheduler().runLater(this::save, Duration.ofSeconds(30)); + } + } + + @Blocking + public void save() { + synchronized (this) { + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(file))) { + discordSRV.json().writeValue(outputStream, model); + } catch (IOException e) { + discordSRV.logger().error("Failed to save temporary local data", e); + } + } + } + + @Blocking + private Model resolve() { + synchronized (this) { + if (model != null) { + return model; + } + + if (!Files.exists(file)) { + return new Model(); + } + + try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(file))) { + return discordSRV.json().readValue(inputStream, Model.class); + } catch (IOException e) { + discordSRV.logger().error("Failed to load temporary local data, resetting", e); + return new Model(); + } + } + } + + /** + * Saved/loaded via {@link DiscordSRV#json()} ({@link com.fasterxml.jackson.databind.ObjectMapper}). + */ + public static class Model { + + /** + * {@link com.discordsrv.common.feature.console.SingleConsoleHandler} thread rotation. + */ + public Map> consoleThreadRotationIds; + + + } +} diff --git a/common/src/main/java/com/discordsrv/common/helper/Timeout.java b/common/src/main/java/com/discordsrv/common/helper/Timeout.java index df04951a..2005fefd 100644 --- a/common/src/main/java/com/discordsrv/common/helper/Timeout.java +++ b/common/src/main/java/com/discordsrv/common/helper/Timeout.java @@ -20,7 +20,7 @@ package com.discordsrv.common.helper; import org.jetbrains.annotations.NotNull; -import java.util.concurrent.TimeUnit; +import java.time.Duration; import java.util.concurrent.atomic.AtomicLong; /** @@ -31,8 +31,8 @@ public class Timeout { private final AtomicLong last = new AtomicLong(0); private final long timeoutMS; - public Timeout(long time, @NotNull TimeUnit unit) { - this(unit.toMillis(time)); + public Timeout(@NotNull Duration duration) { + this(duration.toMillis()); } public Timeout(long timeoutMS) {