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

This commit is contained in:
Vankka 2021-12-20 01:03:38 +02:00
parent b415e5419e
commit 9ffce74061
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
43 changed files with 1166 additions and 425 deletions

View File

@ -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<? extends DiscordMessageChannel> 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<DiscordDMChannel> 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<DiscordTextChannel> 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<DiscordGuild> 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<DiscordUser> 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<DiscordUser> 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
*/

View File

@ -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<DiscordDMChannel> 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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<ReceivedDiscordMessage> 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();
}

View File

@ -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();
}

View File

@ -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<DiscordGuildMember> getMemberById(long id);
/**
* Gets the members of this server that are in the cache.
* @return the Discord server members that are currently cached
*/
Set<DiscordGuildMember> 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<DiscordRole> getRoleById(long id);
/**
* Gets the roles in this Discord server.
* @return an ordered list of the roles in this Discord server
*/
List<DiscordRole> 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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<Attachment> 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<DiscordTextChannel> 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<DiscordDMChannel> 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<DiscordGuildMember> 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<DiscordGuild> getGuild() {
@ -109,4 +116,23 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak
*/
@NotNull
CompletableFuture<ReceivedDiscordMessage> 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;
}
}
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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");
}
}

View File

@ -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);

View File

@ -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

View File

@ -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 {

View File

@ -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<C extends MainConfig, CC extends Connec
private DiscordConnectionDetails discordConnectionDetails;
// DiscordSRV
private final DefaultGlobalChannel defaultGlobalChannel = new DefaultGlobalChannel(this);
private ChannelConfigHelper channelConfig;
private ModuleManager moduleManager;
private DiscordConnectionManager discordConnectionManager;
// Internal
@ -132,11 +134,6 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// DiscordSRV
@Override
public DefaultGlobalChannel defaultGlobalChannel() {
return defaultGlobalChannel;
}
@Override
public ChannelConfigHelper channelConfig() {
return channelConfig;
@ -164,6 +161,21 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
return configManager().config();
}
@Override
public <T extends Module> T getModule(Class<T> 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<C extends MainConfig, CC extends Connec
// Register PlayerProvider listeners
playerProvider().subscribe();
// Register listeners
// DiscordAPI
eventBus().subscribe(new DiscordAPIListener(this));
// Chat
eventBus().subscribe(new ChannelLookupListener(this));
eventBus().subscribe(new GameChatListener(this));
eventBus().subscribe(new DiscordChatListener(this));
// Register modules
moduleManager = new ModuleManager(this);
for (Module module : Arrays.asList(
new DiscordAPIEventModule(this),
new DiscordToMinecraftModule(this),
new GlobalChannelLookupModule(this),
new MinecraftToDiscordModule(this)
)) {
registerModule(module);
}
}
@OverridingMethodsMustInvokeSuper

View File

@ -20,7 +20,6 @@ package com.discordsrv.common;
import com.discordsrv.api.DiscordSRVApi;
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;
@ -29,10 +28,11 @@ import com.discordsrv.common.config.manager.MainConfigManager;
import com.discordsrv.common.console.Console;
import com.discordsrv.common.discord.api.DiscordAPIImpl;
import com.discordsrv.common.discord.connection.DiscordConnectionManager;
import com.discordsrv.logging.Logger;
import com.discordsrv.common.module.Module;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import com.discordsrv.common.player.provider.AbstractPlayerProvider;
import com.discordsrv.common.scheduler.Scheduler;
import com.discordsrv.logging.Logger;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ -72,12 +72,17 @@ public interface DiscordSRV extends DiscordSRVApi {
ConnectionConfig connectionConfig();
MainConfigManager<? extends MainConfig> configManager();
MainConfig config();
// Config helper
ChannelConfigHelper channelConfig();
// Internal
DefaultGlobalChannel defaultGlobalChannel();
ChannelConfigHelper channelConfig();
DiscordConnectionManager discordConnectionManager();
// Modules
<T extends Module> T getModule(Class<T> moduleType);
void registerModule(Module module);
void unregisterModule(Module module);
Locale locale();
void setStatus(Status status);

View File

@ -88,9 +88,7 @@ public class ChannelConfigHelper {
synchronized (discordToConfigMap) {
discordToConfigMap.clear();
for (Map.Entry<Long, Pair<String, ChannelConfig>> entry : newMap.entrySet()) {
discordToConfigMap.put(entry.getKey(), entry.getValue());
}
discordToConfigMap.putAll(newMap);
}
}

View File

@ -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());
}

