From 9ffce7406199724a7c514a35c677a0415c29a126 Mon Sep 17 00:00:00 2001 From: Vankka Date: Mon, 20 Dec 2021 01:03:38 +0200 Subject: [PATCH] Change common listeners to a module system, update to JDA5, fix typos, simplify 1st party api, improved the way mentions are translated between Minecraft and Discord --- .../api/discord/api/DiscordAPI.java | 30 +- .../api/discord/api/entity/DiscordUser.java | 20 +- .../Mentionable.java} | 15 +- .../api/entity/channel/DiscordDMChannel.java | 9 + .../entity/channel/DiscordMessageChannel.java | 8 + .../entity/channel/DiscordTextChannel.java | 14 +- .../api/entity/guild/DiscordGuild.java | 22 ++ .../api/entity/guild/DiscordGuildMember.java | 12 +- .../discord/api/entity/guild/DiscordRole.java | 12 +- .../message/ReceivedDiscordMessage.java | 34 +- .../impl/SendableDiscordMessageImpl.java | 3 +- ... => EntityNoLongerAvailableException.java} | 14 +- .../exception/RestErrorResponseException.java | 10 +- .../api/util/DiscordFormattingUtil.java | 14 +- .../discordsrv/api/event/bus/EventBus.java | 4 +- .../DiscordMessageProcessingEvent.java | 7 +- build.gradle | 4 +- .../discordsrv/common/AbstractDiscordSRV.java | 51 ++- .../com/discordsrv/common/DiscordSRV.java | 13 +- .../common/channel/ChannelConfigHelper.java | 4 +- .../common/component/ComponentFactory.java | 4 +- .../renderer/DiscordSRVMinecraftRenderer.java | 134 ++++++-- .../DiscordToMinecraftChatConfig.java | 40 ++- .../MinecraftToDiscordChatConfig.java | 12 + .../common/discord/api/DiscordAPIImpl.java | 55 ++- .../api/{user => }/DiscordUserImpl.java | 53 ++- .../api/channel/DiscordDMChannelImpl.java | 55 ++- .../channel/DiscordMessageChannelImpl.java | 34 -- .../api/channel/DiscordTextChannelImpl.java | 49 ++- .../discord/api/guild/DiscordGuildImpl.java | 77 +++-- .../api/guild/DiscordGuildMemberImpl.java | 33 +- .../discord/api/guild/DiscordRoleImpl.java | 24 +- .../message/ReceivedDiscordMessageImpl.java | 60 +++- .../connection/jda/JDAConnectionManager.java | 6 +- .../util/EventUtil.java} | 14 +- .../common/listener/GameChatListener.java | 107 ------ .../com/discordsrv/common/module/Module.java | 65 ++++ .../common/module/ModuleManager.java | 104 ++++++ .../modules/DiscordAPIEventModule.java} | 14 +- .../modules/DiscordToMinecraftModule.java} | 14 +- .../modules/GlobalChannelLookupModule.java} | 20 +- .../modules/MinecraftToDiscordModule.java | 315 ++++++++++++++++++ .../common/scheduler/StandardScheduler.java | 2 +- 43 files changed, 1166 insertions(+), 425 deletions(-) rename api/src/main/java/com/discordsrv/api/discord/api/{exception/UnknownMessageException.java => entity/Mentionable.java} (77%) rename api/src/main/java/com/discordsrv/api/discord/api/exception/{UnknownChannelException.java => EntityNoLongerAvailableException.java} (80%) rename common/src/main/java/com/discordsrv/common/discord/api/{user => }/DiscordUserImpl.java (53%) rename common/src/main/java/com/discordsrv/common/{listener/AbstractListener.java => event/util/EventUtil.java} (83%) delete mode 100644 common/src/main/java/com/discordsrv/common/listener/GameChatListener.java create mode 100644 common/src/main/java/com/discordsrv/common/module/Module.java create mode 100644 common/src/main/java/com/discordsrv/common/module/ModuleManager.java rename common/src/main/java/com/discordsrv/common/{listener/DiscordAPIListener.java => module/modules/DiscordAPIEventModule.java} (85%) rename common/src/main/java/com/discordsrv/common/{listener/DiscordChatListener.java => module/modules/DiscordToMinecraftModule.java} (93%) rename common/src/main/java/com/discordsrv/common/{listener/ChannelLookupListener.java => module/modules/GlobalChannelLookupModule.java} (63%) create mode 100644 common/src/main/java/com/discordsrv/common/module/modules/MinecraftToDiscordModule.java diff --git a/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java b/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java index a7292e4d..347cb6bb 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java @@ -23,15 +23,16 @@ package com.discordsrv.api.discord.api; +import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; -import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.guild.DiscordRole; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * A basic Discord API wrapper for a limited amount of functions, with a minimal amount of breaking changes. @@ -39,7 +40,7 @@ import java.util.Optional; public interface DiscordAPI { /** - * Gets a Discord message channel by id, the provided entity can be cached and will not update if it changes on Discord. + * Gets a Discord message channel by id, the provided entity should not be cached. * @param id the id for the message channel * @return the message channel */ @@ -47,7 +48,7 @@ public interface DiscordAPI { Optional getMessageChannelById(long id); /** - * Gets a Discord direct message channel by id, the provided entity can be cached and will not update if it changes on Discord. + * Gets a Discord direct message channel by id, the provided entity should not be cached. * @param id the id for the direct message channel * @return the direct message channel */ @@ -55,7 +56,7 @@ public interface DiscordAPI { Optional getDirectMessageChannelById(long id); /** - * Gets a Discord text channel by id, the provided entity can be cached and will not update if it changes on Discord. + * Gets a Discord text channel by id, the provided entity should not be cached. * @param id the id for the text channel * @return the text channel */ @@ -63,7 +64,7 @@ public interface DiscordAPI { Optional getTextChannelById(long id); /** - * Gets a Discord server by id, the provided entity can be cached and will not update if it changes on Discord. + * Gets a Discord server by id, the provided entity should not be cached. * @param id the id for the Discord server * @return the Discord server */ @@ -71,15 +72,30 @@ public interface DiscordAPI { Optional getGuildById(long id); /** - * Gets a Discord user by id, the provided entity can be cached and will not update if it changes on Discord. + * Gets a Discord user by id, the provided entity should not be cached. + * This will always return an empty optional if {@link #isUserCachingEnabled()} returns {@code false}. * @param id the id for the Discord user * @return the Discord user + * @see #isUserCachingEnabled() */ @NotNull Optional getUserById(long id); /** - * Gets a Discord role by id, the provided entity can be cached and will not update if it changes on Discord. + * Looks up a Discord user by id from Discord, the provided entity can be cached but will not be updated if the entity changes on Discord. + * @param id the id for the Discord user + * @return a future that will result in a {@link DiscordUser} for the id or throw a + */ + CompletableFuture retrieveUserById(long id); + + /** + * Gets if user caching is enabled. + * @return {@code true} if user caching is enabled. + */ + boolean isUserCachingEnabled(); + + /** + * Gets a Discord role by id, the provided entity should not be cached. * @param id the id for the Discord role * @return the Discord role */ diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/DiscordUser.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/DiscordUser.java index 20a884ce..bb1b91f3 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/DiscordUser.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/DiscordUser.java @@ -23,13 +23,18 @@ package com.discordsrv.api.discord.api.entity; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.placeholder.annotation.Placeholder; +import net.dv8tion.jda.api.entities.User; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.CompletableFuture; + /** * A Discord user. */ -public interface DiscordUser extends Snowflake { +public interface DiscordUser extends Snowflake, Mentionable { /** * Gets if this user is the bot this DiscordSRV instance is connected. @@ -67,4 +72,17 @@ public interface DiscordUser extends Snowflake { default String getAsTag() { return getUsername() + "#" + getDiscriminator(); } + + /** + * Opens a private channel with the user or instantly returns the already cached private channel for this user. + * @return a future for the private channel with this Discord user + */ + CompletableFuture openPrivateChannel(); + + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + User getAsJDAUser(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownMessageException.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/Mentionable.java similarity index 77% rename from api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownMessageException.java rename to api/src/main/java/com/discordsrv/api/discord/api/entity/Mentionable.java index 795afc09..ebd0f1d9 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownMessageException.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/Mentionable.java @@ -21,18 +21,9 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.api.exception; +package com.discordsrv.api.discord.api.entity; -import net.dv8tion.jda.api.requests.ErrorResponse; - -public class UnknownMessageException extends RestErrorResponseException { - - public UnknownMessageException() { - super(-1); - } - - public UnknownMessageException(Throwable cause) { - super(ErrorResponse.UNKNOWN_MESSAGE.getCode(), cause); - } +public interface Mentionable { + String getAsMention(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordDMChannel.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordDMChannel.java index 52784dda..06b3a84b 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordDMChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordDMChannel.java @@ -23,7 +23,9 @@ package com.discordsrv.api.discord.api.entity.channel; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.discord.api.entity.DiscordUser; +import net.dv8tion.jda.api.entities.PrivateChannel; /** * A Discord direct message channel. @@ -36,4 +38,11 @@ public interface DiscordDMChannel extends DiscordMessageChannel { */ DiscordUser getUser(); + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + PrivateChannel getAsJDAPrivateChannel(); + } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java index 8a004c1b..253b8cf6 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java @@ -23,9 +23,11 @@ package com.discordsrv.api.discord.api.entity.channel; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.discord.api.entity.Snowflake; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; +import net.dv8tion.jda.api.entities.MessageChannel; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; @@ -64,4 +66,10 @@ public interface DiscordMessageChannel extends Snowflake { @NotNull CompletableFuture editMessageById(long id, SendableDiscordMessage message); + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + MessageChannel getAsJDAMessageChannel(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordTextChannel.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordTextChannel.java index e0b7a8a3..b7d0fd02 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordTextChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordTextChannel.java @@ -23,13 +23,17 @@ package com.discordsrv.api.discord.api.entity.channel; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.api.entity.Mentionable; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; +import net.dv8tion.jda.api.entities.TextChannel; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * A Discord text channel. */ -public interface DiscordTextChannel extends DiscordMessageChannel { +public interface DiscordTextChannel extends DiscordMessageChannel, Mentionable { /** * Gets the name of the text channel. @@ -42,7 +46,7 @@ public interface DiscordTextChannel extends DiscordMessageChannel { * Gets the topic of the text channel. * @return the topic of the channel */ - @NotNull + @Nullable String getTopic(); /** @@ -52,5 +56,11 @@ public interface DiscordTextChannel extends DiscordMessageChannel { @NotNull DiscordGuild getGuild(); + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + TextChannel getAsJDATextChannel(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuild.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuild.java index ba81c7c8..bcebad44 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuild.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuild.java @@ -23,10 +23,14 @@ package com.discordsrv.api.discord.api.entity.guild; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.discord.api.entity.Snowflake; import com.discordsrv.api.placeholder.annotation.Placeholder; +import net.dv8tion.jda.api.entities.Guild; +import java.util.List; import java.util.Optional; +import java.util.Set; /** * A Discord server. @@ -54,6 +58,12 @@ public interface DiscordGuild extends Snowflake { */ Optional getMemberById(long id); + /** + * Gets the members of this server that are in the cache. + * @return the Discord server members that are currently cached + */ + Set getCachedMembers(); + /** * Gets a Discord role by id from the cache, the provided entity can be cached and will not update if it changes on Discord. * @param id the id for the Discord role @@ -61,4 +71,16 @@ public interface DiscordGuild extends Snowflake { */ Optional getRoleById(long id); + /** + * Gets the roles in this Discord server. + * @return an ordered list of the roles in this Discord server + */ + List getRoles(); + + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + Guild getAsJDAGuild(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java index 9a62613a..e6fbc92a 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordGuildMember.java @@ -23,9 +23,12 @@ package com.discordsrv.api.discord.api.entity.guild; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.color.Color; import com.discordsrv.api.discord.api.entity.DiscordUser; +import com.discordsrv.api.discord.api.entity.Mentionable; import com.discordsrv.api.placeholder.annotation.Placeholder; +import net.dv8tion.jda.api.entities.Member; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -34,7 +37,7 @@ import java.util.Optional; /** * A Discord server member. */ -public interface DiscordGuildMember extends DiscordUser { +public interface DiscordGuildMember extends DiscordUser, Mentionable { /** * Gets the Discord server this member is from. @@ -71,4 +74,11 @@ public interface DiscordGuildMember extends DiscordUser { @Placeholder("user_color") Color getColor(); + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + Member getAsJDAMember(); + } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java index 477f1193..2971fce1 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/guild/DiscordRole.java @@ -23,15 +23,18 @@ package com.discordsrv.api.discord.api.entity.guild; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.color.Color; +import com.discordsrv.api.discord.api.entity.Mentionable; import com.discordsrv.api.discord.api.entity.Snowflake; import com.discordsrv.api.placeholder.annotation.Placeholder; +import net.dv8tion.jda.api.entities.Role; import org.jetbrains.annotations.NotNull; /** * A Discord server role. */ -public interface DiscordRole extends Snowflake { +public interface DiscordRole extends Snowflake, Mentionable { /** * The default {@link DiscordRole} color. @@ -68,4 +71,11 @@ public interface DiscordRole extends Snowflake { * @return true if this role is displayed separately in the member list */ boolean isHoisted(); + + /** + * Returns the JDA representation of this object. This should not be used if it can be avoided. + * @return the JDA representation of this object + * @see DiscordSRVApi#jda() + */ + Role getAsJDARole(); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/ReceivedDiscordMessage.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/ReceivedDiscordMessage.java index 33c4a538..d3ca6a32 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/ReceivedDiscordMessage.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/ReceivedDiscordMessage.java @@ -32,6 +32,7 @@ import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -40,6 +41,12 @@ import java.util.concurrent.CompletableFuture; */ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflake { + /** + * Gets the attachments of this message. + * @return this message's attachments + */ + List getAttachments(); + /** * Determines if this message was sent by this DiscordSRV instance's Discord bot, * or a webhook being used by this DiscordSRV instance. @@ -62,14 +69,14 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak /** * Gets the text channel the message was sent in. Not present if this message is a dm. - * @return a optional potentially containing the text channel the message was sent in + * @return an optional potentially containing the text channel the message was sent in */ @NotNull Optional getTextChannel(); /** * Gets the dm channel the message was sent in. Not present if this message was sent in a server. - * @return a optional potentially containing the dm channel the message was sent in + * @return an optional potentially containing the dm channel the message was sent in */ @NotNull Optional getDMChannel(); @@ -77,14 +84,14 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak /** * Gets the Discord server member that sent this message. * This is not present if the message was sent by a webhook. - * @return a optional potentially containing the Discord server member that sent this message + * @return an optional potentially containing the Discord server member that sent this message */ @NotNull Optional getMember(); /** * Gets the Discord server the message was posted in. This is not present if the message was a dm. - * @return a optional potentially containing the Discord server the message was posted in + * @return an optional potentially containing the Discord server the message was posted in */ @NotNull default Optional getGuild() { @@ -109,4 +116,23 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak */ @NotNull CompletableFuture edit(SendableDiscordMessage message); + + class Attachment { + + private final String fileName; + private final String url; + + public Attachment(String fileName, String url) { + this.fileName = fileName; + this.url = url; + } + + public String fileName() { + return fileName; + } + + public String url() { + return url; + } + } } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java index 3191a537..bbff2b29 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java @@ -214,8 +214,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage { throw new IllegalStateException("DiscordSRVApi not available"); } this.replacements.put(PlaceholderService.PATTERN, - wrapFunction(matcher -> - api.placeholderService().getResultAsString(matcher, context))); + wrapFunction(matcher -> api.placeholderService().getResultAsString(matcher, context))); return this; } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownChannelException.java b/api/src/main/java/com/discordsrv/api/discord/api/exception/EntityNoLongerAvailableException.java similarity index 80% rename from api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownChannelException.java rename to api/src/main/java/com/discordsrv/api/discord/api/exception/EntityNoLongerAvailableException.java index 11018a56..5f82a026 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/exception/UnknownChannelException.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/exception/EntityNoLongerAvailableException.java @@ -23,16 +23,4 @@ package com.discordsrv.api.discord.api.exception; -import net.dv8tion.jda.api.requests.ErrorResponse; - -public class UnknownChannelException extends RestErrorResponseException { - - public UnknownChannelException() { - super(-1); - } - - public UnknownChannelException(Throwable cause) { - super(ErrorResponse.UNKNOWN_CHANNEL.getCode(), cause); - } - -} +public class EntityNoLongerAvailableException extends Exception {} diff --git a/api/src/main/java/com/discordsrv/api/discord/api/exception/RestErrorResponseException.java b/api/src/main/java/com/discordsrv/api/discord/api/exception/RestErrorResponseException.java index 5a509724..00d90a4b 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/exception/RestErrorResponseException.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/exception/RestErrorResponseException.java @@ -23,16 +23,18 @@ package com.discordsrv.api.discord.api.exception; +import net.dv8tion.jda.api.requests.ErrorResponse; + public class RestErrorResponseException extends RuntimeException { private final int errorCode; - public RestErrorResponseException(int errorCode) { - this.errorCode = errorCode; + public RestErrorResponseException(ErrorResponse response) { + this(response.getCode(), response.getMeaning(), new EntityNoLongerAvailableException()); } - public RestErrorResponseException(int errorCode, Throwable cause) { - super(cause); + public RestErrorResponseException(int errorCode, String message, Throwable cause) { + super(message + " (" + errorCode + ")", cause); this.errorCode = errorCode; } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java b/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java index b4115674..1fb68db0 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java @@ -23,12 +23,16 @@ package com.discordsrv.api.discord.api.util; +import java.util.regex.Matcher; + public final class DiscordFormattingUtil { private DiscordFormattingUtil() {} public static String escapeContent(String content) { - content = escapeChars(content, '*', '_', '|', '`', '~', '>'); + content = escapeChars(content, '*', '_', '|', '`', '~'); + content = escapeQuote(content); + content = escapeMentions(content); return content; } @@ -40,4 +44,12 @@ public final class DiscordFormattingUtil { } return input; } + + private static String escapeQuote(String input) { + return input.replaceAll("^>", Matcher.quoteReplacement("\\>")); + } + + private static String escapeMentions(String input) { + return input.replaceAll("<([@#])", Matcher.quoteReplacement("\\<") + "$1"); + } } diff --git a/api/src/main/java/com/discordsrv/api/event/bus/EventBus.java b/api/src/main/java/com/discordsrv/api/event/bus/EventBus.java index 2ef901b3..8d9dfae5 100644 --- a/api/src/main/java/com/discordsrv/api/event/bus/EventBus.java +++ b/api/src/main/java/com/discordsrv/api/event/bus/EventBus.java @@ -35,7 +35,7 @@ public interface EventBus { /** * Subscribes the provided event listener to this {@link EventBus}. - * @param eventListener a event listener with at least one valid {@link Subscribe} method. + * @param eventListener an event listener with at least one valid {@link Subscribe} method. * * @throws IllegalArgumentException if the given listener does not contain any valid listeners */ @@ -43,7 +43,7 @@ public interface EventBus { /** * Unsubscribes a listener that was registered before. - * @param eventListener a listener that was subscribed with {@link #subscribe(Object)} before + * @param eventListener an event listener that was subscribed with {@link #subscribe(Object)} before */ void unsubscribe(@NotNull Object eventListener); diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordMessageProcessingEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordMessageProcessingEvent.java index 2eb6b955..105281ef 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordMessageProcessingEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordMessageProcessingEvent.java @@ -24,6 +24,7 @@ package com.discordsrv.api.event.events.message.receive.discord; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; @@ -33,7 +34,7 @@ public class DiscordMessageProcessingEvent implements Cancellable, Processable { private final ReceivedDiscordMessage discordMessage; private String messageContent; - private DiscordTextChannel channel; + private final DiscordTextChannel channel; private boolean cancelled; private boolean processed; @@ -59,8 +60,8 @@ public class DiscordMessageProcessingEvent implements Cancellable, Processable { return channel; } - public void setChannel(DiscordTextChannel channel) { - this.channel = channel; + public DiscordGuild getGuild() { + return channel.getGuild(); } @Override diff --git a/build.gradle b/build.gradle index f16f85ce..ce2cee6b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ ext { // MinecraftDependencyDownload mddVersion = '1.0.0-SNAPSHOT' // JDA - jdaVersion = '4.3.0_334' + jdaVersion = '5.0.0-alpha.2' // Configurate configurateVersion = '4.1.2' // Adventure & Adventure Platform @@ -59,7 +59,7 @@ allprojects { mavenCentral() maven { url 'https://nexus.scarsz.me/content/groups/public/' } - maven { url 'https://m2.dv8tion.net/releases/' } + //maven { url 'https://m2.dv8tion.net/releases/' } } dependencies { diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 502c10e3..ca1bde50 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -24,7 +24,6 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; import com.discordsrv.common.api.util.ApiInstanceUtil; import com.discordsrv.common.channel.ChannelConfigHelper; -import com.discordsrv.common.channel.DefaultGlobalChannel; import com.discordsrv.common.component.ComponentFactory; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; @@ -36,11 +35,13 @@ import com.discordsrv.common.discord.connection.jda.JDAConnectionManager; import com.discordsrv.common.discord.details.DiscordConnectionDetailsImpl; import com.discordsrv.common.event.bus.EventBusImpl; import com.discordsrv.common.function.CheckedRunnable; -import com.discordsrv.common.listener.ChannelLookupListener; -import com.discordsrv.common.listener.DiscordAPIListener; -import com.discordsrv.common.listener.DiscordChatListener; -import com.discordsrv.common.listener.GameChatListener; import com.discordsrv.common.logging.DependencyLoggingHandler; +import com.discordsrv.common.module.Module; +import com.discordsrv.common.module.ModuleManager; +import com.discordsrv.common.module.modules.DiscordAPIEventModule; +import com.discordsrv.common.module.modules.DiscordToMinecraftModule; +import com.discordsrv.common.module.modules.GlobalChannelLookupModule; +import com.discordsrv.common.module.modules.MinecraftToDiscordModule; import com.discordsrv.common.placeholder.ComponentResultStringifier; import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext; @@ -49,6 +50,7 @@ import net.dv8tion.jda.api.JDA; import org.jetbrains.annotations.NotNull; import javax.annotation.OverridingMethodsMustInvokeSuper; +import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -73,8 +75,8 @@ public abstract class AbstractDiscordSRV T getModule(Class moduleType) { + return moduleManager.getModule(moduleType); + } + + @Override + public void registerModule(Module module) { + moduleManager.register(module); + } + + @Override + public void unregisterModule(Module module) { + moduleManager.unregister(module); + } + @Override public Locale locale() { // TODO: config @@ -250,13 +262,16 @@ public abstract class AbstractDiscordSRV configManager(); MainConfig config(); + // Config helper + ChannelConfigHelper channelConfig(); // Internal - DefaultGlobalChannel defaultGlobalChannel(); - ChannelConfigHelper channelConfig(); DiscordConnectionManager discordConnectionManager(); + // Modules + T getModule(Class moduleType); + void registerModule(Module module); + void unregisterModule(Module module); + Locale locale(); void setStatus(Status status); diff --git a/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java b/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java index f8dc35ef..8367265f 100644 --- a/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java +++ b/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java @@ -88,9 +88,7 @@ public class ChannelConfigHelper { synchronized (discordToConfigMap) { discordToConfigMap.clear(); - for (Map.Entry> entry : newMap.entrySet()) { - discordToConfigMap.put(entry.getKey(), entry.getValue()); - } + discordToConfigMap.putAll(newMap); } } diff --git a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java index 7635c8aa..9c00749e 100644 --- a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java +++ b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java @@ -38,8 +38,8 @@ public class ComponentFactory implements MinecraftComponentFactory { public ComponentFactory(DiscordSRV discordSRV) { this.discordSRV = discordSRV; this.minecraftSerializer = new MinecraftSerializer( - MinecraftSerializerOptions.defaults().addRenderer(new DiscordSRVMinecraftRenderer(discordSRV)), - MinecraftSerializerOptions.escapeDefaults() + MinecraftSerializerOptions.defaults() + .addRenderer(new DiscordSRVMinecraftRenderer(discordSRV)) ); this.discordSerializer = new DiscordSerializer(DiscordSerializerOptions.defaults()); } diff --git a/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java b/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java index 6b1cf3b3..41ca24a3 100644 --- a/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java +++ b/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java @@ -18,79 +18,143 @@ package com.discordsrv.common.component.renderer; +import com.discordsrv.api.component.EnhancedTextBuilder; +import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.api.entity.guild.DiscordRole; +import com.discordsrv.api.event.events.message.receive.discord.DiscordMessageProcessingEvent; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; +import com.discordsrv.common.function.OrDefault; import dev.vankka.mcdiscordreserializer.renderer.implementation.DefaultMinecraftRenderer; import lombok.NonNull; -import net.dv8tion.jda.api.entities.AbstractChannel; +import net.dv8tion.jda.api.entities.GuildChannel; import net.dv8tion.jda.api.utils.MiscUtil; import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; -import java.util.Optional; import java.util.function.Supplier; public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { - private static final ThreadLocal GUILD_CONTEXT = ThreadLocal.withInitial(() -> 0L); + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); private final DiscordSRV discordSRV; public DiscordSRVMinecraftRenderer(DiscordSRV discordSRV) { this.discordSRV = discordSRV; } - public static void runInGuildContext(long guildId, Runnable runnable) { - getWithGuildContext(guildId, () -> { + public static void runInContext( + DiscordMessageProcessingEvent event, + OrDefault config, + Runnable runnable + ) { + getWithContext(event, config, () -> { runnable.run(); return null; }); } - public static T getWithGuildContext(long guildId, Supplier supplier) { - GUILD_CONTEXT.set(guildId); + public static T getWithContext( + DiscordMessageProcessingEvent event, + OrDefault config, + Supplier supplier + ) { + CONTEXT.set(new Context(event, config)); T output = supplier.get(); - GUILD_CONTEXT.set(0L); + CONTEXT.remove(); return output; } @Override - public @Nullable Component appendChannelMention(@NonNull Component component, @NonNull String id) { - return component.append(Component.text( - discordSRV.jda() - .map(jda -> jda.getGuildChannelById(id)) - .map(AbstractChannel::getName) - .map(name -> "#" + name) - .orElse("<#" + id + ">") + public @NotNull Component appendChannelMention(@NonNull Component component, @NonNull String id) { + Context context = CONTEXT.get(); + DiscordToMinecraftChatConfig.Mentions.Format format = + context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.channel) : null; + if (format == null) { + return component.append(Component.text("<#" + id + ">")); + } + + GuildChannel guildChannel = discordSRV.jda() + .map(jda -> jda.getGuildChannelById(id)) + .orElse(null); + + return component.append(ComponentUtil.fromAPI( + discordSRV.componentFactory() + .enhancedBuilder(guildChannel != null ? format.format : format.unknownFormat) + .addReplacement("%channel_name%", guildChannel != null ? guildChannel.getName() : null) + .applyPlaceholderService() + .build() )); } @Override - public @Nullable Component appendUserMention(@NonNull Component component, @NonNull String id) { - long guildId = GUILD_CONTEXT.get(); - Optional guild = guildId > 0 - ? discordSRV.discordAPI().getGuildById(guildId) - : Optional.empty(); + public @NotNull Component appendUserMention(@NonNull Component component, @NonNull String id) { + Context context = CONTEXT.get(); + DiscordToMinecraftChatConfig.Mentions.Format format = + context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.user) : null; + DiscordGuild guild = context != null + ? discordSRV.discordAPI() + .getGuildById(context.event.getGuild().getId()) + .orElse(null) + : null; + if (format == null || guild == null) { + return component.append(Component.text("<@" + id + ">")); + } long userId = MiscUtil.parseLong(id); - return component.append(Component.text( - guild.flatMap(g -> g.getMemberById(userId)) - .map(member -> "@" + member.getEffectiveName()) - .orElseGet(() -> discordSRV.discordAPI() - .getUserById(userId) - .map(user -> "@" + user.getUsername()) - .orElse("<@" + id + ">")) + DiscordUser user = discordSRV.discordAPI().getUserById(userId).orElse(null); + DiscordGuildMember member = guild.getMemberById(userId).orElse(null); + + EnhancedTextBuilder builder = discordSRV.componentFactory() + .enhancedBuilder(user != null ? format.format : format.unknownFormat); + + if (user != null) { + builder.addContext(user); + } + if (member != null) { + builder.addContext(member); + } + + return component.append(ComponentUtil.fromAPI( + builder.applyPlaceholderService().build() )); } @Override - public @Nullable Component appendRoleMention(@NonNull Component component, @NonNull String id) { - return component.append(Component.text( - discordSRV.discordAPI() - .getRoleById(MiscUtil.parseLong(id)) - .map(DiscordRole::getName) - .map(name -> "@" + name) - .orElse("<@" + id + ">") + public @NotNull Component appendRoleMention(@NonNull Component component, @NonNull String id) { + Context context = CONTEXT.get(); + DiscordToMinecraftChatConfig.Mentions.Format format = + context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.role) : null; + if (format == null) { + return component.append(Component.text("<#" + id + ">")); + } + + long roleId = MiscUtil.parseLong(id); + DiscordRole role = discordSRV.discordAPI().getRoleById(roleId).orElse(null); + + EnhancedTextBuilder builder = discordSRV.componentFactory() + .enhancedBuilder(role != null ? format.format : format.unknownFormat); + + if (role != null) { + builder.addContext(role); + } + + return component.append(ComponentUtil.fromAPI( + builder.applyPlaceholderService().build() )); } + + private static class Context { + + private final DiscordMessageProcessingEvent event; + private final OrDefault config; + + public Context(DiscordMessageProcessingEvent event, OrDefault config) { + this.event = event; + this.config = config; + } + } } diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java index 411ffe73..72d254ae 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java @@ -31,20 +31,21 @@ import java.util.regex.Pattern; public class DiscordToMinecraftChatConfig { @Comment("The Discord to Minecraft message format for regular users") - public String format = "[ᛩF2Discord&r] [hover:show_text:Tag: %user_tag%&r\\nRoles: %user_roles_, |text_&7&oNone%%]%user_color%%user_effective_name%&r » %message%"; + public String format = "[ᛩF2Discord&r] [hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles_, |text_&7&oNone%]%user_color%%user_effective_name%&r » %message% %message_attachments%"; @Comment("The Discord to Minecraft message format for webhook messages (if enabled)") - public String webhookFormat = "[ᛩF2Discord&r] [hover:show_text:Webhook message]%user_name%&r » %message%"; - - @Comment("Users, bots and webhooks to ignore") - public Ignores ignores = new Ignores(); + public String webhookFormat = "[ᛩF2Discord&r] [hover:show_text:Webhook message]%user_name%&r » %message% %message_attachments%"; // TODO: more info on regex pairs (String#replaceAll) @Comment("Regex filters for Discord message contents (this is the %message% part of the \"format\" option)") public Map contentRegexFilters = new LinkedHashMap<>(); + @Comment("Users, bots and webhooks to ignore") + public Ignores ignores = new Ignores(); + @ConfigSerializable public static class Ignores { + @Comment("User, bot and webhook ids to ignore") public IDs usersAndWebhookIds = new IDs(); @@ -67,4 +68,33 @@ public class DiscordToMinecraftChatConfig { } } + @Comment("The representations of Discord mentions in-game") + public Mentions mentions = new Mentions(); + + @ConfigSerializable + public static class Mentions { + + public Format role = new Format("ᛩf2@%role_name%", "ᛩf2@deleted-role"); + public Format channel = new Format("ᛩf2#%channel_name%", "ᛩf2#deleted-channel"); + public Format user = new Format("[hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles_, |text_&7&oNone%]ᛩf2@%user_effective_name|user_name%", "ᛩf2@Unknown user"); + + @ConfigSerializable + public static class Format { + + @Comment("The format shown in-game") + public String format = ""; + + @Comment("The format when the entity is deleted or can't be looked up") + public String unknownFormat = ""; + + public Format() {} + + public Format(String format, String unknownFormat) { + this.format = format; + this.unknownFormat = unknownFormat; + } + } + + } + } diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/MinecraftToDiscordChatConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/MinecraftToDiscordChatConfig.java index 1c654425..a72c515b 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/MinecraftToDiscordChatConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/MinecraftToDiscordChatConfig.java @@ -37,5 +37,17 @@ public class MinecraftToDiscordChatConfig { // TODO: more info on regex pairs (String#replaceAll) @Comment("Regex filters for Minecraft message contents (this is the %message% part of the \"format\" option)") public Map contentRegexFilters = new LinkedHashMap<>(); + + @Comment("What mentions should be translated from chat messages to mentions (this does not effect if they will cause a notification or not)") + public Mentions mentions = new Mentions(); + + @ConfigSerializable + public static class Mentions { + + public boolean roles = true; + public boolean users = true; + public boolean channels = true; + + } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java index 3e6dab28..2f180de1 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java @@ -28,7 +28,7 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.guild.DiscordRole; import com.discordsrv.api.discord.api.exception.NotReadyException; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; +import com.discordsrv.api.discord.api.exception.RestErrorResponseException; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig; @@ -36,7 +36,6 @@ import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl; import com.discordsrv.common.discord.api.channel.DiscordTextChannelImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.guild.DiscordRoleImpl; -import com.discordsrv.common.discord.api.user.DiscordUserImpl; import com.github.benmanes.caffeine.cache.AsyncCacheLoader; import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.github.benmanes.caffeine.cache.Expiry; @@ -45,6 +44,9 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.Webhook; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; +import net.dv8tion.jda.api.requests.GatewayIntent; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; @@ -80,6 +82,26 @@ public class DiscordAPIImpl implements DiscordAPI { return cachedClients; } + public CompletableFuture mapExceptions(CompletableFuture future) { + return future.handle((msg, t) -> { + if (t instanceof ErrorResponseException) { + ErrorResponseException exception = (ErrorResponseException) t; + int code = exception.getErrorCode(); + ErrorResponse response = exception.getErrorResponse(); + throw new RestErrorResponseException(code, response != null ? response.getMeaning() : "Unknown", t); + } else if (t != null) { + throw (RuntimeException) t; + } + return msg; + }); + } + + public CompletableFuture notReady() { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new NotReadyException()); + return future; + } + @Override public @NotNull Optional getMessageChannelById(long id) { Optional textChannel = getTextChannelById(id); @@ -115,7 +137,26 @@ public class DiscordAPIImpl implements DiscordAPI { public @NotNull Optional getUserById(long id) { return discordSRV.jda() .map(jda -> jda.getUserById(id)) - .map(DiscordUserImpl::new); + .map(user -> new DiscordUserImpl(discordSRV, user)); + } + + @Override + public CompletableFuture retrieveUserById(long id) { + JDA jda = discordSRV.jda().orElse(null); + if (jda == null) { + return notReady(); + } + + return jda.retrieveUserById(id) + .submit() + .thenApply(user -> new DiscordUserImpl(discordSRV, user)); + } + + @Override + public boolean isUserCachingEnabled() { + return discordSRV.discordConnectionDetails() + .getGatewayIntents() + .contains(GatewayIntent.GUILD_MEMBERS); } @Override @@ -129,17 +170,15 @@ public class DiscordAPIImpl implements DiscordAPI { @Override public @NonNull CompletableFuture asyncLoad(@NonNull Long channelId, @NonNull Executor executor) { - CompletableFuture future = new CompletableFuture<>(); - JDA jda = discordSRV.jda().orElse(null); if (jda == null) { - future.completeExceptionally(new NotReadyException()); - return future; + return discordSRV.discordAPI().notReady(); } + CompletableFuture future = new CompletableFuture<>(); TextChannel textChannel = jda.getTextChannelById(channelId); if (textChannel == null) { - future.completeExceptionally(new UnknownChannelException()); + future.completeExceptionally(new IllegalArgumentException("Channel could not be found")); return future; } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/user/DiscordUserImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordUserImpl.java similarity index 53% rename from common/src/main/java/com/discordsrv/common/discord/api/user/DiscordUserImpl.java rename to common/src/main/java/com/discordsrv/common/discord/api/DiscordUserImpl.java index ef2deb52..f95037d4 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/user/DiscordUserImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordUserImpl.java @@ -16,31 +16,33 @@ * along with this program. If not, see . */ -package com.discordsrv.common.discord.api.user; +package com.discordsrv.common.discord.api; import com.discordsrv.api.discord.api.entity.DiscordUser; +import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.User; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.CompletableFuture; + public class DiscordUserImpl implements DiscordUser { - private final long id; + private final DiscordSRV discordSRV; + private final User user; private final boolean self; - private final boolean bot; - private final String username; - private final String discriminator; - public DiscordUserImpl(User user) { - this.id = user.getIdLong(); + public DiscordUserImpl(DiscordSRV discordSRV, User user) { + this.discordSRV = discordSRV; + this.user = user; this.self = user.getIdLong() == user.getJDA().getSelfUser().getIdLong(); - this.bot = user.isBot(); - this.username = user.getName(); - this.discriminator = user.getDiscriminator(); } @Override public long getId() { - return id; + return user.getIdLong(); } @Override @@ -50,16 +52,39 @@ public class DiscordUserImpl implements DiscordUser { @Override public boolean isBot() { - return bot; + return user.isBot(); } @Override public @NotNull String getUsername() { - return username; + return user.getName(); } @Override public @NotNull String getDiscriminator() { - return discriminator; + return user.getDiscriminator(); + } + + @Override + public CompletableFuture openPrivateChannel() { + JDA jda = discordSRV.jda().orElse(null); + if (jda == null) { + return discordSRV.discordAPI().notReady(); + } + + return jda.retrieveUserById(getId()) + .submit() + .thenCompose(user -> user.openPrivateChannel().submit()) + .thenApply(privateChannel -> new DiscordDMChannelImpl(discordSRV, privateChannel)); + } + + @Override + public User getAsJDAUser() { + return user; + } + + @Override + public String getAsMention() { + return user.getAsMention(); } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordDMChannelImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordDMChannelImpl.java index d03db805..82886619 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordDMChannelImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordDMChannelImpl.java @@ -18,17 +18,15 @@ package com.discordsrv.common.discord.api.channel; +import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; -import com.discordsrv.api.discord.api.entity.DiscordUser; -import com.discordsrv.api.discord.api.exception.NotReadyException; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.discord.api.DiscordUserImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil; -import com.discordsrv.common.discord.api.user.DiscordUserImpl; -import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.PrivateChannel; import org.jetbrains.annotations.NotNull; @@ -37,32 +35,18 @@ import java.util.concurrent.CompletableFuture; public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements DiscordDMChannel { private final DiscordSRV discordSRV; - private final long id; + private final PrivateChannel privateChannel; private final DiscordUser user; public DiscordDMChannelImpl(DiscordSRV discordSRV, PrivateChannel privateChannel) { this.discordSRV = discordSRV; - this.id = privateChannel.getIdLong(); - this.user = new DiscordUserImpl(privateChannel.getUser()); - } - - private PrivateChannel privateChannel() { - JDA jda = discordSRV.jda().orElse(null); - if (jda == null) { - throw new NotReadyException(); - } - - PrivateChannel privateChannel = jda.getPrivateChannelById(id); - if (privateChannel == null) { - throw new UnknownChannelException(); - } - - return privateChannel; + this.privateChannel = privateChannel; + this.user = new DiscordUserImpl(discordSRV, privateChannel.getUser()); } @Override public long getId() { - return id; + return privateChannel.getIdLong(); } @Override @@ -70,25 +54,28 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D return user; } + @Override + public PrivateChannel getAsJDAPrivateChannel() { + return privateChannel; + } + @Override public @NotNull CompletableFuture sendMessage(SendableDiscordMessage message) { if (message.isWebhookMessage()) { throw new IllegalArgumentException("Cannot send webhook messages to DMChannels"); } - CompletableFuture future = privateChannel() + CompletableFuture future = privateChannel .sendMessage(SendableDiscordMessageUtil.toJDA(message)) .submit() .thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); - return mapExceptions(future); + + return discordSRV.discordAPI().mapExceptions(future); } @Override public CompletableFuture deleteMessageById(long id) { - CompletableFuture future = privateChannel() - .deleteMessageById(id) - .submit(); - return mapExceptions(future); + return discordSRV.discordAPI().mapExceptions(privateChannel.deleteMessageById(id).submit()); } @Override @@ -97,10 +84,16 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D throw new IllegalArgumentException("Cannot send webhook messages to DMChannels"); } - CompletableFuture future = privateChannel() + CompletableFuture future = privateChannel .editMessageById(id, SendableDiscordMessageUtil.toJDA(message)) .submit() .thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); - return mapExceptions(future); + + return discordSRV.discordAPI().mapExceptions(future); + } + + @Override + public MessageChannel getAsJDAMessageChannel() { + return privateChannel; } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordMessageChannelImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordMessageChannelImpl.java index 426cf110..3cb1b228 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordMessageChannelImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordMessageChannelImpl.java @@ -19,19 +19,10 @@ package com.discordsrv.common.discord.api.channel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; -import com.discordsrv.api.discord.api.exception.RestErrorResponseException; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; -import com.discordsrv.api.discord.api.exception.UnknownMessageException; import com.discordsrv.common.DiscordSRV; -import lombok.SneakyThrows; import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.PrivateChannel; import net.dv8tion.jda.api.entities.TextChannel; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.requests.ErrorResponse; - -import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; public abstract class DiscordMessageChannelImpl implements DiscordMessageChannel { @@ -44,29 +35,4 @@ public abstract class DiscordMessageChannelImpl implements DiscordMessageChannel throw new IllegalArgumentException("Unknown MessageChannel type"); } } - - @SuppressWarnings("Convert2Lambda") // SneakyThrows - protected final CompletableFuture mapExceptions(CompletableFuture future) { - return future.handle(new BiFunction() { - - @SneakyThrows - @Override - public T apply(T msg, Throwable t) { - if (t instanceof ErrorResponseException) { - ErrorResponse errorResponse = ((ErrorResponseException) t).getErrorResponse(); - if (errorResponse != null) { - if (errorResponse == ErrorResponse.UNKNOWN_MESSAGE) { - throw new UnknownMessageException(t); - } else if (errorResponse == ErrorResponse.UNKNOWN_CHANNEL) { - throw new UnknownChannelException(t); - } - } - throw new RestErrorResponseException(((ErrorResponseException) t).getErrorCode(), t); - } else if (t != null) { - throw t; - } - return msg; - } - }); - } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordTextChannelImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordTextChannelImpl.java index 726afbfc..29434eda 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordTextChannelImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/channel/DiscordTextChannelImpl.java @@ -25,18 +25,16 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; -import com.discordsrv.api.discord.api.exception.NotReadyException; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil; -import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.requests.restaction.MessageAction; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; @@ -44,32 +42,28 @@ import java.util.function.BiFunction; public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements DiscordTextChannel { private final DiscordSRV discordSRV; - private final long id; - private final String name; - private final String topic; + private final TextChannel textChannel; private final DiscordGuild guild; public DiscordTextChannelImpl(DiscordSRV discordSRV, TextChannel textChannel) { this.discordSRV = discordSRV; - this.id = textChannel.getIdLong(); - this.name = textChannel.getName(); - this.topic = textChannel.getTopic(); + this.textChannel = textChannel; this.guild = new DiscordGuildImpl(discordSRV, textChannel.getGuild()); } @Override public long getId() { - return id; + return textChannel.getIdLong(); } @Override public @NotNull String getName() { - return name; + return textChannel.getName(); } @Override - public @NotNull String getTopic() { - return topic; + public @Nullable String getTopic() { + return textChannel.getTopic(); } @Override @@ -77,6 +71,11 @@ public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements return guild; } + @Override + public TextChannel getAsJDATextChannel() { + return textChannel; + } + @Override public @NotNull CompletableFuture sendMessage(SendableDiscordMessage message) { return message(message, WebhookClient::send, MessageChannel::sendMessage); @@ -96,6 +95,11 @@ public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements ); } + @Override + public MessageChannel getAsJDAMessageChannel() { + return textChannel; + } + private CompletableFuture message( SendableDiscordMessage message, BiFunction> webhookFunction, @@ -107,24 +111,17 @@ public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements client, SendableDiscordMessageUtil.toWebhook(message))) .thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg)); } else { - JDA jda = discordSRV.jda().orElse(null); - if (jda == null) { - throw new NotReadyException(); - } - - TextChannel textChannel = jda.getTextChannelById(getId()); - if (textChannel == null) { - future = new CompletableFuture<>(); - future.completeExceptionally(new UnknownChannelException()); - return future; - } - future = jdaFunction .apply(textChannel, SendableDiscordMessageUtil.toJDA(message)) .submit() .thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); } - return mapExceptions(future); + return discordSRV.discordAPI().mapExceptions(future); + } + + @Override + public String getAsMention() { + return textChannel.getAsMention(); } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildImpl.java index 60935433..b78ae77b 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildImpl.java @@ -23,54 +23,89 @@ import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.api.entity.guild.DiscordRole; import com.discordsrv.common.DiscordSRV; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; -import java.util.Optional; +import java.util.*; public class DiscordGuildImpl implements DiscordGuild { private final DiscordSRV discordSRV; - private final long id; - private final String name; - private final int memberCount; + private final Guild guild; public DiscordGuildImpl(DiscordSRV discordSRV, Guild guild) { this.discordSRV = discordSRV; - this.id = guild.getIdLong(); - this.name = guild.getName(); - this.memberCount = guild.getMemberCount(); + this.guild = guild; } @Override public long getId() { - return id; + return guild.getIdLong(); } @Override public String getName() { - return name; + return guild.getName(); } @Override public int getMemberCount() { - return memberCount; - } - - private Optional guild() { - return discordSRV.jda() - .map(jda -> jda.getGuildById(id)); + return guild.getMemberCount(); } @Override public Optional getMemberById(long id) { - return guild() - .map(guild -> guild.getMemberById(id)) - .map(member -> new DiscordGuildMemberImpl(discordSRV, member)); + Member member = guild.getMemberById(id); + if (member == null) { + return Optional.empty(); + } + + return Optional.of(new DiscordGuildMemberImpl(discordSRV, member)); + } + + @Override + public Set getCachedMembers() { + Set members = new HashSet<>(); + for (Member member : guild.getMembers()) { + members.add(new DiscordGuildMemberImpl(discordSRV, member)); + } + return members; } @Override public Optional getRoleById(long id) { - return guild() - .map(guild -> guild.getRoleById(id)) - .map(DiscordRoleImpl::new); + Role role = guild.getRoleById(id); + if (role == null) { + return Optional.empty(); + } + + return Optional.of(new DiscordRoleImpl(role)); + } + + @Override + public List getRoles() { + List roles = new ArrayList<>(); + for (Role role : guild.getRoles()) { + roles.add(new DiscordRoleImpl(role)); + } + return roles; + } + + @Override + public Guild getAsJDAGuild() { + return guild; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DiscordGuildImpl that = (DiscordGuildImpl) o; + return getId() == that.getId(); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildMemberImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildMemberImpl.java index 28220c70..e84679dd 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildMemberImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordGuildMemberImpl.java @@ -25,9 +25,10 @@ import com.discordsrv.api.discord.api.entity.guild.DiscordRole; import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder; import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.discord.api.user.DiscordUserImpl; +import com.discordsrv.common.discord.api.DiscordUserImpl; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.TextColor; @@ -39,15 +40,15 @@ import java.util.Optional; public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGuildMember { + private final Member member; private final DiscordGuild guild; - private final String nickname; private final List roles; private final Color color; public DiscordGuildMemberImpl(DiscordSRV discordSRV, Member member) { - super(member.getUser()); + super(discordSRV, member.getUser()); + this.member = member; this.guild = new DiscordGuildImpl(discordSRV, member.getGuild()); - this.nickname = member.getNickname(); List roles = new ArrayList<>(); for (Role role : member.getRoles()) { @@ -64,7 +65,7 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu @Override public @NotNull Optional getNickname() { - return Optional.ofNullable(nickname); + return Optional.ofNullable(member.getNickname()); } @Override @@ -77,6 +78,20 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu return color; } + @Override + public Member getAsJDAMember() { + return member; + } + + @Override + public User getAsJDAUser() { + return member.getUser(); + } + + // + // Placeholders + // + @Placeholder(value = "user_highest_role", relookup = "role") public DiscordRole _highestRole() { return !roles.isEmpty() ? roles.get(0) : null; @@ -92,14 +107,8 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu return null; } - @Placeholder(value = "user_roles") + @Placeholder("user_roles_") public Component _allRoles(@PlaceholderRemainder String suffix) { - if (suffix.startsWith("_")) { - suffix = suffix.substring(1); - } else { - return null; - } - List components = new ArrayList<>(); for (DiscordRole role : getRoles()) { components.add(Component.text(role.getName()).color(TextColor.color(role.getColor().rgb()))); diff --git a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordRoleImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordRoleImpl.java index 4a551d53..a7fe57fd 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordRoleImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/guild/DiscordRoleImpl.java @@ -25,26 +25,22 @@ import org.jetbrains.annotations.NotNull; public class DiscordRoleImpl implements DiscordRole { - private final long id; - private final String name; + private final Role role; private final Color color; - private final boolean hoisted; public DiscordRoleImpl(Role role) { - this.id = role.getIdLong(); - this.name = role.getName(); + this.role = role; this.color = new Color(role.getColorRaw()); - this.hoisted = role.isHoisted(); } @Override public long getId() { - return id; + return role.getIdLong(); } @Override public @NotNull String getName() { - return name; + return role.getName(); } @Override @@ -54,6 +50,16 @@ public class DiscordRoleImpl implements DiscordRole { @Override public boolean isHoisted() { - return hoisted; + return role.isHoisted(); + } + + @Override + public Role getAsJDARole() { + return role; + } + + @Override + public String getAsMention() { + return role.getAsMention(); } } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java index 02bb700f..157ee6a1 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java @@ -19,6 +19,7 @@ package com.discordsrv.common.discord.api.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.receive.ReadonlyUser; @@ -33,15 +34,20 @@ import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.api.entity.message.impl.SendableDiscordMessageImpl; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; +import com.discordsrv.api.discord.api.exception.RestErrorResponseException; +import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.discord.api.DiscordUserImpl; import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildMemberImpl; -import com.discordsrv.common.discord.api.user.DiscordUserImpl; 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.User; +import net.dv8tion.jda.api.requests.ErrorResponse; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -63,7 +69,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple String webhookAvatarUrl = webhookMessage ? message.getAuthor().getEffectiveAvatarUrl() : null; DiscordMessageChannel channel = DiscordMessageChannelImpl.get(discordSRV, message.getChannel()); - DiscordUser user = new DiscordUserImpl(message.getAuthor()); + DiscordUser user = new DiscordUserImpl(discordSRV, message.getAuthor()); Member member = message.getMember(); DiscordGuildMember apiMember = member != null ? new DiscordGuildMemberImpl(discordSRV, member) : null; @@ -82,8 +88,14 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple self = user.isSelf(); } + List attachments = new ArrayList<>(); + for (Message.Attachment attachment : message.getAttachments()) { + attachments.add(new Attachment(attachment.getFileName(), attachment.getUrl())); + } + return new ReceivedDiscordMessageImpl( discordSRV, + attachments, self, channel, apiMember, @@ -142,10 +154,16 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple webhookMessage.getAuthor().getId()).orElse(null); DiscordGuildMember member = channel instanceof DiscordTextChannel && user != null ? ((DiscordTextChannel) channel).getGuild().getMemberById(user.getId()).orElse(null) : null; + + List attachments = new ArrayList<>(); + for (ReadonlyAttachment attachment : webhookMessage.getAttachments()) { + attachments.add(new Attachment(attachment.getFileName(), attachment.getUrl())); + } + return new ReceivedDiscordMessageImpl( discordSRV, - // These are always from rest responses - true, + attachments, + true, // These are always from rest responses channel, member, user, @@ -159,6 +177,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple } private final DiscordSRV discordSRV; + private final List attachments; private final boolean fromSelf; private final DiscordMessageChannel channel; private final DiscordGuildMember member; @@ -168,6 +187,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple private ReceivedDiscordMessageImpl( DiscordSRV discordSRV, + List attachments, boolean fromSelf, DiscordMessageChannel channel, DiscordGuildMember member, @@ -181,6 +201,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple ) { super(content, embeds, Collections.emptySet(), webhookUsername, webhookAvatarUrl); this.discordSRV = discordSRV; + this.attachments = attachments; this.fromSelf = fromSelf; this.channel = channel; this.member = member; @@ -194,6 +215,11 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple return id; } + @Override + public List getAttachments() { + return attachments; + } + @Override public boolean isFromSelf() { return fromSelf; @@ -233,7 +259,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null); if (textChannel == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new UnknownChannelException()); + future.completeExceptionally(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL)); return future; } @@ -249,10 +275,30 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null); if (textChannel == null) { CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(new UnknownChannelException()); + future.completeExceptionally(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL)); return future; } return textChannel.editMessageById(getId(), message); } + + // + // Placeholders + // + + @Placeholder("message_attachments") + public Component _attachments() { + // TODO: customizable + + TextComponent.Builder builder = Component.text(); + for (Attachment attachment : attachments) { + builder.append( + Component.text() + .content("[" + attachment.fileName() + "]") + .clickEvent(ClickEvent.openUrl(attachment.url())) + ) + .append(Component.text(" ")); + } + return builder.build(); + } } diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index b8e9fa79..38663d6d 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -33,7 +33,7 @@ import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildMemberImpl; import com.discordsrv.common.discord.api.guild.DiscordRoleImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; -import com.discordsrv.common.discord.api.user.DiscordUserImpl; +import com.discordsrv.common.discord.api.DiscordUserImpl; import com.discordsrv.common.discord.connection.DiscordConnectionManager; import com.discordsrv.common.scheduler.Scheduler; import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; @@ -156,7 +156,7 @@ public class JDAConnectionManager implements DiscordConnectionManager { CompletableFuture future = instance.retrieveApplicationInfo() .timeout(10, TimeUnit.SECONDS) - .map(applicationInfo -> (DiscordUser) new DiscordUserImpl(applicationInfo.getOwner())) + .map(applicationInfo -> (DiscordUser) new DiscordUserImpl(discordSRV, applicationInfo.getOwner())) .submit(); botOwnerRequest.set(future); @@ -188,7 +188,7 @@ public class JDAConnectionManager implements DiscordConnectionManager { } else if (o instanceof ReceivedMessage) { converted = ReceivedDiscordMessageImpl.fromJDA(discordSRV, (Message) o); } else if (o instanceof User) { - converted = new DiscordUserImpl((User) o); + converted = new DiscordUserImpl(discordSRV, (User) o); } else { converted = o; isConversion = false; diff --git a/common/src/main/java/com/discordsrv/common/listener/AbstractListener.java b/common/src/main/java/com/discordsrv/common/event/util/EventUtil.java similarity index 83% rename from common/src/main/java/com/discordsrv/common/listener/AbstractListener.java rename to common/src/main/java/com/discordsrv/common/event/util/EventUtil.java index 2baff814..028a14a6 100644 --- a/common/src/main/java/com/discordsrv/common/listener/AbstractListener.java +++ b/common/src/main/java/com/discordsrv/common/event/util/EventUtil.java @@ -16,22 +16,18 @@ * along with this program. If not, see . */ -package com.discordsrv.common.listener; +package com.discordsrv.common.event.util; import com.discordsrv.api.event.bus.EventListener; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; import com.discordsrv.common.DiscordSRV; -public abstract class AbstractListener { +public final class EventUtil { - protected final DiscordSRV discordSRV; + private EventUtil() {} - public AbstractListener(DiscordSRV discordSRV) { - this.discordSRV = discordSRV; - } - - public boolean checkProcessor(Processable event) { + public static boolean checkProcessor(DiscordSRV discordSRV,Processable event) { if (!event.isProcessed()) { return false; } @@ -45,7 +41,7 @@ public abstract class AbstractListener { return true; } - public boolean checkCancellation(Cancellable event) { + public static boolean checkCancellation(DiscordSRV discordSRV, Cancellable event) { if (!event.isCancelled()) { return false; } diff --git a/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java b/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java deleted file mode 100644 index 7c9feeb0..00000000 --- a/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * This file is part of DiscordSRV, licensed under the GPLv3 License - * Copyright (c) 2016-2021 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 . - */ - -package com.discordsrv.common.listener; - -import com.discordsrv.api.channel.GameChannel; -import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; -import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; -import com.discordsrv.api.event.bus.EventPriority; -import com.discordsrv.api.event.bus.Subscribe; -import com.discordsrv.api.event.events.message.forward.game.ChatMessageForwardedEvent; -import com.discordsrv.api.event.events.message.receive.game.ChatMessageProcessingEvent; -import com.discordsrv.api.placeholder.util.Placeholders; -import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.component.util.ComponentUtil; -import com.discordsrv.common.config.main.channels.BaseChannelConfig; -import com.discordsrv.common.config.main.channels.ChannelConfig; -import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; -import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageClusterImpl; -import com.discordsrv.common.function.OrDefault; -import net.kyori.adventure.text.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public class GameChatListener extends AbstractListener { - - public GameChatListener(DiscordSRV discordSRV) { - super(discordSRV); - } - - @Subscribe(priority = EventPriority.LAST) - public void onChatReceive(ChatMessageProcessingEvent event) { - if (checkProcessor(event) || checkCancellation(event) || !discordSRV.isReady()) { - return; - } - - GameChannel gameChannel = event.getGameChannel(); - - OrDefault channelConfig = discordSRV.channelConfig().orDefault(gameChannel); - OrDefault chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord); - - SendableDiscordMessage.Builder builder = chatConfig.get(cfg -> cfg.format); - if (builder == null) { - return; - } - - Component message = ComponentUtil.fromAPI(event.message()); - Placeholders serializedMessage = new Placeholders(discordSRV.componentFactory().discordSerializer().serialize(message)); - - chatConfig.opt(cfg -> cfg.contentRegexFilters) - .ifPresent(patterns -> patterns.forEach(serializedMessage::replaceAll)); - - SendableDiscordMessage.Formatter formatter = builder.toFormatter() - .addContext(event.getPlayer(), gameChannel) - .addReplacement("%message%", serializedMessage.toString()); - - formatter.applyPlaceholderService(); - - SendableDiscordMessage discordMessage = formatter.build(); - - List channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null); - if (channelIds == null || channelIds.isEmpty()) { - return; - } - - List> futures = new ArrayList<>(); - for (Long channelId : channelIds) { - discordSRV.discordAPI().getTextChannelById(channelId).ifPresent(textChannel -> - futures.add(textChannel.sendMessage(discordMessage))); - } - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .whenComplete((v, t) -> { - if (t != null) { - discordSRV.logger().error("Failed to deliver message to Discord", t); - return; - } - - List messages = new ArrayList<>(); - for (CompletableFuture future : futures) { - messages.add(future.join()); - } - - discordSRV.eventBus().publish( - new ChatMessageForwardedEvent( - new ReceivedDiscordMessageClusterImpl(messages))); - }); - } - -} diff --git a/common/src/main/java/com/discordsrv/common/module/Module.java b/common/src/main/java/com/discordsrv/common/module/Module.java new file mode 100644 index 00000000..21267646 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/module/Module.java @@ -0,0 +1,65 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.module; + +import com.discordsrv.api.event.events.Cancellable; +import com.discordsrv.api.event.events.Processable; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.event.util.EventUtil; + +public abstract class Module { + + protected final DiscordSRV discordSRV; + private boolean hasBeenEnabled = false; + + public Module(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + protected boolean isEnabled() { + return true; + } + + protected void enable() {} + protected void disable() {} + protected void reload() {} + + public final void enableModule() { + if (hasBeenEnabled || !isEnabled()) { + return; + } + + hasBeenEnabled = true; + enable(); + + try { + discordSRV.eventBus().subscribe(this); + // Ignore not having listener methods exception + } catch (IllegalArgumentException ignored) {} + } + + // Utility + protected final boolean checkProcessor(Processable event) { + return EventUtil.checkProcessor(discordSRV, event); + } + + protected final boolean checkCancellation(Cancellable event) { + return EventUtil.checkCancellation(discordSRV, event); + } +} diff --git a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java new file mode 100644 index 00000000..4257866b --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java @@ -0,0 +1,104 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.module; + +import com.discordsrv.api.event.bus.EventPriority; +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; +import com.discordsrv.common.DiscordSRV; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +public class ModuleManager { + + private final Set modules = new CopyOnWriteArraySet<>(); + private final Map moduleLookupTable = new ConcurrentHashMap<>(); + private final DiscordSRV discordSRV; + + public ModuleManager(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @SuppressWarnings("unchecked") + public T getModule(Class moduleType) { + return (T) moduleLookupTable.computeIfAbsent(moduleType.getName(), key -> { + for (Module module : modules) { + if (moduleType.isAssignableFrom(module.getClass())) { + return module; + } + } + return null; + }); + } + + public void register(Module module) { + this.modules.add(module); + this.moduleLookupTable.put(module.getClass().getName(), module); + + enable(module); + } + + private void enable(Module module) { + try { + module.enableModule(); + } catch (Throwable t) { + discordSRV.logger().error("Failed to enable " + module.getClass().getSimpleName(), t); + } + } + + public void unregister(Module module) { + this.modules.remove(module); + this.moduleLookupTable.values().removeIf(mod -> mod == module); + + disable(module); + } + + private void disable(Module module) { + try { + module.disable(); + } catch (Throwable t) { + discordSRV.logger().error("Failed to disable " + module.getClass().getSimpleName(), t); + } + } + + @Subscribe(priority = EventPriority.EARLY) + public void onShuttingDown(DiscordSRVShuttingDownEvent event) { + for (Module module : modules) { + unregister(module); + } + } + + @Subscribe(priority = EventPriority.EARLY) + public void onReload(DiscordSRVReloadEvent event) { + for (Module module : modules) { + // Check if the module needs to be enabled due to reload + enable(module); + + try { + module.reload(); + } catch (Throwable t) { + discordSRV.logger().error("Failed to reload " + module.getClass().getSimpleName(), t); + } + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/listener/DiscordAPIListener.java b/common/src/main/java/com/discordsrv/common/module/modules/DiscordAPIEventModule.java similarity index 85% rename from common/src/main/java/com/discordsrv/common/listener/DiscordAPIListener.java rename to common/src/main/java/com/discordsrv/common/module/modules/DiscordAPIEventModule.java index eef6dd73..b88b3bfa 100644 --- a/common/src/main/java/com/discordsrv/common/listener/DiscordAPIListener.java +++ b/common/src/main/java/com/discordsrv/common/module/modules/DiscordAPIEventModule.java @@ -16,27 +16,27 @@ * along with this program. If not, see . */ -package com.discordsrv.common.listener; +package com.discordsrv.common.module.modules; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.discord.DiscordMessageReceivedEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; +import com.discordsrv.common.module.Module; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -public class DiscordAPIListener { +public class DiscordAPIEventModule extends Module { - private final DiscordSRV discordSRV; - - public DiscordAPIListener(DiscordSRV discordSRV) { - this.discordSRV = discordSRV; + public DiscordAPIEventModule(DiscordSRV discordSRV) { + super(discordSRV); } @Subscribe public void onMessageReceivedEvent(MessageReceivedEvent event) { discordSRV.eventBus().publish(new DiscordMessageReceivedEvent( ReceivedDiscordMessageImpl.fromJDA(discordSRV, event.getMessage()), - DiscordMessageChannelImpl.get(discordSRV, event.getChannel()))); + DiscordMessageChannelImpl.get(discordSRV, event.getChannel())) + ); } } diff --git a/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java b/common/src/main/java/com/discordsrv/common/module/modules/DiscordToMinecraftModule.java similarity index 93% rename from common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java rename to common/src/main/java/com/discordsrv/common/module/modules/DiscordToMinecraftModule.java index cd1dd108..ddfa54d0 100644 --- a/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java +++ b/common/src/main/java/com/discordsrv/common/module/modules/DiscordToMinecraftModule.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.discordsrv.common.listener; +package com.discordsrv.common.module.modules; import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.component.EnhancedTextBuilder; @@ -35,14 +35,15 @@ import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; import com.discordsrv.common.function.OrDefault; +import com.discordsrv.common.module.Module; import net.kyori.adventure.text.Component; import org.apache.commons.lang3.tuple.Pair; import java.util.Optional; -public class DiscordChatListener extends AbstractListener { +public class DiscordToMinecraftModule extends Module { - public DiscordChatListener(DiscordSRV discordSRV) { + public DiscordToMinecraftModule(DiscordSRV discordSRV) { super(discordSRV); } @@ -53,10 +54,7 @@ public class DiscordChatListener extends AbstractListener { return; } - discordSRV.eventBus().publish( - new DiscordMessageProcessingEvent( - event.getMessage(), - channel)); + discordSRV.eventBus().publish(new DiscordMessageProcessingEvent(event.getMessage(), channel)); } @Subscribe @@ -113,7 +111,7 @@ public class DiscordChatListener extends AbstractListener { chatConfig.opt(cfg -> cfg.contentRegexFilters) .ifPresent(filters -> filters.forEach(message::replaceAll)); - Component messageComponent = DiscordSRVMinecraftRenderer.getWithGuildContext(channel.getGuild().getId(), () -> + Component messageComponent = DiscordSRVMinecraftRenderer.getWithContext(event, chatConfig, () -> discordSRV.componentFactory().minecraftSerializer().serialize(message.toString())); EnhancedTextBuilder componentBuilder = discordSRV.componentFactory() diff --git a/common/src/main/java/com/discordsrv/common/listener/ChannelLookupListener.java b/common/src/main/java/com/discordsrv/common/module/modules/GlobalChannelLookupModule.java similarity index 63% rename from common/src/main/java/com/discordsrv/common/listener/ChannelLookupListener.java rename to common/src/main/java/com/discordsrv/common/module/modules/GlobalChannelLookupModule.java index b0a8f89b..b7fd2291 100644 --- a/common/src/main/java/com/discordsrv/common/listener/ChannelLookupListener.java +++ b/common/src/main/java/com/discordsrv/common/module/modules/GlobalChannelLookupModule.java @@ -16,25 +16,33 @@ * along with this program. If not, see . */ -package com.discordsrv.common.listener; +package com.discordsrv.common.module.modules; import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.channel.GameChannelLookupEvent; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.channel.DefaultGlobalChannel; +import com.discordsrv.common.event.util.EventUtil; +import com.discordsrv.common.module.Module; -public class ChannelLookupListener extends AbstractListener { +public class GlobalChannelLookupModule extends Module { - public ChannelLookupListener(DiscordSRV discordSRV) { + private final DefaultGlobalChannel defaultGlobalChannel; + + public GlobalChannelLookupModule(DiscordSRV discordSRV) { super(discordSRV); + defaultGlobalChannel = new DefaultGlobalChannel(discordSRV); } - @Subscribe(priority = EventPriority.LAST) + @Subscribe(priority = EventPriority.LATE) public void onGameChannelLookup(GameChannelLookupEvent event) { - if (!event.getChannelName().equalsIgnoreCase("global") || checkProcessor(event)) { + if (EventUtil.checkProcessor(discordSRV, event)) { return; } - event.process(discordSRV.defaultGlobalChannel()); + if (event.getChannelName().equalsIgnoreCase("global")) { + event.process(defaultGlobalChannel); + } } } diff --git a/common/src/main/java/com/discordsrv/common/module/modules/MinecraftToDiscordModule.java b/common/src/main/java/com/discordsrv/common/module/modules/MinecraftToDiscordModule.java new file mode 100644 index 00000000..13b94483 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/module/modules/MinecraftToDiscordModule.java @@ -0,0 +1,315 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.module.modules; + +import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; +import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; +import com.discordsrv.api.discord.api.util.DiscordFormattingUtil; +import com.discordsrv.api.event.bus.EventPriority; +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.api.event.events.message.forward.game.ChatMessageForwardedEvent; +import com.discordsrv.api.event.events.message.receive.game.ChatMessageProcessingEvent; +import com.discordsrv.api.placeholder.FormattedText; +import com.discordsrv.api.placeholder.util.Placeholders; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.config.main.channels.BaseChannelConfig; +import com.discordsrv.common.config.main.channels.ChannelConfig; +import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; +import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageClusterImpl; +import com.discordsrv.common.function.OrDefault; +import com.discordsrv.common.module.Module; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.GuildChannel; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.channel.ChannelCreateEvent; +import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent; +import net.dv8tion.jda.api.events.channel.update.ChannelUpdateNameEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; +import net.dv8tion.jda.api.events.role.RoleCreateEvent; +import net.dv8tion.jda.api.events.role.RoleDeleteEvent; +import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent; +import net.kyori.adventure.text.Component; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class MinecraftToDiscordModule extends Module { + + private final Map> memberMentions = new ConcurrentHashMap<>(); + private final Map> roleMentions = new ConcurrentHashMap<>(); + private final Map> channelMentions = new ConcurrentHashMap<>(); + + public MinecraftToDiscordModule(DiscordSRV discordSRV) { + super(discordSRV); + } + + @Subscribe(priority = EventPriority.LAST) + public void onChatReceive(ChatMessageProcessingEvent event) { + if (checkProcessor(event) || checkCancellation(event) || !discordSRV.isReady()) { + return; + } + + GameChannel gameChannel = event.getGameChannel(); + + OrDefault channelConfig = discordSRV.channelConfig().orDefault(gameChannel); + OrDefault chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord); + + SendableDiscordMessage.Builder builder = chatConfig.get(cfg -> cfg.format); + if (builder == null) { + return; + } + + List channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null); + if (channelIds == null || channelIds.isEmpty()) { + return; + } + + Component message = ComponentUtil.fromAPI(event.message()); + Placeholders messagePlaceholders = new Placeholders(discordSRV.componentFactory().discordSerializer().serialize(message)); + + chatConfig.opt(cfg -> cfg.contentRegexFilters) + .ifPresent(patterns -> patterns.forEach(messagePlaceholders::replaceAll)); + + Map> channels = new LinkedHashMap<>(); + for (Long channelId : channelIds) { + discordSRV.discordAPI().getTextChannelById(channelId) + .ifPresent(textChannel -> channels + .computeIfAbsent(textChannel.getGuild(), key -> new LinkedHashSet<>()) + .add(textChannel)); + } + + String serializedMessage = DiscordFormattingUtil.escapeContent(messagePlaceholders.toString()); + List> futures = new ArrayList<>(); + + OrDefault mentionConfig = chatConfig.map(cfg -> cfg.mentions); + // Format messages per-Guild + for (Map.Entry> entry : channels.entrySet()) { + Guild guild = entry.getKey().getAsJDAGuild(); + + Placeholders channelMessagePlaceholders = new Placeholders(serializedMessage); + List mentions = new ArrayList<>(); + if (mentionConfig.get(cfg -> cfg.roles, false)) { + mentions.addAll(getRoleMentions(guild).values()); + } + if (mentionConfig.get(cfg -> cfg.users, false)) { + mentions.addAll(getMemberMentions(guild).values()); + } + if (mentionConfig.get(cfg -> cfg.roles, true)) { + mentions.addAll(getChannelMentions(guild).values()); + } + + // From longest to shortest + mentions.stream() + .sorted(Comparator.comparingInt(mention -> ((CachedMention) mention).searchLength).reversed()) + .forEachOrdered(mention -> channelMessagePlaceholders.replaceAll(mention.search, mention.mention)); + + SendableDiscordMessage discordMessage = builder.toFormatter() + .addContext(event.getPlayer(), gameChannel) + .addReplacement("%message%", new FormattedText(channelMessagePlaceholders.toString())) + .applyPlaceholderService() + .build(); + + for (DiscordTextChannel textChannel : entry.getValue()) { + futures.add(textChannel.sendMessage(discordMessage)); + } + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .whenComplete((v, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to deliver message to Discord", t); + return; + } + + List messages = new ArrayList<>(); + for (CompletableFuture future : futures) { + messages.add(future.join()); + } + + discordSRV.eventBus().publish( + new ChatMessageForwardedEvent( + new ReceivedDiscordMessageClusterImpl(messages))); + }); + } + + // + // Mention caching + // + + private Map getRoleMentions(Guild guild) { + return roleMentions.computeIfAbsent(guild.getIdLong(), key -> { + Map mentions = new LinkedHashMap<>(); + for (Role role : guild.getRoles()) { + mentions.put(role.getIdLong(), convertRole(role)); + } + return mentions; + }); + } + + private CachedMention convertRole(Role role) { + return new CachedMention( + "@" + role.getName(), + role.getAsMention(), + role.getIdLong() + ); + } + + @Subscribe + public void onRoleCreate(RoleCreateEvent event) { + Role role = event.getRole(); + getRoleMentions(event.getGuild()).put(role.getIdLong(), convertRole(role)); + } + + @Subscribe + public void onRoleUpdate(RoleUpdateNameEvent event) { + Role role = event.getRole(); + getRoleMentions(event.getGuild()).put(role.getIdLong(), convertRole(role)); + } + + @Subscribe + public void onRoleDelete(RoleDeleteEvent event) { + Role role = event.getRole(); + getRoleMentions(event.getGuild()).remove(role.getIdLong()); + } + + private Map getMemberMentions(Guild guild) { + return channelMentions.computeIfAbsent(guild.getIdLong(), key -> { + Map mentions = new LinkedHashMap<>(); + for (Member member : guild.getMembers()) { + mentions.put(member.getIdLong(), convertMember(member)); + } + return mentions; + }); + } + + private CachedMention convertMember(Member member) { + return new CachedMention( + "@" + member.getEffectiveName(), + member.getAsMention(), + member.getIdLong() + ); + } + + @Subscribe + public void onMemberAdd(GuildMemberJoinEvent event) { + Member member = event.getMember(); + getMemberMentions(event.getGuild()).put(member.getIdLong(), convertMember(member)); + } + + @Subscribe + public void onMemberUpdate(GuildMemberUpdateNicknameEvent event) { + Member member = event.getMember(); + getMemberMentions(event.getGuild()).put(member.getIdLong(), convertMember(member)); + } + + @Subscribe + public void onMemberDelete(GuildMemberRemoveEvent event) { + Member member = event.getMember(); + if (member == null) { + return; + } + + getMemberMentions(event.getGuild()).remove(member.getIdLong()); + } + + private Map getChannelMentions(Guild guild) { + return memberMentions.computeIfAbsent(guild.getIdLong(), key -> { + Map mentions = new LinkedHashMap<>(); + for (GuildChannel channel : guild.getChannels()) { + mentions.put(channel.getIdLong(), convertChannel(channel)); + } + return mentions; + }); + } + + private CachedMention convertChannel(GuildChannel channel) { + return new CachedMention( + "#" + channel.getName(), + channel.getAsMention(), + channel.getIdLong() + ); + } + + @Subscribe + public void onChannelCreate(ChannelCreateEvent event) { + if (!event.getChannelType().isGuild()) { + return; + } + + GuildChannel channel = (GuildChannel) event.getChannel(); + getMemberMentions(event.getGuild()).put(channel.getIdLong(), convertChannel(channel)); + } + + @Subscribe + public void onChannelUpdate(ChannelUpdateNameEvent event) { + if (!event.getChannelType().isGuild()) { + return; + } + + GuildChannel channel = (GuildChannel) event.getChannel(); + getMemberMentions(event.getGuild()).put(channel.getIdLong(), convertChannel(channel)); + } + + @Subscribe + public void onChannelDelete(ChannelDeleteEvent event) { + if (!event.getChannelType().isGuild()) { + return; + } + + GuildChannel channel = (GuildChannel) event.getChannel(); + getMemberMentions(event.getGuild()).remove(channel.getIdLong()); + } + + public static class CachedMention { + + private final Pattern search; + private final int searchLength; + private final String mention; + private final long id; + + public CachedMention(String search, String mention, long id) { + this.search = Pattern.compile(search, Pattern.LITERAL); + this.searchLength = search.length(); + this.mention = mention; + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CachedMention that = (CachedMention) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java b/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java index 4f708390..ce592b33 100644 --- a/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java +++ b/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java @@ -66,7 +66,7 @@ public class StandardScheduler implements Scheduler { try { runnable.run(); } catch (Throwable t) { - discordSRV.logger().error(Thread.currentThread().getName() + " caught a exception", t); + discordSRV.logger().error(Thread.currentThread().getName() + " ran into an exception", t); } }; }