Finalize console rotation

This commit is contained in:
Vankka 2024-11-14 19:57:21 +02:00
commit b8f552e5e6
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
16 changed files with 298 additions and 30 deletions

View File

@ -29,6 +29,8 @@ import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderPrefix;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
@PlaceholderPrefix("channel_")
public interface DiscordGuildChannel extends DiscordChannel, Snowflake {
@ -55,4 +57,10 @@ public interface DiscordGuildChannel extends DiscordChannel, Snowflake {
@NotNull
@Placeholder("jump_url")
String getJumpUrl();
/**
* Deletes the channel.
* @return a future completing upon deletion
*/
CompletableFuture<Void> delete();
}

View File

@ -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;
@ -108,6 +109,7 @@ import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@ -154,6 +156,7 @@ public abstract class AbstractDiscordSRV<
private JDAConnectionManager discordConnectionManager;
private ChannelConfigHelper channelConfig;
private DestinationLookupHelper destinationLookupHelper;
private TemporaryLocalData temporaryLocalData;
private Storage storage;
private LinkProvider linkProvider;
@ -162,6 +165,8 @@ public abstract class AbstractDiscordSRV<
private UpdateChecker updateChecker;
protected VersionInfo versionInfo;
private final ZonedDateTime initializeTime = ZonedDateTime.now();
private OkHttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
@ -191,6 +196,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 +369,11 @@ public abstract class AbstractDiscordSRV<
return storage;
}
@Override
public TemporaryLocalData temporaryLocalData() {
return temporaryLocalData;
}
@Override
public final LinkProvider linkProvider() {
return linkProvider;
@ -532,6 +543,10 @@ public abstract class AbstractDiscordSRV<
// Lifecycle
public ZonedDateTime getInitializeTime() {
return initializeTime;
}
@Override
public final void runEnable() {
try {
@ -707,6 +722,7 @@ public abstract class AbstractDiscordSRV<
} catch (Throwable t) {
logger().error("Failed to close storage connection", t);
}
temporaryLocalData.save();
this.status.set(Status.SHUTDOWN);
}

View File

@ -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;
@ -61,6 +62,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@ -110,6 +112,7 @@ public interface DiscordSRV extends DiscordSRVApi {
// Storage
Storage storage();
TemporaryLocalData temporaryLocalData();
// Link Provider
LinkProvider linkProvider();
@ -174,6 +177,7 @@ public interface DiscordSRV extends DiscordSRVApi {
List<ReloadResult> runReload(Set<ReloadFlag> flags, boolean silent);
CompletableFuture<Void> invokeDisable();
boolean isServerStarted();
ZonedDateTime getInitializeTime();
@Nullable
default GameCommandExecutionHelper executeHelper() {

View File

@ -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();

View File

@ -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)) {

View File

@ -48,14 +48,13 @@ public class ThreadConfig {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThreadConfig that = (ThreadConfig) o;
return unarchiveExisting == that.unarchiveExisting
&& privateThread == that.privateThread
return privateThread == that.privateThread
&& Objects.equals(channelId, that.channelId)
&& Objects.equals(threadName, that.threadName);
}
@Override
public int hashCode() {
return Objects.hash(channelId, threadName, unarchiveExisting, privateThread);
return Objects.hash(channelId, threadName, privateThread);
}
}

View File

@ -20,41 +20,57 @@ package com.discordsrv.common.core.placeholder.context;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.api.placeholder.format.FormattedText;
import com.discordsrv.common.DiscordSRV;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.time.DateTimeException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.concurrent.TimeUnit;
public class GlobalDateFormattingContext {
private static final String TIMESTAMP_IDENTIFIER = "timestamp";
private final DiscordSRV discordSRV;
private final LoadingCache<String, DateTimeFormatter> cache;
public GlobalDateFormattingContext(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
this.cache = discordSRV.caffeineBuilder()
.expireAfterAccess(30, TimeUnit.SECONDS)
.build(DateTimeFormatter::ofPattern);
}
@Placeholder("date")
public String formatDate(ZonedDateTime time, @PlaceholderRemainder String format) {
public CharSequence formatDate(TemporalAccessor time, @PlaceholderRemainder String format) {
if (format.startsWith(TIMESTAMP_IDENTIFIER)) {
String style = format.substring(TIMESTAMP_IDENTIFIER.length());
if (!style.isEmpty() && !style.startsWith(":")) {
if ((!style.isEmpty() && !style.startsWith(":")) || !time.isSupported(ChronoField.INSTANT_SECONDS)) {
return null;
}
return "<t:" + time.toEpochSecond() + style + ">";
return FormattedText.of("<t:" + time.getLong(ChronoField.INSTANT_SECONDS) + style + ">");
}
DateTimeFormatter formatter = cache.get(format);
if (formatter == null) {
throw new IllegalStateException("Illegal state");
}
return formatter.format(time);
try {
return formatter.format(time);
} catch (DateTimeException e) {
return e.getMessage();
}
}
@Placeholder(value = "start_date", relookup = "date")
public ZonedDateTime getStartDate() {
return discordSRV.getInitializeTime();
}
}

View File

@ -157,4 +157,8 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
return discordSRV.discordAPI().mapExceptions(future);
}
@Override
public CompletableFuture<Void> delete() {
return discordSRV.discordAPI().mapExceptions(() -> channel.delete().submit());
}
}

View File

@ -76,6 +76,11 @@ public class DiscordForumChannelImpl implements DiscordForumChannel {
return channel.getJumpUrl();
}
@Override
public CompletableFuture<Void> delete() {
return discordSRV.discordAPI().mapExceptions(() -> channel.delete().submit());
}
@Override
public @NotNull List<DiscordThreadChannel> getActiveThreads() {
List<ThreadChannel> threads = channel.getThreadChannels();

View File

@ -36,6 +36,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

View File

@ -94,12 +94,12 @@ public class JDAConnectionManager implements DiscordConnectionManager {
private final Set<DiscordMemberCachePolicy> 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<CompletableFuture<DiscordUser>> 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;

View File

@ -29,6 +29,7 @@ import com.discordsrv.common.core.logging.NamedLogger;
import com.discordsrv.common.core.logging.backend.LoggingBackend;
import com.discordsrv.common.core.module.type.AbstractModule;
import com.discordsrv.common.feature.console.entry.LogEntry;
import com.discordsrv.common.helper.TemporaryLocalData;
import com.discordsrv.common.logging.LogAppender;
import com.discordsrv.common.logging.LogLevel;
import org.jetbrains.annotations.NotNull;
@ -104,6 +105,13 @@ public class ConsoleModule extends AbstractModule<DiscordSRV> implements LogAppe
handlers.add(new SingleConsoleHandler(discordSRV, logger(), config));
}
TemporaryLocalData.Model temporaryData = discordSRV.temporaryLocalData().get();
synchronized (temporaryData) {
temporaryData.consoleThreadRotationIds.keySet()
.removeIf(key -> handlers.stream().noneMatch(handler -> handler.getKey().equals(key)));
}
logger().debug(handlers.size() + " console handlers active");
}

View File

@ -19,6 +19,7 @@
package com.discordsrv.common.feature.console;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordChannel;
import com.discordsrv.api.discord.entity.channel.DiscordGuildChannel;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
@ -38,6 +39,7 @@ import com.discordsrv.common.core.logging.Logger;
import com.discordsrv.common.feature.console.entry.LogEntry;
import com.discordsrv.common.feature.console.entry.LogMessage;
import com.discordsrv.common.feature.console.message.ConsoleMessage;
import com.discordsrv.common.helper.TemporaryLocalData;
import com.discordsrv.common.logging.LogLevel;
import net.dv8tion.jda.api.entities.Message;
import org.apache.commons.lang3.StringUtils;
@ -45,6 +47,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@ -60,6 +63,7 @@ public class SingleConsoleHandler {
private final DiscordSRV discordSRV;
private final Logger logger;
private ConsoleConfig config;
private String key;
private Queue<LogEntry> messageQueue;
private Deque<Pair<SendableDiscordMessage, Boolean>> sendQueue;
private Future<?> queueProcessingFuture;
@ -67,6 +71,7 @@ public class SingleConsoleHandler {
// Editing
private List<LogMessage> messageCache;
private final AtomicLong mostRecentMessageChannelId = new AtomicLong(0);
private final AtomicLong mostRecentMessageId = new AtomicLong(0);
// Sending
@ -176,6 +181,10 @@ public class SingleConsoleHandler {
return config;
}
public String getKey() {
return key;
}
public void setConfig(ConsoleConfig config) {
if (queueProcessingFuture != null) {
queueProcessingFuture.cancel(false);
@ -183,6 +192,11 @@ public class SingleConsoleHandler {
this.config = config;
DestinationConfig.Single destination = config.channel;
this.key = Long.toUnsignedString(config.channel.channelId)
+ "-" + destination.thread.threadName
+ "-" + config.channel.thread.privateThread;
boolean sendOn = config.appender.outputMode != ConsoleConfig.OutputMode.OFF;
if (sendOn) {
if (messageQueue == null) {
@ -205,6 +219,8 @@ public class SingleConsoleHandler {
if (messageCache != null) {
this.messageCache = null;
}
mostRecentMessageChannelId.set(0);
mostRecentMessageId.set(0);
}
timeQueueProcess();
@ -511,7 +527,23 @@ public class SingleConsoleHandler {
}
sendFuture = sendFuture
.thenCompose(__ -> discordSRV.destinations().lookupDestination(config.channel.asDestination(), true, true))
.thenCompose(__ -> {
if (mostRecentMessageId.get() != 0) {
long channelId = mostRecentMessageChannelId.get();
DiscordMessageChannel channel = discordSRV.discordAPI().getMessageChannelById(channelId);
if (channel instanceof DiscordGuildMessageChannel) {
return CompletableFuture.completedFuture(
Collections.singletonList((DiscordGuildMessageChannel) channel)
);
}
}
return discordSRV.destinations().lookupDestination(
config.channel.asDestination(),
true,
true,
ZonedDateTime.now());
})
.thenCompose(channels -> {
if (channels.isEmpty()) {
// Nowhere to send to
@ -519,6 +551,43 @@ public class SingleConsoleHandler {
}
DiscordGuildMessageChannel channel = channels.iterator().next();
int amountOfChannels = config.threadsToKeepInRotation;
if (amountOfChannels > 0) {
TemporaryLocalData.Model temporaryData = discordSRV.temporaryLocalData().get();
List<Long> channelsToDelete = null;
synchronized (temporaryData) {
Map<String, List<Long>> rotationIds = temporaryData.consoleThreadRotationIds;
List<Long> channelIds = rotationIds.computeIfAbsent(key, k -> new ArrayList<>(amountOfChannels));
if (channelIds.isEmpty() || channelIds.get(0) != channel.getId()) {
channelIds.add(0, channel.getId());
}
if (channelIds.size() > amountOfChannels) {
rotationIds.put(key, channelIds.subList(0, amountOfChannels));
channelsToDelete = channelIds.subList(amountOfChannels, channelIds.size());
}
}
discordSRV.temporaryLocalData().saveLater();
if (channelsToDelete != null) {
for (Long channelId : channelsToDelete) {
DiscordChannel channelToDelete = discordSRV.discordAPI().getChannelById(channelId);
if (channelToDelete instanceof DiscordGuildChannel) {
((DiscordGuildChannel) channelToDelete).delete();
}
}
}
}
return CompletableFuture.completedFuture(channel);
})
.thenCompose(channel -> {
if (channel == null) {
return null;
}
synchronized (mostRecentMessageId) {
long messageId = mostRecentMessageId.get();
if (messageId != 0) {
@ -527,6 +596,9 @@ public class SingleConsoleHandler {
}
return channel.editMessageById(messageId, sendableMessage);
}
// Rotation may cause channel to change
mostRecentMessageChannelId.set(channel.getId());
}
return channel.sendMessage(sendableMessage);
@ -540,6 +612,10 @@ public class SingleConsoleHandler {
sentFirstBatch = true;
return msg;
}).exceptionally(ex -> {
synchronized (mostRecentMessageId) {
mostRecentMessageId.set(0);
}
String error = "Failed to send message to console channel";
String messageContent = sendableMessage.getContent();
if (messageContent != null && messageContent.contains(error)) {

View File

@ -48,7 +48,8 @@ public class DestinationLookupHelper {
public CompletableFuture<List<DiscordGuildMessageChannel>> lookupDestination(
DestinationConfig config,
boolean allowRequests,
boolean logFailures
boolean logFailures,
Object... threadNameContext
) {
List<CompletableFuture<? extends DiscordGuildMessageChannel>> futures = new ArrayList<>();
@ -90,7 +91,10 @@ public class DestinationLookupHelper {
continue;
}
DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadConfig);
String threadName = discordSRV.placeholderService().replacePlaceholders(threadConfig.threadName, threadNameContext);
boolean privateThread = threadConfig.privateThread && !(threadContainer instanceof DiscordForumChannel);
DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadName, privateThread);
if (existingThread != null && !existingThread.isArchived()) {
futures.add(CompletableFuture.completedFuture(existingThread));
continue;
@ -100,7 +104,7 @@ public class DestinationLookupHelper {
continue;
}
String threadKey = Long.toUnsignedString(channelId) + ":" + threadConfig.threadName + "/" + threadConfig.privateThread;
String threadKey = Long.toUnsignedString(channelId) + ":" + threadName + "/" + privateThread;
CompletableFuture<DiscordThreadChannel> future;
synchronized (threadActions) {
@ -110,26 +114,26 @@ public class DestinationLookupHelper {
future = existingFuture;
} else if (!threadConfig.unarchiveExisting) {
// Unarchiving not allowed, create new
future = createThread(threadContainer, threadConfig, logFailures);
future = createThread(threadContainer, threadName, privateThread, logFailures);
} else if (existingThread != null) {
// Unarchive existing thread
future = unarchiveThread(existingThread, logFailures);
} else {
// Lookup threads
CompletableFuture<List<DiscordThreadChannel>> threads =
threadConfig.privateThread
privateThread
? threadContainer.retrieveArchivedPrivateThreads()
: threadContainer.retrieveArchivedPublicThreads();
future = threads.thenCompose(archivedThreads -> {
DiscordThreadChannel archivedThread = findThread(archivedThreads, threadConfig);
DiscordThreadChannel archivedThread = findThread(archivedThreads, threadName, privateThread);
if (archivedThread != null) {
// Unarchive existing thread
return unarchiveThread(archivedThread, logFailures);
}
// Create thread
return createThread(threadContainer, threadConfig, logFailures);
return createThread(threadContainer, threadName, privateThread, logFailures);
}).exceptionally(t -> {
if (logFailures) {
logger.error("Failed to lookup threads in channel #" + threadContainer.getName(), t);
@ -164,9 +168,9 @@ public class DestinationLookupHelper {
});
}
private DiscordThreadChannel findThread(Collection<DiscordThreadChannel> threads, ThreadConfig config) {
private DiscordThreadChannel findThread(Collection<DiscordThreadChannel> threads, String threadName, boolean privateThread) {
for (DiscordThreadChannel thread : threads) {
if (thread.getName().equals(config.threadName)) {
if (thread.getName().equals(threadName) && thread.isPublic() != privateThread) {
return thread;
}
}
@ -175,11 +179,11 @@ public class DestinationLookupHelper {
private CompletableFuture<DiscordThreadChannel> createThread(
DiscordThreadContainer threadContainer,
ThreadConfig threadConfig,
String threadName,
boolean privateThread,
boolean logFailures
) {
boolean forum = threadContainer instanceof DiscordForumChannel;
boolean privateThread = !forum && threadConfig.privateThread;
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<DiscordThreadChannel> 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;

View File

@ -0,0 +1,116 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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.HashMap;
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<String, List<Long>> consoleThreadRotationIds = new HashMap<>();
}
}

View File

@ -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) {