Merge branch 'main' into execute-command

This commit is contained in:
Vankka 2023-07-01 17:32:15 +03:00
commit bb24dc7b32
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
114 changed files with 1609 additions and 1128 deletions

View File

@ -37,7 +37,8 @@ java {
}
jar {
from sourceSets.ap.output
from sourceSets.ap.output.classesDirs
from sourceSets.ap.output.resourcesDir
}
//license {

View File

@ -70,6 +70,14 @@ public interface DiscordAPI {
@Nullable
DiscordTextChannel getTextChannelById(long id);
/**
* Gets a Discord forum channel by id, the provided entity should not be stored for long periods of time.
* @param id the id for the text channel
* @return the forum channel
*/
@Nullable
DiscordForumChannel getForumChannelById(long id);
/**
* Gets a Discord voice channel by id, the provided entity should be stored for long periods of time.
* @param id the id for the voice channel

View File

@ -56,6 +56,14 @@ public interface DiscordUser extends JDAEntity<User>, Snowflake, Mentionable {
@NotNull
String getUsername();
/**
* Gets the effective display name of the Discord user.
* @return the user's effective display name
*/
@Placeholder("user_effective_name")
@NotNull
String getEffectiveName();
/**
* Gets the Discord user's discriminator.
* @return the user's discriminator
@ -82,12 +90,17 @@ public interface DiscordUser extends JDAEntity<User>, Snowflake, Mentionable {
String getEffectiveAvatarUrl();
/**
* Gets the Discord user's username followed by a {@code #} and their discriminator.
* @return the Discord user's username and discriminator in the following format {@code Username#1234}
* Gets the Discord user's username, including discriminator if any.
* @return the Discord user's username
*/
@Placeholder("user_tag")
default String getAsTag() {
return getUsername() + "#" + getDiscriminator();
String username = getUsername();
String discriminator = getDiscriminator();
if (!discriminator.replace("0", "").isEmpty()) {
username = username + "#" + discriminator;
}
return username;
}
/**

View File

@ -0,0 +1,12 @@
package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.discord.entity.Snowflake;
public interface DiscordChannel extends Snowflake {
/**
* Returns the type of channel this is.
* @return the type of the channel
*/
DiscordChannelType getType();
}

View File

