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; 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.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; 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.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; 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 com.discordsrv.api.discord.api.entity.guild.DiscordRole;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional; 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. * 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 { 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 * @param id the id for the message channel
* @return the message channel * @return the message channel
*/ */
@ -47,7 +48,7 @@ public interface DiscordAPI {
Optional<? extends DiscordMessageChannel> getMessageChannelById(long id); 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 * @param id the id for the direct message channel
* @return the direct message channel * @return the direct message channel
*/ */
@ -55,7 +56,7 @@ public interface DiscordAPI {
Optional<DiscordDMChannel> getDirectMessageChannelById(long id); 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 * @param id the id for the text channel
* @return the text channel * @return the text channel
*/ */
@ -63,7 +64,7 @@ public interface DiscordAPI {
Optional<DiscordTextChannel> getTextChannelById(long id); 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 * @param id the id for the Discord server
* @return the Discord server * @return the Discord server
*/ */
@ -71,15 +72,30 @@ public interface DiscordAPI {
Optional<DiscordGuild> getGuildById(long id); 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 * @param id the id for the Discord user
* @return the Discord user * @return the Discord user
* @see #isUserCachingEnabled()
*/ */
@NotNull @NotNull
Optional<DiscordUser> getUserById(long id); 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 * @param id the id for the Discord role
* @return the Discord role * @return the Discord role
*/ */

View File

@ -23,13 +23,18 @@
package com.discordsrv.api.discord.api.entity; 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 com.discordsrv.api.placeholder.annotation.Placeholder;
import net.dv8tion.jda.api.entities.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
/** /**
* A Discord user. * 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. * Gets if this user is the bot this DiscordSRV instance is connected.
@ -67,4 +72,17 @@ public interface DiscordUser extends Snowflake {
default String getAsTag() { default String getAsTag() {
return getUsername() + "#" + getDiscriminator(); 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. * SOFTWARE.
*/ */
package com.discordsrv.api.discord.api.exception; package com.discordsrv.api.discord.api.entity;
import net.dv8tion.jda.api.requests.ErrorResponse; public interface Mentionable {
public class UnknownMessageException extends RestErrorResponseException {
public UnknownMessageException() {
super(-1);
}
public UnknownMessageException(Throwable cause) {
super(ErrorResponse.UNKNOWN_MESSAGE.getCode(), cause);
}
String getAsMention();
} }

View File

@ -23,7 +23,9 @@
package com.discordsrv.api.discord.api.entity.channel; package com.discordsrv.api.discord.api.entity.channel;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.DiscordUser;
import net.dv8tion.jda.api.entities.PrivateChannel;
/** /**
* A Discord direct message channel. * A Discord direct message channel.
@ -36,4 +38,11 @@ public interface DiscordDMChannel extends DiscordMessageChannel {
*/ */
DiscordUser getUser(); 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; 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.Snowflake;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; 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.SendableDiscordMessage;
import net.dv8tion.jda.api.entities.MessageChannel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -64,4 +66,10 @@ public interface DiscordMessageChannel extends Snowflake {
@NotNull @NotNull
CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, SendableDiscordMessage message); 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; 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 com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import net.dv8tion.jda.api.entities.TextChannel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* A Discord text channel. * A Discord text channel.
*/ */
public interface DiscordTextChannel extends DiscordMessageChannel { public interface DiscordTextChannel extends DiscordMessageChannel, Mentionable {
/** /**
* Gets the name of the text channel. * Gets the name of the text channel.
@ -42,7 +46,7 @@ public interface DiscordTextChannel extends DiscordMessageChannel {
* Gets the topic of the text channel. * Gets the topic of the text channel.
* @return the topic of the channel * @return the topic of the channel
*/ */
@NotNull @Nullable
String getTopic(); String getTopic();
/** /**
@ -52,5 +56,11 @@ public interface DiscordTextChannel extends DiscordMessageChannel {
@NotNull @NotNull
DiscordGuild getGuild(); 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; 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.discord.api.entity.Snowflake;
import com.discordsrv.api.placeholder.annotation.Placeholder; 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.Optional;
import java.util.Set;
/** /**
* A Discord server. * A Discord server.
@ -54,6 +58,12 @@ public interface DiscordGuild extends Snowflake {
*/ */
Optional<DiscordGuildMember> getMemberById(long id); 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. * 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 * @param id the id for the Discord role
@ -61,4 +71,16 @@ public interface DiscordGuild extends Snowflake {
*/ */
Optional<DiscordRole> getRoleById(long id); 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; package com.discordsrv.api.discord.api.entity.guild;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.color.Color; import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.api.entity.DiscordUser; import com.discordsrv.api.discord.api.entity.DiscordUser;
import com.discordsrv.api.discord.api.entity.Mentionable;
import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.api.placeholder.annotation.Placeholder;
import net.dv8tion.jda.api.entities.Member;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@ -34,7 +37,7 @@ import java.util.Optional;
/** /**
* A Discord server member. * A Discord server member.
*/ */
public interface DiscordGuildMember extends DiscordUser { public interface DiscordGuildMember extends DiscordUser, Mentionable {
/** /**
* Gets the Discord server this member is from. * Gets the Discord server this member is from.
@ -71,4 +74,11 @@ public interface DiscordGuildMember extends DiscordUser {
@Placeholder("user_color") @Placeholder("user_color")
Color getColor(); 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; package com.discordsrv.api.discord.api.entity.guild;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.color.Color; 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.discord.api.entity.Snowflake;
import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.api.placeholder.annotation.Placeholder;
import net.dv8tion.jda.api.entities.Role;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* A Discord server role. * A Discord server role.
*/ */
public interface DiscordRole extends Snowflake { public interface DiscordRole extends Snowflake, Mentionable {
/** /**
* The default {@link DiscordRole} color. * 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 * @return true if this role is displayed separately in the member list
*/ */
boolean isHoisted(); 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 com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -40,6 +41,12 @@ import java.util.concurrent.CompletableFuture;
*/ */
public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflake { 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, * Determines if this message was sent by this DiscordSRV instance's Discord bot,
* or a webhook being used by this DiscordSRV instance. * 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. * 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 @NotNull
Optional<DiscordTextChannel> getTextChannel(); Optional<DiscordTextChannel> getTextChannel();
/** /**
* Gets the dm channel the message was sent in. Not present if this message was sent in a server. * 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 @NotNull
Optional<DiscordDMChannel> getDMChannel(); Optional<DiscordDMChannel> getDMChannel();
@ -77,14 +84,14 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak
/** /**
* Gets the Discord server member that sent this message. * Gets the Discord server member that sent this message.
* This is not present if the message was sent by a webhook. * 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 @NotNull
Optional<DiscordGuildMember> getMember(); Optional<DiscordGuildMember> getMember();
/** /**
* Gets the Discord server the message was posted in. This is not present if the message was a dm. * 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 @NotNull
default Optional<DiscordGuild> getGuild() { default Optional<DiscordGuild> getGuild() {
@ -109,4 +116,23 @@ public interface ReceivedDiscordMessage extends SendableDiscordMessage, Snowflak
*/ */
@NotNull @NotNull
CompletableFuture<ReceivedDiscordMessage> edit(SendableDiscordMessage message); 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"); throw new IllegalStateException("DiscordSRVApi not available");
} }
this.replacements.put(PlaceholderService.PATTERN, this.replacements.put(PlaceholderService.PATTERN,
wrapFunction(matcher -> wrapFunction(matcher -> api.placeholderService().getResultAsString(matcher, context)));
api.placeholderService().getResultAsString(matcher, context)));
return this; return this;
} }

View File

@ -23,16 +23,4 @@
package com.discordsrv.api.discord.api.exception; package com.discordsrv.api.discord.api.exception;
import net.dv8tion.jda.api.requests.ErrorResponse; public class EntityNoLongerAvailableException extends Exception {}
public class UnknownChannelException extends RestErrorResponseException {
public UnknownChannelException() {
super(-1);
}
public UnknownChannelException(Throwable cause) {
super(ErrorResponse.UNKNOWN_CHANNEL.getCode(), cause);
}
}

View File

@ -23,16 +23,18 @@
package com.discordsrv.api.discord.api.exception; package com.discordsrv.api.discord.api.exception;
import net.dv8tion.jda.api.requests.ErrorResponse;
public class RestErrorResponseException extends RuntimeException { public class RestErrorResponseException extends RuntimeException {
private final int errorCode; private final int errorCode;
public RestErrorResponseException(int errorCode) { public RestErrorResponseException(ErrorResponse response) {
this.errorCode = errorCode; this(response.getCode(), response.getMeaning(), new EntityNoLongerAvailableException());
} }
public RestErrorResponseException(int errorCode, Throwable cause) { public RestErrorResponseException(int errorCode, String message, Throwable cause) {
super(cause); super(message + " (" + errorCode + ")", cause);
this.errorCode = errorCode; this.errorCode = errorCode;
} }

View File

@ -23,12 +23,16 @@
package com.discordsrv.api.discord.api.util; package com.discordsrv.api.discord.api.util;
import java.util.regex.Matcher;
public final class DiscordFormattingUtil { public final class DiscordFormattingUtil {
private DiscordFormattingUtil() {} private DiscordFormattingUtil() {}
public static String escapeContent(String content) { public static String escapeContent(String content) {
content = escapeChars(content, '*', '_', '|', '`', '~', '>'); content = escapeChars(content, '*', '_', '|', '`', '~');
content = escapeQuote(content);
content = escapeMentions(content);
return content; return content;
} }
@ -40,4 +44,12 @@ public final class DiscordFormattingUtil {
} }
return input; 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}. * 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 * @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. * 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); void unsubscribe(@NotNull Object eventListener);

View File

@ -24,6 +24,7 @@
package com.discordsrv.api.event.events.message.receive.discord; 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.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.ReceivedDiscordMessage;
import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Cancellable;
import com.discordsrv.api.event.events.Processable; import com.discordsrv.api.event.events.Processable;
@ -33,7 +34,7 @@ public class DiscordMessageProcessingEvent implements Cancellable, Processable {
private final ReceivedDiscordMessage discordMessage; private final ReceivedDiscordMessage discordMessage;
private String messageContent; private String messageContent;
private DiscordTextChannel channel; private final DiscordTextChannel channel;
private boolean cancelled; private boolean cancelled;
private boolean processed; private boolean processed;
@ -59,8 +60,8 @@ public class DiscordMessageProcessingEvent implements Cancellable, Processable {
return channel; return channel;
} }
public void setChannel(DiscordTextChannel channel) { public DiscordGuild getGuild() {
this.channel = channel; return channel.getGuild();
} }
@Override @Override

View File

@ -14,7 +14,7 @@ ext {
// MinecraftDependencyDownload // MinecraftDependencyDownload
mddVersion = '1.0.0-SNAPSHOT' mddVersion = '1.0.0-SNAPSHOT'
// JDA // JDA
jdaVersion = '4.3.0_334' jdaVersion = '5.0.0-alpha.2'
// Configurate // Configurate
configurateVersion = '4.1.2' configurateVersion = '4.1.2'
// Adventure & Adventure Platform // Adventure & Adventure Platform
@ -59,7 +59,7 @@ allprojects {
mavenCentral() mavenCentral()
maven { url 'https://nexus.scarsz.me/content/groups/public/' } maven { url 'https://nexus.scarsz.me/content/groups/public/' }
maven { url 'https://m2.dv8tion.net/releases/' } //maven { url 'https://m2.dv8tion.net/releases/' }
} }
dependencies { 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.api.event.events.lifecycle.DiscordSRVShuttingDownEvent;
import com.discordsrv.common.api.util.ApiInstanceUtil; import com.discordsrv.common.api.util.ApiInstanceUtil;
import com.discordsrv.common.channel.ChannelConfigHelper; import com.discordsrv.common.channel.ChannelConfigHelper;
import com.discordsrv.common.channel.DefaultGlobalChannel;
import com.discordsrv.common.component.ComponentFactory; import com.discordsrv.common.component.ComponentFactory;
import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.main.MainConfig; 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.discord.details.DiscordConnectionDetailsImpl;
import com.discordsrv.common.event.bus.EventBusImpl; import com.discordsrv.common.event.bus.EventBusImpl;
import com.discordsrv.common.function.CheckedRunnable; 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.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.ComponentResultStringifier;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext; import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext;
@ -49,6 +50,7 @@ import net.dv8tion.jda.api.JDA;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.annotation.OverridingMethodsMustInvokeSuper; import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -73,8 +75,8 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
private DiscordConnectionDetails discordConnectionDetails; private DiscordConnectionDetails discordConnectionDetails;
// DiscordSRV // DiscordSRV
private final DefaultGlobalChannel defaultGlobalChannel = new DefaultGlobalChannel(this);
private ChannelConfigHelper channelConfig; private ChannelConfigHelper channelConfig;
private ModuleManager moduleManager;
private DiscordConnectionManager discordConnectionManager; private DiscordConnectionManager discordConnectionManager;
// Internal // Internal
@ -132,11 +134,6 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// DiscordSRV // DiscordSRV
@Override
public DefaultGlobalChannel defaultGlobalChannel() {
return defaultGlobalChannel;
}
@Override @Override
public ChannelConfigHelper channelConfig() { public ChannelConfigHelper channelConfig() {
return channelConfig; return channelConfig;
@ -164,6 +161,21 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
return configManager().config(); 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 @Override
public Locale locale() { public Locale locale() {
// TODO: config // TODO: config
@ -250,13 +262,16 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// Register PlayerProvider listeners // Register PlayerProvider listeners
playerProvider().subscribe(); playerProvider().subscribe();
// Register listeners // Register modules
// DiscordAPI moduleManager = new ModuleManager(this);
eventBus().subscribe(new DiscordAPIListener(this)); for (Module module : Arrays.asList(
// Chat new DiscordAPIEventModule(this),
eventBus().subscribe(new ChannelLookupListener(this)); new DiscordToMinecraftModule(this),
eventBus().subscribe(new GameChatListener(this)); new GlobalChannelLookupModule(this),
eventBus().subscribe(new DiscordChatListener(this)); new MinecraftToDiscordModule(this)
)) {
registerModule(module);
}
} }
@OverridingMethodsMustInvokeSuper @OverridingMethodsMustInvokeSuper

View File

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

View File

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

View File

@ -38,8 +38,8 @@ public class ComponentFactory implements MinecraftComponentFactory {
public ComponentFactory(DiscordSRV discordSRV) { public ComponentFactory(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.minecraftSerializer = new MinecraftSerializer( this.minecraftSerializer = new MinecraftSerializer(
MinecraftSerializerOptions.defaults().addRenderer(new DiscordSRVMinecraftRenderer(discordSRV)), MinecraftSerializerOptions.defaults()
MinecraftSerializerOptions.escapeDefaults() .addRenderer(new DiscordSRVMinecraftRenderer(discordSRV))
); );
this.discordSerializer = new DiscordSerializer(DiscordSerializerOptions.defaults()); this.discordSerializer = new DiscordSerializer(DiscordSerializerOptions.defaults());
} }

View File

@ -18,79 +18,143 @@
package com.discordsrv.common.component.renderer; 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.DiscordGuild;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.api.entity.guild.DiscordRole; 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.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 dev.vankka.mcdiscordreserializer.renderer.implementation.DefaultMinecraftRenderer;
import lombok.NonNull; 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.dv8tion.jda.api.utils.MiscUtil;
import net.kyori.adventure.text.Component; 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; import java.util.function.Supplier;
public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { 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; private final DiscordSRV discordSRV;
public DiscordSRVMinecraftRenderer(DiscordSRV discordSRV) { public DiscordSRVMinecraftRenderer(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
} }
public static void runInGuildContext(long guildId, Runnable runnable) { public static void runInContext(
getWithGuildContext(guildId, () -> { DiscordMessageProcessingEvent event,
OrDefault<DiscordToMinecraftChatConfig> config,
Runnable runnable
) {
getWithContext(event, config, () -> {
runnable.run(); runnable.run();
return null; return null;
}); });
} }
public static <T> T getWithGuildContext(long guildId, Supplier<T> supplier) { public static <T> T getWithContext(
GUILD_CONTEXT.set(guildId); DiscordMessageProcessingEvent event,
OrDefault<DiscordToMinecraftChatConfig> config,
Supplier<T> supplier
) {
CONTEXT.set(new Context(event, config));
T output = supplier.get(); T output = supplier.get();
GUILD_CONTEXT.set(0L); CONTEXT.remove();
return output; return output;
} }
@Override @Override
public @Nullable Component appendChannelMention(@NonNull Component component, @NonNull String id) { public @NotNull Component appendChannelMention(@NonNull Component component, @NonNull String id) {
return component.append(Component.text( Context context = CONTEXT.get();
discordSRV.jda() DiscordToMinecraftChatConfig.Mentions.Format format =
.map(jda -> jda.getGuildChannelById(id)) context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.channel) : null;
.map(AbstractChannel::getName) if (format == null) {
.map(name -> "#" + name) return component.append(Component.text("<#" + id + ">"));
.orElse("<#" + 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 @Override
public @Nullable Component appendUserMention(@NonNull Component component, @NonNull String id) { public @NotNull Component appendUserMention(@NonNull Component component, @NonNull String id) {
long guildId = GUILD_CONTEXT.get(); Context context = CONTEXT.get();
Optional<DiscordGuild> guild = guildId > 0 DiscordToMinecraftChatConfig.Mentions.Format format =
? discordSRV.discordAPI().getGuildById(guildId) context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.user) : null;
: Optional.empty(); 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); long userId = MiscUtil.parseLong(id);
return component.append(Component.text( DiscordUser user = discordSRV.discordAPI().getUserById(userId).orElse(null);
guild.flatMap(g -> g.getMemberById(userId)) DiscordGuildMember member = guild.getMemberById(userId).orElse(null);
.map(member -> "@" + member.getEffectiveName())
.orElseGet(() -> discordSRV.discordAPI() EnhancedTextBuilder builder = discordSRV.componentFactory()
.getUserById(userId) .enhancedBuilder(user != null ? format.format : format.unknownFormat);
.map(user -> "@" + user.getUsername())
.orElse("<@" + id + ">")) if (user != null) {
builder.addContext(user);
}
if (member != null) {
builder.addContext(member);
}
return component.append(ComponentUtil.fromAPI(
builder.applyPlaceholderService().build()
)); ));
} }
@Override @Override
public @Nullable Component appendRoleMention(@NonNull Component component, @NonNull String id) { public @NotNull Component appendRoleMention(@NonNull Component component, @NonNull String id) {
return component.append(Component.text( Context context = CONTEXT.get();
discordSRV.discordAPI() DiscordToMinecraftChatConfig.Mentions.Format format =
.getRoleById(MiscUtil.parseLong(id)) context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.role) : null;
.map(DiscordRole::getName) if (format == null) {
.map(name -> "@" + name) return component.append(Component.text("<#" + id + ">"));
.orElse("<@" + 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 { public class DiscordToMinecraftChatConfig {
@Comment("The Discord to Minecraft message format for regular users") @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)") @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%"; public String webhookFormat = "[&#5865F2Discord&r] [hover:show_text:Webhook message]%user_name%&r » %message% %message_attachments%";
@Comment("Users, bots and webhooks to ignore")
public Ignores ignores = new Ignores();
// TODO: more info on regex pairs (String#replaceAll) // TODO: more info on regex pairs (String#replaceAll)
@Comment("Regex filters for Discord message contents (this is the %message% part of the \"format\" option)") @Comment("Regex filters for Discord message contents (this is the %message% part of the \"format\" option)")
public Map<Pattern, String> contentRegexFilters = new LinkedHashMap<>(); public Map<Pattern, String> contentRegexFilters = new LinkedHashMap<>();
@Comment("Users, bots and webhooks to ignore")
public Ignores ignores = new Ignores();
@ConfigSerializable @ConfigSerializable
public static class Ignores { public static class Ignores {
@Comment("User, bot and webhook ids to ignore") @Comment("User, bot and webhook ids to ignore")
public IDs usersAndWebhookIds = new IDs(); 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)") @Comment("Regex filters for Minecraft message contents (this is the %message% part of the \"format\" option)")
public Map<Pattern, String> contentRegexFilters = new LinkedHashMap<>(); 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.DiscordGuild;
import com.discordsrv.api.discord.api.entity.guild.DiscordRole; import com.discordsrv.api.discord.api.entity.guild.DiscordRole;
import com.discordsrv.api.discord.api.exception.NotReadyException; 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.DiscordSRV;
import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfig; 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.channel.DiscordTextChannelImpl;
import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.guild.DiscordRoleImpl; 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.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry; 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.TextChannel;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.Webhook; 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.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -80,6 +82,26 @@ public class DiscordAPIImpl implements DiscordAPI {
return cachedClients; 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 @Override
public @NotNull Optional<? extends DiscordMessageChannel> getMessageChannelById(long id) { public @NotNull Optional<? extends DiscordMessageChannel> getMessageChannelById(long id) {
Optional<DiscordTextChannel> textChannel = getTextChannelById(id); Optional<DiscordTextChannel> textChannel = getTextChannelById(id);
@ -115,7 +137,26 @@ public class DiscordAPIImpl implements DiscordAPI {
public @NotNull Optional<DiscordUser> getUserById(long id) { public @NotNull Optional<DiscordUser> getUserById(long id) {
return discordSRV.jda() return discordSRV.jda()
.map(jda -> jda.getUserById(id)) .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 @Override
@ -129,17 +170,15 @@ public class DiscordAPIImpl implements DiscordAPI {
@Override @Override
public @NonNull CompletableFuture<WebhookClient> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) { public @NonNull CompletableFuture<WebhookClient> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) {
CompletableFuture<WebhookClient> future = new CompletableFuture<>();
JDA jda = discordSRV.jda().orElse(null); JDA jda = discordSRV.jda().orElse(null);
if (jda == null) { if (jda == null) {
future.completeExceptionally(new NotReadyException()); return discordSRV.discordAPI().notReady();
return future;
} }
CompletableFuture<WebhookClient> future = new CompletableFuture<>();
TextChannel textChannel = jda.getTextChannelById(channelId); TextChannel textChannel = jda.getTextChannelById(channelId);
if (textChannel == null) { if (textChannel == null) {
future.completeExceptionally(new UnknownChannelException()); future.completeExceptionally(new IllegalArgumentException("Channel could not be found"));
return future; return future;
} }

View File

@ -16,31 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.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 net.dv8tion.jda.api.entities.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class DiscordUserImpl implements DiscordUser { public class DiscordUserImpl implements DiscordUser {
private final long id; private final DiscordSRV discordSRV;
private final User user;
private final boolean self; private final boolean self;
private final boolean bot;
private final String username;
private final String discriminator;
public DiscordUserImpl(User user) { public DiscordUserImpl(DiscordSRV discordSRV, User user) {
this.id = user.getIdLong(); this.discordSRV = discordSRV;
this.user = user;
this.self = user.getIdLong() == user.getJDA().getSelfUser().getIdLong(); this.self = user.getIdLong() == user.getJDA().getSelfUser().getIdLong();
this.bot = user.isBot();
this.username = user.getName();
this.discriminator = user.getDiscriminator();
} }
@Override @Override
public long getId() { public long getId() {
return id; return user.getIdLong();
} }
@Override @Override
@ -50,16 +52,39 @@ public class DiscordUserImpl implements DiscordUser {
@Override @Override
public boolean isBot() { public boolean isBot() {
return bot; return user.isBot();
} }
@Override @Override
public @NotNull String getUsername() { public @NotNull String getUsername() {
return username; return user.getName();
} }
@Override @Override
public @NotNull String getDiscriminator() { 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; 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.channel.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; 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.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.DiscordSRV;
import com.discordsrv.common.discord.api.DiscordUserImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil; import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil;
import com.discordsrv.common.discord.api.user.DiscordUserImpl; import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.PrivateChannel; import net.dv8tion.jda.api.entities.PrivateChannel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -37,32 +35,18 @@ import java.util.concurrent.CompletableFuture;
public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements DiscordDMChannel { public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements DiscordDMChannel {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final long id; private final PrivateChannel privateChannel;
private final DiscordUser user; private final DiscordUser user;
public DiscordDMChannelImpl(DiscordSRV discordSRV, PrivateChannel privateChannel) { public DiscordDMChannelImpl(DiscordSRV discordSRV, PrivateChannel privateChannel) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.id = privateChannel.getIdLong(); this.privateChannel = privateChannel;
this.user = new DiscordUserImpl(privateChannel.getUser()); this.user = new DiscordUserImpl(discordSRV, 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;
} }
@Override @Override
public long getId() { public long getId() {
return id; return privateChannel.getIdLong();
} }
@Override @Override
@ -70,25 +54,28 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D
return user; return user;
} }
@Override
public PrivateChannel getAsJDAPrivateChannel() {
return privateChannel;
}
@Override @Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) { public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) {
if (message.isWebhookMessage()) { if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels"); throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
} }
CompletableFuture<ReceivedDiscordMessage> future = privateChannel() CompletableFuture<ReceivedDiscordMessage> future = privateChannel
.sendMessage(SendableDiscordMessageUtil.toJDA(message)) .sendMessage(SendableDiscordMessageUtil.toJDA(message))
.submit() .submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); .thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
return mapExceptions(future);
return discordSRV.discordAPI().mapExceptions(future);
} }
@Override @Override
public CompletableFuture<Void> deleteMessageById(long id) { public CompletableFuture<Void> deleteMessageById(long id) {
CompletableFuture<Void> future = privateChannel() return discordSRV.discordAPI().mapExceptions(privateChannel.deleteMessageById(id).submit());
.deleteMessageById(id)
.submit();
return mapExceptions(future);
} }
@Override @Override
@ -97,10 +84,16 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels"); throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
} }
CompletableFuture<ReceivedDiscordMessage> future = privateChannel() CompletableFuture<ReceivedDiscordMessage> future = privateChannel
.editMessageById(id, SendableDiscordMessageUtil.toJDA(message)) .editMessageById(id, SendableDiscordMessageUtil.toJDA(message))
.submit() .submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); .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; package com.discordsrv.common.discord.api.channel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; 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 com.discordsrv.common.DiscordSRV;
import lombok.SneakyThrows;
import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.PrivateChannel; import net.dv8tion.jda.api.entities.PrivateChannel;
import net.dv8tion.jda.api.entities.TextChannel; 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 { public abstract class DiscordMessageChannelImpl implements DiscordMessageChannel {
@ -44,29 +35,4 @@ public abstract class DiscordMessageChannelImpl implements DiscordMessageChannel
throw new IllegalArgumentException("Unknown MessageChannel type"); 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.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; 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.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.DiscordSRV;
import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil; 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.Message;
import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.requests.restaction.MessageAction; import net.dv8tion.jda.api.requests.restaction.MessageAction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@ -44,32 +42,28 @@ import java.util.function.BiFunction;
public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements DiscordTextChannel { public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements DiscordTextChannel {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final long id; private final TextChannel textChannel;
private final String name;
private final String topic;
private final DiscordGuild guild; private final DiscordGuild guild;
public DiscordTextChannelImpl(DiscordSRV discordSRV, TextChannel textChannel) { public DiscordTextChannelImpl(DiscordSRV discordSRV, TextChannel textChannel) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.id = textChannel.getIdLong(); this.textChannel = textChannel;
this.name = textChannel.getName();
this.topic = textChannel.getTopic();
this.guild = new DiscordGuildImpl(discordSRV, textChannel.getGuild()); this.guild = new DiscordGuildImpl(discordSRV, textChannel.getGuild());
} }
@Override @Override
public long getId() { public long getId() {
return id; return textChannel.getIdLong();
} }
@Override @Override
public @NotNull String getName() { public @NotNull String getName() {
return name; return textChannel.getName();
} }
@Override @Override
public @NotNull String getTopic() { public @Nullable String getTopic() {
return topic; return textChannel.getTopic();
} }
@Override @Override
@ -77,6 +71,11 @@ public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements
return guild; return guild;
} }
@Override
public TextChannel getAsJDATextChannel() {
return textChannel;
}
@Override @Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) { public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) {
return message(message, WebhookClient::send, MessageChannel::sendMessage); 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( private CompletableFuture<ReceivedDiscordMessage> message(
SendableDiscordMessage message, SendableDiscordMessage message,
BiFunction<WebhookClient, WebhookMessage, CompletableFuture<ReadonlyMessage>> webhookFunction, BiFunction<WebhookClient, WebhookMessage, CompletableFuture<ReadonlyMessage>> webhookFunction,
@ -107,24 +111,17 @@ public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements
client, SendableDiscordMessageUtil.toWebhook(message))) client, SendableDiscordMessageUtil.toWebhook(message)))
.thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg)); .thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg));
} else { } 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 future = jdaFunction
.apply(textChannel, SendableDiscordMessageUtil.toJDA(message)) .apply(textChannel, SendableDiscordMessageUtil.toJDA(message))
.submit() .submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg)); .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.api.discord.api.entity.guild.DiscordRole;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.Guild; 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 { public class DiscordGuildImpl implements DiscordGuild {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final long id; private final Guild guild;
private final String name;
private final int memberCount;
public DiscordGuildImpl(DiscordSRV discordSRV, Guild guild) { public DiscordGuildImpl(DiscordSRV discordSRV, Guild guild) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.id = guild.getIdLong(); this.guild = guild;
this.name = guild.getName();
this.memberCount = guild.getMemberCount();
} }
@Override @Override
public long getId() { public long getId() {
return id; return guild.getIdLong();
} }
@Override @Override
public String getName() { public String getName() {
return name; return guild.getName();
} }
@Override @Override
public int getMemberCount() { public int getMemberCount() {
return memberCount; return guild.getMemberCount();
}
private Optional<Guild> guild() {
return discordSRV.jda()
.map(jda -> jda.getGuildById(id));
} }
@Override @Override
public Optional<DiscordGuildMember> getMemberById(long id) { public Optional<DiscordGuildMember> getMemberById(long id) {
return guild() Member member = guild.getMemberById(id);
.map(guild -> guild.getMemberById(id)) if (member == null) {
.map(member -> new DiscordGuildMemberImpl(discordSRV, member)); 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 @Override
public Optional<DiscordRole> getRoleById(long id) { public Optional<DiscordRole> getRoleById(long id) {
return guild() Role role = guild.getRoleById(id);
.map(guild -> guild.getRoleById(id)) if (role == null) {
.map(DiscordRoleImpl::new); 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.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder; import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.common.DiscordSRV; 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.Member;
import net.dv8tion.jda.api.entities.Role; 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.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
@ -39,15 +40,15 @@ import java.util.Optional;
public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGuildMember { public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGuildMember {
private final Member member;
private final DiscordGuild guild; private final DiscordGuild guild;
private final String nickname;
private final List<DiscordRole> roles; private final List<DiscordRole> roles;
private final Color color; private final Color color;
public DiscordGuildMemberImpl(DiscordSRV discordSRV, Member member) { public DiscordGuildMemberImpl(DiscordSRV discordSRV, Member member) {
super(member.getUser()); super(discordSRV, member.getUser());
this.member = member;
this.guild = new DiscordGuildImpl(discordSRV, member.getGuild()); this.guild = new DiscordGuildImpl(discordSRV, member.getGuild());
this.nickname = member.getNickname();
List<DiscordRole> roles = new ArrayList<>(); List<DiscordRole> roles = new ArrayList<>();
for (Role role : member.getRoles()) { for (Role role : member.getRoles()) {
@ -64,7 +65,7 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu
@Override @Override
public @NotNull Optional<String> getNickname() { public @NotNull Optional<String> getNickname() {
return Optional.ofNullable(nickname); return Optional.ofNullable(member.getNickname());
} }
@Override @Override
@ -77,6 +78,20 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu
return color; return color;
} }
@Override
public Member getAsJDAMember() {
return member;
}
@Override
public User getAsJDAUser() {
return member.getUser();
}
//
// Placeholders
//
@Placeholder(value = "user_highest_role", relookup = "role") @Placeholder(value = "user_highest_role", relookup = "role")
public DiscordRole _highestRole() { public DiscordRole _highestRole() {
return !roles.isEmpty() ? roles.get(0) : null; return !roles.isEmpty() ? roles.get(0) : null;
@ -92,14 +107,8 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu
return null; return null;
} }
@Placeholder(value = "user_roles") @Placeholder("user_roles_")
public Component _allRoles(@PlaceholderRemainder String suffix) { public Component _allRoles(@PlaceholderRemainder String suffix) {
if (suffix.startsWith("_")) {
suffix = suffix.substring(1);
} else {
return null;
}
List<Component> components = new ArrayList<>(); List<Component> components = new ArrayList<>();
for (DiscordRole role : getRoles()) { for (DiscordRole role : getRoles()) {
components.add(Component.text(role.getName()).color(TextColor.color(role.getColor().rgb()))); 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 { public class DiscordRoleImpl implements DiscordRole {
private final long id; private final Role role;
private final String name;
private final Color color; private final Color color;
private final boolean hoisted;
public DiscordRoleImpl(Role role) { public DiscordRoleImpl(Role role) {
this.id = role.getIdLong(); this.role = role;
this.name = role.getName();
this.color = new Color(role.getColorRaw()); this.color = new Color(role.getColorRaw());
this.hoisted = role.isHoisted();
} }
@Override @Override
public long getId() { public long getId() {
return id; return role.getIdLong();
} }
@Override @Override
public @NotNull String getName() { public @NotNull String getName() {
return name; return role.getName();
} }
@Override @Override
@ -54,6 +50,16 @@ public class DiscordRoleImpl implements DiscordRole {
@Override @Override
public boolean isHoisted() { 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; package com.discordsrv.common.discord.api.message;
import club.minnced.discord.webhook.WebhookClient; 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.ReadonlyEmbed;
import club.minnced.discord.webhook.receive.ReadonlyMessage; import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.receive.ReadonlyUser; 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.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; 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.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.DiscordSRV;
import com.discordsrv.common.discord.api.DiscordUserImpl;
import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl; import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.guild.DiscordGuildMemberImpl; 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.Member;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User; 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 org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
@ -63,7 +69,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
String webhookAvatarUrl = webhookMessage ? message.getAuthor().getEffectiveAvatarUrl() : null; String webhookAvatarUrl = webhookMessage ? message.getAuthor().getEffectiveAvatarUrl() : null;
DiscordMessageChannel channel = DiscordMessageChannelImpl.get(discordSRV, message.getChannel()); DiscordMessageChannel channel = DiscordMessageChannelImpl.get(discordSRV, message.getChannel());
DiscordUser user = new DiscordUserImpl(message.getAuthor()); DiscordUser user = new DiscordUserImpl(discordSRV, message.getAuthor());
Member member = message.getMember(); Member member = message.getMember();
DiscordGuildMember apiMember = member != null ? new DiscordGuildMemberImpl(discordSRV, member) : null; DiscordGuildMember apiMember = member != null ? new DiscordGuildMemberImpl(discordSRV, member) : null;
@ -82,8 +88,14 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
self = user.isSelf(); 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( return new ReceivedDiscordMessageImpl(
discordSRV, discordSRV,
attachments,
self, self,
channel, channel,
apiMember, apiMember,
@ -142,10 +154,16 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
webhookMessage.getAuthor().getId()).orElse(null); webhookMessage.getAuthor().getId()).orElse(null);
DiscordGuildMember member = channel instanceof DiscordTextChannel && user != null DiscordGuildMember member = channel instanceof DiscordTextChannel && user != null
? ((DiscordTextChannel) channel).getGuild().getMemberById(user.getId()).orElse(null) : 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( return new ReceivedDiscordMessageImpl(
discordSRV, discordSRV,
// These are always from rest responses attachments,
true, true, // These are always from rest responses
channel, channel,
member, member,
user, user,
@ -159,6 +177,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
} }
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final List<Attachment> attachments;
private final boolean fromSelf; private final boolean fromSelf;
private final DiscordMessageChannel channel; private final DiscordMessageChannel channel;
private final DiscordGuildMember member; private final DiscordGuildMember member;
@ -168,6 +187,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
private ReceivedDiscordMessageImpl( private ReceivedDiscordMessageImpl(
DiscordSRV discordSRV, DiscordSRV discordSRV,
List<Attachment> attachments,
boolean fromSelf, boolean fromSelf,
DiscordMessageChannel channel, DiscordMessageChannel channel,
DiscordGuildMember member, DiscordGuildMember member,
@ -181,6 +201,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
) { ) {
super(content, embeds, Collections.emptySet(), webhookUsername, webhookAvatarUrl); super(content, embeds, Collections.emptySet(), webhookUsername, webhookAvatarUrl);
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.attachments = attachments;
this.fromSelf = fromSelf; this.fromSelf = fromSelf;
this.channel = channel; this.channel = channel;
this.member = member; this.member = member;
@ -194,6 +215,11 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
return id; return id;
} }
@Override
public List<Attachment> getAttachments() {
return attachments;
}
@Override @Override
public boolean isFromSelf() { public boolean isFromSelf() {
return fromSelf; return fromSelf;
@ -233,7 +259,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null); DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null);
if (textChannel == null) { if (textChannel == null) {
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(new UnknownChannelException()); future.completeExceptionally(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
return future; return future;
} }
@ -249,10 +275,30 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null); DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null);
if (textChannel == null) { if (textChannel == null) {
CompletableFuture<ReceivedDiscordMessage> future = new CompletableFuture<>(); CompletableFuture<ReceivedDiscordMessage> future = new CompletableFuture<>();
future.completeExceptionally(new UnknownChannelException()); future.completeExceptionally(new RestErrorResponseException(ErrorResponse.UNKNOWN_CHANNEL));
return future; return future;
} }
return textChannel.editMessageById(getId(), message); 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.DiscordGuildMemberImpl;
import com.discordsrv.common.discord.api.guild.DiscordRoleImpl; import com.discordsrv.common.discord.api.guild.DiscordRoleImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; 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.discord.connection.DiscordConnectionManager;
import com.discordsrv.common.scheduler.Scheduler; import com.discordsrv.common.scheduler.Scheduler;
import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory;
@ -156,7 +156,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
CompletableFuture<DiscordUser> future = instance.retrieveApplicationInfo() CompletableFuture<DiscordUser> future = instance.retrieveApplicationInfo()
.timeout(10, TimeUnit.SECONDS) .timeout(10, TimeUnit.SECONDS)
.map(applicationInfo -> (DiscordUser) new DiscordUserImpl(applicationInfo.getOwner())) .map(applicationInfo -> (DiscordUser) new DiscordUserImpl(discordSRV, applicationInfo.getOwner()))
.submit(); .submit();
botOwnerRequest.set(future); botOwnerRequest.set(future);
@ -188,7 +188,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
} else if (o instanceof ReceivedMessage) { } else if (o instanceof ReceivedMessage) {
converted = ReceivedDiscordMessageImpl.fromJDA(discordSRV, (Message) o); converted = ReceivedDiscordMessageImpl.fromJDA(discordSRV, (Message) o);
} else if (o instanceof User) { } else if (o instanceof User) {
converted = new DiscordUserImpl((User) o); converted = new DiscordUserImpl(discordSRV, (User) o);
} else { } else {
converted = o; converted = o;
isConversion = false; isConversion = false;

View File

@ -16,22 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.bus.EventListener;
import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Cancellable;
import com.discordsrv.api.event.events.Processable; import com.discordsrv.api.event.events.Processable;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
public abstract class AbstractListener { public final class EventUtil {
protected final DiscordSRV discordSRV; private EventUtil() {}
public AbstractListener(DiscordSRV discordSRV) { public static boolean checkProcessor(DiscordSRV discordSRV,Processable event) {
this.discordSRV = discordSRV;
}
public boolean checkProcessor(Processable event) {
if (!event.isProcessed()) { if (!event.isProcessed()) {
return false; return false;
} }
@ -45,7 +41,7 @@ public abstract class AbstractListener {
return true; return true;
} }
public boolean checkCancellation(Cancellable event) { public static boolean checkCancellation(DiscordSRV discordSRV, Cancellable event) {
if (!event.isCancelled()) { if (!event.isCancelled()) {
return false; 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/>. * 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.bus.Subscribe;
import com.discordsrv.api.event.events.discord.DiscordMessageReceivedEvent; import com.discordsrv.api.event.events.discord.DiscordMessageReceivedEvent;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl; import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.module.Module;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public class DiscordAPIListener { public class DiscordAPIEventModule extends Module {
private final DiscordSRV discordSRV; public DiscordAPIEventModule(DiscordSRV discordSRV) {
super(discordSRV);
public DiscordAPIListener(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
} }
@Subscribe @Subscribe
public void onMessageReceivedEvent(MessageReceivedEvent event) { public void onMessageReceivedEvent(MessageReceivedEvent event) {
discordSRV.eventBus().publish(new DiscordMessageReceivedEvent( discordSRV.eventBus().publish(new DiscordMessageReceivedEvent(
ReceivedDiscordMessageImpl.fromJDA(discordSRV, event.getMessage()), 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/>. * 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.channel.GameChannel;
import com.discordsrv.api.component.EnhancedTextBuilder; 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.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig;
import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.module.Module;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.util.Optional; import java.util.Optional;
public class DiscordChatListener extends AbstractListener { public class DiscordToMinecraftModule extends Module {
public DiscordChatListener(DiscordSRV discordSRV) { public DiscordToMinecraftModule(DiscordSRV discordSRV) {
super(discordSRV); super(discordSRV);
} }
@ -53,10 +54,7 @@ public class DiscordChatListener extends AbstractListener {
return; return;
} }
discordSRV.eventBus().publish( discordSRV.eventBus().publish(new DiscordMessageProcessingEvent(event.getMessage(), channel));
new DiscordMessageProcessingEvent(
event.getMessage(),
channel));
} }
@Subscribe @Subscribe
@ -113,7 +111,7 @@ public class DiscordChatListener extends AbstractListener {
chatConfig.opt(cfg -> cfg.contentRegexFilters) chatConfig.opt(cfg -> cfg.contentRegexFilters)
.ifPresent(filters -> filters.forEach(message::replaceAll)); .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())); discordSRV.componentFactory().minecraftSerializer().serialize(message.toString()));
EnhancedTextBuilder componentBuilder = discordSRV.componentFactory() EnhancedTextBuilder componentBuilder = discordSRV.componentFactory()

View File

@ -16,25 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.EventPriority;
import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.channel.GameChannelLookupEvent; import com.discordsrv.api.event.events.channel.GameChannelLookupEvent;
import com.discordsrv.common.DiscordSRV; 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); super(discordSRV);
defaultGlobalChannel = new DefaultGlobalChannel(discordSRV);
} }
@Subscribe(priority = EventPriority.LAST) @Subscribe(priority = EventPriority.LATE)
public void onGameChannelLookup(GameChannelLookupEvent event) { public void onGameChannelLookup(GameChannelLookupEvent event) {
if (!event.getChannelName().equalsIgnoreCase("global") || checkProcessor(event)) { if (EventUtil.checkProcessor(discordSRV, event)) {
return; 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 { try {
runnable.run(); runnable.run();
} catch (Throwable t) { } catch (Throwable t) {
discordSRV.logger().error(Thread.currentThread().getName() + " caught a exception", t); discordSRV.logger().error(Thread.currentThread().getName() + " ran into an exception", t);
} }
}; };
} }