View File

@ -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<Long> GUILD_CONTEXT = ThreadLocal.withInitial(() -> 0L);
private static final ThreadLocal<Context> 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<DiscordToMinecraftChatConfig> config,
Runnable runnable
) {
getWithContext(event, config, () -> {
runnable.run();
return null;
});
}
public static <T> T getWithGuildContext(long guildId, Supplier<T> supplier) {
GUILD_CONTEXT.set(guildId);
public static <T> T getWithContext(
DiscordMessageProcessingEvent event,
OrDefault<DiscordToMinecraftChatConfig> config,
Supplier<T> 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<DiscordGuild> 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<DiscordToMinecraftChatConfig> config;
public Context(DiscordMessageProcessingEvent event, OrDefault<DiscordToMinecraftChatConfig> config) {
this.event = event;
this.config = config;
}
}
}

View File

@ -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 = "[&#5865F2Discord&r] [hover:show_text:Tag: %user_tag%&r\\nRoles: %user_roles_, |text_&7&oNone%%]%user_color%%user_effective_name%&r » %message%";
public String format = "[&#5865F2Discord&r] [hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles_, |text_&7&oNone%]%user_color%%user_effective_name%&r » %message% %message_attachments%";
@Comment("The Discord to Minecraft message format for webhook messages (if enabled)")
public String webhookFormat = "[&#5865F2Discord&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 = "[&#5865F2Discord&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<Pattern, String> 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("&#5865f2@%role_name%", "&#5865f2@deleted-role");
public Format channel = new Format("&#5865f2#%channel_name%", "&#5865f2#deleted-channel");
public Format user = new Format("[hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles_, |text_&7&oNone%]&#5865f2@%user_effective_name|user_name%", "&#5865f2@Unknown user");
@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;
}
}
}
}

View File

@ -38,4 +38,16 @@ public class MinecraftToDiscordChatConfig {
@Comment("Regex filters for Minecraft message contents (this is the %message% part of the \"format\" option)")
public Map<Pattern, String> 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;
}
}

View File

@ -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 <T> CompletableFuture<T> mapExceptions(CompletableFuture<T> 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 <T> CompletableFuture<T> notReady() {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(new NotReadyException());
return future;
}
@Override
public @NotNull Optional<? extends DiscordMessageChannel> getMessageChannelById(long id) {
Optional<DiscordTextChannel> textChannel = getTextChannelById(id);
@ -115,7 +137,26 @@ public class DiscordAPIImpl implements DiscordAPI {
public @NotNull Optional<DiscordUser> getUserById(long id) {
return discordSRV.jda()
.map(jda -> jda.getUserById(id))
.map(DiscordUserImpl::new);
.map(user -> new DiscordUserImpl(discordSRV, user));
}
@Override
public CompletableFuture<DiscordUser> 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<WebhookClient> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) {
CompletableFuture<WebhookClient> future = new CompletableFuture<>();
JDA jda = discordSRV.jda().orElse(null);
if (jda == null) {
future.completeExceptionally(new NotReadyException());
return future;
return discordSRV.discordAPI().notReady();
}
CompletableFuture<WebhookClient> 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;
}

View File

@ -16,31 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<DiscordDMChannel> 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();
}
}

View File

@ -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<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) {
if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
}
CompletableFuture<ReceivedDiscordMessage> future = privateChannel()
CompletableFuture<ReceivedDiscordMessage> future = privateChannel
.sendMessage(SendableDiscordMessageUtil.toJDA(message))
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
return mapExceptions(future);
return discordSRV.discordAPI().mapExceptions(future);
}
@Override
public CompletableFuture<Void> deleteMessageById(long id) {
CompletableFuture<Void> 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<ReceivedDiscordMessage> future = privateChannel()
CompletableFuture<ReceivedDiscordMessage> 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;
}
}

View File