@ -36,6 +36,7 @@ public enum DiscordChannelType implements JDAEntity<ChannelType> {
VOICE(ChannelType.VOICE),
GROUP(ChannelType.GROUP),
CATEGORY(ChannelType.CATEGORY),
FORUM(ChannelType.FORUM),
NEWS(ChannelType.NEWS),
STAGE(ChannelType.STAGE),
GUILD_NEWS_THREAD(ChannelType.GUILD_NEWS_THREAD),

View File

@ -0,0 +1,7 @@
package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.discord.entity.JDAEntity;
import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel;
public interface DiscordForumChannel extends DiscordChannel, DiscordThreadContainer, JDAEntity<ForumChannel> {
}

View File

@ -24,27 +24,17 @@
package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.Snowflake;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* A Discord channel that can send/receive messages.
*/
public interface DiscordMessageChannel extends Snowflake {
/**
* Returns the type of channel this is.
* @return the type of the channel
*/
DiscordChannelType getType();
public interface DiscordMessageChannel extends DiscordChannel {
/**
* Sends the provided message to the channel.
@ -53,21 +43,7 @@ public interface DiscordMessageChannel extends Snowflake {
* @return a future returning the message after being sent
*/
@NotNull
default CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message) {
return sendMessage(message, Collections.emptyMap());
}
/**
* Sends the provided message to the channel with the provided attachments.
*
* @param message the message to send to the channel
* @param attachments the attachments (in a map of file name and input stream pairs) to include in the message, the streams will be closed upon execution
* @return a future returning the message after being sent
*/
CompletableFuture<ReceivedDiscordMessage> sendMessage(
@NotNull SendableDiscordMessage message,
@NotNull Map<String, InputStream> attachments
);
CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message);
/**
* Deletes the message identified by the id.

View File

@ -54,6 +54,13 @@ public interface DiscordGuild extends JDAEntity<Guild>, Snowflake {
@Placeholder("server_member_count")
int getMemberCount();
/**
* Gets the bot's membership in the server.
* @return the connected bot's member
*/
@NotNull
DiscordGuildMember getSelfMember();
/**
* Retrieves a Discord guild member from Discord by id.
* @param id the id for the Discord guild member

View File

@ -76,6 +76,13 @@ public interface DiscordGuildMember extends JDAEntity<Member>, Mentionable {
*/
boolean hasRole(@NotNull DiscordRole role);
/**
* If this member can interact (edit, add/take from members) with the specified role.
* @param role the role
* @return {@code true} if the member has a role above the specified role or is the server owner
*/
boolean canInteract(@NotNull DiscordRole role);
/**
* Gives the given role to this member.
* @param role the role to give
@ -94,18 +101,18 @@ public interface DiscordGuildMember extends JDAEntity<Member>, Mentionable {
* Gets the effective name of this Discord server member.
* @return the Discord server member's effective name
*/
@Placeholder("user_effective_name")
@Placeholder("user_effective_server_name")
@NotNull
default String getEffectiveName() {
default String getEffectiveServerName() {
String nickname = getNickname();
return nickname != null ? nickname : getUser().getUsername();
return nickname != null ? nickname : getUser().getEffectiveName();
}
/**
* Gets the avatar url that is active for this user in this server.
* @return the user's avatar url in this server
*/
@Placeholder("user_effective_avatar_url")
@Placeholder("user_effective_server_avatar_url")
@NotNull
String getEffectiveServerAvatarUrl();

View File

@ -30,6 +30,7 @@ import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
@ -69,6 +70,7 @@ public interface ReceivedDiscordMessage extends Snowflake {
* @return the jump url
*/
@NotNull
@Placeholder("message_jump_url")
String getJumpUrl();
/**
@ -155,8 +157,7 @@ public interface ReceivedDiscordMessage extends Snowflake {
* @throws IllegalArgumentException if the message is not a webhook message,
* but the provided {@link SendableDiscordMessage} specifies a webhook username.
*/
@NotNull
CompletableFuture<ReceivedDiscordMessage> edit(SendableDiscordMessage message);
CompletableFuture<ReceivedDiscordMessage> edit(@NotNull SendableDiscordMessage message);
class Attachment {

View File

@ -31,8 +31,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
@ -107,6 +109,8 @@ public interface SendableDiscordMessage {
return getWebhookUsername() != null;
}
Map<InputStream, String> getAttachments();
@SuppressWarnings("UnusedReturnValue") // API
interface Builder {
@ -238,6 +242,20 @@ public interface SendableDiscordMessage {
@NotNull
Builder setWebhookAvatarUrl(String webhookAvatarUrl);
/**
* Adds an attachment to this builder.
* @param inputStream an input stream containing the file contents
* @param fileName the name of the file
* @return the builder, useful for chaining
*/
Builder addAttachment(InputStream inputStream, String fileName);
/**
* Checks if this builder has any sendable content.
* @return {@code true} if there is no sendable content
*/
boolean isEmpty();
/**
* Builds a {@link SendableDiscordMessage} from this builder.
* @return the new {@link SendableDiscordMessage}

View File

@ -37,6 +37,7 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
@ -50,6 +51,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
private final Set<AllowedMention> allowedMentions;
private final String webhookUsername;
private final String webhookAvatarUrl;
private final Map<InputStream, String> attachments;
protected SendableDiscordMessageImpl(
String content,
@ -57,7 +59,8 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
List<MessageActionRow> actionRows,
Set<AllowedMention> allowedMentions,
String webhookUsername,
String webhookAvatarUrl
String webhookAvatarUrl,
Map<InputStream, String> attachments
) {
this.content = content;
this.embeds = Collections.unmodifiableList(embeds);
@ -65,6 +68,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
this.allowedMentions = Collections.unmodifiableSet(allowedMentions);
this.webhookUsername = webhookUsername;
this.webhookAvatarUrl = webhookAvatarUrl;
this.attachments = Collections.unmodifiableMap(attachments);
}
@Override
@ -98,6 +102,11 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
return webhookAvatarUrl;
}
@Override
public Map<InputStream, String> getAttachments() {
return attachments;
}
public static class BuilderImpl implements SendableDiscordMessage.Builder {
private String content;
@ -106,6 +115,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
private final Set<AllowedMention> allowedMentions = new LinkedHashSet<>();
private String webhookUsername;
private String webhookAvatarUrl;
private final Map<InputStream, String> attachments = new LinkedHashMap<>();
@Override
public String getContent() {
@ -205,9 +215,20 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
return this;
}
@Override
public Builder addAttachment(InputStream inputStream, String fileName) {
this.attachments.put(inputStream, fileName);
return this;
}
@Override
public boolean isEmpty() {
return (content == null || content.isEmpty()) && embeds.isEmpty() && attachments.isEmpty() && actionRows.isEmpty();
}
@Override
public @NotNull SendableDiscordMessage build() {
return new SendableDiscordMessageImpl(content, embeds, actionRows, allowedMentions, webhookUsername, webhookAvatarUrl);
return new SendableDiscordMessageImpl(content, embeds, actionRows, allowedMentions, webhookUsername, webhookAvatarUrl, attachments);
}
@Override

View File

@ -53,7 +53,8 @@ subprojects {
includeGroup 'me.scarsz'
includeGroup 'me.minecraftauth'
includeGroup 'org.spongepowered' // Configurate yamlbranch
includeGroup 'org.spongepowered' // SpongePowered/Configurate feature/yaml-improvements branch
includeGroup 'net.dv8tion' // DiscordSRV/JDA v5-webhooks branch
}
}
maven {

View File

@ -52,11 +52,11 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
|| version.startsWith("v1_2")) {
// 1.19.4+
nms = new NMS("org.bukkit.craftbukkit." + version + ".advancement.CraftAdvancement",
"k", "d", "i", "a");
"d", "i", "a");
} else {
// <1.19.4
nms = new NMS("org.bukkit.craftbukkit." + version + ".advancement.CraftAdvancement",
"j", "c", "i", "a");
"c", "i", "a");
}
} catch (Throwable t) {
logger.error("Could not get NMS methods for advancements.");
@ -81,7 +81,7 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
event,
event.getPlayer(),
data.titleJson != null ? ComponentUtil.toAPI(BukkitComponentSerializer.gson().deserialize(data.titleJson)) : null,
data.nameJson != null ? ComponentUtil.toAPI(BukkitComponentSerializer.gson().deserialize(data.nameJson)) : null,
null,
false);
} catch (ReflectiveOperationException e) {
logger.debug("Failed to get advancement data", e);
@ -91,7 +91,6 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
private static class NMS {
private final Method handleMethod;
private final Method advancementNameMethod;
private final Method advancementDisplayMethod;
private final Method broadcastToChatMethod;
private final Method titleMethod;
@ -99,7 +98,6 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
public NMS(
String craftAdvancementClassName,
String nameMethodName,
String displayMethodName,
String broadcastToChatMethodName,
String titleMethodName
@ -107,7 +105,6 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
Class<?> clazz = Class.forName(craftAdvancementClassName);
handleMethod = clazz.getDeclaredMethod("getHandle");
Class<?> nmsClass = handleMethod.getReturnType();
advancementNameMethod = nmsClass.getDeclaredMethod(nameMethodName);
advancementDisplayMethod = nmsClass.getDeclaredMethod(displayMethodName);
Class<?> displayClass = advancementDisplayMethod.getReturnType();
broadcastToChatMethod = displayClass.getDeclaredMethod(broadcastToChatMethodName);
@ -133,27 +130,22 @@ public class BukkitAdvancementListener extends AbstractBukkitAwardListener {
return null;
}
Object nameChat = advancementNameMethod.invoke(nms);
Object titleChat = titleMethod.invoke(display);
return new ReturnData(
toJson(nameChat),
toJson(titleChat)
);
}
private String toJson(Object chat) throws ReflectiveOperationException {
return (String) toJsonMethod.invoke(chat);
return (String) toJsonMethod.invoke(null, chat);
}
}
private static class ReturnData {
private final String nameJson;
private final String titleJson;
public ReturnData(String nameJson, String titleJson) {
this.nameJson = nameJson;
public ReturnData(String titleJson) {
this.titleJson = titleJson;
}
}

View File

@ -183,12 +183,6 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
// Command handler
commandHandler = AbstractBukkitCommandHandler.get(this);
// Register listeners
server().getPluginManager().registerEvents(BukkitAwardForwarder.get(this), plugin());
server().getPluginManager().registerEvents(BukkitChatForwarder.get(this), plugin());
server().getPluginManager().registerEvents(new BukkitDeathListener(this), plugin());
server().getPluginManager().registerEvents(new BukkitStatusMessageListener(this), plugin());
// Modules
registerModule(MinecraftToDiscordChatModule::new);
registerModule(BukkitRequiredLinkingModule::new);
@ -207,12 +201,18 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
super.enable();
// Register listeners
server().getPluginManager().registerEvents(BukkitAwardForwarder.get(this), plugin());
server().getPluginManager().registerEvents(BukkitChatForwarder.get(this), plugin());
server().getPluginManager().registerEvents(new BukkitDeathListener(this), plugin());
server().getPluginManager().registerEvents(new BukkitStatusMessageListener(this), plugin());
// Connection listener
server().getPluginManager().registerEvents(new BukkitConnectionListener(this), plugin());
}
@Override
protected List<ReloadResult> reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
public List<ReloadResult> reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
List<ReloadResult> results = super.reload(flags, initial);
if (flags.contains(ReloadFlag.TRANSLATIONS)) {

View File

@ -92,11 +92,12 @@ public abstract class AbstractBukkitCommandHandler implements ICommandHandler {
String label = gameCommand.getLabel();
PluginCommand pluginCommand = discordSRV.plugin().getCommand(label);
if (pluginCommand != null) {
logger.debug("PluginCommand available for \"" + label + "\"");
return pluginCommand;
}
if (COMMAND_MAP_HANDLE == null) {
// CommandMap unusable, can't get the command from it
logger.debug("Unable to get command from command map");
return null;
}
@ -107,8 +108,16 @@ public abstract class AbstractBukkitCommandHandler implements ICommandHandler {
command = (PluginCommand) constructor.newInstance(label, discordSRV.plugin());
CommandMap commandMap = (CommandMap) COMMAND_MAP_HANDLE.invokeExact(discordSRV.server());
commandMap.register(label, discordSRV.plugin().getName().toLowerCase(Locale.ROOT), command);
} catch (Throwable ignored) {}
boolean result = commandMap.register(label, discordSRV.plugin().getName().toLowerCase(Locale.ROOT), command);
if (result) {
logger.debug("Registered command \"" + label + "\" in CommandMap successfully");
} else {
logger.debug("Registered command \"" + label + "\" into CommandMap but with fallback prefix");
}
} catch (Throwable t) {
logger.debug("Failed to register command \"" + label + "\" to CommandMap", t);
}
return command;
}

View File

@ -48,6 +48,7 @@ public class BukkitBasicCommandHandler extends AbstractBukkitCommandExecutor imp
@Override
public void registerCommand(GameCommand command) {
discordSRV.scheduler().runOnMainThread(() -> {
PluginCommand pluginCommand = command(command);
if (pluginCommand == null) {
logger.error("Failed to create command " + command.getLabel());
@ -59,5 +60,6 @@ public class BukkitBasicCommandHandler extends AbstractBukkitCommandExecutor imp
handler.registerCommand(command);
pluginCommand.setExecutor(this);
pluginCommand.setTabCompleter(this);
});
}
}

View File

@ -21,29 +21,21 @@ package com.discordsrv.bukkit.command.game.handler;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.handler.util.BrigadierUtil;
import com.discordsrv.common.command.game.sender.ICommandSender;
import com.mojang.brigadier.tree.LiteralCommandNode;
import me.lucko.commodore.Commodore;
import me.lucko.commodore.CommodoreProvider;
import org.bukkit.command.PluginCommand;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
/**
* No avoiding basic handler on bukkit. Unfortunately it isn't possible to use brigadier for executing.
* No avoiding basic handler on Bukkit. Commodore only sends the command tree to the client, nothing else.
*/
public class CommodoreHandler extends AbstractBukkitCommandExecutor {
public class CommodoreHandler extends BukkitBasicCommandHandler {
private final Commodore commodore;
private final Function<?, ICommandSender> senderFunction;
public CommodoreHandler(BukkitDiscordSRV discordSRV) {
super(discordSRV);
this.commodore = CommodoreProvider.getCommodore(discordSRV.plugin());
this.senderFunction = wrapper -> sender(commodore.getBukkitSender(wrapper));
}
@Override
@ -58,38 +50,11 @@ public class CommodoreHandler extends AbstractBukkitCommandExecutor {
handler.registerCommand(command);
pluginCommand.setExecutor(this);
pluginCommand.setTabCompleter(this);
List<LiteralCommandNode<?>> nodes = getAliases(command, pluginCommand);
discordSRV.scheduler().runOnMainThread(() -> {
for (LiteralCommandNode<?> node : nodes) {
commodore.register(node);
}
LiteralCommandNode<?> commandNode = BrigadierUtil.convertToBrigadier(command, null);
commodore.register(pluginCommand, commandNode);
});
}
private List<LiteralCommandNode<?>> getAliases(GameCommand command, PluginCommand pluginCommand) {
String commandName = pluginCommand.getName();
String pluginName = pluginCommand.getPlugin().getName().toLowerCase(Locale.ROOT);
List<String> allAliases = new ArrayList<>();
allAliases.add(commandName);
allAliases.addAll(pluginCommand.getAliases());
List<LiteralCommandNode<?>> nodes = new ArrayList<>();
for (String alias : allAliases) {
if (alias.equals(commandName)) {
LiteralCommandNode<?> node = BrigadierUtil.convertToBrigadier(command, senderFunction);
if (node.getRedirect() != null) {
throw new IllegalStateException("Cannot register a redirected node!");
}
nodes.add(node);
} else {
nodes.add(BrigadierUtil.convertToBrigadier(GameCommand.literal(alias).redirect(command), senderFunction));
}
// plugin:command
nodes.add(BrigadierUtil.convertToBrigadier(GameCommand.literal(pluginName + ":" + alias).redirect(command), senderFunction));
}
return nodes;
}
}

View File

@ -19,6 +19,8 @@
package com.discordsrv.bukkit.scheduler;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.bukkit.DiscordSRVBukkitBootstrap;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.scheduler.ServerScheduler;
import com.discordsrv.common.scheduler.StandardScheduler;
import org.bukkit.Server;
@ -35,6 +37,16 @@ public abstract class AbstractBukkitScheduler extends StandardScheduler implemen
this.discordSRV = discordSRV;
}
protected void checkDisable(Runnable task, BiConsumer<Server, Plugin> runNormal) {
// Can't run tasks when disabling, so we'll push those to the bootstrap to run after disable
if (!discordSRV.plugin().isEnabled() && discordSRV.status() == DiscordSRV.Status.SHUTTING_DOWN) {
((DiscordSRVBukkitBootstrap) discordSRV.bootstrap()).mainThreadTasksForDisable().add(task);
return;
}
runWithArgs(runNormal);
}
@Override
public void runWithArgs(BiConsumer<Server, Plugin> runNormal) {
runNormal.accept(discordSRV.server(), discordSRV.plugin());

View File

@ -19,12 +19,6 @@
package com.discordsrv.bukkit.scheduler;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.bukkit.DiscordSRVBukkitBootstrap;
import com.discordsrv.common.DiscordSRV;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import java.util.function.BiConsumer;
public class BukkitScheduler extends AbstractBukkitScheduler {
@ -32,16 +26,6 @@ public class BukkitScheduler extends AbstractBukkitScheduler {
super(discordSRV);
}
protected void checkDisable(Runnable task, BiConsumer<Server, Plugin> runNormal) {
// Can't run tasks when disabling, so we'll push those to the bootstrap to run after disable
if (!discordSRV.plugin().isEnabled() && discordSRV.status() == DiscordSRV.Status.SHUTTING_DOWN) {
((DiscordSRVBukkitBootstrap) discordSRV.bootstrap()).mainThreadTasksForDisable().add(task);
return;
}
runWithArgs(runNormal);
}
@Override
public void runOnMainThread(Runnable task) {
checkDisable(task, (server, plugin) -> server.getScheduler().runTask(plugin, task));

View File

@ -25,4 +25,19 @@ public class FoliaScheduler extends AbstractBukkitScheduler implements IFoliaSch
public FoliaScheduler(BukkitDiscordSRV discordSRV) {
super(discordSRV);
}
@Override
public void runOnMainThread(Runnable task) {
checkDisable(task, (server, plugin) -> IFoliaScheduler.super.runOnMainThread(task));
}
@Override
public void runOnMainThreadLaterInTicks(Runnable task, int ticks) {
checkDisable(task, (server, plugin) -> IFoliaScheduler.super.runOnMainThreadLaterInTicks(task, ticks));
}
@Override
public void runOnMainThreadAtFixedRateInTicks(Runnable task, int initialTicks, int rateTicks) {
checkDisable(task, (server, plugin) -> IFoliaScheduler.super.runOnMainThreadAtFixedRateInTicks(task, initialTicks, rateTicks));
}
}

View File

@ -42,12 +42,6 @@ dependencies {
// DependencyDownload
api(libs.dependencydownload.runtime)
// Discord Webhooks
runtimeDownloadApi(libs.webhooks) {
// okhttp is already included
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
// Apache Commons
runtimeDownloadApi(libs.commons.lang)
runtimeDownloadApi(libs.commons.io)

View File

@ -27,8 +27,8 @@ import com.discordsrv.common.api.util.ApiInstanceUtil;
import com.discordsrv.common.bootstrap.IBootstrap;
import com.discordsrv.common.channel.ChannelConfigHelper;
import com.discordsrv.common.channel.ChannelLockingModule;
import com.discordsrv.common.channel.ChannelUpdaterModule;
import com.discordsrv.common.channel.GlobalChannelLookupModule;
import com.discordsrv.common.channel.TimedUpdaterModule;
import com.discordsrv.common.command.discord.DiscordCommandModule;
import com.discordsrv.common.command.game.GameCommandModule;
import com.discordsrv.common.command.game.commands.subcommand.reload.ReloadResults;
@ -48,7 +48,6 @@ import com.discordsrv.common.discord.connection.jda.JDAConnectionManager;
import com.discordsrv.common.event.bus.EventBusImpl;
import com.discordsrv.common.exception.StorageException;
import com.discordsrv.common.function.CheckedFunction;
import com.discordsrv.common.function.CheckedRunnable;
import com.discordsrv.common.groupsync.GroupSyncModule;
import com.discordsrv.common.invite.DiscordInviteModule;
import com.discordsrv.common.linking.LinkProvider;
@ -100,7 +99,6 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@ -114,7 +112,7 @@ import java.util.jar.Manifest;
public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainConfig, CC extends ConnectionConfig> implements DiscordSRV {
private final AtomicReference<Status> status = new AtomicReference<>(Status.INITIALIZED);
private CompletableFuture<Void> enableFuture;
private final AtomicReference<Boolean> beenReady = new AtomicReference<>(false);
// DiscordSRVApi
private EventBusImpl eventBus;
@ -147,9 +145,6 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Internal
private final ReentrantLock lifecycleLock = new ReentrantLock();
public AbstractDiscordSRV(B bootstrap) {
ApiInstanceUtil.setInstance(this);
this.bootstrap = bootstrap;
@ -449,6 +444,12 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
if (status == Status.CONNECTED) {
eventBus().publish(new DiscordSRVConnectedEvent());
synchronized (beenReady) {
if (!beenReady.get()) {
eventBus.publish(new DiscordSRVReadyEvent());
beenReady.set(true);
}
}
}
}
@ -483,63 +484,33 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
// Lifecycle
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable<?> runnable) {
return invokeLifecycle(() -> {
try {
lifecycleLock.lock();
runnable.run();
} finally {
lifecycleLock.unlock();
}
return null;
}, "Failed to enable", true);
}
protected <T> CompletableFuture<T> invokeLifecycle(CheckedRunnable<T> runnable, String message, boolean enable) {
return CompletableFuture.supplyAsync(() -> {
if (status().isShutdown()) {
// Already shutdown/shutting down, don't bother
return null;
}
try {
return runnable.run();
} catch (Throwable t) {
if (status().isShutdown() && t instanceof NoClassDefFoundError) {
// Already shutdown, ignore errors for classes that already got unloaded
return null;
}
if (enable) {
setStatus(Status.FAILED_TO_START);
disable();
}
logger().error(message, t);
}
return null;
}, scheduler().executorService());
}
@Override
public final CompletableFuture<Void> invokeEnable() {
return enableFuture = invokeLifecycle(() -> {
public final void runEnable() {
try {
this.enable();
waitForStatus(Status.CONNECTED);
eventBus().publish(new DiscordSRVReadyEvent());
return null;
});
} catch (Throwable t) {
logger.error("Failed to enable", t);
setStatus(Status.FAILED_TO_START);
}
}
@Override
public final CompletableFuture<Void> invokeDisable() {
if (enableFuture != null && !enableFuture.isDone()) {
logger().warning("Start cancelled");
enableFuture.cancel(true);
}
return CompletableFuture.runAsync(this::disable, scheduler().executorService());
}
@Override
public final CompletableFuture<List<ReloadResult>> invokeReload(Set<ReloadFlag> flags, boolean silent) {
return invokeLifecycle(() -> reload(flags, silent), "Failed to reload", false);
public final List<ReloadResult> runReload(Set<ReloadFlag> flags, boolean silent) {
try {
return reload(flags, silent);
} catch (Throwable e) {
if (silent) {
throw new RuntimeException(e);
} else {
logger.error("Failed to reload", e);
}
return Collections.singletonList(ReloadResults.FAILED);
}
}
@OverridingMethodsMustInvokeSuper
@ -553,16 +524,13 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
// Logging
DependencyLoggerAdapter.setAppender(new DependencyLoggingHandler(this));
// Register PlayerProvider listeners
playerProvider().subscribe();
// Placeholder result stringifiers & global contexts
placeholderService().addResultMapper(new ComponentResultStringifier(this));
placeholderService().addGlobalContext(new GlobalTextHandlingContext(this));
// Modules
registerModule(ChannelLockingModule::new);
registerModule(ChannelUpdaterModule::new);
registerModule(TimedUpdaterModule::new);
registerModule(DiscordCommandModule::new);
registerModule(GameCommandModule::new);
registerModule(GlobalChannelLookupModule::new);
@ -580,10 +548,13 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
// Initial load
try {
invokeReload(ReloadFlag.ALL, true).get();
} catch (ExecutionException e) {
runReload(ReloadFlag.ALL, true);
} catch (RuntimeException e) {
throw e.getCause();
}
// Register PlayerProvider listeners
playerProvider().subscribe();
}
protected final void startedMessage() {
@ -610,11 +581,18 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
this.status.set(Status.SHUTTING_DOWN);
eventBus().publish(new DiscordSRVShuttingDownEvent());
eventBus().shutdown();
try {
if (storage != null) {
storage.close();
}
} catch (Throwable t) {
logger().error("Failed to close storage connection", t);
}
this.status.set(Status.SHUTDOWN);
}
@OverridingMethodsMustInvokeSuper
protected List<ReloadResult> reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
public List<ReloadResult> reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
if (!initial) {
logger().info("Reloading DiscordSRV...");
}
@ -626,7 +604,9 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
channelConfig().reload();
} catch (Throwable t) {
if (initial) {
setStatus(Status.FAILED_TO_LOAD_CONFIG);
}
throw t;
}
}
@ -658,7 +638,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
String provider = linkedAccountConfig.provider;
boolean permitMinecraftAuth = connectionConfig().minecraftAuth.allow;
if (provider.equals("auto")) {
provider = permitMinecraftAuth ? "minecraftauth" : "storage";
provider = permitMinecraftAuth && onlineMode().isOnline() ? "minecraftauth" : "storage";
}
switch (provider) {
case "minecraftauth":
@ -719,7 +699,9 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
} catch (StorageException e) {
e.log(this);
logger().error("Failed to connect to storage");
if (initial) {
setStatus(Status.FAILED_TO_START);
}
return Collections.singletonList(ReloadResults.STORAGE_CONNECTION_FAILED);
}
}

View File

@ -149,9 +149,9 @@ public interface DiscordSRV extends DiscordSRVApi {
ObjectMapper json();
// Lifecycle
CompletableFuture<Void> invokeEnable();
void runEnable();
List<ReloadResult> runReload(Set<ReloadFlag> flags, boolean silent);
CompletableFuture<Void> invokeDisable();
CompletableFuture<List<ReloadResult>> invokeReload(Set<ReloadFlag> flags, boolean silent);
default ExecuteCommand.AutoCompleteHelper autoCompleteHelper() {
return null;

View File

@ -51,13 +51,24 @@ public abstract class ServerDiscordSRV<B extends IBootstrap, C extends MainConfi
}
public final CompletableFuture<Void> invokeServerStarted() {
return invokeLifecycle(() -> {
return CompletableFuture.supplyAsync(() -> {
if (status().isShutdown()) {
// Already shutdown/shutting down, don't bother
return null;
}
try {
this.serverStarted();
} catch (Throwable t) {
if (status().isShutdown() && t instanceof NoClassDefFoundError) {
// Already shutdown, ignore errors for classes that already got unloaded
return null;
});
}
setStatus(Status.FAILED_TO_START);
disable();
logger().error("Failed to start", t);
}
return null;
}, scheduler().executorService());
}
@OverridingMethodsMustInvokeSuper

View File

@ -90,14 +90,14 @@ public class LifecycleManager {
if (!completableFuture.isDone()) {
return;
}
discordSRVSupplier.get().invokeEnable();
discordSRVSupplier.get().runEnable();
}
public void reload(DiscordSRV discordSRV) {
if (discordSRV == null) {
return;
}
discordSRV.invokeReload(DiscordSRVApi.ReloadFlag.DEFAULT_FLAGS, false);
discordSRV.runReload(DiscordSRVApi.ReloadFlag.DEFAULT_FLAGS, false);
}
public void disable(DiscordSRV discordSRV) {

View File

@ -27,7 +27,8 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.ChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.channels.base.ThreadConfig;
import com.discordsrv.common.config.main.generic.ThreadConfig;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import com.discordsrv.common.config.manager.MainConfigManager;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
@ -126,9 +127,9 @@ public class ChannelConfigHelper {
String channelName = entry.getKey();
BaseChannelConfig value = entry.getValue();
if (value instanceof IChannelConfig) {
IChannelConfig channelConfig = (IChannelConfig) value;
DestinationConfig destination = ((IChannelConfig) value).destination();
List<Long> channelIds = channelConfig.channelIds();
List<Long> channelIds = destination.channelIds;
if (channelIds != null) {
for (long channelId : channelIds) {
text.computeIfAbsent(channelId, key -> new LinkedHashMap<>())
@ -136,7 +137,7 @@ public class ChannelConfigHelper {
}
}
List<ThreadConfig> threads = channelConfig.threads();
List<ThreadConfig> threads = destination.threads;
if (threads != null) {
for (ThreadConfig threadConfig : threads) {
Pair<Long, String> pair = Pair.of(

View File

@ -18,18 +18,19 @@
package com.discordsrv.common.channel;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.ChannelLockingConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.module.type.AbstractModule;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.IPermissionHolder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.attribute.IPermissionContainer;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction;
import java.util.ArrayList;
@ -54,42 +55,57 @@ public class ChannelLockingModule extends AbstractModule<DiscordSRV> {
ChannelLockingConfig.Channels channels = shutdownConfig.channels;
ChannelLockingConfig.Threads threads = shutdownConfig.threads;
if (threads.unarchive) {
discordSRV.discordAPI().findOrCreateThreads(config, channelConfig, __ -> {}, new ArrayList<>(), false);
discordSRV.discordAPI()
.findOrCreateDestinations((BaseChannelConfig & IChannelConfig) config, threads.unarchive, true)
.whenComplete((destinations, t) -> {
if (channels.everyone || !channels.roleIds.isEmpty()) {
for (DiscordGuildMessageChannel destination : destinations) {
channelPermissions(channels, destination, true);
}
channelPermissions(channelConfig, channels, true);
}
});
});
}
@Override
public void disable() {
doForAllChannels((config, channelConfig) -> {
if (!(config instanceof IChannelConfig)) {
return;
}
ChannelLockingConfig shutdownConfig = config.channelLocking;
ChannelLockingConfig.Channels channels = shutdownConfig.channels;
ChannelLockingConfig.Threads threads = shutdownConfig.threads;
if (threads.archive) {
for (DiscordThreadChannel thread : discordSRV.discordAPI().findThreads(config, channelConfig)) {
thread.asJDA().getManager()
boolean archive = threads.archive;
boolean isChannels = channels.everyone || !channels.roleIds.isEmpty();
if (!threads.archive && !isChannels) {
return;
}
List<DiscordGuildMessageChannel> destinations = discordSRV.discordAPI()
.findDestinations((BaseChannelConfig & IChannelConfig) config, true);
for (DiscordGuildMessageChannel destination : destinations) {
if (archive && destination instanceof DiscordThreadChannel) {
((DiscordThreadChannel) destination).asJDA().getManager()
.setArchived(true)
.reason("DiscordSRV channel locking")
.queue();
}
if (isChannels) {
channelPermissions(channels, destination, false);
}
}
channelPermissions(channelConfig, channels, false);
});
}
private void channelPermissions(
IChannelConfig channelConfig,
ChannelLockingConfig.Channels shutdownConfig,
DiscordGuildMessageChannel channel,
boolean state
) {
JDA jda = discordSRV.jda();
if (jda == null) {
return;
}
boolean everyone = shutdownConfig.everyone;
List<Long> roleIds = shutdownConfig.roleIds;
if (!everyone && roleIds.isEmpty()) {
@ -107,33 +123,31 @@ public class ChannelLockingModule extends AbstractModule<DiscordSRV> {
permissions.add(Permission.MESSAGE_ADD_REACTION);
}
for (Long channelId : channelConfig.channelIds()) {
TextChannel channel = jda.getTextChannelById(channelId);
if (channel == null) {
continue;
GuildMessageChannel messageChannel = (GuildMessageChannel) channel.getAsJDAMessageChannel();
if (!(messageChannel instanceof IPermissionContainer)) {
return;
}
Guild guild = channel.getGuild();
if (!guild.getSelfMember().hasPermission(channel, Permission.MANAGE_PERMISSIONS)) {
Guild guild = messageChannel.getGuild();
if (!guild.getSelfMember().hasPermission(messageChannel, Permission.MANAGE_PERMISSIONS)) {
logger().error("Cannot change permissions of " + channel + ": lacking \"Manage Permissions\" permission");
continue;
return;
}
if (everyone) {
setPermission(channel, guild.getPublicRole(), permissions, state);
setPermission((IPermissionContainer) messageChannel, guild.getPublicRole(), permissions, state);
}
for (Long roleId : roleIds) {
Role role = channel.getGuild().getRoleById(roleId);
Role role = guild.getRoleById(roleId);
if (role == null) {
continue;
}
setPermission(channel, role, permissions, state);
}
setPermission((IPermissionContainer) messageChannel, role, permissions, state);
}
}
private void setPermission(TextChannel channel, IPermissionHolder holder, List<Permission> permissions, boolean state) {
private void setPermission(IPermissionContainer channel, IPermissionHolder holder, List<Permission> permissions, boolean state) {
PermissionOverrideAction action = channel.upsertPermissionOverride(holder);
if ((state ? action.getAllowedPermissions() : action.getDeniedPermissions()).containsAll(permissions)) {
// Already correct

View File

@ -20,7 +20,7 @@ package com.discordsrv.common.channel;
import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.ChannelUpdaterConfig;
import com.discordsrv.common.config.main.TimedUpdaterConfig;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.module.type.AbstractModule;
import net.dv8tion.jda.api.JDA;
@ -31,24 +31,27 @@ import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class ChannelUpdaterModule extends AbstractModule<DiscordSRV> {
public class TimedUpdaterModule extends AbstractModule<DiscordSRV> {
private final Set<ScheduledFuture<?>> futures = new LinkedHashSet<>();
private boolean firstReload = true;
public ChannelUpdaterModule(DiscordSRV discordSRV) {
public TimedUpdaterModule(DiscordSRV discordSRV) {
super(discordSRV, new NamedLogger(discordSRV, "CHANNEL_UPDATER"));
}
@Override
public boolean isEnabled() {
boolean any = false;
for (ChannelUpdaterConfig channelUpdater : discordSRV.config().channelUpdaters) {
if (!channelUpdater.channelIds.isEmpty()) {
TimedUpdaterConfig config = discordSRV.config().timedUpdater;
for (TimedUpdaterConfig.UpdaterConfig updaterConfig : config.getConfigs()) {
if (updaterConfig.any()) {
any = true;
break;
}
@ -65,18 +68,21 @@ public class ChannelUpdaterModule extends AbstractModule<DiscordSRV> {
futures.forEach(future -> future.cancel(false));
futures.clear();
for (ChannelUpdaterConfig config : discordSRV.config().channelUpdaters) {
TimedUpdaterConfig config = discordSRV.config().timedUpdater;
for (TimedUpdaterConfig.UpdaterConfig updaterConfig : config.getConfigs()) {
long time = Math.max(updaterConfig.timeSeconds(), updaterConfig.minimumSeconds());
futures.add(discordSRV.scheduler().runAtFixedRate(
() -> update(config),
firstReload ? 0 : config.timeMinutes,
config.timeMinutes,
TimeUnit.MINUTES
() -> update(updaterConfig),
firstReload ? 0 : time,
time,
TimeUnit.SECONDS
));
}
firstReload = false;
}
public void update(ChannelUpdaterConfig config) {
public void update(TimedUpdaterConfig.UpdaterConfig config) {
try {
// Wait a moment in case we're (re)connecting at the time
discordSRV.waitForStatus(DiscordSRV.Status.CONNECTED, 15, TimeUnit.SECONDS);
@ -89,9 +95,26 @@ public class ChannelUpdaterModule extends AbstractModule<DiscordSRV> {
return;
}
String topicFormat = config.topicFormat;
String nameFormat = config.nameFormat;
if (config instanceof TimedUpdaterConfig.VoiceChannelConfig) {
updateChannel(
jda,
((TimedUpdaterConfig.VoiceChannelConfig) config).channelIds,
((TimedUpdaterConfig.VoiceChannelConfig) config).nameFormat,
null
);
} else if (config instanceof TimedUpdaterConfig.TextChannelConfig) {
updateChannel(
jda,
((TimedUpdaterConfig.TextChannelConfig) config).channelIds,
((TimedUpdaterConfig.TextChannelConfig) config).nameFormat,
((TimedUpdaterConfig.TextChannelConfig) config).topicFormat
);
}
}
private void updateChannel(JDA jda, List<Long> channelIds, String nameFormat, String topicFormat) {
if (topicFormat != null) {
topicFormat = discordSRV.placeholderService().replacePlaceholders(topicFormat);
}
@ -99,7 +122,7 @@ public class ChannelUpdaterModule extends AbstractModule<DiscordSRV> {
nameFormat = discordSRV.placeholderService().replacePlaceholders(nameFormat);
}
for (Long channelId : config.channelIds) {
for (Long channelId : channelIds) {
GuildChannel channel = jda.getGuildChannelById(channelId);
if (channel == null) {
continue;

View File

@ -5,12 +5,23 @@ import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.sender.ICommandSender;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.util.Collection;
import java.util.regex.Pattern;
public class GameCommandExecution implements CommandExecution {
private static final TextReplacementConfig URL_REPLACEMENT = TextReplacementConfig.builder()
.match(Pattern.compile("^https?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"))
.replacement((matchResult, builder) -> {
String url = matchResult.group();
return builder.clickEvent(ClickEvent.openUrl(url));
})
.build();
private final DiscordSRV discordSRV;
private final ICommandSender sender;
private final GameCommandArguments arguments;
@ -37,7 +48,7 @@ public class GameCommandExecution implements CommandExecution {
if (!extra.isEmpty()) {
builder.hoverEvent(HoverEvent.showText(render(extra)));
}
sender.sendMessage(builder);
sender.sendMessage(builder.build().replaceText(URL_REPLACEMENT));
}
private TextComponent.Builder render(Collection<Text> texts) {

View File

@ -23,7 +23,7 @@ import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.commands.DiscordSRVGameCommand;
import com.discordsrv.common.command.game.commands.subcommand.LinkCommand;
import com.discordsrv.common.command.game.handler.ICommandHandler;
import com.discordsrv.common.config.main.CommandConfig;
import com.discordsrv.common.config.main.GameCommandConfig;
import com.discordsrv.common.module.type.AbstractModule;
import java.util.HashSet;
@ -48,7 +48,7 @@ public class GameCommandModule extends AbstractModule<DiscordSRV> {
@Override
public void reloadNoResult() {
CommandConfig config = discordSRV.config().command;
GameCommandConfig config = discordSRV.config().gameCommand;
if (config == null) {
return;
}

View File

@ -68,7 +68,7 @@ public class DiscordSRVGameCommand implements GameCommandExecutor {
@Override
public void execute(ICommandSender sender, GameCommandArguments arguments) {
MinecraftComponent component = discordSRV.componentFactory()
.textBuilder(discordSRV.config().command.discordFormat)
.textBuilder(discordSRV.config().gameCommand.discordFormat)
.addContext(sender)
.applyPlaceholderService()
.build();

View File

@ -19,9 +19,8 @@
package com.discordsrv.common.command.game.commands.subcommand;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommand;
@ -32,7 +31,6 @@ import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@ -98,11 +96,16 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
@Override
public void execute(ICommandSender sender, GameCommandArguments arguments) {
doExecute(sender, arguments);
}
@SuppressWarnings("unchecked") // Wacky generics
private <CC extends BaseChannelConfig & IChannelConfig> void doExecute(ICommandSender sender, GameCommandArguments arguments) {
String channel = arguments.getString("channel");
String content = arguments.getString("content");
List<DiscordMessageChannel> channels = new ArrayList<>();
List<CompletableFuture<DiscordThreadChannel>> futures = new ArrayList<>();
CompletableFuture<List<DiscordGuildMessageChannel>> future = null;
try {
long id = Long.parseUnsignedLong(channel);
@ -112,24 +115,17 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
}
} catch (IllegalArgumentException ignored) {
BaseChannelConfig channelConfig = discordSRV.channelConfig().resolve(null, channel);
IChannelConfig config = channelConfig instanceof IChannelConfig ? (IChannelConfig) channelConfig : null;
CC config = channelConfig instanceof IChannelConfig ? (CC) channelConfig : null;
if (config != null) {
for (Long channelId : config.channelIds()) {
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
if (textChannel != null) {
channels.add(textChannel);
future = discordSRV.discordAPI().findOrCreateDestinations(config, true, false);
}
}
discordSRV.discordAPI().findOrCreateThreads(channelConfig, config, channels::add, futures, false);
}
}
if (!futures.isEmpty()) {
CompletableFutureUtil.combine(futures).whenComplete((v, t) -> execute(sender, content, channel, channels));
if (future != null) {
future.whenComplete((messageChannels, t) -> doBroadcast(sender, content, channel, messageChannels));
} else {
execute(sender, content, channel, channels);
doBroadcast(sender, content, channel, channels);
}
}
@ -145,7 +141,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
.collect(Collectors.toList());
}
private void execute(ICommandSender sender, String content, String channel, List<DiscordMessageChannel> channels) {
private void doBroadcast(ICommandSender sender, String content, String channel, List<? extends DiscordMessageChannel> channels) {
if (channels.isEmpty()) {
sender.sendMessage(
Component.text()

View File

@ -23,6 +23,7 @@ import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.abstraction.GameCommandExecutor;
import com.discordsrv.common.command.game.sender.ICommandSender;
import net.kyori.adventure.text.Component;
public class LinkCommand implements GameCommandExecutor {
@ -46,6 +47,6 @@ public class LinkCommand implements GameCommandExecutor {
@Override
public void execute(ICommandSender sender, GameCommandArguments arguments) {
sender.sendMessage(Component.text("Not currently implemented")); // TODO
}
}

View File

@ -79,20 +79,16 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester
return;
}
discordSRV.invokeReload(flags, false).whenComplete((results, t) -> {
if (t != null) {
discordSRV.logger().error("Failed to reload", t);
List<DiscordSRVApi.ReloadResult> results = discordSRV.runReload(flags, false);
for (DiscordSRV.ReloadResult result : results) {
String res = result.name();
if (res.equals(ReloadResults.FAILED.name())) {
sender.sendMessage(
Component.text()
.append(Component.text("Reload failed.", NamedTextColor.DARK_RED, TextDecoration.BOLD))
.append(Component.text("Please check the server console/log for more details."))
);
return;
}
for (DiscordSRV.ReloadResult result : results) {
String res = result.name();
if (res.equals(ReloadResults.SECURITY_FAILED.name())) {
} else if (res.equals(ReloadResults.SECURITY_FAILED.name())) {
sender.sendMessage(Component.text(
"DiscordSRV is disabled due to a security check failure. "
+ "Please check console for more details", NamedTextColor.DARK_RED));
@ -124,7 +120,6 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester
);
}
}
});
}
private Set<DiscordSRV.ReloadFlag> getFlagsFromArguments(ICommandSender sender, GameCommandArguments arguments, AtomicBoolean dangerousFlags) {

View File

@ -3,6 +3,7 @@ package com.discordsrv.common.command.game.commands.subcommand.reload;
import com.discordsrv.api.DiscordSRVApi;
public enum ReloadResults implements DiscordSRVApi.ReloadResult {
FAILED,
SUCCESS,
SECURITY_FAILED,
STORAGE_CONNECTION_FAILED,

View File

@ -23,7 +23,6 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig;
@ -50,23 +49,23 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
}
public static void runInContext(
DiscordChatMessageProcessingEvent event,
DiscordGuild guild,
DiscordToMinecraftChatConfig config,
Runnable runnable
) {
getWithContext(event, config, () -> {
getWithContext(guild, config, () -> {
runnable.run();
return null;
});
}
public static <T> T getWithContext(
DiscordChatMessageProcessingEvent event,
DiscordGuild guild,
DiscordToMinecraftChatConfig config,
Supplier<T> supplier
) {
Context oldValue = CONTEXT.get();
CONTEXT.set(new Context(event, config));
CONTEXT.set(new Context(guild, config));
T output = supplier.get();
CONTEXT.set(oldValue);
return output;
@ -135,9 +134,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
public @NotNull Component appendUserMention(@NotNull Component component, @NotNull String id) {
Context context = CONTEXT.get();
DiscordToMinecraftChatConfig.Mentions.Format format = context != null ? context.config.mentions.user : null;
DiscordGuild guild = context != null
? discordSRV.discordAPI().getGuildById(context.event.getGuild().getId())
: null;
DiscordGuild guild = context != null ? context.guild : null;
if (format == null || guild == null) {
return component.append(Component.text("<@" + id + ">"));
}
@ -186,11 +183,11 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
private static class Context {
private final DiscordChatMessageProcessingEvent event;
private final DiscordGuild guild;
private final DiscordToMinecraftChatConfig config;
public Context(DiscordChatMessageProcessingEvent event, DiscordToMinecraftChatConfig config) {
this.event = event;
public Context(DiscordGuild guild, DiscordToMinecraftChatConfig config) {
this.guild = guild;
this.config = config;
}
}

View File

@ -35,7 +35,7 @@ public class UpdateConfig {
public boolean notificationInGame = true;
@Setting(value = "enable-first-party-api-for-notifications")
@Comment("Weather the DiscordSRV download API should be used for update checks\n"
@Comment("Whether the DiscordSRV download API should be used for update checks\n"
+ "Requires a connection to: download.discordsrv.com")
public boolean firstPartyNotification = true;
@ -49,7 +49,7 @@ public class UpdateConfig {
public static class GitHub {
@Setting(value = "enabled")
@Comment("Weather the GitHub API should be used for update checks\n"
@Comment("Whether the GitHub API should be used for update checks\n"
+ "This will be the secondary API if both first party and GitHub sources are enabled\n"
+ "Requires a connection to: api.github.com")
public boolean enabled = true;

View File

@ -0,0 +1,5 @@
package com.discordsrv.common.config.documentation;
public class DocumentationURLs {
public static final String CREATE_TOKEN = "https://docs.discordsrv.com/installation/initial-setup/#setting-up-the-bot";
}

View File

@ -0,0 +1,19 @@
package com.discordsrv.common.config.main;
import com.discordsrv.common.config.annotation.Untranslated;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class AvatarProviderConfig {
@Comment("Whether to let DiscordSRV decide an appropriate avatar URL automatically\n" +
"This will result in appropriate head renders being provided for Bedrock players (when using Floodgate) and Offline Mode players (via username).")
public boolean autoDecideAvatarUrl = true;
@Untranslated(Untranslated.Type.VALUE)
@Comment("The template for URLs of player avatars\n" +
"This will be used for offical Java players only if auto-decide-avatar-url is set to true\n" +
"This will be used ALWAYS if auto-decide-avatar-url is set to false")
public String avatarUrlTemplate = "https://crafatar.com/avatars/%player_uuid_nodashes%.png?size=128&overlay#%player_texture%";
}

View File

@ -1,43 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.ArrayList;
import java.util.List;
@ConfigSerializable
public class ChannelUpdaterConfig {
@Comment("The channel IDs.\n"
+ "The bot will need the \"View Channel\" and \"Manage Channels\" permissions for the provided channels, "
+ "additionally \"Connect\" is required for voice channels")
public List<Long> channelIds = new ArrayList<>();
@Comment("If this is blank, the name will not be updated")
public String nameFormat = "";
@Comment("If this is blank, the topic will not be updated. Unavailable for voice channels")
public String topicFormat = "";
@Comment("The time between updates in minutes. The minimum time is 10 minutes.")
public int timeMinutes = 10;
}

View File

@ -0,0 +1,19 @@
package com.discordsrv.common.config.main;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ConfigSerializable
public class DebugConfig {
@Comment("If debug messages should be logged into the config")
public boolean logToConsole = false;
@Comment("Additional levels to log\nExample value: {\"AWARD_LISTENER\":[\"TRACE\"]}")
public Map<String, List<String>> additionalLevels = new HashMap<>();
}

View File

@ -27,12 +27,12 @@ public class DiscordInviteConfig {
@Comment("Manually enter a invite url here, if this isn't set this is ignored and the options below will take effect")
public String inviteUrl = "";
@Comment("If the bot is only in one Discord server, it will attempt to get it's vanity url")
@Comment("If the bot is only in one Discord server, it will attempt to get its vanity url")
public boolean attemptToUseVanityUrl = true;
@Comment("If the bot is only in one Discord server, it will attempt to automatically create a invite for it.\n"
+ "The bot will only attempt to do so if it has permission to \"Create Invite\"\n"
+ "The server must also have a rules channel (available for community servers) or default channel (automatically determined by Discord)")
public boolean autoCreateInvite = true;
public boolean autoCreateInvite = false;
}

View File

@ -22,7 +22,7 @@ import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class CommandConfig {
public class GameCommandConfig {
@Comment("If the /discord command should be set by DiscordSRV")
public boolean useDiscordCommand = true;
@ -30,6 +30,6 @@ public class CommandConfig {
@Comment("If /link should be used as a alias for /discord link")
public boolean useLinkAlias = false;
@Comment("Discord command format, player placeholders may be used")
public String discordFormat = "[click:open_url:%discord_invite%]&b&lClick here &r&ato join our Discord server!";
@Comment("The Discord command response format (/discord), player placeholders may be used")
public String discordFormat = "[click:open_url:%discord_invite%][color:aqua][bold:on]Click here [color][bold][color:green]to join our Discord server!";
}

View File

@ -21,6 +21,8 @@ package com.discordsrv.common.config.main;
import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.common.config.Config;
import com.discordsrv.common.config.annotation.DefaultOnly;
import com.discordsrv.common.config.annotation.Order;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.ChannelConfig;
import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
@ -34,6 +36,14 @@ public abstract class MainConfig implements Config {
public static final String FILE_NAME = "config.yaml";
public static final String HEADER = String.join("\n", Arrays.asList(
"Welcome to the DiscordSRV configuration file",
"",
"Looking for the \"BotToken\" option? It has been moved into the " + ConnectionConfig.FILE_NAME,
"Need help with the format for Minecraft messages? https://github.com/Vankka/EnhancedLegacyText/wiki/Format",
"Need help with Discord markdown? https://support.discord.com/hc/en-us/articles/210298617"
));
@Override
public final String getFileName() {
return FILE_NAME;
@ -48,6 +58,12 @@ public abstract class MainConfig implements Config {
}
@DefaultOnly(ChannelConfig.DEFAULT_KEY)
@Comment("Channels configuration\n\n"
+ "This is where everything related to in-game chat channels is configured.\n"
+ "The key of this option is the in-game channel name (the default keys are \"global\" and \"default\")\n"
+ "channel-ids and threads can be configured for all channels except \"default\"\n"
+ "\"default\" is a special section which has the default values for all channels unless they are specified (overridden) under the channel's own section\n"
+ "So if you don't specify a certain option under a channel's own section, the option will take its value from the \"default\" section")
public Map<String, BaseChannelConfig> channels = new LinkedHashMap<String, BaseChannelConfig>() {{
put(GameChannel.DEFAULT_NAME, createDefaultChannel());
put(ChannelConfig.DEFAULT_KEY, createDefaultBaseChannel());
@ -55,18 +71,27 @@ public abstract class MainConfig implements Config {
public LinkedAccountConfig linkedAccounts = new LinkedAccountConfig();
public MemberCachingConfig memberCaching = new MemberCachingConfig();
public List<ChannelUpdaterConfig> channelUpdaters = new ArrayList<>(Collections.singletonList(new ChannelUpdaterConfig()));
public TimedUpdaterConfig timedUpdater = new TimedUpdaterConfig();
@Comment("Configuration options for group-role synchronization")
public GroupSyncConfig groupSync = new GroupSyncConfig();
@Comment("Command configuration")
public CommandConfig command = new CommandConfig();
@Comment("In-game command configuration")
public GameCommandConfig gameCommand = new GameCommandConfig();
@Comment("Configuration for the %discord_invite% placeholder. The below options will be attempted in the order they are in")
public DiscordInviteConfig invite = new DiscordInviteConfig();
@Order(10) // To go below required linking config @ 5
@Comment("Configuration for the %player_avatar_url% placeholder")
public AvatarProviderConfig avatarProvider = new AvatarProviderConfig();
public abstract PluginIntegrationConfig integrations();
@Order(1000)
public MemberCachingConfig memberCaching = new MemberCachingConfig();
@Order(5000)
@Comment("Options for diagnosing DiscordSRV, you do not need to touch these options during normal operation")
public DebugConfig debug = new DebugConfig();
}

View File

@ -34,7 +34,7 @@ public class MemberCachingConfig {
public boolean all = false;
@Comment("If members should be cached at startup, this requires the \"Server Members Intent\"")
public boolean chunk = false;
public boolean chunk = true;
@Comment("Filter for which servers should be cached at startup")
public GuildFilter chunkingServerFilter = new GuildFilter();

View File

@ -0,0 +1,111 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ConfigSerializable
public class TimedUpdaterConfig {
public List<VoiceChannelConfig> voiceChannels = new ArrayList<>(Collections.singletonList(new VoiceChannelConfig()));
public List<TextChannelConfig> textChannels = new ArrayList<>(Collections.singletonList(new TextChannelConfig()));
public List<UpdaterConfig> getConfigs() {
List<UpdaterConfig> configs = new ArrayList<>();
configs.addAll(voiceChannels);
configs.addAll(textChannels);
return configs;
}
public interface UpdaterConfig {
boolean any();
long timeSeconds();
long minimumSeconds();
}
public static class VoiceChannelConfig implements UpdaterConfig {
@Comment("The channel IDs.\n"
+ "The bot will need the \"View Channel\", \"Manage Channels\" and \"Connection\" permissions for the provided channels")
public List<Long> channelIds = new ArrayList<>();
@Comment("The format for the channel name(s), placeholders are supported.")
public String nameFormat = "";
@Comment("The time between updates in minutes. The minimum time is 10 minutes.")
public int timeMinutes = 10;
@Override
public boolean any() {
return !channelIds.isEmpty();
}
@Override
public long timeSeconds() {
return TimeUnit.MINUTES.toSeconds(timeMinutes);
}
@Override
public long minimumSeconds() {
return TimeUnit.MINUTES.toSeconds(10);
}
}
public static class TextChannelConfig implements UpdaterConfig {
@Comment("The channel IDs.\n"
+ "The bot will need the \"View Channel\" and \"Manage Channels\" permissions for the provided channels")
public List<Long> channelIds = new ArrayList<>();
@Comment("The format for the channel name(s), placeholders are supported.\n"
+ "If this is blank, the name will not be updated")
public String nameFormat = "";
@Comment("The format for the channel topic(s), placeholders are supported.\n"
+ "If this is blank, the topic will not be updated")
public String topicFormat = "";
@Comment("The time between updates in minutes. The minimum time is 10 minutes.")
public int timeMinutes = 10;
@Override
public boolean any() {
return !channelIds.isEmpty();
}
@Override
public long timeSeconds() {
return TimeUnit.MINUTES.toSeconds(timeMinutes);
}
@Override
public long minimumSeconds() {
return TimeUnit.MINUTES.toSeconds(10);
}
}
}

View File

@ -39,8 +39,11 @@ public class ChannelLockingConfig {
@Comment("Role ids for roles that should have the permissions taken while the server is offline")
public List<Long> roleIds = new ArrayList<>();
@Comment("If the \"View Channel\" permission should be taken from the specified roles")
public boolean read = false;
@Comment("If the \"Send Messages\" permission should be taken from the specified roles")
public boolean write = true;
@Comment("If the \"Add Reactions\" permission should be taken from the specified roles")
public boolean addReactions = true;
}
@ -51,7 +54,7 @@ public class ChannelLockingConfig {
@Comment("If the configured threads should be archived while the server is shutdown")
public boolean archive = true;
@Comment("If the bot will attempt to unarchive threads rather than make new threads")
@Comment("If the bot should attempt to unarchive threads rather than make new threads")
public boolean unarchive = true;
}

View File

@ -19,7 +19,7 @@
package com.discordsrv.common.config.main.channels;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -30,27 +30,30 @@ import java.util.regex.Pattern;
@ConfigSerializable
public class DiscordToMinecraftChatConfig {
@Comment("Is Discord to Minecraft chat forwarding enabled")
public boolean enabled = true;
@Comment("The Discord to Minecraft message format for regular users and bots")
@Untranslated(Untranslated.Type.VALUE)
public String format = "[&#5865F2Discord&r] [hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles:', '|text:'&7&oNone'%]%user_color%%user_effective_name%&r » %message%%message_attachments%";
public String format = "[[color:#5865F2]Discord[color]] [hover:show_text:Username: @%user_tag%\nRoles: %user_roles:', '|text:'[color:gray][italics:on]None[color][italics]'%]%user_color%%user_effective_server_name%[color][hover]%message_reply% » %message%%message_attachments%";
@Comment("The Discord to Minecraft message format for webhook messages (if enabled)")
@Untranslated(Untranslated.Type.VALUE)
public String webhookFormat = "[&#5865F2Discord&r] [hover:show_text:Webhook message]%user_name%&r » %message%%message_attachments%";
public String webhookFormat = "[[color:#5865F2]Discord[color]] [hover:show_text:Bot message]%user_effective_name%[hover] » %message%%message_attachments%";
@Comment("Attachment format")
@Comment("Format for a single attachment in the %message_attachments% placeholder")
@Untranslated(Untranslated.Type.VALUE)
public String attachmentFormat = " [hover:show_text:Open %file_name% in browser][click:open_url:%file_url%]&a[&f%file_name%&a]&r";
public String attachmentFormat = " [hover:show_text:Open %file_name% in browser][click:open_url:%file_url%][color:green][[color:white]%file_name%[color:green]][color][click][hover]";
@Comment("Format for the %message_reply% placeholder, when the message is a reply to another message")
@Untranslated(Untranslated.Type.VALUE)
public String replyFormat = " [hover:show_text:%message%][click:open_url:%message_jump_url%]replying to %user_color|text:''%%user_effective_server_name|user_effective_name%[color][click][hover]";
// TODO: more info on regex pairs (String#replaceAll)
@Comment("Regex filters for Discord message contents (this is the %message% part of the \"format\" option)")
@Untranslated(Untranslated.Type.VALUE)
public Map<Pattern, String> contentRegexFilters = new LinkedHashMap<>();
@Comment("Users, bots and webhooks to ignore")
@Comment("Users, bots, roles and webhooks to ignore")
public DiscordIgnoresConfig ignores = new DiscordIgnoresConfig();
@Comment("The representations of Discord mentions in-game")
@ -59,11 +62,11 @@ public class DiscordToMinecraftChatConfig {
@ConfigSerializable
public static class Mentions {
public Format role = new Format("&#5865f2@%role_name%", "&#5865f2@deleted-role");
public Format channel = new Format("[hover:show_text:Click to go to channel][click:open_url:%channel_jump_url%]&#5865f2#%channel_name%", "&#5865f2#deleted-channel");
public Format user = new Format("[hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles:', '|text:'&7&oNone'%]&#5865f2@%user_effective_name|user_name%", "&#5865f2@Unknown user");
public Format role = new Format("%role_color%@%role_name%", "[color:#5865F2]@deleted-role");
public Format channel = new Format("[hover:show_text:Click to go to channel][click:open_url:%channel_jump_url%][color:#5865F2]#%channel_name%", "[color:#5865F2]#Unknown");
public Format user = new Format("[hover:show_text:Username: @%user_tag%\nRoles: %user_roles:', '|text:'[color:gray][italics:on]None[color][italics]'%][color:#5865F2]@%user_effective_server_name|user_effective_name%", "[color:#5865F2]@Unknown user");
public String messageUrl = "[hover:show_text:Click to go to message][click:open_url:%jump_url%]&#5865f2#%channel_name% > ...";
public String messageUrl = "[hover:show_text:Click to go to message][click:open_url:%jump_url%][color:#5865F2]#%channel_name% > ...";
@ConfigSerializable
public static class Format {

View File

@ -22,6 +22,7 @@ import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.event.events.message.receive.game.JoinMessageReceiveEvent;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;

View File

@ -21,6 +21,7 @@ package com.discordsrv.common.config.main.channels;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable

View File

@ -21,6 +21,7 @@ package com.discordsrv.common.config.main.channels;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.DefaultOnly;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -31,7 +32,6 @@ import java.util.regex.Pattern;
@ConfigSerializable
public class MinecraftToDiscordChatConfig implements IMessageConfig {
@Comment("Is Minecraft to Discord chat forwarding enabled")
public boolean enabled = true;
@Untranslated(Untranslated.Type.VALUE)

View File

@ -18,7 +18,7 @@
package com.discordsrv.common.config.main.channels;
import com.discordsrv.common.config.main.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -27,16 +27,18 @@ public class MirroringConfig {
public boolean enabled = true;
@Comment("Users, bots and webhooks to ignore when mirroring")
@Comment("Users, bots, roles and webhooks to ignore when mirroring")
public DiscordIgnoresConfig ignores = new DiscordIgnoresConfig();
@Comment("The format of the username of mirrored messages\n"
+ "It's recommended to include some special character if in-game messages use webhooks,\n"
+ "in order to prevent Discord users and in-game players with the same name being grouped together")
public String usernameFormat = "%user_effective_name% \uD83D\uDD03";
public String usernameFormat = "%user_effective_server_name|user_effective_name% \uD83D\uDD03";
@Comment("Content to append to the beginning of a message if the message is replying to another")
public String replyFormat = "[In reply to %user_effective_name|user_name%](%message_jump_url%)\n";
@Comment("The format when a message is a reply.\n"
+ "%message% will be replaced with the message content\n"
+ "%message_jump_url% will be replaced with the url to the replied message in the channel the message is sent in")
public String replyFormat = "[In reply to %user_effective_server_name|user_effective_name%](%message_jump_url%)\n%message%";
@Comment("Attachment related options")
public AttachmentConfig attachments = new AttachmentConfig();

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.config.main.channels;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.config.main.channels;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable

View File

@ -39,10 +39,6 @@ public class BaseChannelConfig {
@Order(2)
public LeaveMessageConfig leaveMessages = new LeaveMessageConfig();
@Untranslated(Untranslated.Type.VALUE)
@Order(10)
public String avatarUrlProvider = "https://heads.discordsrv.com/head.png?texture=%texture%&uuid=%uuid%&name=%username%&overlay";
@Order(20)
public StartMessageConfig startMessage = new StartMessageConfig();
@Order(20)

View File

@ -18,12 +18,10 @@
package com.discordsrv.common.config.main.channels.base;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.List;
@ConfigSerializable
public class ChannelConfig extends BaseChannelConfig implements IChannelConfig {
@ -31,22 +29,11 @@ public class ChannelConfig extends BaseChannelConfig implements IChannelConfig {
initialize();
}
@Setting(CHANNEL_IDS_OPTION_NAME)
@Comment(CHANNEL_IDS_COMMENT)
public List<Long> channelIds = CHANNEL_IDS_VALUE;
@Setting(nodeFromParent = true)
public DestinationConfig destination = new DestinationConfig();
@Override
public List<Long> channelIds() {
return channelIds;
public DestinationConfig destination() {
return destination;
}
@Setting(THREADS_OPTION_NAME)
@Comment(THREADS_COMMENT)
public List<ThreadConfig> threads = THREADS_VALUE;
@Override
public List<ThreadConfig> threads() {
return threads;
}
}

View File

@ -18,38 +18,22 @@
package com.discordsrv.common.config.main.channels.base;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public interface IChannelConfig {
String DEFAULT_KEY = "default";
String CHANNEL_IDS_OPTION_NAME = "channel-ids";
String CHANNEL_IDS_COMMENT = "The channels this in-game channel will forward to in Discord";
List<Long> CHANNEL_IDS_VALUE = new ArrayList<>();
List<Long> channelIds();
String THREADS_OPTION_NAME = "threads";
String THREADS_COMMENT = "The threads that this in-game channel will forward to in Discord (this can be used instead of or with the channel-ids option)";
List<ThreadConfig> THREADS_VALUE = new ArrayList<>(Collections.singletonList(new ThreadConfig()));
List<String> VALUES = Arrays.asList(CHANNEL_IDS_OPTION_NAME, THREADS_OPTION_NAME);
List<ThreadConfig> threads();
DestinationConfig destination();
default void initialize() {
// Clear everything besides channelIds by default (these will be filled back in by Configurate if they are in the config itself)
@ -61,11 +45,6 @@ public interface IChannelConfig {
continue;
}
Setting setting = field.getAnnotation(Setting.class);
if (setting != null && VALUES.contains(setting.value())) {
continue;
}
try {
field.set(this, null);
} catch (IllegalAccessException ignored) {}

View File

@ -20,7 +20,7 @@ package com.discordsrv.common.config.main.channels.base.proxy;
import com.discordsrv.common.config.annotation.Order;
import com.discordsrv.common.config.main.channels.JoinMessageConfig;
import com.discordsrv.common.config.main.channels.ServerSwitchMessageConfig;
import com.discordsrv.common.config.main.channels.proxy.ServerSwitchMessageConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;

View File

@ -19,13 +19,10 @@
package com.discordsrv.common.config.main.channels.base.proxy;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.channels.base.ThreadConfig;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.List;
@ConfigSerializable
public class ProxyChannelConfig extends ProxyBaseChannelConfig implements IChannelConfig {
@ -33,21 +30,11 @@ public class ProxyChannelConfig extends ProxyBaseChannelConfig implements IChann
initialize();
}
@Setting(CHANNEL_IDS_OPTION_NAME)
@Comment(CHANNEL_IDS_COMMENT)
public List<Long> channelIds = CHANNEL_IDS_VALUE;
@Setting(nodeFromParent = true)
public DestinationConfig destination = new DestinationConfig();
@Override
public List<Long> channelIds() {
return channelIds;
}
@Setting(THREADS_OPTION_NAME)
@Comment(THREADS_COMMENT)
public List<ThreadConfig> threads = THREADS_VALUE;
@Override
public List<ThreadConfig> threads() {
return threads;
public DestinationConfig destination() {
return destination;
}
}

View File

@ -19,10 +19,12 @@
package com.discordsrv.common.config.main.channels.base.server;
import com.discordsrv.common.config.annotation.Order;
import com.discordsrv.common.config.main.channels.AwardMessageConfig;
import com.discordsrv.common.config.main.channels.DeathMessageConfig;
import com.discordsrv.common.config.main.channels.server.AwardMessageConfig;
import com.discordsrv.common.config.main.channels.server.DeathMessageConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.server.ServerJoinMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class ServerBaseChannelConfig extends BaseChannelConfig {
@ -31,6 +33,7 @@ public class ServerBaseChannelConfig extends BaseChannelConfig {
public ServerJoinMessageConfig joinMessages = new ServerJoinMessageConfig();
@Order(3)
@Comment("Advancement/Achievement message configuration")
public AwardMessageConfig awardMessages = new AwardMessageConfig();
@Order(3)

View File

@ -19,13 +19,10 @@
package com.discordsrv.common.config.main.channels.base.server;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.channels.base.ThreadConfig;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.List;
@ConfigSerializable
public class ServerChannelConfig extends ServerBaseChannelConfig implements IChannelConfig {
@ -33,21 +30,11 @@ public class ServerChannelConfig extends ServerBaseChannelConfig implements ICha
initialize();
}
@Setting(CHANNEL_IDS_OPTION_NAME)
@Comment(CHANNEL_IDS_COMMENT)
public List<Long> channelIds = CHANNEL_IDS_VALUE;
@Setting(nodeFromParent = true)
public DestinationConfig destination = new DestinationConfig();
@Override
public List<Long> channelIds() {
return channelIds;
}
@Setting(THREADS_OPTION_NAME)
@Comment(THREADS_COMMENT)
public List<ThreadConfig> threads = THREADS_VALUE;
@Override
public List<ThreadConfig> threads() {
return threads;
public DestinationConfig destination() {
return destination;
}
}

View File

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
package com.discordsrv.common.config.main.channels.proxy;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable

View File

@ -16,18 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
package com.discordsrv.common.config.main.channels.server;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class AwardMessageConfig implements IMessageConfig {
@Comment("Enable achievement/advancement message forwarding")
public boolean enabled = true;
@Untranslated(Untranslated.Type.VALUE)
@ -35,7 +34,7 @@ public class AwardMessageConfig implements IMessageConfig {
.addEmbed(
DiscordMessageEmbed.builder()
.setAuthor(
"%award_title|text:'{player_name} made the achievement {award_name}'%",
"%award_title|text:'{player_name} made the advancement {award_name}'%",
null,
"%player_avatar_url%"
)

View File

@ -16,18 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
package com.discordsrv.common.config.main.channels.server;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class DeathMessageConfig implements IMessageConfig {
@Comment("Enable death message forwarding")
public boolean enabled = true;
@Untranslated(Untranslated.Type.VALUE)

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels.base.server;
package com.discordsrv.common.config.main.channels.server;
import com.discordsrv.common.config.annotation.Order;
import com.discordsrv.common.config.main.channels.JoinMessageConfig;

View File

@ -0,0 +1,22 @@
package com.discordsrv.common.config.main.generic;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ConfigSerializable
public class DestinationConfig {
@Setting("channel-ids")
@Comment("The channels this in-game channel will forward to in Discord")
public List<Long> channelIds = new ArrayList<>();
@Setting("threads")
@Comment("The threads that this in-game channel will forward to in Discord (this can be used instead of or with the channel-ids option)")
public List<ThreadConfig> threads = new ArrayList<>(Collections.singletonList(new ThreadConfig()));
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main;
package com.discordsrv.common.config.main.generic;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
@ -31,7 +31,7 @@ import java.util.Optional;
public class DiscordIgnoresConfig {
@Comment("User, bot and webhook ids to ignore")
public IDs usersAndWebhookIds = new IDs();
public IDs userBotAndWebhookIds = new IDs();
@Comment("Role ids for users and bots to ignore")
public IDs roleIds = new IDs();
@ -58,7 +58,7 @@ public class DiscordIgnoresConfig {
return true;
}
DiscordIgnoresConfig.IDs users = usersAndWebhookIds;
DiscordIgnoresConfig.IDs users = userBotAndWebhookIds;
if (users != null && users.ids.contains(author.getId()) != users.whitelist) {
return true;
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
package com.discordsrv.common.config.main.generic;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels.base;
package com.discordsrv.common.config.main.generic;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;

View File

@ -30,7 +30,7 @@ public class LinkedAccountConfig {
@Comment("The linked account provider\n"
+ "\n"
+ " - auto: Defaults to using \"minecraftauth\" (if the " + ConnectionConfig.FILE_NAME + " permits it) otherwise \"storage\"\n"
+ " - auto: Uses \"minecraftauth\" if the " + ConnectionConfig.FILE_NAME + " permits it and the server is in online mode, otherwise \"storage\"\n"
+ " - minecraftauth: Uses minecraftauth.me as the linked account provider\n"
+ " - storage: Use the configured database for linked accounts")
public String provider = "auto";

View File

@ -20,12 +20,10 @@ package com.discordsrv.common.config.main.linking;
import com.discordsrv.common.config.annotation.Order;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public abstract class RequiredLinkingConfig {
@Comment("If required linking is enabled")
@Order(-10)
public boolean enabled = false;
}

View File

@ -19,6 +19,7 @@
package com.discordsrv.common.config.main.linking;
import com.discordsrv.common.config.annotation.DefaultOnly;
import com.discordsrv.common.config.connection.ConnectionConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
@ -43,7 +44,8 @@ public class RequirementsConfig {
+ "DiscordServer(Server ID)\n"
+ "DiscordBoosting(Server ID)\n"
+ "DiscordRole(Role ID)\n"
+ "The following are available if you're using MinecraftAuth.me for linked accounts:\n"
+ "\n"
+ "The following are available if you're using MinecraftAuth.me for linked accounts and a MinecraftAuth.me token is specified in the " + ConnectionConfig.FILE_NAME + ":\n"
+ "PatreonSubscriber() or PatreonSubscriber(Tier Title)\n"
+ "GlimpseSubscriber() or GlimpseSubscriber(Level Name)\n"
+ "TwitchFollower()\n"

View File

@ -22,6 +22,8 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider;
import com.discordsrv.common.config.manager.manager.TranslatedConfigManager;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
public abstract class MainConfigManager<C extends MainConfig>
@ -32,6 +34,12 @@ public abstract class MainConfigManager<C extends MainConfig>
super(discordSRV);
}
@Override
public ConfigurationOptions defaultOptions(ObjectMapper.Factory objectMapper) {
return super.defaultOptions(objectMapper)
.header(MainConfig.HEADER);
}
@Override
protected String fileName() {
return MainConfig.FILE_NAME;

View File

@ -60,6 +60,8 @@ import java.util.regex.Pattern;
public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>>
implements ConfigManager<T>, ConfigLoaderProvider<LT> {
public static ThreadLocal<Boolean> CLEAN_MAPPER = ThreadLocal.withInitial(() -> false);
public static NamingScheme NAMING_SCHEME = in -> {
in = Character.toLowerCase(in.charAt(0)) + in.substring(1);
in = NamingSchemes.LOWER_CASE_DASHED.coerce(in);
@ -226,7 +228,16 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
}
private CommentedConfigurationNode getDefault(T defaultConfig, boolean cleanMapper) throws SerializationException {
try {
if (cleanMapper) {
CLEAN_MAPPER.set(true);
}
return getDefault(defaultConfig, cleanMapper ? defaultObjectMapper() : configObjectMapper());
} finally {
if (cleanMapper) {
CLEAN_MAPPER.set(false);
}
}
}
@SuppressWarnings("unchecked")

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.config.serializer;
import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.common.config.manager.manager.ConfigurateConfigManager;
import net.dv8tion.jda.api.entities.Role;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
@ -45,6 +46,9 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
@Override
public DiscordMessageEmbed.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException {
if (ConfigurateConfigManager.CLEAN_MAPPER.get()) {
return null;
}
if (!node.node(map("Enabled")).getBoolean(node.node(map("Enable")).getBoolean(true))) {
return null;
}
@ -86,7 +90,7 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
@Override
public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node)
throws SerializationException {
if (obj == null) {
if (obj == null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
node.set(null);
return;
}

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.config.serializer;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.manager.manager.ConfigurateConfigManager;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
@ -47,7 +48,7 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
public SendableDiscordMessage.Builder deserialize(Type type, ConfigurationNode node)
throws SerializationException {
String contentOnly = node.getString();
if (contentOnly != null) {
if (contentOnly != null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
return SendableDiscordMessage.builder()
.setContent(contentOnly);
}
@ -81,7 +82,7 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
@Override
public void serialize(Type type, SendableDiscordMessage.@Nullable Builder obj, ConfigurationNode node)
throws SerializationException {
if (obj == null) {
if (obj == null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
node.set(null);
return;
}

View File

@ -20,10 +20,20 @@ package com.discordsrv.common.debug.data;
public enum OnlineMode {
ONLINE,
OFFLINE,
BUNGEE,
VELOCITY;
ONLINE(true),
OFFLINE(false),
BUNGEE(true),
VELOCITY(true);
private final boolean online;
OnlineMode(boolean online) {
this.online = online;
}
public boolean isOnline() {
return online;
}
public static OnlineMode of(boolean onlineMode) {
return onlineMode ? OnlineMode.ONLINE : OnlineMode.OFFLINE;

View File

@ -18,8 +18,6 @@
package com.discordsrv.common.discord.api;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.WebhookClientBuilder;
import com.discordsrv.api.discord.DiscordAPI;
import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent;
import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext;
@ -34,7 +32,8 @@ import com.discordsrv.api.discord.exception.RestErrorResponseException;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.channels.base.ThreadConfig;
import com.discordsrv.common.config.main.generic.ThreadConfig;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import com.discordsrv.common.discord.api.entity.DiscordUserImpl;
import com.discordsrv.common.discord.api.entity.channel.*;
import com.discordsrv.common.discord.api.entity.guild.DiscordGuildImpl;
@ -47,6 +46,7 @@ import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
@ -58,6 +58,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
@ -68,7 +69,7 @@ public class DiscordAPIImpl implements DiscordAPI {
private final DiscordSRV discordSRV;
private final DiscordCommandRegistry commandRegistry;
private final AsyncLoadingCache<Long, WebhookClient> cachedClients;
private final AsyncLoadingCache<Long, WebhookClient<Message>> cachedClients;
private final List<ThreadChannelLookup> threadLookups = new CopyOnWriteArrayList<>();
public DiscordAPIImpl(DiscordSRV discordSRV) {
@ -79,46 +80,46 @@ public class DiscordAPIImpl implements DiscordAPI {
.buildAsync(new WebhookCacheLoader());
}
public CompletableFuture<WebhookClient> queryWebhookClient(long channelId) {
public CompletableFuture<WebhookClient<Message>> queryWebhookClient(long channelId) {
return cachedClients.get(channelId);
}
public AsyncLoadingCache<Long, WebhookClient> getCachedClients() {
public AsyncLoadingCache<Long, WebhookClient<Message>> getCachedClients() {
return cachedClients;
}
/**
* Finds active threads based for the provided {@link IChannelConfig}.
* @param config the config that specified the threads
* @return the list of active threads
*/
public List<DiscordThreadChannel> findThreads(BaseChannelConfig config, IChannelConfig channelConfig) {
List<DiscordThreadChannel> channels = new ArrayList<>();
findOrCreateThreads(config, channelConfig, channels::add, null, false);
return channels;
}
/**
* Finds or potentially unarchives or creates threads based on the provided {@link IChannelConfig}.
* @param config the config
* @param channelConsumer the consumer that will take the channels as they are gathered
* @param futures a possibly null list of {@link CompletableFuture} for tasks that need to be completed to get all threads
*/
public void findOrCreateThreads(
BaseChannelConfig config,
IChannelConfig channelConfig,
Consumer<DiscordThreadChannel> channelConsumer,
@Nullable List<CompletableFuture<DiscordThreadChannel>> futures,
public <T extends BaseChannelConfig & IChannelConfig> List<DiscordGuildMessageChannel> findDestinations(
T config,
boolean log
) {
List<ThreadConfig> threads = channelConfig.threads();
if (threads == null) {
return;
return findOrCreateDestinations(config, false, log).join();
}
for (ThreadConfig threadConfig : threads) {
public <T extends BaseChannelConfig & IChannelConfig> CompletableFuture<List<DiscordGuildMessageChannel>> findOrCreateDestinations(
T config,
boolean create,
boolean log
) {
DestinationConfig destination = config.destination();
List<DiscordGuildMessageChannel> channels = new CopyOnWriteArrayList<>();
for (Long channelId : destination.channelIds) {
DiscordMessageChannel channel = getMessageChannelById(channelId);
if (!(channel instanceof DiscordGuildMessageChannel)) {
continue;
}
channels.add((DiscordGuildMessageChannel) channel);
}
List<CompletableFuture<Void>> threadFutures = new ArrayList<>();
List<ThreadConfig> threadConfigs = destination.threads;
if (threadConfigs != null && !threadConfigs.isEmpty()) {
for (ThreadConfig threadConfig : threadConfigs) {
long channelId = threadConfig.channelId;
DiscordTextChannel channel = getTextChannelById(channelId);
DiscordThreadContainer channel = getTextChannelById(channelId);
if (channel == null) {
channel = getForumChannelById(channelId);
}
if (channel == null) {
if (channelId > 0 && log) {
discordSRV.logger().error("Unable to find channel with ID " + Long.toUnsignedString(channelId));
@ -131,13 +132,12 @@ public class DiscordAPIImpl implements DiscordAPI {
if (thread != null) {
ThreadChannel jdaChannel = thread.asJDA();
if (!jdaChannel.isArchived()) {
channelConsumer.accept(thread);
channels.add(getThreadChannel(jdaChannel));
continue;
}
}
if (futures == null) {
// Futures not specified, don't try to unarchive or create threads
if (!create) {
continue;
}
@ -151,23 +151,27 @@ public class DiscordAPIImpl implements DiscordAPI {
future = findOrCreateThread(config, threadConfig, channel);
}
futures.add(future.handle((threadChannel, t) -> {
DiscordThreadContainer container = channel;
threadFutures.add(future.handle((threadChannel, t) -> {
if (t != null) {
ErrorCallbackContext.context(
"Failed to deliver message to thread \""
+ threadConfig.threadName + "\" in channel " + channel
+ threadConfig.threadName + "\" in channel " + container
).accept(t);
throw new RuntimeException(); // Just here to fail the future
}
if (threadChannel != null) {
channelConsumer.accept(threadChannel);
channels.add(threadChannel);
}
return threadChannel;
return null;
}));
}
}
return CompletableFutureUtil.combine(threadFutures).thenApply(v -> channels);
}
private DiscordThreadChannel findThread(ThreadConfig config, List<DiscordThreadChannel> threads) {
for (DiscordThreadChannel thread : threads) {
if (thread.getName().equals(config.threadName)) {
@ -180,17 +184,17 @@ public class DiscordAPIImpl implements DiscordAPI {
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(
BaseChannelConfig config,
ThreadConfig threadConfig,
DiscordTextChannel textChannel
DiscordThreadContainer container
) {
if (!config.channelLocking.threads.unarchive) {
return textChannel.createThread(threadConfig.threadName, threadConfig.privateThread);
return container.createThread(threadConfig.threadName, threadConfig.privateThread);
}
CompletableFuture<DiscordThreadChannel> completableFuture = new CompletableFuture<>();
lookupThreads(
textChannel,
container,
threadConfig.privateThread,
lookup -> findOrCreateThread(threadConfig, textChannel, lookup, completableFuture),
lookup -> findOrCreateThread(threadConfig, container, lookup, completableFuture),
(thread, throwable) -> {
if (throwable != null) {
completableFuture.completeExceptionally(throwable);
@ -203,7 +207,7 @@ public class DiscordAPIImpl implements DiscordAPI {
private void findOrCreateThread(
ThreadConfig config,
DiscordTextChannel textChannel,
DiscordThreadContainer container,
ThreadChannelLookup lookup,
CompletableFuture<DiscordThreadChannel> completableFuture
) {
@ -222,7 +226,7 @@ public class DiscordAPIImpl implements DiscordAPI {
}
DiscordThreadChannel thread = findThread(config, channels);
unarchiveOrCreateThread(config, textChannel, thread, completableFuture);
unarchiveOrCreateThread(config, container, thread, completableFuture);
}).exceptionally(t -> {
if (t instanceof CompletionException) {
completableFuture.completeExceptionally(t.getCause());
@ -235,7 +239,7 @@ public class DiscordAPIImpl implements DiscordAPI {
private void unarchiveOrCreateThread(
ThreadConfig config,
DiscordTextChannel textChannel,
DiscordThreadContainer container,
DiscordThreadChannel thread,
CompletableFuture<DiscordThreadChannel> future
) {
@ -256,7 +260,7 @@ public class DiscordAPIImpl implements DiscordAPI {
return;
}
textChannel.createThread(config.threadName, config.privateThread).whenComplete(((threadChannel, t) -> {
container.createThread(config.threadName, config.privateThread).whenComplete(((threadChannel, t) -> {
if (t != null) {
future.completeExceptionally(t);
} else {
@ -266,7 +270,7 @@ public class DiscordAPIImpl implements DiscordAPI {
}
public void lookupThreads(
DiscordTextChannel textChannel,
DiscordThreadContainer container,
boolean privateThreads,
Consumer<ThreadChannelLookup> lookupConsumer,
BiConsumer<DiscordThreadChannel, Throwable> channelConsumer
@ -275,7 +279,7 @@ public class DiscordAPIImpl implements DiscordAPI {
synchronized (threadLookups) {
for (ThreadChannelLookup threadLookup : threadLookups) {
if (threadLookup.isPrivateThreads() != privateThreads
|| threadLookup.getChannelId() != textChannel.getId()) {
|| threadLookup.getChannelId() != container.getId()) {
continue;
}
@ -284,10 +288,10 @@ public class DiscordAPIImpl implements DiscordAPI {
}
lookup = new ThreadChannelLookup(
textChannel.getId(), privateThreads,
container.getId(), privateThreads,
privateThreads
? textChannel.retrieveArchivedPrivateThreads()
: textChannel.retrieveArchivedPublicThreads()
? container.retrieveArchivedPrivateThreads()
: container.retrieveArchivedPublicThreads()
);
threadLookups.add(lookup);
}
@ -338,6 +342,16 @@ public class DiscordAPIImpl implements DiscordAPI {
return getDirectMessageChannelById(id);
}
public DiscordChannel getChannel(Channel jda) {
if (jda instanceof ForumChannel) {
return getForumChannel((ForumChannel) jda);
} else if (jda instanceof MessageChannel) {
return getMessageChannel((MessageChannel) jda);
} else {
throw new IllegalArgumentException("Unmappable Channel type: " + jda.getClass().getName());
}
}
public AbstractDiscordMessageChannel<?> getMessageChannel(MessageChannel jda) {
if (jda instanceof TextChannel) {
return getTextChannel((TextChannel) jda);
@ -393,6 +407,15 @@ public class DiscordAPIImpl implements DiscordAPI {
return new DiscordTextChannelImpl(discordSRV, jda);
}
@Override
public @Nullable DiscordForumChannel getForumChannelById(long id) {
return mapJDAEntity(jda -> jda.getForumChannelById(id), this::getForumChannel);
}
public DiscordForumChannelImpl getForumChannel(ForumChannel jda) {
return new DiscordForumChannelImpl(discordSRV, jda);
}
@Override
public @Nullable DiscordVoiceChannel getVoiceChannelById(long id) {
return mapJDAEntity(jda -> jda.getVoiceChannelById(id), this::getVoiceChannel);
@ -489,10 +512,10 @@ public class DiscordAPIImpl implements DiscordAPI {
return commandRegistry;
}
private class WebhookCacheLoader implements AsyncCacheLoader<Long, WebhookClient> {
private class WebhookCacheLoader implements AsyncCacheLoader<Long, WebhookClient<Message>> {
@Override
public @NonNull CompletableFuture<WebhookClient> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) {
public @NonNull CompletableFuture<WebhookClient<Message>> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) {
JDA jda = discordSRV.jda();
if (jda == null) {
return notReady();
@ -525,22 +548,32 @@ public class DiscordAPIImpl implements DiscordAPI {
return textChannel.createWebhook("DSRV").submit();
}).thenApply(webhook ->
WebhookClientBuilder.fromJDA(webhook)
.setHttpClient(jda.getHttpClient())
.setExecutorService(discordSRV.scheduler().scheduledExecutorService())
.build()
WebhookClient.createClient(
webhook.getJDA(),
webhook.getId(),
Objects.requireNonNull(webhook.getToken())
)
);
}
}
private class WebhookCacheExpiry implements Expiry<Long, WebhookClient> {
private class WebhookCacheExpiry implements Expiry<Long, WebhookClient<Message>> {
private boolean isConfiguredChannel(Long channelId) {
for (BaseChannelConfig config : discordSRV.config().channels.values()) {
if (config instanceof IChannelConfig
&& ((IChannelConfig) config).channelIds().contains(channelId)) {
DestinationConfig destination = config instanceof IChannelConfig ? ((IChannelConfig) config).destination() : null;
if (destination == null) {
continue;
}
if (destination.channelIds.contains(channelId)) {
return true;
}
for (ThreadConfig thread : destination.threads) {
if (Objects.equals(thread.channelId, channelId)) {
return true;
}
}
}
return false;
}

View File

@ -61,6 +61,11 @@ public class DiscordUserImpl implements DiscordUser {
return user.getName();
}
@Override
public @NotNull String getEffectiveName() {
return user.getEffectiveName();
}
@Override
public @NotNull String getDiscriminator() {
return user.getDiscriminator();

View File

@ -18,9 +18,6 @@
package com.discordsrv.common.discord.api.entity.channel;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.send.WebhookMessageBuilder;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
@ -29,16 +26,16 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.entity.message.util.SendableDiscordMessageUtil;
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.FluentRestAction;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import net.dv8tion.jda.api.utils.FileUpload;
import net.dv8tion.jda.api.requests.RestAction;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.utils.messages.MessageCreateRequest;
import net.dv8tion.jda.api.utils.messages.MessageEditData;
import net.dv8tion.jda.api.utils.messages.MessageEditRequest;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageChannel>
extends AbstractDiscordMessageChannel<T>
@ -51,7 +48,7 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
this.guild = discordSRV.discordAPI().getGuild(channel.getGuild());
}
public CompletableFuture<WebhookClient> queryWebhookClient() {
public CompletableFuture<WebhookClient<Message>> queryWebhookClient() {
return discordSRV.discordAPI().queryWebhookClient(getId());
}
@ -76,51 +73,55 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
}
@Override
public CompletableFuture<ReceivedDiscordMessage> sendMessage(
@NotNull SendableDiscordMessage message, @NotNull Map<String, InputStream> attachments
) {
return message(message, (webhookClient, webhookMessage) -> {
for (Map.Entry<String, InputStream> entry : attachments.entrySet()) {
webhookMessage.addFile(entry.getKey(), entry.getValue());
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message) {
return sendInternal(message);
}
return webhookClient.send(webhookMessage.build());
}, (channel, msg) -> {
MessageCreateAction action = channel.sendMessage(SendableDiscordMessageUtil.toJDASend(msg));
for (Map.Entry<String, InputStream> entry : attachments.entrySet()) {
action = action.addFiles(FileUpload.fromData(entry.getValue(), entry.getKey()));
@SuppressWarnings("unchecked") // Generics
private <R extends MessageCreateRequest<? extends MessageCreateRequest<?>> & RestAction<Message>> CompletableFuture<ReceivedDiscordMessage> sendInternal(SendableDiscordMessage message) {
MessageCreateData createData = SendableDiscordMessageUtil.toJDASend(message);
CompletableFuture<R> createRequest;
if (message.isWebhookMessage()) {
createRequest = queryWebhookClient()
.thenApply(client -> (R) client.sendMessage(createData)
.setUsername(message.getWebhookUsername())
.setAvatarUrl(message.getWebhookAvatarUrl())
);
} else {
createRequest = CompletableFuture.completedFuture(((R) channel.sendMessage(createData)));
}
return action;
});
return createRequest
.thenCompose(RestAction::submit)
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, @NotNull SendableDiscordMessage message) {
return message(
message,
(client, msg) -> client.edit(id, msg.build()),
(textChannel, msg) -> textChannel.editMessageById(id, SendableDiscordMessageUtil.toJDAEdit(msg))
);
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(
long id,
@NotNull SendableDiscordMessage message
) {
return editInternal(id, message);
}
private CompletableFuture<ReceivedDiscordMessage> message(
SendableDiscordMessage message,
BiFunction<WebhookClient, WebhookMessageBuilder, CompletableFuture<ReadonlyMessage>> webhookFunction,
BiFunction<T, SendableDiscordMessage, FluentRestAction<? extends Message, ?>> jdaFunction) {
return discordSRV.discordAPI().mapExceptions(() -> {
CompletableFuture<ReceivedDiscordMessage> future;
@SuppressWarnings("unchecked") // Generics
private <R extends MessageEditRequest<? extends MessageEditRequest<?>> & RestAction<Message>> CompletableFuture<ReceivedDiscordMessage> editInternal(
long id,
SendableDiscordMessage message
) {
MessageEditData editData = SendableDiscordMessageUtil.toJDAEdit(message);
CompletableFuture<R> editRequest;
if (message.isWebhookMessage()) {
future = queryWebhookClient()
.thenCompose(client -> webhookFunction.apply(
client, SendableDiscordMessageUtil.toWebhook(message)))
.thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg));
editRequest = queryWebhookClient().thenApply(client -> (R) client.editMessageById(id, editData));
} else {
future = jdaFunction
.apply(channel, message)
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
editRequest = CompletableFuture.completedFuture(((R) channel.editMessageById(id, editData)));
}
return future;
});
return editRequest
.thenCompose(RestAction::submit)
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
}
@Override
@ -131,7 +132,7 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
} else {
future = discordSRV.discordAPI()
.queryWebhookClient(channel.getIdLong())
.thenCompose(client -> client.delete(id));
.thenCompose(client -> client.deleteMessageById(id).submit());
}
return discordSRV.discordAPI().mapExceptions(future);
}

View File

@ -28,11 +28,8 @@ import com.discordsrv.common.discord.api.entity.message.util.SendableDiscordMess
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import net.dv8tion.jda.api.utils.FileUpload;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@ -52,18 +49,12 @@ public class DiscordDMChannelImpl extends AbstractDiscordMessageChannel<PrivateC
}
@Override
public CompletableFuture<ReceivedDiscordMessage> sendMessage(
@NotNull SendableDiscordMessage message,
@NotNull Map<String, InputStream> attachments
) {
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message) {
if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
}
MessageCreateAction action = channel.sendMessage(SendableDiscordMessageUtil.toJDASend(message));
for (Map.Entry<String, InputStream> entry : attachments.entrySet()) {
action = action.addFiles(FileUpload.fromData(entry.getValue(), entry.getKey()));
}
CompletableFuture<ReceivedDiscordMessage> future = action.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
@ -80,7 +71,10 @@ public class DiscordDMChannelImpl extends AbstractDiscordMessageChannel<PrivateC
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, @NotNull SendableDiscordMessage message) {
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(
long id,
@NotNull SendableDiscordMessage message
) {
if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
}

View File

@ -0,0 +1,119 @@
package com.discordsrv.common.discord.api.entity.channel;
import com.discordsrv.api.discord.entity.channel.DiscordChannelType;
import com.discordsrv.api.discord.entity.channel.DiscordForumChannel;
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer;
import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction;
import net.dv8tion.jda.api.requests.restaction.pagination.ThreadChannelPaginationAction;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
public class DiscordForumChannelImpl implements DiscordForumChannel {
private final DiscordSRV discordSRV;
private final ForumChannel channel;
private final DiscordGuild guild;
public DiscordForumChannelImpl(DiscordSRV discordSRV, ForumChannel channel) {
this.discordSRV = discordSRV;
this.channel = channel;
this.guild = discordSRV.discordAPI().getGuild(channel.getGuild());
}
@Override
public ForumChannel asJDA() {
return channel;
}
@Override
public long getId() {
return channel.getIdLong();
}
@Override
public @NotNull String getName() {
return channel.getName();
}
@Override
public @NotNull DiscordGuild getGuild() {
return guild;
}
@Override
public @NotNull String getJumpUrl() {
return channel.getJumpUrl();
}
@Override
public @NotNull List<DiscordThreadChannel> getActiveThreads() {
List<ThreadChannel> threads = channel.getThreadChannels();
List<DiscordThreadChannel> threadChannels = new ArrayList<>(threads.size());
for (ThreadChannel thread : threads) {
threadChannels.add(discordSRV.discordAPI().getThreadChannel(thread));
}
return threadChannels;
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPrivateThreads() {
return threads(IThreadContainer::retrieveArchivedPrivateThreadChannels);
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedJoinedPrivateThreads() {
return threads(IThreadContainer::retrieveArchivedPrivateJoinedThreadChannels);
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPublicThreads() {
return threads(IThreadContainer::retrieveArchivedPublicThreadChannels);
}
@SuppressWarnings("CodeBlock2Expr")
private CompletableFuture<List<DiscordThreadChannel>> threads(
Function<IThreadContainer, ThreadChannelPaginationAction> action) {
return discordSRV.discordAPI().mapExceptions(() -> {
return action.apply(channel)
.submit()
.thenApply(channels -> channels.stream()
.map(channel -> discordSRV.discordAPI().getThreadChannel(channel))
.collect(Collectors.toList())
);
});
}
@Override
public CompletableFuture<DiscordThreadChannel> createThread(String name, boolean privateThread) {
return thread(channel -> channel.createThreadChannel(name, privateThread));
}
@Override
public CompletableFuture<DiscordThreadChannel> createThread(String name, long messageId) {
return thread(channel -> channel.createThreadChannel(name, messageId));
}
@SuppressWarnings("CodeBlock2Expr")
private CompletableFuture<DiscordThreadChannel> thread(Function<ForumChannel, ThreadChannelAction> action) {
return discordSRV.discordAPI().mapExceptions(() -> {
return action.apply(channel)
.submit()
.thenApply(channel -> discordSRV.discordAPI().getThreadChannel(channel));
});
}
@Override
public DiscordChannelType getType() {
return DiscordChannelType.FORUM;
}
}

View File

@ -18,16 +18,17 @@
package com.discordsrv.common.discord.api.entity.channel;
import club.minnced.discord.webhook.WebhookClient;
import com.discordsrv.api.discord.entity.channel.DiscordChannelType;
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.entity.channel.DiscordThreadContainer;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.WebhookClient;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.internal.requests.IncomingWebhookClient;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
@ -41,22 +42,15 @@ public class DiscordThreadChannelImpl extends AbstractDiscordGuildMessageChannel
super(discordSRV, thread);
IThreadContainer container = thread.getParentChannel();
this.threadContainer = container instanceof MessageChannel
? (DiscordThreadContainer) discordSRV.discordAPI().getMessageChannel((MessageChannel) container)
: null;
this.threadContainer = (DiscordThreadContainer) discordSRV.discordAPI().getChannel(container);
this.guild = discordSRV.discordAPI().getGuild(thread.getGuild());
}
@Override
public CompletableFuture<WebhookClient> queryWebhookClient() {
public CompletableFuture<WebhookClient<Message>> queryWebhookClient() {
return discordSRV.discordAPI()
.queryWebhookClient(getParentChannel().getId())
.thenApply(client -> client.onThread(getId()));
}
@Override
public @NotNull String getName() {
return channel.getName();
.thenApply(client -> ((IncomingWebhookClient) client).withThreadId(Long.toUnsignedString(getId())));
}
@Override

View File

@ -56,6 +56,11 @@ public class DiscordGuildImpl implements DiscordGuild {
return guild.getMemberCount();
}
@Override
public @NotNull DiscordGuildMember getSelfMember() {
return discordSRV.discordAPI().getGuildMember(guild.getSelfMember());
}
@Override
public @NotNull CompletableFuture<DiscordGuildMember> retrieveMemberById(long id) {
return discordSRV.discordAPI().mapExceptions(() -> guild.retrieveMemberById(id)

View File

@ -87,6 +87,11 @@ public class DiscordGuildMemberImpl implements DiscordGuildMember {
return roles.stream().anyMatch(role::equals);
}
@Override
public boolean canInteract(@NotNull DiscordRole role) {
return member.canInteract(role.asJDA());
}
@Override
public CompletableFuture<Void> addRole(@NotNull DiscordRole role) {
return discordSRV.discordAPI().mapExceptions(() ->

View File

@ -18,12 +18,6 @@
package com.discordsrv.common.discord.api.entity.message;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.receive.ReadonlyAttachment;
import club.minnced.discord.webhook.receive.ReadonlyEmbed;
import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.send.WebhookEmbed;
import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordDMChannel;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
@ -44,6 +38,7 @@ import com.discordsrv.common.future.util.CompletableFutureUtil;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.WebhookClient;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
@ -71,7 +66,7 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
boolean self = false;
if (webhookMessage) {
CompletableFuture<WebhookClient> clientFuture = discordSRV.discordAPI()
CompletableFuture<WebhookClient<Message>> clientFuture = discordSRV.discordAPI()
.getCachedClients()
.getIfPresent(channel instanceof DiscordThreadChannel
? ((DiscordThreadChannel) channel).getParentChannel().getId()
@ -79,7 +74,7 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
);
if (clientFuture != null) {
long clientId = clientFuture.join().getId();
long clientId = clientFuture.join().getIdLong();
self = clientId == user.getId();
}
} else {
@ -113,71 +108,6 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
);
}
public static ReceivedDiscordMessage fromWebhook(DiscordSRV discordSRV, ReadonlyMessage webhookMessage) {
List<DiscordMessageEmbed> mappedEmbeds = new ArrayList<>();
for (ReadonlyEmbed embed : webhookMessage.getEmbeds()) {
List<DiscordMessageEmbed.Field> fields = new ArrayList<>();
for (WebhookEmbed.EmbedField field : embed.getFields()) {
fields.add(new DiscordMessageEmbed.Field(field.getName(), field.getValue(), field.isInline()));
}
Integer color = embed.getColor();
WebhookEmbed.EmbedAuthor author = embed.getAuthor();
WebhookEmbed.EmbedTitle title = embed.getTitle();
ReadonlyEmbed.EmbedImage thumbnail = embed.getThumbnail();
ReadonlyEmbed.EmbedImage image = embed.getImage();
WebhookEmbed.EmbedFooter footer = embed.getFooter();
mappedEmbeds.add(new DiscordMessageEmbed(
color != null ? new Color(color) : null,
author != null ? author.getName() : null,
author != null ? author.getUrl() : null,
author != null ? author.getIconUrl() : null,
title != null ? title.getText() : null,
title != null ? title.getUrl() : null,
embed.getDescription(),
fields,
thumbnail != null ? thumbnail.getUrl() : null,
image != null ? image.getUrl() : null,
embed.getTimestamp(),
footer != null ? footer.getText() : null,
footer != null ? footer.getIconUrl() : null
));
}
DiscordMessageChannel channel = discordSRV.discordAPI().getMessageChannelById(
webhookMessage.getChannelId());
DiscordUser user = discordSRV.discordAPI().getUserById(
webhookMessage.getAuthor().getId());
DiscordGuildMember member = channel instanceof DiscordTextChannel && user != null
? ((DiscordTextChannel) channel).getGuild().getMemberById(user.getId()) : null;
List<Attachment> attachments = new ArrayList<>();
for (ReadonlyAttachment attachment : webhookMessage.getAttachments()) {
attachments.add(new Attachment(
attachment.getFileName(),
attachment.getUrl(),
attachment.getProxyUrl(),
attachment.getSize()
));
}
return new ReceivedDiscordMessageImpl(
discordSRV,
attachments,
true, // These are always from rest responses
channel,
null,
member,
user,
webhookMessage.getChannelId(),
webhookMessage.getId(),
webhookMessage.getContent(),
mappedEmbeds,
true
);
}
private final DiscordSRV discordSRV;
private final List<Attachment> attachments;
private final boolean fromSelf;
@ -305,7 +235,9 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> edit(SendableDiscordMessage message) {
public @NotNull CompletableFuture<ReceivedDiscordMessage> edit(
@NotNull SendableDiscordMessage message
) {
if (!webhookMessage && message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot edit a non-webhook message into a webhook message");
}
@ -322,6 +254,29 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
// Placeholders
//
@Placeholder("message_reply")
public Component _reply(BaseChannelConfig config, @PlaceholderRemainder String suffix) {
if (replyingTo == null) {
return null;
}
String content = replyingTo.getContent();
if (content == null) {
return null;
}
Component component = discordSRV.componentFactory().minecraftSerializer().serialize(content);
String replyFormat = config.discordToMinecraft.replyFormat;
return ComponentUtil.fromAPI(
discordSRV.componentFactory().textBuilder(replyFormat)
.applyPlaceholderService()
.addPlaceholder("message", component)
.addContext(replyingTo.getMember(), replyingTo.getAuthor(), replyingTo)
.build()
);
}
@Placeholder("message_attachments")
public Component _attachments(BaseChannelConfig config, @PlaceholderRemainder String suffix) {
String attachmentFormat = config.discordToMinecraft.attachmentFormat;

View File

@ -18,7 +18,6 @@
package com.discordsrv.common.discord.api.entity.message.util;
import club.minnced.discord.webhook.send.WebhookMessageBuilder;
import com.discordsrv.api.discord.entity.interaction.component.actionrow.MessageActionRow;
import com.discordsrv.api.discord.entity.message.AllowedMention;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
@ -26,11 +25,14 @@ import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.utils.FileUpload;
import net.dv8tion.jda.api.utils.messages.*;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class SendableDiscordMessageUtil {
@ -62,12 +64,18 @@ public final class SendableDiscordMessageUtil {
embeds.add(embed.toJDA());
}
List<FileUpload> uploads = new ArrayList<>();
for (Map.Entry<InputStream, String> attachment : message.getAttachments().entrySet()) {
uploads.add(FileUpload.fromData(attachment.getKey(), attachment.getValue()));
}
return (T) builder
.setContent(message.getContent())
.setEmbeds(embeds)
.setAllowedMentions(allowedTypes)
.mentionUsers(allowedUsers.stream().mapToLong(l -> l).toArray())
.mentionRoles(allowedRoles.stream().mapToLong(l -> l).toArray());
.mentionRoles(allowedRoles.stream().mapToLong(l -> l).toArray())
.setFiles(uploads);
}
public static MessageCreateData toJDASend(@NotNull SendableDiscordMessage message) {
@ -91,10 +99,4 @@ public final class SendableDiscordMessageUtil {
.setComponents(actionRows)
.build();
}
public static WebhookMessageBuilder toWebhook(@NotNull SendableDiscordMessage message) {
return WebhookMessageBuilder.fromJDA(null/*toJDA(message)*/) // TODO: lib update? lib replacement?
.setUsername(message.getWebhookUsername())
.setAvatarUrl(message.getWebhookAvatarUrl());
}
}

View File

@ -33,6 +33,7 @@ import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.BotConfig;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.documentation.DocumentationURLs;
import com.discordsrv.common.config.main.MemberCachingConfig;
import com.discordsrv.common.debug.DebugGenerateEvent;
import com.discordsrv.common.debug.file.TextDebugFile;
@ -310,10 +311,8 @@ public class JDAConnectionManager implements DiscordConnectionManager {
MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching;
DiscordConnectionDetailsImpl connectionDetails = discordSRV.discordConnectionDetails();
Set<GatewayIntent> intents = new LinkedHashSet<>();
this.intents.clear();
this.intents.addAll(connectionDetails.getGatewayIntents());
this.intents.forEach(intent -> intents.add(intent.asJDA()));
Set<CacheFlag> cacheFlags = new LinkedHashSet<>();
this.cacheFlags.clear();
@ -322,7 +321,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
cacheFlags.add(flag.asJDA());
DiscordGatewayIntent intent = flag.requiredIntent();
if (intent != null) {
intents.add(intent.asJDA());
this.intents.add(intent);
}
});
@ -354,6 +353,9 @@ public class JDAConnectionManager implements DiscordConnectionManager {
chunkingFilter = ChunkingFilter.NONE;
}
Set<GatewayIntent> intents = new LinkedHashSet<>();
this.intents.forEach(intent -> intents.add(intent.asJDA()));
// Start with everything disabled & enable stuff that we actually need
JDABuilder jdaBuilder = JDABuilder.createLight(token, intents);
jdaBuilder.enableCache(cacheFlags);
@ -562,7 +564,10 @@ public class JDAConnectionManager implements DiscordConnectionManager {
discordSRV.logger().error("| The token provided in the");
discordSRV.logger().error("| " + ConnectionConfig.FILE_NAME + " is invalid");
discordSRV.logger().error("|");
discordSRV.logger().error("| You can get the token for your bot from:");
discordSRV.logger().error("| Haven't created a bot yet? Installing the plugin for the first time?");
discordSRV.logger().error("| See " + DocumentationURLs.CREATE_TOKEN);
discordSRV.logger().error("|");
discordSRV.logger().error("| Already have a bot? You can get the token for your bot from:");
discordSRV.logger().error("| https://discord.com/developers/applications");
discordSRV.logger().error("| by selecting the application, going to the \"Bot\" tab");
discordSRV.logger().error("| and clicking on \"Reset Token\"");
@ -638,7 +643,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
discordSRV.logger().error("| server requiring 2FA for moderation actions");
if (user != null) {
discordSRV.logger().error("|");
discordSRV.logger().error("| The Discord bot's owner is " + user.getAsTag() + " (" + user.getId() + ")");
discordSRV.logger().error("| The Discord bot's owner is " + user.getUsername() + " (" + user.getId() + ")");
}
discordSRV.logger().error("|");
discordSRV.logger().error("| You can view instructions for enabling 2FA here:");

View File

@ -18,7 +18,6 @@
package com.discordsrv.common.groupsync;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleAddEvent;
import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleRemoveEvent;
@ -316,7 +315,11 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST);
}
DiscordGuildMember member = role.getGuild().getMemberById(userId);
if (!role.getGuild().getSelfMember().canInteract(role)) {
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_CANNOT_INTERACT);
}
return role.getGuild().retrieveMemberById(userId).thenCompose(member -> {
if (member == null) {
return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER);
}
@ -395,6 +398,7 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
});
return resultFuture;
});
}
// Listeners & methods to indicate something changed
@ -636,7 +640,11 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_DOESNT_EXIST);
}
DiscordGuildMember member = role.getGuild().getMemberById(userId);
if (!role.getGuild().getSelfMember().canInteract(role)) {
return CompletableFuture.completedFuture(GroupSyncResult.ROLE_CANNOT_INTERACT);
}
return role.getGuild().retrieveMemberById(userId).thenCompose(member -> {
if (member == null) {
return CompletableFuture.completedFuture(GroupSyncResult.NOT_A_GUILD_MEMBER);
}
@ -675,5 +683,6 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
resultFuture.complete(result);
});
return resultFuture;
});
}
}

View File

@ -34,6 +34,7 @@ public enum GroupSyncResult {
// Errors
ROLE_DOESNT_EXIST("Role doesn't exist"),
ROLE_CANNOT_INTERACT("Bot doesn't have a role above the synced role (cannot interact)"),
NOT_A_GUILD_MEMBER("User is not part of the server the role is in"),
PERMISSION_BACKEND_FAIL_CHECK("Failed to check group status, error printed"),
UPDATE_FAILED("Failed to modify role/group, error printed"),

View File

@ -21,6 +21,9 @@ package com.discordsrv.common.logging;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
public interface Logger {
default void info(String message) {
@ -69,4 +72,11 @@ public interface Logger {
void log(@Nullable String loggerName, @NotNull LogLevel logLevel, @Nullable String message, @Nullable Throwable throwable);
default String getStackTrace(Throwable throwable) {
final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter, true);
throwable.printStackTrace(printWriter);
return stringWriter.getBuffer().toString();
}
}

View File

@ -23,8 +23,6 @@ import com.discordsrv.common.logging.LogLevel;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.backend.LogFilter;
import com.discordsrv.common.logging.backend.LoggingBackend;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -39,12 +37,18 @@ import java.util.logging.LogRecord;
public class JavaLoggerImpl implements Logger, LoggingBackend {
private static final DualHashBidiMap<Level, LogLevel> LEVELS = new DualHashBidiMap<>();
private static final Map<Level, LogLevel> LEVELS = new HashMap<>();
private static final Map<LogLevel, Level> LEVELS_REVERSE = new HashMap<>();
private static void put(Level level, LogLevel logLevel) {
LEVELS.put(level, logLevel);
LEVELS_REVERSE.put(logLevel, level);
}
static {
LEVELS.put(Level.INFO, LogLevel.INFO);
LEVELS.put(Level.WARNING, LogLevel.WARNING);
LEVELS.put(Level.SEVERE, LogLevel.ERROR);
put(Level.INFO, LogLevel.INFO);
put(Level.WARNING, LogLevel.WARNING);
put(Level.SEVERE, LogLevel.ERROR);
}
private final java.util.logging.Logger logger;
@ -61,7 +65,7 @@ public class JavaLoggerImpl implements Logger, LoggingBackend {
@Override
public void log(@Nullable String loggerName, @NotNull LogLevel level, @Nullable String message, @Nullable Throwable throwable) {
Level logLevel = LEVELS.getKey(level);
Level logLevel = LEVELS_REVERSE.get(level);
if (logLevel != null) {
List<String> contents = new ArrayList<>(2);
if (message != null) {
@ -69,7 +73,7 @@ public class JavaLoggerImpl implements Logger, LoggingBackend {
}
if (throwable != null) {
// Exceptions aren't always logged correctly by the logger itself
contents.add(ExceptionUtils.getStackTrace(throwable));
contents.add(getStackTrace(throwable));
}
logger.log(logLevel, String.join("\n", contents));
}

View File

@ -23,7 +23,6 @@ import com.discordsrv.common.logging.LogLevel;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.backend.LogFilter;
import com.discordsrv.common.logging.backend.LoggingBackend;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Marker;
@ -41,14 +40,20 @@ import java.util.Map;
public class Log4JLoggerImpl implements Logger, LoggingBackend {
private static final DualHashBidiMap<Level, LogLevel> LEVELS = new DualHashBidiMap<>();
private static final Map<Level, LogLevel> LEVELS = new HashMap<>();
private static final Map<LogLevel, Level> LEVELS_REVERSE = new HashMap<>();
private static void put(Level level, LogLevel logLevel) {
LEVELS.put(level, logLevel);
LEVELS_REVERSE.put(logLevel, level);
}
static {
LEVELS.put(Level.INFO, LogLevel.INFO);
LEVELS.put(Level.WARN, LogLevel.WARNING);
LEVELS.put(Level.ERROR, LogLevel.ERROR);
LEVELS.put(Level.DEBUG, LogLevel.DEBUG);
LEVELS.put(Level.TRACE, LogLevel.TRACE);
put(Level.INFO, LogLevel.INFO);
put(Level.WARN, LogLevel.WARNING);
put(Level.ERROR, LogLevel.ERROR);
put(Level.DEBUG, LogLevel.DEBUG);
put(Level.TRACE, LogLevel.TRACE);
}
private final org.apache.logging.log4j.Logger logger;
@ -65,7 +70,7 @@ public class Log4JLoggerImpl implements Logger, LoggingBackend {
@Override
public void log(@Nullable String loggerName, @NotNull LogLevel level, @Nullable String message, @Nullable Throwable throwable) {
Level logLevel = LEVELS.getKey(level);
Level logLevel = LEVELS_REVERSE.get(level);
logger.log(logLevel, message, throwable);
}

View File

@ -19,6 +19,8 @@
package com.discordsrv.common.logging.impl;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.DebugConfig;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.logging.LogLevel;
import com.discordsrv.common.logging.Logger;
import net.dv8tion.jda.api.Permission;
@ -36,6 +38,7 @@ import java.nio.file.StandardOpenOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -47,6 +50,8 @@ public class DiscordSRVLogger implements Logger {
private static final String LOG_LINE_FORMAT = "[%s] [%s] %s";
private static final String LOG_FILE_NAME_FORMAT = "%s-%s.log";
private static final List<String> DISABLE_DEBUG_BY_DEFAULT = Collections.singletonList("Hikari");
private final Queue<LogEntry> linesToAdd = new ConcurrentLinkedQueue<>();
private final DiscordSRV discordSRV;
@ -123,13 +128,34 @@ public class DiscordSRVLogger implements Logger {
}
private void doLog(String loggerName, LogLevel logLevel, String message, Throwable throwable) {
if (logLevel != LogLevel.DEBUG && logLevel != LogLevel.TRACE) {
discordSRV.platformLogger().log(null, logLevel, message, throwable);
MainConfig config = discordSRV.config();
DebugConfig debugConfig = config != null ? config.debug : null;
if (logLevel == LogLevel.TRACE || (loggerName != null && logLevel == LogLevel.DEBUG && DISABLE_DEBUG_BY_DEFAULT.contains(loggerName))) {
if (loggerName == null
|| debugConfig == null
|| debugConfig.additionalLevels == null
|| !debugConfig.additionalLevels.getOrDefault(loggerName, Collections.emptyList()).contains(logLevel.name())) {
return;
}
}
boolean debugOrTrace = logLevel == LogLevel.DEBUG || logLevel == LogLevel.TRACE;
boolean logToConsole = debugConfig != null && debugConfig.logToConsole;
if (!debugOrTrace || logToConsole) {
String consoleMessage = message;
LogLevel consoleLevel = logLevel;
if (debugOrTrace) {
// Normally DEBUG/TRACE logging isn't enabled, so we convert it to INFO and add the level
consoleMessage = "[" + logLevel.name() + "]" + (message != null ? " " + message : "");
consoleLevel = LogLevel.INFO;
}
discordSRV.platformLogger().log(null, consoleLevel, consoleMessage, throwable);
}
// TODO: handle trace & hikari
Path debugLog = debugLogs.isEmpty() ? null : debugLogs.get(0);
if (debugLog == null || logLevel == LogLevel.TRACE/* || loggerName.equals("Hikari")*/) {
if (debugLog == null) {
return;
}
long time = System.currentTimeMillis();

View File

@ -33,7 +33,7 @@ import com.discordsrv.api.placeholder.util.Placeholders;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.renderer.DiscordSRVMinecraftRenderer;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.logging.NamedLogger;
@ -120,7 +120,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
Placeholders message = new Placeholders(event.getMessageContent());
chatConfig.contentRegexFilters.forEach(message::replaceAll);
Component messageComponent = DiscordSRVMinecraftRenderer.getWithContext(event, chatConfig, () ->
Component messageComponent = DiscordSRVMinecraftRenderer.getWithContext(event.getGuild(), chatConfig, () ->
discordSRV.componentFactory().minecraftSerializer().serialize(message.toString()));
GameTextBuilder componentBuilder = discordSRV.componentFactory()
@ -133,7 +133,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
componentBuilder.applyPlaceholderService();
MinecraftComponent component = componentBuilder.build();
MinecraftComponent component = DiscordSRVMinecraftRenderer.getWithContext(event.getGuild(), chatConfig, componentBuilder::build);
if (ComponentUtil.isEmpty(component)) {
// Empty
return;

View File

@ -32,12 +32,14 @@ import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.discord.events.message.DiscordMessageDeleteEvent;
import com.discordsrv.api.discord.events.message.DiscordMessageUpdateEvent;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.message.forward.game.AbstractGameMessageForwardedEvent;
import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent;
import com.discordsrv.api.placeholder.provider.SinglePlaceholder;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.channels.MirroringConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.module.type.AbstractModule;
@ -46,8 +48,8 @@ import net.dv8tion.jda.api.entities.Message;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -58,13 +60,13 @@ import java.util.concurrent.TimeUnit;
public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
private final Cache<String, Mirror> mapping;
private final Cache<Long, Cache<Long, Sync>> mapping;
public DiscordMessageMirroringModule(DiscordSRV discordSRV) {
super(discordSRV, new NamedLogger(discordSRV, "DISCORD_MIRRORING"));
this.mapping = discordSRV.caffeineBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.expireAfterWrite(60, TimeUnit.MINUTES)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build();
}
@ -83,8 +85,9 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
return Arrays.asList(DiscordGatewayIntent.GUILD_MESSAGES, DiscordGatewayIntent.MESSAGE_CONTENT);
}
@SuppressWarnings("unchecked") // Wacky generics
@Subscribe
public void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) {
public <CC extends BaseChannelConfig & IChannelConfig> void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) {
if (checkCancellation(event)) {
return;
}
@ -95,16 +98,14 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
}
ReceivedDiscordMessage message = event.getDiscordMessage();
DiscordMessageChannel channel = event.getChannel();
List<Pair<DiscordGuildMessageChannel, MirroringConfig>> mirrorChannels = new ArrayList<>();
List<CompletableFuture<DiscordThreadChannel>> futures = new ArrayList<>();
List<CompletableFuture<MirrorOperation>> futures = new ArrayList<>();
Map<ReceivedDiscordMessage.Attachment, byte[]> attachments = new LinkedHashMap<>();
DiscordMessageEmbed.Builder attachmentEmbed = DiscordMessageEmbed.builder().setDescription("Attachments");
for (Map.Entry<GameChannel, BaseChannelConfig> entry : channels.entrySet()) {
BaseChannelConfig channelConfig = entry.getValue();
MirroringConfig config = channelConfig.mirroring;
BaseChannelConfig baseChannelConfig = entry.getValue();
MirroringConfig config = baseChannelConfig.mirroring;
if (!config.enabled) {
continue;
}
@ -114,13 +115,13 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
continue;
}
IChannelConfig iChannelConfig = channelConfig instanceof IChannelConfig ? (IChannelConfig) channelConfig : null;
if (iChannelConfig == null) {
CC channelConfig = baseChannelConfig instanceof IChannelConfig ? (CC) baseChannelConfig : null;
if (channelConfig == null) {
continue;
}
MirroringConfig.AttachmentConfig attachmentConfig = config.attachments;
int maxSize = attachmentConfig.maximumSizeKb;
int maxSize = attachmentConfig.maximumSizeKb * 1000;
boolean embedAttachments = attachmentConfig.embedAttachments;
if (maxSize >= 0 || embedAttachments) {
for (ReceivedDiscordMessage.Attachment attachment : message.getAttachments()) {
@ -128,10 +129,11 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
continue;
}
if (maxSize == 0 || attachment.sizeBytes() <= (maxSize * 1000)) {
if (maxSize == 0 || attachment.sizeBytes() <= maxSize) {
Request request = new Request.Builder()
.url(attachment.proxyUrl())
.url(attachment.url())
.get()
.addHeader("Accept", "*/*")
.build();
byte[] bytes = null;
@ -141,7 +143,7 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
bytes = body.bytes();
}
} catch (IOException e) {
discordSRV.logger().error("Failed to download attachment for mirroring", e);
logger().error("Failed to download attachment for mirroring", e);
}
attachments.put(attachment, bytes);
continue;
@ -156,28 +158,29 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
}
}
List<Long> channelIds = iChannelConfig.channelIds();
if (channelIds != null) {
for (Long channelId : channelIds) {
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
if (textChannel != null && textChannel.getId() != channel.getId()) {
mirrorChannels.add(Pair.of(textChannel, config));
}
futures.add(
discordSRV.discordAPI().findOrCreateDestinations(channelConfig, true, true)
.thenApply(messageChannels -> {
List<MirrorTarget> targets = new ArrayList<>();
for (DiscordGuildMessageChannel messageChannel : messageChannels) {
targets.add(new MirrorTarget(messageChannel, config));
}
return new MirrorOperation(message, config, targets);
})
);
}
discordSRV.discordAPI().findOrCreateThreads(channelConfig, iChannelConfig, threadChannel -> {
if (threadChannel.getId() != channel.getId()) {
mirrorChannels.add(Pair.of(threadChannel, config));
}
}, futures, false);
CompletableFutureUtil.combine(futures).whenComplete((lists, t) -> {
for (MirrorOperation operation : lists) {
List<CompletableFuture<MirroredMessage>> mirrorFutures = new ArrayList<>();
for (MirrorTarget target : operation.targets) {
DiscordGuildMessageChannel mirrorChannel = target.targetChannel;
if (mirrorChannel.getId() == event.getChannel().getId()) {
continue;
}
CompletableFutureUtil.combine(futures).whenComplete((v, t) -> {
List<CompletableFuture<Pair<ReceivedDiscordMessage, MirroringConfig>>> messageFutures = new ArrayList<>();
for (Pair<DiscordGuildMessageChannel, MirroringConfig> pair : mirrorChannels) {
DiscordGuildMessageChannel mirrorChannel = pair.getKey();
MirroringConfig config = pair.getValue();
MirroringConfig config = target.config;
MirroringConfig.AttachmentConfig attachmentConfig = config.attachments;
SendableDiscordMessage.Builder messageBuilder = convert(event.getDiscordMessage(), mirrorChannel, config);
@ -185,85 +188,164 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
messageBuilder.addEmbed(attachmentEmbed.build());
}
int maxSize = attachmentConfig.maximumSizeKb;
Map<String, InputStream> currentAttachments;
if (!attachments.isEmpty() && maxSize > 0) {
currentAttachments = new LinkedHashMap<>();
int maxSize = attachmentConfig.maximumSizeKb * 1000;
List<InputStream> streams = new ArrayList<>();
if (!attachments.isEmpty() && maxSize >= 0) {
attachments.forEach((attachment, bytes) -> {
if (bytes != null && attachment.sizeBytes() <= maxSize) {
currentAttachments.put(attachment.fileName(), new ByteArrayInputStream(bytes));
if (bytes != null && (maxSize == 0 || attachment.sizeBytes() <= maxSize)) {
InputStream stream = new ByteArrayInputStream(bytes);
streams.add(stream);
messageBuilder.addAttachment(stream, attachment.fileName());
}
});
} else {
currentAttachments = Collections.emptyMap();
}
CompletableFuture<Pair<ReceivedDiscordMessage, MirroringConfig>> future =
mirrorChannel.sendMessage(messageBuilder.build(), currentAttachments)
.thenApply(msg -> Pair.of(msg, config));
if (messageBuilder.isEmpty()) {
logger().debug("Nothing to mirror to " + mirrorChannel + ", skipping");
for (InputStream stream : streams) {
try {
stream.close();
} catch (IOException ignored) {}
}
return;
}
messageFutures.add(future);
CompletableFuture<MirroredMessage> future =
mirrorChannel.sendMessage(messageBuilder.build())
.thenApply(msg -> new MirroredMessage(msg, config));
mirrorFutures.add(future);
future.exceptionally(t2 -> {
discordSRV.logger().error("Failed to mirror message to " + mirrorChannel, t2);
logger().error("Failed to mirror message to " + mirrorChannel, t2);
for (InputStream stream : streams) {
try {
stream.close();
} catch (IOException ignored) {}
}
return null;
});
}
CompletableFutureUtil.combine(messageFutures).whenComplete((messages, t2) -> {
Map<Long, MessageReference> references = new LinkedHashMap<>();
for (Pair<ReceivedDiscordMessage, MirroringConfig> pair : messages) {
ReceivedDiscordMessage msg = pair.getKey();
references.put(msg.getChannel().getId(), getReference(msg, pair.getValue()));
CompletableFutureUtil.combine(mirrorFutures).whenComplete((messages, t2) -> {
MessageReference reference = getReference(operation.originalMessage, operation.configForOriginalMessage);
Map<ReceivedDiscordMessage, MessageReference> references = new LinkedHashMap<>();
references.put(message, reference);
for (MirroredMessage mirroredMessage : messages) {
references.put(mirroredMessage.message, getReference(mirroredMessage));
}
mapping.put(getCacheKey(message), new Mirror(getReference(message, null), references));
putIntoCache(reference, references);
});
}
}).exceptionally(t -> {
logger().error("Failed to mirror message", t);
return null;
});
}
@Subscribe
public void onDiscordMessageUpdate(DiscordMessageUpdateEvent event) {
ReceivedDiscordMessage message = event.getMessage();
Mirror mirror = mapping.get(getCacheKey(message), k -> null);
if (mirror == null) {
Cache<Long, Sync> syncs = mapping.getIfPresent(event.getChannel().getId());
if (syncs == null) {
return;
}
for (MessageReference reference : mirror.mirrors.values()) {
ReceivedDiscordMessage message = event.getMessage();
Sync sync = syncs.getIfPresent(message.getId());
if (sync == null || sync.original == null || !sync.original.isMatching(message)) {
return;
}
for (MessageReference reference : sync.mirrors) {
DiscordGuildMessageChannel channel = reference.getMessageChannel(discordSRV);
if (channel == null) {
continue;
}
SendableDiscordMessage sendableMessage = convert(message, channel, reference.config).build();
channel.editMessageById(reference.messageId, sendableMessage).whenComplete((v, t) -> {
if (t != null) {
discordSRV.logger().error("Failed to update mirrored message in " + channel);
}
channel.editMessageById(reference.messageId, sendableMessage).exceptionally(t -> {
logger().error("Failed to update mirrored message in " + channel);
return null;
});
}
}
@Subscribe
public void onDiscordMessageDelete(DiscordMessageDeleteEvent event) {
Mirror mirror = mapping.get(getCacheKey(event.getChannel(), event.getMessageId()), k -> null);
if (mirror == null) {
Cache<Long, Sync> syncs = mapping.getIfPresent(event.getChannel().getId());
if (syncs == null) {
return;
}
for (MessageReference reference : mirror.mirrors.values()) {
Sync sync = syncs.getIfPresent(event.getMessageId());
if (sync == null || sync.original == null || !sync.original.isMatching(event.getChannel())
|| sync.original.messageId != event.getMessageId()) {
return;
}
for (MessageReference reference : sync.mirrors) {
DiscordMessageChannel channel = reference.getMessageChannel(discordSRV);
if (channel == null) {
continue;
}
channel.deleteMessageById(reference.messageId, reference.webhookMessage).whenComplete((v, t) -> {
if (t != null) {
discordSRV.logger().error("Failed to delete mirrored message in " + channel);
}
channel.deleteMessageById(reference.messageId, reference.webhookMessage).exceptionally(t -> {
logger().error("Failed to delete mirrored message in " + channel);
return null;
});
}
}
@Subscribe
public void onGameMessageForwarded(AbstractGameMessageForwardedEvent event) {
Set<? extends ReceivedDiscordMessage> messages = event.getDiscordMessage().getMessages();
Map<ReceivedDiscordMessage, MessageReference> references = new LinkedHashMap<>();
for (ReceivedDiscordMessage message : messages) {
DiscordMessageChannel channel = message.getChannel();
MirroringConfig config = discordSRV.channelConfig().resolve(channel).values().iterator().next().mirroring; // TODO: add channel to event
MessageReference reference = getReference(message, config);
references.put(message, reference);
}
putIntoCache(null, references);
}
@SuppressWarnings("DataFlowIssue") // Supplier always returns a non-null value
@NotNull
private Cache<Long, Sync> getCache(long channelId) {
return mapping.get(
channelId,
k -> discordSRV.caffeineBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()
);
}
private void putIntoCache(@Nullable MessageReference original, Map<ReceivedDiscordMessage, MessageReference> references) {
if (original == null && references.size() <= 1) {
return;
}
for (Map.Entry<ReceivedDiscordMessage, MessageReference> entry : references.entrySet()) {
ReceivedDiscordMessage message = entry.getKey();
MessageReference reference = entry.getValue();
List<MessageReference> refs = new ArrayList<>();
for (MessageReference ref : references.values()) {
if (ref == reference) {
continue;
}
refs.add(ref);
}
getCache(message.getChannel().getId()).put(message.getId(), new Sync(original, refs));
}
}
/**
* Converts a given received message to a sendable message.
*/
@ -274,10 +356,7 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
) {
DiscordGuildMember member = message.getMember();
DiscordUser user = message.getAuthor();
String username = discordSRV.placeholderService().replacePlaceholders(
config.usernameFormat, "%user_effective_name% [M]",
member, user
);
String username = discordSRV.placeholderService().replacePlaceholders(config.usernameFormat, member, user);
if (username.length() > 32) {
username = username.substring(0, 32);
}
@ -288,15 +367,12 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
if (replyMessage != null) {
MessageReference matchingReference = null;
for (Mirror mirror : mapping.asMap().values()) {
if (!mirror.hasMessage(replyMessage)) {
continue;
}
MessageReference ref = mirror.getForChannel(destinationChannel);
if (ref != null) {
matchingReference = ref;
break;
Cache<Long, Sync> syncs = mapping.getIfPresent(replyMessage.getChannel().getId());
if (syncs != null) {
Sync sync = syncs.getIfPresent(replyMessage.getId());
if (sync != null) {
matchingReference = sync.getForChannel(destinationChannel);
}
}
@ -308,8 +384,13 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
) : replyMessage.getJumpUrl();
content = discordSRV.placeholderService()
.replacePlaceholders(config.replyFormat, replyMessage.getMember(), replyMessage.getAuthor())
.replace("%message_jump_url%", jumpUrl) + content;
.replacePlaceholders(
config.replyFormat,
replyMessage.getMember(),
replyMessage.getAuthor(),
new SinglePlaceholder("message_jump_url", jumpUrl),
new SinglePlaceholder("message", content)
);
}
SendableDiscordMessage.Builder builder = SendableDiscordMessage.builder()
@ -321,13 +402,16 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
? member.getEffectiveServerAvatarUrl()
: user.getEffectiveAvatarUrl()
);
builder.getAllowedMentions().clear();
for (DiscordMessageEmbed embed : message.getEmbeds()) {
builder.addEmbed(embed);
}
return builder;
}
private MessageReference getReference(MirroredMessage message) {
return getReference(message.message, message.config);
}
private MessageReference getReference(ReceivedDiscordMessage message, MirroringConfig config) {
return getReference(message.getChannel(), message.getId(), message.isWebhookMessage(), config);
}
@ -348,24 +432,61 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
throw new IllegalStateException("Unexpected channel type: " + channel.getClass().getName());
}
private static String getCacheKey(ReceivedDiscordMessage message) {
return getCacheKey(message.getChannel(), message.getId());
private static class MirrorOperation {
private final ReceivedDiscordMessage originalMessage;
private final MirroringConfig configForOriginalMessage;
private final List<MirrorTarget> targets;
public MirrorOperation(ReceivedDiscordMessage originalMessage, MirroringConfig configForOriginalMessage, List<MirrorTarget> targets) {
this.originalMessage = originalMessage;
this.configForOriginalMessage = configForOriginalMessage;
this.targets = targets;
}
private static String getCacheKey(DiscordMessageChannel channel, long messageId) {
if (channel instanceof DiscordTextChannel) {
return getCacheKey(channel.getId(), 0L, messageId);
} else if (channel instanceof DiscordThreadChannel) {
long parentId = ((DiscordThreadChannel) channel).getParentChannel().getId();
return getCacheKey(parentId, channel.getId(), messageId);
}
throw new IllegalStateException("Unexpected channel type: " + channel.getClass().getName());
}
private static String getCacheKey(long channelId, long threadId, long messageId) {
return Long.toUnsignedString(channelId)
+ (threadId > 0 ? ":" + Long.toUnsignedString(threadId) : "")
+ ":" + Long.toUnsignedString(messageId);
private static class MirrorTarget {
private final DiscordGuildMessageChannel targetChannel;
private final MirroringConfig config;
public MirrorTarget(DiscordGuildMessageChannel targetChannel, MirroringConfig config) {
this.targetChannel = targetChannel;
this.config = config;
}
}
private static class MirroredMessage {
private final ReceivedDiscordMessage message;
private final MirroringConfig config;
public MirroredMessage(ReceivedDiscordMessage message, MirroringConfig config) {
this.message = message;
this.config = config;
}
}
private static class Sync {
private final MessageReference original;
private final List<MessageReference> mirrors;
public Sync(MessageReference original, List<MessageReference> mirrors) {
this.original = original;
this.mirrors = mirrors;
}
public MessageReference getForChannel(DiscordGuildMessageChannel channel) {
for (MessageReference mirror : mirrors) {
if (mirror.isMatching(channel)) {
return mirror;
}
}
return null;
}
}
private static class MessageReference {
@ -424,44 +545,15 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
return null;
}
public boolean isMatching(ReceivedDiscordMessage message) {
return isMatching((DiscordGuildMessageChannel) message.getChannel())
&& message.getId() == messageId;
}
public boolean isMatching(DiscordGuildMessageChannel channel) {
public boolean isMatching(DiscordMessageChannel channel) {
return channel instanceof DiscordThreadChannel
? channel.getId() == threadId
&& ((DiscordThreadChannel) channel).getParentChannel().getId() == channelId
: channel.getId() == channelId;
}
}
private static class Mirror {
private final MessageReference original;
private final Map<Long, MessageReference> mirrors; // thread/channel id -> reference
public Mirror(MessageReference original, Map<Long, MessageReference> mirrors) {
this.original = original;
this.mirrors = mirrors;
}
public boolean hasMessage(ReceivedDiscordMessage message) {
if (original.isMatching(message)) {
return true;
}
MessageReference reference = mirrors.get(message.getChannel().getId());
return reference != null && reference.isMatching(message);
}
public MessageReference getForChannel(DiscordGuildMessageChannel channel) {
long id = channel.getId();
if (original.isMatching(channel)) {
return original;
} else {
return mirrors.get(id);
}
public boolean isMatching(ReceivedDiscordMessage message) {
return isMatching(message.getChannel()) && messageId == message.getId();
}
}
}

View File

@ -20,18 +20,16 @@ package com.discordsrv.common.messageforwarding.game;
import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.event.events.message.receive.game.AbstractGameMessageReceiveEvent;
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.IMessageConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.config.main.generic.IMessageConfig;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageClusterImpl;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.logging.NamedLogger;
@ -44,7 +42,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class AbstractGameMessageModule<T extends IMessageConfig, E extends AbstractGameMessageReceiveEvent> extends AbstractModule<DiscordSRV> {
@ -92,7 +89,8 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
return forwardToChannel(event, srvPlayer, channelConfig);
}
private CompletableFuture<Void> forwardToChannel(
@SuppressWarnings("unchecked") // Wacky generis
private <CC extends BaseChannelConfig & IChannelConfig> CompletableFuture<Void> forwardToChannel(
@Nullable E event,
@Nullable IPlayer player,
@NotNull BaseChannelConfig config
@ -102,37 +100,18 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
return null;
}
IChannelConfig channelConfig = config instanceof IChannelConfig ? (IChannelConfig) config : null;
CC channelConfig = config instanceof IChannelConfig ? (CC) config : null;
if (channelConfig == null) {
return null;
}
List<DiscordMessageChannel> messageChannels = new CopyOnWriteArrayList<>();
List<CompletableFuture<DiscordThreadChannel>> futures = new ArrayList<>();
List<Long> channelIds = channelConfig.channelIds();
if (channelIds != null) {
for (Long channelId : channelConfig.channelIds()) {
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId);
if (textChannel != null) {
messageChannels.add(textChannel);
} else if (channelId > 0) {
discordSRV.logger().error("Unable to find channel with ID "
+ Long.toUnsignedString(channelId)
+ ", unable to forward message to Discord");
}
}
}
discordSRV.discordAPI().findOrCreateThreads(config, channelConfig, messageChannels::add, futures, true);
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenCompose((v) -> {
return discordSRV.discordAPI().findOrCreateDestinations(channelConfig, true, true).thenCompose(messageChannels -> {
SendableDiscordMessage.Builder format = moduleConfig.format();
if (format == null) {
return CompletableFuture.completedFuture(null);
}
Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> messageFutures;
Map<CompletableFuture<ReceivedDiscordMessage>, DiscordGuildMessageChannel> messageFutures;
messageFutures = sendMessageToChannels(
moduleConfig, player, format, messageChannels, event,
// Context
@ -142,7 +121,7 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
return CompletableFuture.allOf(messageFutures.keySet().toArray(new CompletableFuture[0]))
.whenComplete((vo, t2) -> {
Set<ReceivedDiscordMessage> messages = new LinkedHashSet<>();
for (Map.Entry<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> entry : messageFutures.entrySet()) {
for (Map.Entry<CompletableFuture<ReceivedDiscordMessage>, DiscordGuildMessageChannel> entry : messageFutures.entrySet()) {
CompletableFuture<ReceivedDiscordMessage> future = entry.getKey();
if (future.isCompletedExceptionally()) {
future.exceptionally(t -> {
@ -184,11 +163,11 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
return discordSRV.componentFactory().discordSerializer().serialize(component);
}
public Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> sendMessageToChannels(
public Map<CompletableFuture<ReceivedDiscordMessage>, DiscordGuildMessageChannel> sendMessageToChannels(
T config,
IPlayer player,
SendableDiscordMessage.Builder format,
List<DiscordMessageChannel> channels,
List<DiscordGuildMessageChannel> channels,
E event,
Object... context
) {
@ -201,8 +180,8 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
SendableDiscordMessage discordMessage = formatter
.build();
Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> futures = new LinkedHashMap<>();
for (DiscordMessageChannel channel : channels) {
Map<CompletableFuture<ReceivedDiscordMessage>, DiscordGuildMessageChannel> futures = new LinkedHashMap<>();
for (DiscordGuildMessageChannel channel : channels) {
futures.put(channel.sendMessage(discordMessage), channel);
}

View File

@ -28,7 +28,7 @@ import com.discordsrv.api.event.events.message.receive.game.AwardMessageReceiveE
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.AwardMessageConfig;
import com.discordsrv.common.config.main.channels.server.AwardMessageConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.server.ServerBaseChannelConfig;
import com.github.benmanes.caffeine.cache.Cache;
@ -68,7 +68,7 @@ public class AwardMessageModule extends AbstractGameMessageModule<AwardMessageCo
return;
}
if (!checkIfShouldPermit(event.getPlayer())) {
if (checkIfShouldPermit(event.getPlayer())) {
process(event, event.getPlayer(), event.getGameChannel());
}

View File

@ -28,7 +28,7 @@ import com.discordsrv.api.event.events.message.receive.game.DeathMessageReceiveE
import com.discordsrv.api.placeholder.FormattedText;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.DeathMessageConfig;
import com.discordsrv.common.config.main.channels.server.DeathMessageConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.server.ServerBaseChannelConfig;

Some files were not shown because too many files have changed in this diff Show More