package net.essentialsx.discord; import club.minnced.discord.webhook.WebhookClient; import club.minnced.discord.webhook.WebhookClientBuilder; import club.minnced.discord.webhook.send.WebhookMessage; import club.minnced.discord.webhook.send.WebhookMessageBuilder; import com.earth2me.essentials.utils.FormatUtil; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.Webhook; import net.dv8tion.jda.api.events.ShutdownEvent; import net.dv8tion.jda.api.hooks.EventListener; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.essentialsx.api.v2.events.discord.DiscordMessageEvent; import net.essentialsx.api.v2.services.discord.DiscordService; import net.essentialsx.api.v2.services.discord.InteractionController; import net.essentialsx.api.v2.services.discord.InteractionException; import net.essentialsx.api.v2.services.discord.MessageType; import net.essentialsx.api.v2.services.discord.Unsafe; import net.essentialsx.discord.interactions.InteractionControllerImpl; import net.essentialsx.discord.interactions.commands.ExecuteCommand; import net.essentialsx.discord.interactions.commands.ListCommand; import net.essentialsx.discord.interactions.commands.MessageCommand; import net.essentialsx.discord.listeners.BukkitListener; import net.essentialsx.discord.listeners.DiscordCommandDispatcher; import net.essentialsx.discord.listeners.DiscordListener; import net.essentialsx.discord.util.ConsoleInjector; import net.essentialsx.discord.util.DiscordUtil; import org.bukkit.Bukkit; import org.bukkit.event.HandlerList; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.ServicePriority; import org.jetbrains.annotations.NotNull; import javax.security.auth.login.LoginException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import static com.earth2me.essentials.I18n.tl; public class JDADiscordService implements DiscordService { private final static Logger logger = Logger.getLogger("EssentialsDiscord"); private final EssentialsDiscord plugin; private final Unsafe unsafe = this::getJda; private JDA jda; private Guild guild; private TextChannel primaryChannel; private WebhookClient consoleWebhook; private String lastConsoleId; private final Map registeredTypes = new HashMap<>(); private final Map typeToChannelId = new HashMap<>(); private final Map channelIdToWebhook = new HashMap<>(); private ConsoleInjector injector; private DiscordCommandDispatcher commandDispatcher; private InteractionControllerImpl interactionController; public JDADiscordService(EssentialsDiscord plugin) { this.plugin = plugin; for (final MessageType type : MessageType.DefaultTypes.values()) { registerMessageType(plugin, type); } } public TextChannel getChannel(String key, boolean primaryFallback) { long resolvedId; try { resolvedId = Long.parseLong(key); } catch (NumberFormatException ignored) { resolvedId = getSettings().getChannelId(getSettings().getMessageChannel(key)); } if (isDebug()) { logger.log(Level.INFO, "Channel definition " + key + " resolved as " + resolvedId); } TextChannel channel = guild.getTextChannelById(resolvedId); if (channel == null && primaryFallback) { if (isDebug()) { logger.log(Level.WARNING, "Resolved channel id " + resolvedId + " was not found! Falling back to primary channel."); } channel = primaryChannel; } return channel; } public WebhookMessage getWebhookMessage(String message) { return getWebhookMessage(message, jda.getSelfUser().getAvatarUrl(), getSettings().getConsoleWebhookName(), false); } public WebhookMessage getWebhookMessage(String message, String avatarUrl, String name, boolean groupMentions) { return new WebhookMessageBuilder() .setAvatarUrl(avatarUrl) .setAllowedMentions(groupMentions ? DiscordUtil.ALL_MENTIONS_WEBHOOK : DiscordUtil.NO_GROUP_MENTIONS_WEBHOOK) .setUsername(name) .setContent(message) .build(); } public void sendMessage(DiscordMessageEvent event, String message, boolean groupMentions) { final TextChannel channel = getChannel(event.getType().getKey(), true); final String strippedContent = FormatUtil.stripFormat(message); final String webhookChannelId = typeToChannelId.get(event.getType()); if (webhookChannelId != null) { final WebhookClient client = channelIdToWebhook.get(webhookChannelId); if (client != null) { final String avatarUrl = event.getAvatarUrl() != null ? event.getAvatarUrl() : jda.getSelfUser().getAvatarUrl(); final String name = event.getName() != null ? event.getName() : guild.getSelfMember().getEffectiveName(); client.send(getWebhookMessage(strippedContent, avatarUrl, name, groupMentions)); return; } } if (!channel.canTalk()) { logger.warning(tl("discordNoSendPermission", channel.getName())); return; } channel.sendMessage(strippedContent) .allowedMentions(groupMentions ? null : DiscordUtil.NO_GROUP_MENTIONS) .queue(); } public void startup() throws LoginException, InterruptedException { shutdown(); logger.log(Level.INFO, tl("discordLoggingIn")); if (plugin.getSettings().getBotToken().replace("INSERT-TOKEN-HERE", "").trim().isEmpty()) { throw new IllegalArgumentException(tl("discordErrorNoToken")); } jda = JDABuilder.createDefault(plugin.getSettings().getBotToken()) .addEventListeners(new DiscordListener(this)) .setContextEnabled(false) .setRawEventsEnabled(true) .build() .awaitReady(); updatePresence(); logger.log(Level.INFO, tl("discordLoggingInDone", jda.getSelfUser().getAsTag())); if (jda.getGuilds().isEmpty()) { throw new IllegalArgumentException(tl("discordErrorNoGuildSize")); } guild = jda.getGuildById(plugin.getSettings().getGuildId()); if (guild == null) { throw new IllegalArgumentException(tl("discordErrorNoGuild")); } interactionController = new InteractionControllerImpl(this); try { interactionController.registerCommand(new ExecuteCommand(this)); interactionController.registerCommand(new MessageCommand(this)); interactionController.registerCommand(new ListCommand(this)); } catch (InteractionException ignored) { // won't happen } updatePrimaryChannel(); updateConsoleRelay(); updateTypesRelay(); Bukkit.getPluginManager().registerEvents(new BukkitListener(this), plugin); Bukkit.getServicesManager().register(DiscordService.class, this, plugin, ServicePriority.Normal); } @Override public boolean isRegistered(String key) { return registeredTypes.containsKey(key); } @Override public void registerMessageType(Plugin plugin, MessageType type) { if (!type.getKey().matches("^[a-z0-9-]*$")) { throw new IllegalArgumentException("MessageType key must match \"^[a-z0-9-]*$\""); } if (registeredTypes.containsKey(type.getKey())) { throw new IllegalArgumentException("A MessageType with that key is already registered!"); } registeredTypes.put(type.getKey(), type); } @Override public void sendMessage(MessageType type, String message, boolean allowGroupMentions) { if (!registeredTypes.containsKey(type.getKey())) { logger.warning("Sending message to channel \"" + type.getKey() + "\" which is an unregistered type! If you are a plugin author, you should be registering your MessageType before using them."); } final DiscordMessageEvent event = new DiscordMessageEvent(type, FormatUtil.stripFormat(message), allowGroupMentions); if (Bukkit.getServer().isPrimaryThread()) { Bukkit.getPluginManager().callEvent(event); } else { Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getPluginManager().callEvent(event)); } } @Override public InteractionController getInteractionController() { return interactionController; } public void updatePrimaryChannel() { TextChannel channel = guild.getTextChannelById(plugin.getSettings().getPrimaryChannelId()); if (channel == null) { channel = guild.getDefaultChannel(); if (channel == null || !channel.canTalk()) { throw new RuntimeException(tl("discordErrorNoPerms")); } } primaryChannel = channel; } public void updatePresence() { jda.getPresence().setPresence(plugin.getSettings().getStatus(), plugin.getSettings().getStatusActivity()); } public void updateTypesRelay() { if (!getSettings().isShowAvatar() && !getSettings().isShowName()) { for (WebhookClient webhook : channelIdToWebhook.values()) { webhook.close(); } typeToChannelId.clear(); channelIdToWebhook.clear(); return; } for (MessageType type : MessageType.DefaultTypes.values()) { if (!type.isPlayer()) { continue; } final TextChannel channel = getChannel(type.getKey(), true); if (channel.getId().equals(typeToChannelId.get(type))) { continue; } final String webhookName = "EssX Advanced Relay"; Webhook webhook = DiscordUtil.getAndCleanWebhooks(channel, webhookName).join(); webhook = webhook == null ? DiscordUtil.createWebhook(channel, webhookName).join() : webhook; if (webhook == null) { final WebhookClient current = channelIdToWebhook.get(channel.getId()); if (current != null) { current.close(); } channelIdToWebhook.remove(channel.getId()); continue; } typeToChannelId.put(type, channel.getId()); channelIdToWebhook.put(channel.getId(), DiscordUtil.getWebhookClient(webhook.getIdLong(), webhook.getToken(), jda.getHttpClient())); } } public void updateConsoleRelay() { final String consoleDef = getSettings().getConsoleChannelDef(); final Matcher matcher = WebhookClientBuilder.WEBHOOK_PATTERN.matcher(consoleDef); final long webhookId; final String webhookToken; if (matcher.matches()) { webhookId = Long.parseUnsignedLong(matcher.group(1)); webhookToken = matcher.group(2); if (commandDispatcher != null) { jda.removeEventListener(commandDispatcher); commandDispatcher = null; } } else { final TextChannel channel = getChannel(consoleDef, false); if (channel != null) { if (getSettings().isConsoleCommandRelay()) { if (commandDispatcher == null) { commandDispatcher = new DiscordCommandDispatcher(this); jda.addEventListener(commandDispatcher); } commandDispatcher.setChannelId(channel.getId()); } else if (commandDispatcher != null) { jda.removeEventListener(commandDispatcher); commandDispatcher = null; } if (channel.getId().equals(lastConsoleId)) { return; } final String webhookName = "EssX Console Relay"; Webhook webhook = DiscordUtil.getAndCleanWebhooks(channel, webhookName).join(); webhook = webhook == null ? DiscordUtil.createWebhook(channel, webhookName).join() : webhook; if (webhook == null) { logger.info(tl("discordErrorLoggerNoPerms")); return; } webhookId = webhook.getIdLong(); webhookToken = webhook.getToken(); lastConsoleId = channel.getId(); } else if (!getSettings().getConsoleChannelDef().equals("none") && !getSettings().getConsoleChannelDef().startsWith("0")) { logger.info(tl("discordErrorLoggerInvalidChannel")); shutdownConsoleRelay(true); return; } else { // It's either not configured at all or knowingly disabled. shutdownConsoleRelay(true); return; } } shutdownConsoleRelay(false); consoleWebhook = DiscordUtil.getWebhookClient(webhookId, webhookToken, jda.getHttpClient()); if (injector == null) { injector = new ConsoleInjector(this); injector.start(); } } private void shutdownConsoleRelay(final boolean closeInjector) { if (consoleWebhook != null && !consoleWebhook.isShutdown()) { consoleWebhook.close(); } consoleWebhook = null; if (closeInjector) { if (injector != null) { injector.remove(); injector = null; } if (commandDispatcher != null) { jda.removeEventListener(commandDispatcher); commandDispatcher = null; } } } public void shutdown() { if (interactionController != null) { interactionController.shutdown(); } if (jda != null) { shutdownConsoleRelay(true); // Unregister leftover jda listeners for (Object obj : jda.getRegisteredListeners()) { if (!(obj instanceof EventListener)) { // Yeah bro I wish I knew too :/ jda.removeEventListener(obj); } } // Unregister Bukkit Events HandlerList.unregisterAll(plugin); // Creates a future which will be completed when JDA fully shutdowns final CompletableFuture future = new CompletableFuture<>(); jda.addEventListener(new ListenerAdapter() { @Override public void onShutdown(@NotNull ShutdownEvent event) { future.complete(null); } }); // Tell JDA to wrap it up jda.shutdown(); try { // Wait for JDA to wrap it up future.get(5, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.warning("JDA took longer than expected to shutdown, this may have caused some problems."); } finally { jda = null; } } } public JDA getJda() { return jda; } @Override public Unsafe getUnsafe() { return unsafe; } public Guild getGuild() { return guild; } public EssentialsDiscord getPlugin() { return plugin; } public DiscordSettings getSettings() { return plugin.getSettings(); } public WebhookClient getConsoleWebhook() { return consoleWebhook; } public boolean isDebug() { return plugin.getEss().getSettings().isDebug(); } }