@ -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 <T> CompletableFuture<T> mapExceptions(CompletableFuture<T> future) {
return future.handle(new BiFunction<T, Throwable, T>() {
@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;
}
});
}
}

View File

@ -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<ReceivedDiscordMessage> 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<ReceivedDiscordMessage> message(
SendableDiscordMessage message,
BiFunction<WebhookClient, WebhookMessage, CompletableFuture<ReadonlyMessage>> 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();
}
}

View File

@ -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> guild() {
return discordSRV.jda()
.map(jda -> jda.getGuildById(id));
return guild.getMemberCount();
}
@Override
public Optional<DiscordGuildMember> 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<DiscordGuildMember> getCachedMembers() {
Set<DiscordGuildMember> members = new HashSet<>();
for (Member member : guild.getMembers()) {
members.add(new DiscordGuildMemberImpl(discordSRV, member));
}
return members;
}
@Override
public Optional<DiscordRole> 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<DiscordRole> getRoles() {
List<DiscordRole> 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());
}
}

View File

@ -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<DiscordRole> 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<DiscordRole> roles = new ArrayList<>();
for (Role role : member.getRoles()) {
@ -64,7 +65,7 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu
@Override
public @NotNull Optional<String> 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<Component> components = new ArrayList<>();
for (DiscordRole role : getRoles()) {
components.add(Component.text(role.getName()).color(TextColor.color(role.getColor().rgb())));

View File

@ -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();
}
}

View File

@ -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<Attachment> 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<Attachment> 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<Attachment> 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<Attachment> 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<Attachment> 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<Void> 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<ReceivedDiscordMessage> 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();
}
}

View File

@ -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<DiscordUser> 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;

View File

@ -16,22 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel);
OrDefault<MinecraftToDiscordChatConfig> 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<Long> channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null);
if (channelIds == null || channelIds.isEmpty()) {
return;
}
List<CompletableFuture<ReceivedDiscordMessage>> 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<ReceivedDiscordMessage> messages = new ArrayList<>();
for (CompletableFuture<ReceivedDiscordMessage> future : futures) {
messages.add(future.join());
}
discordSRV.eventBus().publish(
new ChatMessageForwardedEvent(
new ReceivedDiscordMessageClusterImpl(messages)));
});
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Module> modules = new CopyOnWriteArraySet<>();
private final Map<String, Module> moduleLookupTable = new ConcurrentHashMap<>();
private final DiscordSRV discordSRV;
public ModuleManager(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
@SuppressWarnings("unchecked")
public <T extends Module> T getModule(Class<T> 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);
}
}
}
}

View File

@ -16,27 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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()))
);
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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()

View File

@ -16,25 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Long, Map<Long, CachedMention>> memberMentions = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> roleMentions = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> 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<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel);
OrDefault<MinecraftToDiscordChatConfig> chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord);
SendableDiscordMessage.Builder builder = chatConfig.get(cfg -> cfg.format);
if (builder == null) {
return;
}
List<Long> 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<DiscordGuild, Set<DiscordTextChannel>> 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<CompletableFuture<ReceivedDiscordMessage>> futures = new ArrayList<>();
OrDefault<MinecraftToDiscordChatConfig.Mentions> mentionConfig = chatConfig.map(cfg -> cfg.mentions);
// Format messages per-Guild
for (Map.Entry<DiscordGuild, Set<DiscordTextChannel>> entry : channels.entrySet()) {
Guild guild = entry.getKey().getAsJDAGuild();
Placeholders channelMessagePlaceholders = new Placeholders(serializedMessage);
List<CachedMention> 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<ReceivedDiscordMessage> messages = new ArrayList<>();
for (CompletableFuture<ReceivedDiscordMessage> future : futures) {
messages.add(future.join());
}
discordSRV.eventBus().publish(
new ChatMessageForwardedEvent(
new ReceivedDiscordMessageClusterImpl(messages)));
});
}
//
// Mention caching
//
private Map<Long, CachedMention> getRoleMentions(Guild guild) {
return roleMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> 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<Long, CachedMention> getMemberMentions(Guild guild) {
return channelMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> 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<Long, CachedMention> getChannelMentions(Guild guild) {
return memberMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> 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);
}
}
}

View File

@ -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);
}
};
}