mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2024-11-26 12:35:20 +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);
|
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 {
|
class Attachment {
|
||||||
|
|
||||||
private final String fileName;
|
private final String fileName;
|
||||||
|
@ -109,8 +109,38 @@ public interface SendableDiscordMessage {
|
|||||||
return getWebhookUsername() != null;
|
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();
|
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
|
@SuppressWarnings("UnusedReturnValue") // API
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
||||||
@ -250,6 +280,54 @@ public interface SendableDiscordMessage {
|
|||||||
*/
|
*/
|
||||||
Builder addAttachment(InputStream inputStream, String fileName);
|
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.
|
* Checks if this builder has any sendable content.
|
||||||
* @return {@code true} if there is no 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 webhookUsername;
|
||||||
private final String webhookAvatarUrl;
|
private final String webhookAvatarUrl;
|
||||||
private final Map<InputStream, String> attachments;
|
private final Map<InputStream, String> attachments;
|
||||||
|
private final boolean suppressedNotifications;
|
||||||
|
private final boolean suppressedEmbeds;
|
||||||
|
private final Long replyingToMessageId;
|
||||||
|
|
||||||
protected SendableDiscordMessageImpl(
|
protected SendableDiscordMessageImpl(
|
||||||
String content,
|
String content,
|
||||||
@ -60,7 +63,10 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
|||||||
Set<AllowedMention> allowedMentions,
|
Set<AllowedMention> allowedMentions,
|
||||||
String webhookUsername,
|
String webhookUsername,
|
||||||
String webhookAvatarUrl,
|
String webhookAvatarUrl,
|
||||||
Map<InputStream, String> attachments
|
Map<InputStream, String> attachments,
|
||||||
|
boolean suppressedNotifications,
|
||||||
|
boolean suppressedEmbeds,
|
||||||
|
Long replyingToMessageId
|
||||||
) {
|
) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.embeds = Collections.unmodifiableList(embeds);
|
this.embeds = Collections.unmodifiableList(embeds);
|
||||||
@ -69,6 +75,24 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
|||||||
this.webhookUsername = webhookUsername;
|
this.webhookUsername = webhookUsername;
|
||||||
this.webhookAvatarUrl = webhookAvatarUrl;
|
this.webhookAvatarUrl = webhookAvatarUrl;
|
||||||
this.attachments = Collections.unmodifiableMap(attachments);
|
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
|
@Override
|
||||||
@ -102,6 +126,21 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
|||||||
return webhookAvatarUrl;
|
return webhookAvatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSuppressedNotifications() {
|
||||||
|
return suppressedNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSuppressedEmbeds() {
|
||||||
|
return suppressedEmbeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getMessageIdToReplyTo() {
|
||||||
|
return replyingToMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<InputStream, String> getAttachments() {
|
public Map<InputStream, String> getAttachments() {
|
||||||
return attachments;
|
return attachments;
|
||||||
@ -116,6 +155,9 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
|||||||
private String webhookUsername;
|
private String webhookUsername;
|
||||||
private String webhookAvatarUrl;
|
private String webhookAvatarUrl;
|
||||||
private final Map<InputStream, String> attachments = new LinkedHashMap<>();
|
private final Map<InputStream, String> attachments = new LinkedHashMap<>();
|
||||||
|
private boolean suppressedNotifications;
|
||||||
|
private boolean suppressedEmbeds;
|
||||||
|
private Long replyingToMessageId;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getContent() {
|
public String getContent() {
|
||||||
@ -226,9 +268,42 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
|
|||||||
return (content == null || content.isEmpty()) && embeds.isEmpty() && attachments.isEmpty() && actionRows.isEmpty();
|
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
|
@Override
|
||||||
public @NotNull SendableDiscordMessage build() {
|
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
|
@Override
|
||||||
|
@ -85,12 +85,13 @@ dependencies {
|
|||||||
// Logging
|
// Logging
|
||||||
compileOnly(libs.log4j.core)
|
compileOnly(libs.log4j.core)
|
||||||
|
|
||||||
// Adventure, MCDiscordReserializer, EnhancedLegacyText
|
// Adventure, ANSI (version upgrade for serializer), MCDiscordReserializer, EnhancedLegacyText
|
||||||
runtimeDownloadApi(libs.adventure.api)
|
runtimeDownloadApi(libs.adventure.api)
|
||||||
runtimeDownloadApi(libs.adventure.serializer.plain)
|
runtimeDownloadApi(libs.adventure.serializer.plain)
|
||||||
runtimeDownloadApi(libs.adventure.serializer.legacy)
|
runtimeDownloadApi(libs.adventure.serializer.legacy)
|
||||||
runtimeDownloadApi(libs.adventure.serializer.gson)
|
runtimeDownloadApi(libs.adventure.serializer.gson)
|
||||||
runtimeDownloadApi(libs.adventure.serializer.ansi)
|
runtimeDownloadApi(libs.adventure.serializer.ansi)
|
||||||
|
runtimeDownloadApi(libs.kyori.ansi)
|
||||||
runtimeDownloadApi(libs.mcdiscordreserializer)
|
runtimeDownloadApi(libs.mcdiscordreserializer)
|
||||||
runtimeDownloadApi(libs.enhancedlegacytext)
|
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.MainConfig;
|
||||||
import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
|
import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
|
||||||
import com.discordsrv.common.config.messages.MessagesConfig;
|
import com.discordsrv.common.config.messages.MessagesConfig;
|
||||||
|
import com.discordsrv.common.console.ConsoleModule;
|
||||||
import com.discordsrv.common.debug.data.VersionInfo;
|
import com.discordsrv.common.debug.data.VersionInfo;
|
||||||
import com.discordsrv.common.dependency.DiscordSRVDependencyManager;
|
import com.discordsrv.common.dependency.DiscordSRVDependencyManager;
|
||||||
import com.discordsrv.common.discord.api.DiscordAPIEventModule;
|
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.module.type.AbstractModule;
|
||||||
import com.discordsrv.common.placeholder.DiscordPlaceholdersImpl;
|
import com.discordsrv.common.placeholder.DiscordPlaceholdersImpl;
|
||||||
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
|
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.context.GlobalTextHandlingContext;
|
||||||
import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
|
import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
|
||||||
import com.discordsrv.common.profile.ProfileManager;
|
import com.discordsrv.common.profile.ProfileManager;
|
||||||
@ -559,8 +561,10 @@ public abstract class AbstractDiscordSRV<
|
|||||||
// Placeholder result stringifiers & global contexts
|
// Placeholder result stringifiers & global contexts
|
||||||
placeholderService().addResultMapper(new ComponentResultStringifier(this));
|
placeholderService().addResultMapper(new ComponentResultStringifier(this));
|
||||||
placeholderService().addGlobalContext(new GlobalTextHandlingContext(this));
|
placeholderService().addGlobalContext(new GlobalTextHandlingContext(this));
|
||||||
|
placeholderService().addGlobalContext(new GlobalDateFormattingContext(this));
|
||||||
|
|
||||||
// Modules
|
// Modules
|
||||||
|
registerModule(ConsoleModule::new);
|
||||||
registerModule(ChannelLockingModule::new);
|
registerModule(ChannelLockingModule::new);
|
||||||
registerModule(TimedUpdaterModule::new);
|
registerModule(TimedUpdaterModule::new);
|
||||||
registerModule(DiscordCommandModule::new);
|
registerModule(DiscordCommandModule::new);
|
||||||
|
@ -68,19 +68,19 @@ public class ComponentFactory implements MinecraftComponentFactory {
|
|||||||
MinecraftSerializerOptions.defaults()
|
MinecraftSerializerOptions.defaults()
|
||||||
.addRenderer(new DiscordSRVMinecraftRenderer(discordSRV))
|
.addRenderer(new DiscordSRVMinecraftRenderer(discordSRV))
|
||||||
);
|
);
|
||||||
this.discordSerializer = new DiscordSerializer(
|
|
||||||
DiscordSerializerOptions.defaults()
|
|
||||||
.withTranslationProvider(this::provideTranslation)
|
|
||||||
);
|
|
||||||
|
|
||||||
ComponentFlattener flattener = ComponentFlattener.basic().toBuilder()
|
ComponentFlattener flattener = ComponentFlattener.basic().toBuilder()
|
||||||
.mapper(TranslatableComponent.class, this::provideTranslation)
|
.mapper(TranslatableComponent.class, this::provideTranslation)
|
||||||
.build();
|
.build();
|
||||||
|
this.discordSerializer = new DiscordSerializer(
|
||||||
|
DiscordSerializerOptions.defaults()
|
||||||
|
.withFlattener(flattener)
|
||||||
|
);
|
||||||
this.plainSerializer = PlainTextComponentSerializer.builder()
|
this.plainSerializer = PlainTextComponentSerializer.builder()
|
||||||
.flattener(flattener)
|
.flattener(flattener)
|
||||||
.build();
|
.build();
|
||||||
this.ansiSerializer = ANSIComponentSerializer.builder()
|
this.ansiSerializer = ANSIComponentSerializer.builder()
|
||||||
.colorLevel(ColorLevel.INDEXED_16)
|
.colorLevel(ColorLevel.INDEXED_8)
|
||||||
.flattener(flattener)
|
.flattener(flattener)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -137,4 +137,5 @@ public class ComponentFactory implements MinecraftComponentFactory {
|
|||||||
public TranslationRegistry translationRegistry() {
|
public TranslationRegistry translationRegistry() {
|
||||||
return 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.ConfigSerializable;
|
||||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@ConfigSerializable
|
@ConfigSerializable
|
||||||
public abstract class MainConfig implements Config {
|
public abstract class MainConfig implements Config {
|
||||||
@ -88,6 +86,9 @@ public abstract class MainConfig implements Config {
|
|||||||
@Comment("Discord command configuration")
|
@Comment("Discord command configuration")
|
||||||
public DiscordCommandConfig discordCommand = new DiscordCommandConfig();
|
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")
|
@Comment("Configuration for the %1 placeholder. The below options will be attempted in the order they are in")
|
||||||
@Constants.Comment("%discord_invite%")
|
@Constants.Comment("%discord_invite%")
|
||||||
public DiscordInviteConfig invite = new DiscordInviteConfig();
|
public DiscordInviteConfig invite = new DiscordInviteConfig();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.discordsrv.common.config.main.generic;
|
package com.discordsrv.common.config.main.generic;
|
||||||
|
|
||||||
import com.discordsrv.common.config.configurate.annotation.Constants;
|
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.ConfigSerializable;
|
||||||
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||||||
import org.spongepowered.configurate.objectmapping.meta.Setting;
|
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)")
|
@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")
|
@Constants.Comment("channel-ids")
|
||||||
public List<ThreadConfig> threads = new ArrayList<>(Collections.singletonList(new ThreadConfig()));
|
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) {
|
if (!match) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String configCommand : commands) {
|
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 create,
|
||||||
boolean log
|
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<>();
|
List<DiscordGuildMessageChannel> channels = new CopyOnWriteArrayList<>();
|
||||||
for (Long channelId : destination.channelIds) {
|
for (Long channelId : destination.channelIds) {
|
||||||
@ -148,7 +156,7 @@ public class DiscordAPIImpl implements DiscordAPI {
|
|||||||
unarchiveOrCreateThread(threadConfig, channel, thread, future);
|
unarchiveOrCreateThread(threadConfig, channel, thread, future);
|
||||||
} else {
|
} else {
|
||||||
// Find or create the thread
|
// Find or create the thread
|
||||||
future = findOrCreateThread(config, threadConfig, channel);
|
future = findOrCreateThread(unarchive, threadConfig, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscordThreadContainer container = channel;
|
DiscordThreadContainer container = channel;
|
||||||
@ -181,12 +189,8 @@ public class DiscordAPIImpl implements DiscordAPI {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(
|
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(boolean unarchive, ThreadConfig threadConfig, DiscordThreadContainer container) {
|
||||||
BaseChannelConfig config,
|
if (!unarchive) {
|
||||||
ThreadConfig threadConfig,
|
|
||||||
DiscordThreadContainer container
|
|
||||||
) {
|
|
||||||
if (!config.channelLocking.threads.unarchive) {
|
|
||||||
return container.createThread(threadConfig.threadName, threadConfig.privateThread);
|
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.WebhookClient;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
||||||
import net.dv8tion.jda.api.requests.RestAction;
|
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.MessageCreateData;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateRequest;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateRequest;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
||||||
@ -89,7 +90,15 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
|
|||||||
.setAvatarUrl(message.getWebhookAvatarUrl())
|
.setAvatarUrl(message.getWebhookAvatarUrl())
|
||||||
);
|
);
|
||||||
} else {
|
} 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
|
return createRequest
|
||||||
|
@ -56,9 +56,13 @@ public class DiscordDMChannelImpl extends AbstractDiscordMessageChannel<PrivateC
|
|||||||
|
|
||||||
MessageCreateAction action = channel.sendMessage(SendableDiscordMessageUtil.toJDASend(message));
|
MessageCreateAction action = channel.sendMessage(SendableDiscordMessageUtil.toJDASend(message));
|
||||||
|
|
||||||
|
Long referencedMessageId = message.getMessageIdToReplyTo();
|
||||||
|
if (referencedMessageId != null) {
|
||||||
|
action = action.setMessageReference(referencedMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
CompletableFuture<ReceivedDiscordMessage> future = action.submit()
|
CompletableFuture<ReceivedDiscordMessage> future = action.submit()
|
||||||
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
|
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
|
||||||
|
|
||||||
return discordSRV.discordAPI().mapExceptions(future);
|
return discordSRV.discordAPI().mapExceptions(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,12 +226,12 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull CompletableFuture<Void> delete() {
|
public @NotNull CompletableFuture<Void> delete() {
|
||||||
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
|
DiscordMessageChannel messageChannel = discordSRV.discordAPI().getMessageChannelById(channelId);
|
||||||
if (textChannel == null) {
|
if (messageChannel == null) {
|
||||||
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
||||||
}
|
}
|
||||||
|
|
||||||
return textChannel.deleteMessageById(getId(), fromSelf && webhookMessage);
|
return messageChannel.deleteMessageById(getId(), fromSelf && webhookMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -242,12 +242,26 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
|
|||||||
throw new IllegalArgumentException("Cannot edit a non-webhook message into a webhook message");
|
throw new IllegalArgumentException("Cannot edit a non-webhook message into a webhook message");
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
|
DiscordMessageChannel messageChannel = discordSRV.discordAPI().getMessageChannelById(channelId);
|
||||||
if (textChannel == null) {
|
if (messageChannel == null) {
|
||||||
return CompletableFutureUtil.failed(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
|
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())
|
.setContent(message.getContent())
|
||||||
.setEmbeds(embeds)
|
.setEmbeds(embeds)
|
||||||
.setAllowedMentions(allowedTypes)
|
.setAllowedMentions(allowedTypes)
|
||||||
|
.setSuppressEmbeds(message.isSuppressedEmbeds())
|
||||||
.mentionUsers(allowedUsers.stream().mapToLong(l -> l).toArray())
|
.mentionUsers(allowedUsers.stream().mapToLong(l -> l).toArray())
|
||||||
.mentionRoles(allowedRoles.stream().mapToLong(l -> l).toArray())
|
.mentionRoles(allowedRoles.stream().mapToLong(l -> l).toArray())
|
||||||
.setFiles(uploads);
|
.setFiles(uploads);
|
||||||
@ -86,6 +87,7 @@ public final class SendableDiscordMessageUtil {
|
|||||||
|
|
||||||
return jdaBuilder(message, new MessageCreateBuilder())
|
return jdaBuilder(message, new MessageCreateBuilder())
|
||||||
.addComponents(actionRows)
|
.addComponents(actionRows)
|
||||||
|
.setSuppressedNotifications(message.isSuppressedNotifications())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,8 +149,8 @@ public class Log4JLoggerImpl implements Logger, LoggingBackend {
|
|||||||
public boolean removeAppender(LogAppender appender) {
|
public boolean removeAppender(LogAppender appender) {
|
||||||
if (logger instanceof org.apache.logging.log4j.core.Logger) {
|
if (logger instanceof org.apache.logging.log4j.core.Logger) {
|
||||||
org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) logger;
|
org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) logger;
|
||||||
Appender log4jAppender = appenders.get(appender);
|
Appender log4jAppender = appenders.remove(appender);
|
||||||
loggerImpl.addAppender(log4jAppender);
|
loggerImpl.removeAppender(log4jAppender);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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-platform-bungee', 'net.kyori', 'adventure-platform-bungeecord').versionRef('adventure-platform')
|
||||||
library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-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
|
// MCDiscordReserializer & EnhancedLegacyText
|
||||||
library('mcdiscordreserializer', 'dev.vankka', 'mcdiscordreserializer').version('4.4.0-SNAPSHOT')
|
library('mcdiscordreserializer', 'dev.vankka', 'mcdiscordreserializer').version('4.4.0-SNAPSHOT')
|
||||||
library('enhancedlegacytext', 'dev.vankka', 'enhancedlegacytext').version('2.0.0-SNAPSHOT')
|
library('enhancedlegacytext', 'dev.vankka', 'enhancedlegacytext').version('2.0.0-SNAPSHOT')
|
||||||
|
Loading…
Reference in New Issue
Block a user