mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2025-02-04 23:52:39 +01:00
Finalize console rotation
This commit is contained in:
parent
9d702ef8d9
commit
a7cdcf0859
@ -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();
|
||||
}
|
||||
|
@ -109,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;
|
||||
@ -164,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)
|
||||
@ -540,6 +543,10 @@ public abstract class AbstractDiscordSRV<
|
||||
|
||||
// Lifecycle
|
||||
|
||||
public ZonedDateTime getInitializeTime() {
|
||||
return initializeTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void runEnable() {
|
||||
try {
|
||||
|
@ -62,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;
|
||||
@ -176,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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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,7 +47,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.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@ -61,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;
|
||||
@ -68,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
|
||||
@ -177,6 +181,10 @@ public class SingleConsoleHandler {
|
||||
return config;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setConfig(ConsoleConfig config) {
|
||||
if (queueProcessingFuture != null) {
|
||||
queueProcessingFuture.cancel(false);
|
||||
@ -184,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) {
|
||||
@ -206,6 +219,8 @@ public class SingleConsoleHandler {
|
||||
if (messageCache != null) {
|
||||
this.messageCache = null;
|
||||
}
|
||||
mostRecentMessageChannelId.set(0);
|
||||
mostRecentMessageId.set(0);
|
||||
}
|
||||
|
||||
timeQueueProcess();
|
||||
@ -512,12 +527,23 @@ public class SingleConsoleHandler {
|
||||
}
|
||||
|
||||
sendFuture = sendFuture
|
||||
.thenCompose(__ -> discordSRV.destinations().lookupDestination(
|
||||
config.channel.asDestination(),
|
||||
true,
|
||||
true,
|
||||
OffsetDateTime.now())
|
||||
)
|
||||
.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
|
||||
@ -525,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) {
|
||||
@ -533,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);
|
||||
@ -546,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)) {
|
||||
|
@ -92,8 +92,9 @@ public class DestinationLookupHelper {
|
||||
}
|
||||
|
||||
String threadName = discordSRV.placeholderService().replacePlaceholders(threadConfig.threadName, threadNameContext);
|
||||
boolean privateThread = threadConfig.privateThread && !(threadContainer instanceof DiscordForumChannel);
|
||||
|
||||
DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadName);
|
||||
DiscordThreadChannel existingThread = findThread(threadContainer.getActiveThreads(), threadName, privateThread);
|
||||
if (existingThread != null && !existingThread.isArchived()) {
|
||||
futures.add(CompletableFuture.completedFuture(existingThread));
|
||||
continue;
|
||||
@ -103,7 +104,7 @@ public class DestinationLookupHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
String threadKey = Long.toUnsignedString(channelId) + ":" + threadName + "/" + threadConfig.privateThread;
|
||||
String threadKey = Long.toUnsignedString(channelId) + ":" + threadName + "/" + privateThread;
|
||||
|
||||
CompletableFuture<DiscordThreadChannel> future;
|
||||
synchronized (threadActions) {
|
||||
@ -113,26 +114,26 @@ public class DestinationLookupHelper {
|
||||
future = existingFuture;
|
||||
} else if (!threadConfig.unarchiveExisting) {
|
||||
// Unarchiving not allowed, create new
|
||||
future = createThread(threadContainer, threadName, threadConfig.privateThread, 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, threadName);
|
||||
DiscordThreadChannel archivedThread = findThread(archivedThreads, threadName, privateThread);
|
||||
if (archivedThread != null) {
|
||||
// Unarchive existing thread
|
||||
return unarchiveThread(archivedThread, logFailures);
|
||||
}
|
||||
|
||||
// Create thread
|
||||
return createThread(threadContainer, threadName, threadConfig.privateThread, logFailures);
|
||||
return createThread(threadContainer, threadName, privateThread, logFailures);
|
||||
}).exceptionally(t -> {
|
||||
if (logFailures) {
|
||||
logger.error("Failed to lookup threads in channel #" + threadContainer.getName(), t);
|
||||
@ -167,9 +168,9 @@ public class DestinationLookupHelper {
|
||||
});
|
||||
}
|
||||
|
||||
private DiscordThreadChannel findThread(Collection<DiscordThreadChannel> threads, String threadName) {
|
||||
private DiscordThreadChannel findThread(Collection<DiscordThreadChannel> threads, String threadName, boolean privateThread) {
|
||||
for (DiscordThreadChannel thread : threads) {
|
||||
if (thread.getName().equals(threadName)) {
|
||||
if (thread.getName().equals(threadName) && thread.isPublic() != privateThread) {
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
@ -183,7 +184,6 @@ public class DestinationLookupHelper {
|
||||
boolean logFailures
|
||||
) {
|
||||
boolean forum = threadContainer instanceof DiscordForumChannel;
|
||||
if (forum) privateThread = false;
|
||||
|
||||
Permission createPermission;
|
||||
if (forum) {
|
||||
|
@ -1,3 +1,21 @@
|
||||
/*
|
||||
* 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;
|
||||
@ -8,6 +26,7 @@ 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;
|
||||
@ -90,7 +109,7 @@ public class TemporaryLocalData {
|
||||
/**
|
||||
* {@link com.discordsrv.common.feature.console.SingleConsoleHandler} thread rotation.
|
||||
*/
|
||||
public Map<String, List<Long>> consoleThreadRotationIds;
|
||||
public Map<String, List<Long>> consoleThreadRotationIds = new HashMap<>();
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user