mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2024-11-01 08:39:31 +01:00
Merge branch 'console'
This commit is contained in:
commit
812f459d93
@ -159,6 +159,15 @@ public interface ReceivedDiscordMessage extends Snowflake {
|
||||
*/
|
||||
CompletableFuture<ReceivedDiscordMessage> edit(@NotNull SendableDiscordMessage message);
|
||||
|
||||
/**
|
||||
* Send the provided message in the channel this message was sent in, replying to this message.
|
||||
*
|
||||
* @param message the message
|
||||
* @return a future that will fail if the request fails, otherwise the new message provided by the request response
|
||||
* @throws IllegalArgumentException if the provided message is a webhook message
|
||||
*/
|
||||
CompletableFuture<ReceivedDiscordMessage> reply(@NotNull SendableDiscordMessage message);
|
||||
|
||||
class Attachment {
|
||||
|
||||
private final String fileName;
|
||||
|
@ -109,8 +109,38 @@ public interface SendableDiscordMessage {
|
||||
return getWebhookUsername() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw inputs streams and file names for attachments, for this message.
|
||||
* @return the map of input streams to file names
|
||||
*/
|
||||
Map<InputStream, String> getAttachments();
|
||||
|
||||
/**
|
||||
* If notifications for this message are suppressed.
|
||||
* @return if sending this message doesn't cause a notification
|
||||
*/
|
||||
boolean isSuppressedNotifications();
|
||||
|
||||
/**
|
||||
* If embeds for this message are suppressed.
|
||||
* @return if embeds for this message are suppressed
|
||||
*/
|
||||
boolean isSuppressedEmbeds();
|
||||
|
||||
/**
|
||||
* Gets the id for the message this message is in reply to
|
||||
* @return the message id
|
||||
*/
|
||||
Long getMessageIdToReplyTo();
|
||||
|
||||
/**
|
||||
* Creates a copy of this {@link SendableDiscordMessage} with the specified reply message id.
|
||||
*
|
||||
* @param replyingToMessageId the reply message id
|
||||
* @return a new {@link SendableDiscordMessage} identical to the current instance except for the reply message id
|
||||
*/
|
||||
SendableDiscordMessage withReplyingToMessageId(Long replyingToMessageId);
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue") // API
|
||||
interface Builder {
|
||||
|
||||
@ -250,6 +280,54 @@ public interface SendableDiscordMessage {
|
||||
*/
|
||||
Builder addAttachment(InputStream inputStream, String fileName);
|
||||
|
||||
/**
|
||||
* Sets if this message's notifications will be suppressed.
|
||||
* @param suppressedNotifications if notifications should be suppressed
|
||||
* @return this builder, useful for chaining
|
||||
*/
|
||||
Builder setSuppressedNotifications(boolean suppressedNotifications);
|
||||
|
||||
/**
|
||||
* Checks if this builder has notifications suppressed.
|
||||
* @return {@code true} if notifications should be suppressed for this message
|
||||
*/
|
||||
boolean isSuppressedNotifications();
|
||||
|
||||
/**
|
||||
* Sets if this message's embeds will be suppressed.
|
||||
* @param suppressedEmbeds if embeds should be suppressed
|
||||
* @return this builder, useful for chaining
|
||||
*/
|
||||
Builder setSuppressedEmbeds(boolean suppressedEmbeds);
|
||||
|
||||
/**
|
||||
* Checks if this builder has embeds suppressed.
|
||||
* @return {@code true} if embeds should be suppressed for this message
|
||||
*/
|
||||
boolean isSuppressedEmbeds();
|
||||
|
||||
/**
|
||||
* Sets the message this message should be in reply to.
|
||||
* @param messageId the id for the message this is in reply to
|
||||
* @return this builder, useful for chaining
|
||||
*/
|
||||
Builder setMessageIdToReplyTo(Long messageId);
|
||||
|
||||
/**
|
||||
* Sets the message this message should be in reply to.
|
||||
* @param message the message this is in reply to
|
||||
* @return this builder, useful for chaining
|
||||
*/
|
||||
default Builder setMessageToReplyTo(@NotNull ReceivedDiscordMessage message) {
|
||||
return setMessageIdToReplyTo(message.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id for the message this message is in reply to
|
||||
* @return the message id
|
||||
*/
|
||||
Long getMessageIdToReplyTo();
|
||||
|
||||
/**
|
||||
* Checks if this builder has any sendable content.
|
||||
* @return {@code true} if there is no sendable content
|
||||
|
@ -52,6 +52,9 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
private final String webhookUsername;
|
||||
private final String webhookAvatarUrl;
|
||||
private final Map<InputStream, String> attachments;
|
||||
private final boolean suppressedNotifications;
|
||||
private final boolean suppressedEmbeds;
|
||||
private final Long replyingToMessageId;
|
||||
|
||||
protected SendableDiscordMessageImpl(
|
||||
String content,
|
||||
@ -60,7 +63,10 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
Set<AllowedMention> allowedMentions,
|
||||
String webhookUsername,
|
||||
String webhookAvatarUrl,
|
||||
Map<InputStream, String> attachments
|
||||
Map<InputStream, String> attachments,
|
||||
boolean suppressedNotifications,
|
||||
boolean suppressedEmbeds,
|
||||
Long replyingToMessageId
|
||||
) {
|
||||
this.content = content;
|
||||
this.embeds = Collections.unmodifiableList(embeds);
|
||||
@ -69,6 +75,24 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
this.webhookUsername = webhookUsername;
|
||||
this.webhookAvatarUrl = webhookAvatarUrl;
|
||||
this.attachments = Collections.unmodifiableMap(attachments);
|
||||
this.suppressedNotifications = suppressedNotifications;
|
||||
this.suppressedEmbeds = suppressedEmbeds;
|
||||
this.replyingToMessageId = replyingToMessageId;
|
||||
}
|
||||
|
||||
public SendableDiscordMessageImpl withReplyingToMessageId(Long replyingToMessageId) {
|
||||
return new SendableDiscordMessageImpl(
|
||||
content,
|
||||
embeds,
|
||||
actionRows,
|
||||
allowedMentions,
|
||||
webhookUsername,
|
||||
webhookAvatarUrl,
|
||||
attachments,
|
||||
suppressedNotifications,
|
||||
suppressedEmbeds,
|
||||
replyingToMessageId
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -102,6 +126,21 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
return webhookAvatarUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuppressedNotifications() {
|
||||
return suppressedNotifications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuppressedEmbeds() {
|
||||
return suppressedEmbeds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getMessageIdToReplyTo() {
|
||||
return replyingToMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<InputStream, String> getAttachments() {
|
||||
return attachments;
|
||||
@ -116,6 +155,9 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
private String webhookUsername;
|
||||
private String webhookAvatarUrl;
|
||||
private final Map<InputStream, String> attachments = new LinkedHashMap<>();
|
||||
private boolean suppressedNotifications;
|
||||
private boolean suppressedEmbeds;
|
||||
private Long replyingToMessageId;
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
@ -226,9 +268,42 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
||||
return (content == null || content.isEmpty()) && embeds.isEmpty() && attachments.isEmpty() && actionRows.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setSuppressedNotifications(boolean suppressedNotifications) {
|
||||
this.suppressedNotifications = suppressedNotifications;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuppressedEmbeds() {
|
||||
return suppressedEmbeds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setMessageIdToReplyTo(Long messageId) {
|
||||
replyingToMessageId = messageId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getMessageIdToReplyTo() {
|
||||
return replyingToMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setSuppressedEmbeds(boolean suppressedEmbeds) {
|
||||
this.suppressedEmbeds = suppressedEmbeds;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuppressedNotifications() {
|
||||
return suppressedNotifications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull SendableDiscordMessage build() {
|
||||
return new SendableDiscordMessageImpl(content, embeds, actionRows, allowedMentions, webhookUsername, webhookAvatarUrl, attachments);
|
||||
return new SendableDiscordMessageImpl(content, embeds, actionRows, allowedMentions, webhookUsername, webhookAvatarUrl, attachments, suppressedNotifications, suppressedEmbeds, replyingToMessageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -85,12 +85,13 @@ dependencies {
|
||||
// Logging
|
||||
compileOnly(libs.log4j.core)
|
||||
|
||||
// Adventure, MCDiscordReserializer, EnhancedLegacyText
|
||||
// Adventure, ANSI (version upgrade for serializer), MCDiscordReserializer, EnhancedLegacyText
|
||||
runtimeDownloadApi(libs.adventure.api)
|
||||
runtimeDownloadApi(libs.adventure.serializer.plain)
|
||||
runtimeDownloadApi(libs.adventure.serializer.legacy)
|
||||
runtimeDownloadApi(libs.adventure.serializer.gson)
|
||||
runtimeDownloadApi(libs.adventure.serializer.ansi)
|
||||
runtimeDownloadApi(libs.kyori.ansi)
|
||||
runtimeDownloadApi(libs.mcdiscordreserializer)
|
||||
runtimeDownloadApi(libs.enhancedlegacytext)
|
||||
|
||||
|
@ -42,6 +42,7 @@ import com.discordsrv.common.config.connection.UpdateConfig;
|
||||
import com.discordsrv.common.config.main.MainConfig;
|
||||
import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
|
||||
import com.discordsrv.common.config.messages.MessagesConfig;
|
||||
import com.discordsrv.common.console.ConsoleModule;
|
||||
import com.discordsrv.common.debug.data.VersionInfo;
|
||||
import com.discordsrv.common.dependency.DiscordSRVDependencyManager;
|
||||
import com.discordsrv.common.discord.api.DiscordAPIEventModule;
|
||||
@ -72,6 +73,7 @@ import com.discordsrv.common.module.ModuleManager;
|
||||
import com.discordsrv.common.module.type.AbstractModule;
|
||||
import com.discordsrv.common.placeholder.DiscordPlaceholdersImpl;
|
||||
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
|
||||
import com.discordsrv.common.placeholder.context.GlobalDateFormattingContext;
|
||||
import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext;
|
||||
import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
|
||||
import com.discordsrv.common.profile.ProfileManager;
|
||||
@ -559,8 +561,10 @@ public abstract class AbstractDiscordSRV<
|
||||
// Placeholder result stringifiers & global contexts
|
||||
placeholderService().addResultMapper(new ComponentResultStringifier(this));
|
||||
placeholderService().addGlobalContext(new GlobalTextHandlingContext(this));
|
||||
placeholderService().addGlobalContext(new GlobalDateFormattingContext(this));
|
||||
|
||||
// Modules
|
||||
registerModule(ConsoleModule::new);
|
||||
registerModule(ChannelLockingModule::new);
|
||||
registerModule(TimedUpdaterModule::new);
|
||||
registerModule(DiscordCommandModule::new);
|
||||
|
@ -68,19 +68,19 @@ public class ComponentFactory implements MinecraftComponentFactory {
|
||||
MinecraftSerializerOptions.defaults()
|
||||
.addRenderer(new DiscordSRVMinecraftRenderer(discordSRV))
|
||||
);
|
||||
this.discordSerializer = new DiscordSerializer(
|
||||
DiscordSerializerOptions.defaults()
|
||||
.withTranslationProvider(this::provideTranslation)
|
||||
);
|
||||
|
||||
ComponentFlattener flattener = ComponentFlattener.basic().toBuilder()
|
||||
.mapper(TranslatableComponent.class, this::provideTranslation)
|
||||
.build();
|
||||
this.discordSerializer = new DiscordSerializer(
|
||||
DiscordSerializerOptions.defaults()
|
||||
.withFlattener(flattener)
|
||||
);
|
||||
this.plainSerializer = PlainTextComponentSerializer.builder()
|
||||
.flattener(flattener)
|
||||
.build();
|
||||
this.ansiSerializer = ANSIComponentSerializer.builder()
|
||||
.colorLevel(ColorLevel.INDEXED_16)
|
||||
.colorLevel(ColorLevel.INDEXED_8)
|
||||
.flattener(flattener)
|
||||
.build();
|
||||
}
|
||||
@ -137,4 +137,5 @@ public class ComponentFactory implements MinecraftComponentFactory {
|
||||
public TranslationRegistry translationRegistry() {
|
||||
return translationRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,125 @@
|
||||
package com.discordsrv.common.config.main;
|
||||
|
||||
import com.discordsrv.common.config.main.generic.DestinationConfig;
|
||||
import com.discordsrv.common.config.main.generic.GameCommandFilterConfig;
|
||||
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@ConfigSerializable
|
||||
public class ConsoleConfig {
|
||||
|
||||
@Comment("The console channel or thread")
|
||||
public DestinationConfig.Single channel = new DestinationConfig.Single();
|
||||
|
||||
public Appender appender = new Appender();
|
||||
|
||||
public Execution commandExecution = new Execution();
|
||||
|
||||
@ConfigSerializable
|
||||
public static class Appender {
|
||||
|
||||
@Comment("The format for log lines")
|
||||
public String lineFormat = "[%log_time:'ccc HH:mm:ss zzz'%] [%log_level%] [%logger_name%] %message%";
|
||||
|
||||
@Comment("The mode for the console output, available options are:\n"
|
||||
+ "- ansi: A colored ansi code block\n"
|
||||
+ "- log: A \"accesslog\" code block\n"
|
||||
+ "- diff: A \"diff\" code block highlighting warnings and errors with different colors\n"
|
||||
+ "- plain: Plain text code block\n"
|
||||
+ "- plain_content: Plain text")
|
||||
public OutputMode outputMode = OutputMode.ANSI;
|
||||
|
||||
@Comment("In \"diff\" mode, should exception lines have the prefix character as well")
|
||||
public boolean diffExceptions = true;
|
||||
|
||||
@Comment("If urls should have embeds disabled")
|
||||
public boolean disableLinkEmbeds = true;
|
||||
|
||||
@Comment("Avoids sending new messages by editing the most recent message until it reaches it's maximum length")
|
||||
public boolean useEditing = true;
|
||||
|
||||
@Comment("If console messages should be silent, not causing a notification")
|
||||
public boolean silentMessages = true;
|
||||
|
||||
@Comment("A list of log levels to whitelist or blacklist")
|
||||
public Levels levels = new Levels();
|
||||
|
||||
public static class Levels {
|
||||
public List<String> levels = new ArrayList<>(Arrays.asList("DEBUG", "TRACE"));
|
||||
public boolean blacklist = true;
|
||||
}
|
||||
|
||||
@Comment("A list of logger names to whitelist or blacklist, use \"NONE\" for log messages with no logger name")
|
||||
public Loggers loggers = new Loggers();
|
||||
|
||||
public static class Loggers {
|
||||
public List<String> loggers = new ArrayList<>(Collections.singletonList("ExcludedLogger"));
|
||||
public boolean blacklist = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigSerializable
|
||||
public static class Execution {
|
||||
|
||||
public Execution() {
|
||||
filters.add(
|
||||
new GameCommandFilterConfig(
|
||||
new ArrayList<>(),
|
||||
false,
|
||||
new ArrayList<>(Arrays.asList("list", "whitelist"))
|
||||
)
|
||||
);
|
||||
filters.add(
|
||||
new GameCommandFilterConfig(
|
||||
new ArrayList<>(),
|
||||
true,
|
||||
new ArrayList<>(Arrays.asList(
|
||||
"?",
|
||||
"op",
|
||||
"deop",
|
||||
"execute"
|
||||
))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Comment("At least one condition has to match to allow execution")
|
||||
public List<GameCommandFilterConfig> filters = new ArrayList<>();
|
||||
|
||||
@Comment("If a command is inputted starting with /, a warning response will be given if this is enabled")
|
||||
public boolean enableSlashWarning = true;
|
||||
|
||||
}
|
||||
|
||||
public enum OutputMode {
|
||||
ANSI("```ansi\n", "```"),
|
||||
LOG("```accesslog\n", "```"),
|
||||
DIFF("```diff\n", "```"),
|
||||
MARKDOWN("", ""),
|
||||
PLAIN("```\n", "```"),
|
||||
PLAIN_CONTENT("", "");
|
||||
|
||||
private final String prefix;
|
||||
private final String suffix;
|
||||
|
||||
OutputMode(String prefix, String suffix) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
public String prefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public String suffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
public int blockLength() {
|
||||
return prefix().length() + suffix().length();
|
||||
}
|
||||
}
|
||||
}
|
@ -31,9 +31,7 @@ import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
|
||||
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
@ConfigSerializable
|
||||
public abstract class MainConfig implements Config {
|
||||
@ -88,6 +86,9 @@ public abstract class MainConfig implements Config {
|
||||
@Comment("Discord command configuration")
|
||||
public DiscordCommandConfig discordCommand = new DiscordCommandConfig();
|
||||
|
||||
@Comment("Options for console channel(s) and/or thread(s)")
|
||||
public List<ConsoleConfig> console = new ArrayList<>(Collections.singleton(new ConsoleConfig()));
|
||||
|
||||
@Comment("Configuration for the %1 placeholder. The below options will be attempted in the order they are in")
|
||||
@Constants.Comment("%discord_invite%")
|
||||
public DiscordInviteConfig invite = new DiscordInviteConfig();
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.discordsrv.common.config.main.generic;
|
||||
|
||||
import com.discordsrv.common.config.configurate.annotation.Constants;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||
import org.spongepowered.configurate.objectmapping.meta.Setting;
|
||||
@ -20,4 +21,29 @@ public class DestinationConfig {
|
||||
@Comment("The threads that this in-game channel will forward to in Discord (this can be used instead of or with the %1 option)")
|
||||
@Constants.Comment("channel-ids")
|
||||
public List<ThreadConfig> threads = new ArrayList<>(Collections.singletonList(new ThreadConfig()));
|
||||
|
||||
@ConfigSerializable
|
||||
public static class Single {
|
||||
@Setting("channel-id")
|
||||
public Long channelId = 0L;
|
||||
|
||||
@Setting("thread-name")
|
||||
@Comment("If specified this destination will be a thread in the provided channel-id's channel, if left blank the destination will be the channel")
|
||||
public String threadName = "";
|
||||
public boolean privateThread = false;
|
||||
|
||||
public DestinationConfig asDestination() {
|
||||
DestinationConfig config = new DestinationConfig();
|
||||
if (StringUtils.isEmpty(threadName)) {
|
||||
config.channelIds.add(channelId);
|
||||
} else {
|
||||
ThreadConfig threadConfig = new ThreadConfig();
|
||||
threadConfig.channelId = channelId;
|
||||
threadConfig.threadName = threadName;
|
||||
threadConfig.privateThread = privateThread;
|
||||
config.threads.add(threadConfig);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ public class GameCommandFilterConfig {
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String configCommand : commands) {
|
||||
|
@ -0,0 +1,74 @@
|
||||
package com.discordsrv.common.console;
|
||||
|
||||
import com.discordsrv.api.DiscordSRVApi;
|
||||
import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent;
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.config.main.ConsoleConfig;
|
||||
import com.discordsrv.common.console.entry.LogEntry;
|
||||
import com.discordsrv.common.logging.LogAppender;
|
||||
import com.discordsrv.common.logging.LogLevel;
|
||||
import com.discordsrv.common.logging.NamedLogger;
|
||||
import com.discordsrv.common.logging.backend.LoggingBackend;
|
||||
import com.discordsrv.common.module.type.AbstractModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ConsoleModule extends AbstractModule<DiscordSRV> implements LogAppender {
|
||||
|
||||
private LoggingBackend backend;
|
||||
private final List<SingleConsoleHandler> handlers = new ArrayList<>();
|
||||
|
||||
public ConsoleModule(DiscordSRV discordSRV) {
|
||||
super(discordSRV, new NamedLogger(discordSRV, "CONSOLE"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<DiscordGatewayIntent> requiredIntents() {
|
||||
return Collections.singletonList(DiscordGatewayIntent.MESSAGE_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
backend = discordSRV.console().loggingBackend();
|
||||
backend.addAppender(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) {
|
||||
for (SingleConsoleHandler handler : handlers) {
|
||||
handler.shutdown();
|
||||
}
|
||||
handlers.clear();
|
||||
|
||||
List<ConsoleConfig> configs = discordSRV.config().console;
|
||||
for (ConsoleConfig config : configs) {
|
||||
handlers.add(new SingleConsoleHandler(discordSRV, logger(), config));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (backend != null) {
|
||||
backend.removeAppender(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(
|
||||
@Nullable String loggerName,
|
||||
@NotNull LogLevel logLevel,
|
||||
@Nullable String message,
|
||||
@Nullable Throwable throwable
|
||||
) {
|
||||
LogEntry entry = new LogEntry(loggerName, logLevel, message, throwable);
|
||||
for (SingleConsoleHandler handler : handlers) {
|
||||
handler.queue(entry);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,404 @@
|
||||
package com.discordsrv.common.console;
|
||||
|
||||
import com.discordsrv.api.discord.entity.DiscordUser;
|
||||
import com.discordsrv.api.discord.entity.channel.DiscordGuildChannel;
|
||||
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
|
||||
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
|
||||
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
|
||||
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
|
||||
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
|
||||
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
|
||||
import com.discordsrv.api.discord.events.message.DiscordMessageReceiveEvent;
|
||||
import com.discordsrv.api.event.bus.Subscribe;
|
||||
import com.discordsrv.api.placeholder.provider.SinglePlaceholder;
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.discordsrv.common.command.game.GameCommandExecutionHelper;
|
||||
import com.discordsrv.common.config.main.ConsoleConfig;
|
||||
import com.discordsrv.common.config.main.generic.DestinationConfig;
|
||||
import com.discordsrv.common.config.main.generic.GameCommandFilterConfig;
|
||||
import com.discordsrv.common.console.entry.LogEntry;
|
||||
import com.discordsrv.common.console.entry.LogMessage;
|
||||
import com.discordsrv.common.console.message.ConsoleMessage;
|
||||
import com.discordsrv.common.logging.LogLevel;
|
||||
import com.discordsrv.common.logging.Logger;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The log appending and command handling for a single console channel.
|
||||
*/
|
||||
public class SingleConsoleHandler {
|
||||
|
||||
private static final int MESSAGE_MAX_LENGTH = Message.MAX_CONTENT_LENGTH;
|
||||
|
||||
private final DiscordSRV discordSRV;
|
||||
private final Logger logger;
|
||||
private final ConsoleConfig config;
|
||||
private final Queue<LogEntry> queue = new LinkedBlockingQueue<>();
|
||||
private Future<?> queueProcessingFuture;
|
||||
|
||||
// Editing
|
||||
private final List<LogMessage> messageCache;
|
||||
private Long mostRecentMessageId;
|
||||
|
||||
// Preventing concurrent sends
|
||||
private final Object sendLock = new Object();
|
||||
private CompletableFuture<?> sendFuture;
|
||||
|
||||
// Don't annoy console users twice about using /
|
||||
private final Set<Long> warnedSlashUsageUserIds = new HashSet<>();
|
||||
|
||||
public SingleConsoleHandler(DiscordSRV discordSRV, Logger logger, ConsoleConfig config) {
|
||||
this.discordSRV = discordSRV;
|
||||
this.logger = logger;
|
||||
this.config = config;
|
||||
this.messageCache = config.appender.useEditing ? new ArrayList<>() : null;
|
||||
|
||||
timeQueueProcess();
|
||||
discordSRV.eventBus().subscribe(this);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDiscordMessageReceived(DiscordMessageReceiveEvent event) {
|
||||
DiscordMessageChannel messageChannel = event.getChannel();
|
||||
DiscordGuildChannel channel = messageChannel instanceof DiscordGuildChannel ? (DiscordGuildChannel) messageChannel : null;
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReceivedDiscordMessage message = event.getMessage();
|
||||
if (message.isFromSelf()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String command = event.getMessage().getContent();
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DestinationConfig.Single destination = config.channel;
|
||||
String threadName = destination.threadName;
|
||||
|
||||
DiscordGuildChannel checkChannel;
|
||||
if (StringUtils.isNotEmpty(threadName)) {
|
||||
if (!(channel instanceof DiscordThreadChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channel.getName().equals(threadName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkChannel = ((DiscordThreadChannel) channel).getParentChannel();
|
||||
} else {
|
||||
checkChannel = channel;
|
||||
}
|
||||
if (checkChannel.getId() != destination.channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
DiscordUser user = message.getAuthor();
|
||||
DiscordGuildMember member = message.getMember();
|
||||
GameCommandExecutionHelper helper = discordSRV.executeHelper();
|
||||
|
||||
if (command.startsWith("/") && config.commandExecution.enableSlashWarning) {
|
||||
long userId = user.getId();
|
||||
|
||||
boolean newUser;
|
||||
synchronized (warnedSlashUsageUserIds) {
|
||||
newUser = !warnedSlashUsageUserIds.contains(userId);
|
||||
if (newUser) {
|
||||
warnedSlashUsageUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (newUser) {
|
||||
// TODO: translation
|
||||
message.reply(
|
||||
SendableDiscordMessage.builder()
|
||||
.setContent("Your command was prefixed with `/`, but normally commands in the Minecraft server console should **not** begin with `/`")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
boolean pass = false;
|
||||
for (GameCommandFilterConfig filter : config.commandExecution.filters) {
|
||||
if (filter.isAcceptableCommand(member, user, command, false, helper)) {
|
||||
pass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pass) {
|
||||
if (!user.isBot()) {
|
||||
// TODO: translation
|
||||
message.reply(
|
||||
SendableDiscordMessage.builder()
|
||||
.setContent("You are not allowed to run that command")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Split message when editing
|
||||
if (messageCache != null) {
|
||||
messageCache.clear();
|
||||
}
|
||||
mostRecentMessageId = null;
|
||||
|
||||
// Run the command
|
||||
discordSRV.console().runCommand(command);
|
||||
}
|
||||
|
||||
public void queue(LogEntry entry) {
|
||||
queue.offer(entry);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
discordSRV.eventBus().unsubscribe(this);
|
||||
queueProcessingFuture.cancel(false);
|
||||
queue.clear();
|
||||
if (messageCache != null) {
|
||||
messageCache.clear();
|
||||
}
|
||||
mostRecentMessageId = null;
|
||||
}
|
||||
|
||||
private void timeQueueProcess() {
|
||||
this.queueProcessingFuture = discordSRV.scheduler().runLater(this::processQueue, 2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void processQueue() {
|
||||
try {
|
||||
ConsoleConfig.Appender appenderConfig = config.appender;
|
||||
ConsoleConfig.OutputMode outputMode = appenderConfig.outputMode;
|
||||
|
||||
Queue<LogMessage> currentBuffer = new LinkedBlockingQueue<>();
|
||||
LogEntry entry;
|
||||
while ((entry = queue.poll()) != null) {
|
||||
String level = entry.level().name();
|
||||
if (appenderConfig.levels.levels.contains(level) == appenderConfig.levels.blacklist) {
|
||||
// Ignored level
|
||||
continue;
|
||||
}
|
||||
|
||||
String loggerName = entry.loggerName();
|
||||
if (StringUtils.isEmpty(loggerName)) loggerName = "NONE";
|
||||
if (appenderConfig.loggers.loggers.contains(loggerName) == appenderConfig.loggers.blacklist) {
|
||||
// Ignored logger
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> messages = formatEntry(entry, outputMode, config.appender.diffExceptions);
|
||||
if (messages.size() == 1) {
|
||||
LogMessage message = new LogMessage(entry, messages.get(0));
|
||||
currentBuffer.add(message);
|
||||
} else {
|
||||
clearBuffer(currentBuffer, outputMode);
|
||||
for (String message : messages) {
|
||||
send(message, true, outputMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
clearBuffer(currentBuffer, outputMode);
|
||||
} catch (Exception ex) {
|
||||
logger.error("Failed to process console lines", ex);
|
||||
}
|
||||
|
||||
if (sendFuture != null) {
|
||||
sendFuture.whenComplete((__, ___) -> {
|
||||
sendFuture = null;
|
||||
timeQueueProcess();
|
||||
});
|
||||
} else {
|
||||
timeQueueProcess();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearBuffer(Queue<LogMessage> currentBuffer, ConsoleConfig.OutputMode outputMode) {
|
||||
if (currentBuffer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int blockLength = outputMode.blockLength();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (messageCache != null) {
|
||||
for (LogMessage logMessage : messageCache) {
|
||||
builder.append(logMessage.formatted());
|
||||
}
|
||||
}
|
||||
|
||||
LogMessage current;
|
||||
while ((current = currentBuffer.poll()) != null) {
|
||||
String formatted = current.formatted();
|
||||
if (formatted.length() + builder.length() + blockLength > MESSAGE_MAX_LENGTH) {
|
||||
send(builder.toString(), true, outputMode);
|
||||
builder.setLength(0);
|
||||
if (messageCache != null) {
|
||||
messageCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(formatted);
|
||||
if (messageCache != null) {
|
||||
messageCache.add(current);
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.length() > 0) {
|
||||
send(builder.toString(), false, outputMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String message, boolean isFull, ConsoleConfig.OutputMode outputMode) {
|
||||
SendableDiscordMessage sendableMessage = SendableDiscordMessage.builder()
|
||||
.setContent(outputMode.prefix() + message + outputMode.suffix())
|
||||
.setSuppressedNotifications(config.appender.silentMessages)
|
||||
.setSuppressedEmbeds(config.appender.disableLinkEmbeds)
|
||||
.build();
|
||||
|
||||
synchronized (sendLock) {
|
||||
CompletableFuture<?> future = sendFuture != null ? sendFuture : CompletableFuture.completedFuture(null);
|
||||
|
||||
sendFuture = future
|
||||
.thenCompose(__ ->
|
||||
discordSRV.discordAPI()
|
||||
.findOrCreateDestinations(config.channel.asDestination(), true, true, true)
|
||||
)
|
||||
.thenApply(channels -> {
|
||||
if (channels.isEmpty()) {
|
||||
throw new IllegalStateException("No channel");
|
||||
}
|
||||
|
||||
DiscordGuildMessageChannel channel = channels.get(0);
|
||||
if (mostRecentMessageId != null) {
|
||||
long messageId = mostRecentMessageId;
|
||||
if (isFull) {
|
||||
mostRecentMessageId = null;
|
||||
}
|
||||
return channel.editMessageById(messageId, sendableMessage);
|
||||
}
|
||||
|
||||
return channel.sendMessage(sendableMessage)
|
||||
.whenComplete((receivedMessage, t) -> {
|
||||
if (receivedMessage != null && messageCache != null) {
|
||||
mostRecentMessageId = receivedMessage.getId();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> formatEntry(LogEntry entry, ConsoleConfig.OutputMode outputMode, boolean diffExceptions) {
|
||||
int blockLength = outputMode.blockLength();
|
||||
int maximumPart = MESSAGE_MAX_LENGTH - blockLength - "\n".length();
|
||||
|
||||
String parsedMessage;
|
||||
ConsoleMessage consoleMessage = new ConsoleMessage(discordSRV, entry.message());
|
||||
switch (outputMode) {
|
||||
case ANSI:
|
||||
parsedMessage = consoleMessage.asAnsi();
|
||||
break;
|
||||
case MARKDOWN:
|
||||
parsedMessage = consoleMessage.asMarkdown();
|
||||
break;
|
||||
default:
|
||||
parsedMessage = consoleMessage.asPlain();
|
||||
break;
|
||||
}
|
||||
|
||||
String message = discordSRV.placeholderService().replacePlaceholders(
|
||||
config.appender.lineFormat,
|
||||
entry,
|
||||
new SinglePlaceholder("message", parsedMessage)
|
||||
);
|
||||
|
||||
Throwable thrown = entry.throwable();
|
||||
String throwable = thrown != null ? ExceptionUtils.getStackTrace(thrown) : StringUtils.EMPTY;
|
||||
|
||||
if (outputMode == ConsoleConfig.OutputMode.DIFF) {
|
||||
String diff = getLogLevelDiffCharacter(entry.level());
|
||||
if (!message.isEmpty()) {
|
||||
message = diff + message.replace("\n", "\n" + diff);
|
||||
}
|
||||
|
||||
String exceptionCharacter = diffExceptions ? diff : "";
|
||||
if (!throwable.isEmpty()) {
|
||||
throwable = exceptionCharacter + throwable.replace("\n", "\n" + exceptionCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.isEmpty()) {
|
||||
message += "\n";
|
||||
}
|
||||
if (!throwable.isEmpty()) {
|
||||
throwable += "\n";
|
||||
}
|
||||
|
||||
List<String> formatted = new ArrayList<>();
|
||||
|
||||
// Handle message being longer than a message
|
||||
message = cutToSizeIfNeeded(message, blockLength, maximumPart, formatted);
|
||||
|
||||
// Handle log entry being longer than a message
|
||||
int totalLength = blockLength + throwable.length() + message.length();
|
||||
if (totalLength > MESSAGE_MAX_LENGTH) {
|
||||
StringBuilder builder = new StringBuilder(message);
|
||||
for (String line : throwable.split("\n")) {
|
||||
line += "\n";
|
||||
|
||||
// Handle a single line of a throwable being longer than a message
|
||||
line = cutToSizeIfNeeded(line, blockLength, maximumPart, formatted);
|
||||
|
||||
if (blockLength + line.length() > MESSAGE_MAX_LENGTH) {
|
||||
// Need to split here
|
||||
formatted.add(builder.toString());
|
||||
builder.setLength(0);
|
||||
}
|
||||
builder.append(line);
|
||||
}
|
||||
formatted.add(builder.toString());
|
||||
} else {
|
||||
formatted.add(message + throwable);
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
private String cutToSizeIfNeeded(
|
||||
String content,
|
||||
int blockLength,
|
||||
int maximumPart,
|
||||
List<String> formatted
|
||||
) {
|
||||
while (content.length() + blockLength > MESSAGE_MAX_LENGTH) {
|
||||
String cutToSize = content.substring(0, maximumPart) + "\n";
|
||||
if (cutToSize.endsWith("\n\n")) {
|
||||
// maximumPart excludes the newline at the end of message/line
|
||||
cutToSize = cutToSize.substring(0, cutToSize.length() - 1);
|
||||
}
|
||||
|
||||
formatted.add(cutToSize);
|
||||
content = content.substring(maximumPart);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private String getLogLevelDiffCharacter(LogLevel level) {
|
||||
if (level == LogLevel.StandardLogLevel.WARNING) {
|
||||
return "+ ";
|
||||
} else if (level == LogLevel.StandardLogLevel.ERROR) {
|
||||
return "- ";
|
||||
}
|
||||
return " ";
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.discordsrv.common.console.entry;
|
||||
|
||||
import com.discordsrv.api.placeholder.PlaceholderLookupResult;
|
||||
import com.discordsrv.api.placeholder.annotation.Placeholder;
|
||||
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
|
||||
import com.discordsrv.common.logging.LogLevel;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A raw log entry from a platform logger. May be parsed to become a {@link LogMessage}.
|
||||
*/
|
||||
public class LogEntry {
|
||||
|
||||
private final String loggerName;
|
||||
private final LogLevel level;
|
||||
private final String message;
|
||||
private final Throwable throwable;
|
||||
private final ZonedDateTime logTime;
|
||||
|
||||
public LogEntry(String loggerName, LogLevel level, String message, Throwable throwable) {
|
||||
this.loggerName = loggerName;
|
||||
this.level = level;
|
||||
this.message = message;
|
||||
this.throwable = throwable;
|
||||
this.logTime = ZonedDateTime.now();
|
||||
}
|
||||
|
||||
@Placeholder("logger_name")
|
||||
public String loggerName() {
|
||||
return loggerName;
|
||||
}
|
||||
|
||||
@Placeholder("log_level")
|
||||
public LogLevel level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public String message() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Throwable throwable() {
|
||||
return throwable;
|
||||
}
|
||||
|
||||
public ZonedDateTime logTime() {
|
||||
return logTime;
|
||||
}
|
||||
|
||||
@Placeholder("log_time")
|
||||
public PlaceholderLookupResult _logTimePlaceholder(@PlaceholderRemainder String format) {
|
||||
Set<Object> extras = new LinkedHashSet<>();
|
||||
extras.add(logTime());
|
||||
|
||||
return PlaceholderLookupResult.newLookup("date:'" + format + "'", extras);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.discordsrv.common.console.entry;
|
||||
|
||||
/**
|
||||
* A {@link LogEntry} with formatting.
|
||||
*/
|
||||
public class LogMessage {
|
||||
|
||||
private final LogEntry entry;
|
||||
private final String formatted;
|
||||
|
||||
public LogMessage(LogEntry entry, String formatted) {
|
||||
this.entry = entry;
|
||||
this.formatted = formatted;
|
||||
}
|
||||
|
||||
public LogEntry entry() {
|
||||
return entry;
|
||||
}
|
||||
|
||||
public String formatted() {
|
||||
return formatted;
|
||||
}
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
package com.discordsrv.common.console.message;
|
||||
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.format.*;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Helper class for parsing raw console messages into markdown, ansi or plain content for forwarding to Discord.
|
||||
*/
|
||||
public class ConsoleMessage {
|
||||
|
||||
private static final String ANSI_ESCAPE = "\u001B";
|
||||
// Paper uses 007F as an intermediary
|
||||
private static final String SECTION = "[§\u007F]";
|
||||
|
||||
// Regex pattern for matching ANSI + Legacy
|
||||
private static final Pattern PATTERN = Pattern.compile(
|
||||
// ANSI
|
||||
ANSI_ESCAPE
|
||||
+ "\\["
|
||||
+ "(?<ansi>[0-9]{1,3}"
|
||||
+ "(;[0-9]{1,3}"
|
||||
+ "(;[0-9]{1,3}"
|
||||
+ "(?:(?:;[0-9]{1,3}){2})?"
|
||||
+ ")?"
|
||||
+ ")?"
|
||||
+ ")"
|
||||
+ "m"
|
||||
+ "|"
|
||||
+ "(?<legacy>"
|
||||
// Legacy color/formatting
|
||||
+ "(?:" + SECTION + "[0-9a-fk-or])"
|
||||
+ "|"
|
||||
// Bungee/Spigot legacy
|
||||
+ "(?:" + SECTION + "x(?:" + SECTION + "[0-9a-f]){6})"
|
||||
+ ")"
|
||||
);
|
||||
|
||||
private final TextComponent.Builder builder = Component.text();
|
||||
private final DiscordSRV discordSRV;
|
||||
|
||||
public ConsoleMessage(DiscordSRV discordSRV, String input) {
|
||||
this.discordSRV = discordSRV;
|
||||
parse(input);
|
||||
}
|
||||
|
||||
public String asMarkdown() {
|
||||
Component component = builder.build();
|
||||
return discordSRV.componentFactory().discordSerializer().serialize(component);
|
||||
}
|
||||
|
||||
public String asAnsi() {
|
||||
Component component = builder.build();
|
||||
return discordSRV.componentFactory().ansiSerializer().serialize(component) + (ANSI_ESCAPE + "[0m");
|
||||
}
|
||||
|
||||
public String asPlain() {
|
||||
Component component = builder.build();
|
||||
return discordSRV.componentFactory().plainSerializer().serialize(component);
|
||||
}
|
||||
|
||||
private void parse(String input) {
|
||||
Matcher matcher = PATTERN.matcher(input);
|
||||
|
||||
Style.Builder style = Style.style();
|
||||
|
||||
int lastMatchEnd = 0;
|
||||
while (matcher.find()) {
|
||||
int start = matcher.start();
|
||||
int end = matcher.end();
|
||||
|
||||
if (start != lastMatchEnd) {
|
||||
builder.append(Component.text(input.substring(lastMatchEnd, start), style.build()));
|
||||
}
|
||||
|
||||
String ansi = matcher.group("ansi");
|
||||
if (ansi != null) {
|
||||
parseAnsi(ansi, style);
|
||||
}
|
||||
|
||||
String legacy = matcher.group("legacy");
|
||||
if (legacy != null) {
|
||||
parseLegacy(legacy, style);
|
||||
}
|
||||
|
||||
lastMatchEnd = end;
|
||||
}
|
||||
|
||||
int length = input.length();
|
||||
if (lastMatchEnd != length) {
|
||||
builder.append(Component.text(input.substring(lastMatchEnd, length), style.build()));
|
||||
}
|
||||
}
|
||||
|
||||
private void parseAnsi(String ansiEscape, Style.Builder style) {
|
||||
String[] ansiParts = ansiEscape.split(";");
|
||||
int amount = ansiParts.length;
|
||||
if (amount == 1 || amount == 2) {
|
||||
int number = Integer.parseInt(ansiParts[0]);
|
||||
|
||||
if ((number >= 30 && number <= 37) || (number >= 90 && number <= 97)) {
|
||||
style.color(fourBitAnsiColor(number));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (number) {
|
||||
case 0:
|
||||
style.color(null).decorations(EnumSet.allOf(TextDecoration.class), false);
|
||||
break;
|
||||
case 1:
|
||||
style.decoration(TextDecoration.BOLD, true);
|
||||
break;
|
||||
case 3:
|
||||
style.decoration(TextDecoration.ITALIC, true);
|
||||
break;
|
||||
case 4:
|
||||
style.decoration(TextDecoration.UNDERLINED, true);
|
||||
break;
|
||||
case 8:
|
||||
style.decoration(TextDecoration.OBFUSCATED, true);
|
||||
break;
|
||||
case 9:
|
||||
style.decoration(TextDecoration.STRIKETHROUGH, true);
|
||||
break;
|
||||
case 22:
|
||||
style.decoration(TextDecoration.BOLD, false);
|
||||
break;
|
||||
case 23:
|
||||
style.decoration(TextDecoration.ITALIC, false);
|
||||
break;
|
||||
case 24:
|
||||
style.decoration(TextDecoration.UNDERLINED, false);
|
||||
break;
|
||||
case 28:
|
||||
style.decoration(TextDecoration.OBFUSCATED, false);
|
||||
break;
|
||||
case 29:
|
||||
style.decoration(TextDecoration.STRIKETHROUGH, false);
|
||||
break;
|
||||
case 39:
|
||||
style.color(null);
|
||||
break;
|
||||
}
|
||||
} else if (amount == 3 || amount == 5) {
|
||||
if (Integer.parseInt(ansiParts[0]) != 36 || Integer.parseInt(ansiParts[1]) != 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount == 5) {
|
||||
int red = Integer.parseInt(ansiParts[2]);
|
||||
int green = Integer.parseInt(ansiParts[3]);
|
||||
int blue = Integer.parseInt(ansiParts[4]);
|
||||
|
||||
style.color(TextColor.color(red, green, blue));
|
||||
return;
|
||||
}
|
||||
|
||||
int number = Integer.parseInt(ansiParts[2]);
|
||||
style.color(eightBitAnsiColor(number));
|
||||
}
|
||||
}
|
||||
|
||||
private enum FourBitColor {
|
||||
BLACK(30, TextColor.color(0, 0, 0)),
|
||||
RED(31, TextColor.color(170, 0, 0)),
|
||||
GREEN(32, TextColor.color(0, 170, 0)),
|
||||
YELLOW(33, TextColor.color(170, 85, 0)),
|
||||
BLUE(34, TextColor.color(0, 0, 170)),
|
||||
MAGENTA(35, TextColor.color(170, 0, 170)),
|
||||
CYAN(36, TextColor.color(0, 170, 170)),
|
||||
WHITE(37, TextColor.color(170, 170, 170)),
|
||||
BRIGHT_BLACK(90, TextColor.color(85, 85, 85)),
|
||||
BRIGHT_RED(91, TextColor.color(255, 85, 85)),
|
||||
BRIGHT_GREEN(92, TextColor.color(85, 255, 85)),
|
||||
BRIGHT_YELLOW(93, TextColor.color(255, 255, 85)),
|
||||
BRIGHT_BLUE(94, TextColor.color(85, 85, 255)),
|
||||
BRIGHT_MAGENTA(95, TextColor.color(255, 85, 255)),
|
||||
BRIGHT_CYAN(96, TextColor.color(85, 255, 255)),
|
||||
BRIGHT_WHITE(97, TextColor.color(255, 255, 255));
|
||||
|
||||
private static final Map<Integer, FourBitColor> byFG = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (FourBitColor value : values()) {
|
||||
byFG.put(value.fg, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static FourBitColor getByFG(int fg) {
|
||||
return byFG.get(fg);
|
||||
}
|
||||
|
||||
private final int fg;
|
||||
private final TextColor color;
|
||||
|
||||
FourBitColor(int fg, TextColor color) {
|
||||
this.fg = fg;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public TextColor color() {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
private TextColor fourBitAnsiColor(int color) {
|
||||
FourBitColor fourBitColor = FourBitColor.getByFG(color);
|
||||
return fourBitColor != null ? fourBitColor.color() : null;
|
||||
}
|
||||
|
||||
private TextColor[] colors;
|
||||
private TextColor eightBitAnsiColor(int color) {
|
||||
if (colors == null) {
|
||||
TextColor[] colors = new TextColor[256];
|
||||
|
||||
FourBitColor[] fourBitColors = FourBitColor.values();
|
||||
for (int i = 0; i < fourBitColors.length; i++) {
|
||||
colors[i] = fourBitColors[i].color();
|
||||
}
|
||||
|
||||
// https://gitlab.gnome.org/GNOME/vte/-/blob/19acc51708d9e75ef2b314aa026467570e0bd8ee/src/vte.cc#L2485
|
||||
for (int i = 16; i < 232; i++) {
|
||||
int j = i - 16;
|
||||
|
||||
int red = j / 36;
|
||||
int green = (j / 6) % 6;
|
||||
int blue = j % 6;
|
||||
|
||||
red = red == 0 ? 0 : red * 40 + 55;
|
||||
green = green == 0 ? 0 : green * 40 + 55;
|
||||
blue = blue == 0 ? 0 : blue * 40 + 55;
|
||||
|
||||
colors[i] = TextColor.color(
|
||||
red | red << 8,
|
||||
green | green << 8,
|
||||
blue | blue << 8
|
||||
);
|
||||
}
|
||||
for (int i = 232; i < 256; i++) {
|
||||
int shade = 8 + (i - 232) * 10;
|
||||
colors[i] = TextColor.color(shade, shade, shade);
|
||||
}
|
||||
|
||||
this.colors = colors;
|
||||
}
|
||||
|
||||
return color <= colors.length && color >= 0 ? colors[color] : null;
|
||||
}
|
||||
|
||||
private void parseLegacy(String legacy, Style.Builder style) {
|
||||
if (legacy.length() == 2) {
|
||||
char character = legacy.toCharArray()[1];
|
||||
if (character == 'r') {
|
||||
style.color(null).decorations(EnumSet.allOf(TextDecoration.class), false);
|
||||
} else {
|
||||
TextFormat format = getFormat(character);
|
||||
if (format instanceof TextColor) {
|
||||
style.color((TextColor) format);
|
||||
} else if (format instanceof TextDecoration) {
|
||||
style.decorate((TextDecoration) format);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
char[] characters = legacy.toCharArray();
|
||||
StringBuilder hex = new StringBuilder(7).append(TextColor.HEX_PREFIX);
|
||||
for (int i = 2; i < characters.length; i += 2) {
|
||||
hex.append(characters[i]);
|
||||
}
|
||||
style.color(TextColor.fromHexString(hex.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private TextFormat getFormat(char character) {
|
||||
switch (character) {
|
||||
case '0': return NamedTextColor.BLACK;
|
||||
case '1': return NamedTextColor.DARK_BLUE;
|
||||
case '2': return NamedTextColor.DARK_GREEN;
|
||||
case '3': return NamedTextColor.DARK_AQUA;
|
||||
case '4': return NamedTextColor.DARK_RED;
|
||||
case '5': return NamedTextColor.DARK_PURPLE;
|
||||
case '6': return NamedTextColor.GOLD;
|
||||
case '7': return NamedTextColor.GRAY;
|
||||
case '8': return NamedTextColor.DARK_GRAY;
|
||||
case '9': return NamedTextColor.BLUE;
|
||||
case 'a': return NamedTextColor.GREEN;
|
||||
case 'b': return NamedTextColor.AQUA;
|
||||
case 'c': return NamedTextColor.RED;
|
||||
case 'd': return NamedTextColor.LIGHT_PURPLE;
|
||||
case 'e': return NamedTextColor.YELLOW;
|
||||
case 'f': return NamedTextColor.WHITE;
|
||||
case 'k': return TextDecoration.OBFUSCATED;
|
||||
case 'l': return TextDecoration.BOLD;
|
||||
case 'm': return TextDecoration.STRIKETHROUGH;
|
||||
case 'n': return TextDecoration.UNDERLINED;
|
||||
case 'o': return TextDecoration.ITALIC;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -100,7 +100,15 @@ public class DiscordAPIImpl implements DiscordAPI {
|
||||
boolean create,
|
||||
boolean log
|
||||
) {
|
||||
DestinationConfig destination = config.destination();
|
||||
return findOrCreateDestinations(config.destination(), config.channelLocking.threads.unarchive, create, log);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<DiscordGuildMessageChannel>> findOrCreateDestinations(
|
||||
DestinationConfig destination,
|
||||
boolean unarchive,
|
||||
boolean create,
|
||||
boolean log
|
||||
) {
|
||||
|
||||
List<DiscordGuildMessageChannel> channels = new CopyOnWriteArrayList<>();
|
||||
for (Long channelId : destination.channelIds) {
|
||||
@ -148,7 +156,7 @@ public class DiscordAPIImpl implements DiscordAPI {
|
||||
unarchiveOrCreateThread(threadConfig, channel, thread, future);
|
||||
} else {
|
||||
// Find or create the thread
|
||||
future = findOrCreateThread(config, threadConfig, channel);
|
||||
future = findOrCreateThread(unarchive, threadConfig, channel);
|
||||
}
|
||||
|
||||
DiscordThreadContainer container = channel;
|
||||
@ -181,12 +189,8 @@ public class DiscordAPIImpl implements DiscordAPI {
|
||||
return null;
|
||||
}
|
||||
|
||||
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(
|
||||
BaseChannelConfig config,
|
||||
ThreadConfig threadConfig,
|
||||
DiscordThreadContainer container
|
||||
) {
|
||||
if (!config.channelLocking.threads.unarchive) {
|
||||
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(boolean unarchive, ThreadConfig threadConfig, DiscordThreadContainer container) {
|
||||
if (!unarchive) {
|
||||
return container.createThread(threadConfig.threadName, threadConfig.privateThread);
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.WebhookClient;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
||||
import net.dv8tion.jda.api.requests.RestAction;
|
||||
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateRequest;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
||||
@ -89,7 +90,15 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
|
||||
.setAvatarUrl(message.getWebhookAvatarUrl())
|
||||
);
|
||||
} else {
|
||||
createRequest = CompletableFuture.completedFuture(((R) channel.sendMessage(createData)));
|
||||
MessageCreateAction action = channel.sendMessage(createData)
|
||||
// JDA doesn't properly grab this from MessageCreateData
|
||||
.setSuppressEmbeds(createData.isSuppressEmbeds());
|
||||
|
||||
Long referencedMessageId = message.getMessageIdToReplyTo();
|
||||
if (referencedMessageId != null) {
|
||||
action = action.setMessageReference(referencedMessageId);
|
||||
}
|
||||
createRequest = CompletableFuture.completedFuture((R) action);
|
||||
}
|
||||
|
||||
return createRequest
|
||||
|
@ -56,9 +56,13 @@ public class DiscordDMChannelImpl extends AbstractDiscordMessageChannel<PrivateC
|
||||
|
||||
MessageCreateAction action = channel.sendMessage(SendableDiscordMessageUtil.toJDASend(message));
|
||||
|
||||
Long referencedMessageId = message.getMessageIdToReplyTo();
|
||||
if (referencedMessageId != null) {
|
||||
action = action.setMessageReference(referencedMessageId);
|
||||
}
|
||||
|
||||
CompletableFuture<ReceivedDiscordMessage> future = action.submit()
|
||||
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
|
||||
|
||||
return discordSRV.discordAPI().mapExceptions(future);
|
||||
}
|
||||
|
||||
|
@ -226,12 +226,12 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Void> delete() {
|
||||
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
|
||||
if (textChannel == null) {
|
||||
DiscordMessageChannel messageChannel = discordSRV.discordAPI().getMessageChannelById(channelId);
|
||||
if (messageChannel == null) {
|
||||
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
||||
}
|
||||
|
||||
return textChannel.deleteMessageById(getId(), fromSelf && webhookMessage);
|
||||
return messageChannel.deleteMessageById(getId(), fromSelf && webhookMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -242,12 +242,26 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
|
||||
throw new IllegalArgumentException("Cannot edit a non-webhook message into a webhook message");
|
||||
}
|
||||
|
||||
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
|
||||
if (textChannel == null) {
|
||||
DiscordMessageChannel messageChannel = discordSRV.discordAPI().getMessageChannelById(channelId);
|
||||
if (messageChannel == null) {
|
||||
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
||||
}
|
||||
|
||||
return textChannel.editMessageById(getId(), message);
|
||||
return messageChannel.editMessageById(getId(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ReceivedDiscordMessage> reply(@NotNull SendableDiscordMessage message) {
|
||||
if (message.isWebhookMessage()) {
|
||||
throw new IllegalStateException("Webhook messages cannot be used as replies");
|
||||
}
|
||||
|
||||
DiscordMessageChannel messageChannel = discordSRV.discordAPI().getMessageChannelById(channelId);
|
||||
if (messageChannel == null) {
|
||||
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
||||
}
|
||||
|
||||
return messageChannel.sendMessage(message.withReplyingToMessageId(id));
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -73,6 +73,7 @@ public final class SendableDiscordMessageUtil {
|
||||
.setContent(message.getContent())
|
||||
.setEmbeds(embeds)
|
||||
.setAllowedMentions(allowedTypes)
|
||||
.setSuppressEmbeds(message.isSuppressedEmbeds())
|
||||
.mentionUsers(allowedUsers.stream().mapToLong(l -> l).toArray())
|
||||
.mentionRoles(allowedRoles.stream().mapToLong(l -> l).toArray())
|
||||
.setFiles(uploads);
|
||||
@ -86,6 +87,7 @@ public final class SendableDiscordMessageUtil {
|
||||
|
||||
return jdaBuilder(message, new MessageCreateBuilder())
|
||||
.addComponents(actionRows)
|
||||
.setSuppressedNotifications(message.isSuppressedNotifications())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -149,8 +149,8 @@ public class Log4JLoggerImpl implements Logger, LoggingBackend {
|
||||
public boolean removeAppender(LogAppender appender) {
|
||||
if (logger instanceof org.apache.logging.log4j.core.Logger) {
|
||||
org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) logger;
|
||||
Appender log4jAppender = appenders.get(appender);
|
||||
loggerImpl.addAppender(log4jAppender);
|
||||
Appender log4jAppender = appenders.remove(appender);
|
||||
loggerImpl.removeAppender(log4jAppender);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.discordsrv.common.placeholder.context;
|
||||
|
||||
import com.discordsrv.api.placeholder.annotation.Placeholder;
|
||||
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
|
||||
import com.discordsrv.common.DiscordSRV;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class GlobalDateFormattingContext {
|
||||
|
||||
private static final String TIMESTAMP_IDENTIFIER = "timestamp";
|
||||
|
||||
private final LoadingCache<String, DateTimeFormatter> cache;
|
||||
|
||||
public GlobalDateFormattingContext(DiscordSRV discordSRV) {
|
||||
this.cache = discordSRV.caffeineBuilder()
|
||||
.expireAfterAccess(30, TimeUnit.SECONDS)
|
||||
.build(DateTimeFormatter::ofPattern);
|
||||
}
|
||||
|
||||
@Placeholder("date")
|
||||
public String formatDate(ZonedDateTime time, @PlaceholderRemainder String format) {
|
||||
if (format.startsWith(TIMESTAMP_IDENTIFIER)) {
|
||||
String style = format.substring(TIMESTAMP_IDENTIFIER.length());
|
||||
if (!style.isEmpty() && !style.startsWith(":")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "<t:" + time.toEpochSecond() + style + ">";
|
||||
}
|
||||
|
||||
DateTimeFormatter formatter = cache.get(format);
|
||||
if (formatter == null) {
|
||||
throw new IllegalStateException("Illegal state");
|
||||
}
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
}
|
@ -126,6 +126,9 @@ dependencyResolutionManagement {
|
||||
library('adventure-platform-bungee', 'net.kyori', 'adventure-platform-bungeecord').versionRef('adventure-platform')
|
||||
library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-bungeecord').versionRef('adventure-platform')
|
||||
|
||||
// Upgrade ansi (used by ansi serializer)
|
||||
library('kyori-ansi', 'net.kyori', 'ansi').version('1.1.0-SNAPSHOT')
|
||||
|
||||
// MCDiscordReserializer & EnhancedLegacyText
|
||||
library('mcdiscordreserializer', 'dev.vankka', 'mcdiscordreserializer').version('4.4.0-SNAPSHOT')
|
||||
library('enhancedlegacytext', 'dev.vankka', 'enhancedlegacytext').version('2.0.0-SNAPSHOT')
|
||||
|
Loading…
Reference in New Issue
Block a user