diff --git a/src/main/java/cc/unilock/nilcord/EventListener.java b/src/main/java/cc/unilock/nilcord/EventListener.java index 4832771..117c2ac 100644 --- a/src/main/java/cc/unilock/nilcord/EventListener.java +++ b/src/main/java/cc/unilock/nilcord/EventListener.java @@ -2,40 +2,70 @@ import net.minecraft.entity.player.EntityServerPlayer; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.stats.Achievement; import net.minecraft.util.DamageSource; import net.minecraft.util.Translate; -public class EventListener { - private MinecraftServer server = null; +import java.time.Duration; + +import static cc.unilock.nilcord.NilcordPremain.CONFIG; +import static cc.unilock.nilcord.NilcordPremain.LOGGER; +public class EventListener { public void serverStart() { - NilcordPremain.LOGGER.info("Server started!"); - this.server = MinecraftServer.getServer(); + NilcordPremain.server = (DedicatedServer) MinecraftServer.getServer(); + try { + NilcordPremain.discord.getJda().awaitReady(); + NilcordPremain.discord.sendMessageToDiscord(CONFIG.formatting.discord.server_start_message.value()); + } catch (InterruptedException e) { + LOGGER.error(e.toString()); + } } public void serverStop() { - NilcordPremain.LOGGER.info("Server stopping!"); - this.server = null; + try { + NilcordPremain.discord.sendMessageToDiscord(CONFIG.formatting.discord.server_stop_message.value()); + NilcordPremain.discord.shutdown(); + NilcordPremain.discord.getJda().awaitShutdown(Duration.ofMillis(500)); + } catch (InterruptedException e) { + LOGGER.error(e.toString()); + } + NilcordPremain.server = null; } public void playerChatMessage(EntityServerPlayer player, String message) { - NilcordPremain.LOGGER.info("Chat Message: \""+message+"\" from "+player.username); + NilcordPremain.discord.onPlayerChatMessage(player, message); } public void playerJoin(EntityServerPlayer player) { - NilcordPremain.LOGGER.info(player.username+" joined the game"); + String message = CONFIG.formatting.discord.join_message.value() + .replace("", player.username); + NilcordPremain.discord.sendMessageToDiscord(message); } public void playerLeave(EntityServerPlayer player) { - NilcordPremain.LOGGER.info(player.username+" left the game"); + String message = CONFIG.formatting.discord.leave_message.value() + .replace("", player.username); + NilcordPremain.discord.sendMessageToDiscord(message); } public void playerAchievement(EntityServerPlayer player, Achievement achievement) { - NilcordPremain.LOGGER.info(player.username+" has made the achievement "+Translate.format(achievement.statName)+" - "+Translate.format(achievement.achievementDescription)); + // So, bad news! Statistics aren't server-side in 1.4.7 LOL + + /* + String message = CONFIG.formatting.discord.achievement_message.value() + .replace("", player.username) + .replace("", Translate.format(achievement.statName)) + .replace("", Translate.format(achievement.achievementDescription)); + NilcordPremain.discord.sendMessageToDiscord(message); + */ } public void playerDeath(EntityServerPlayer player, DamageSource source) { - NilcordPremain.LOGGER.info(player.username+" died: "+source.getDeathMessage(player)); + String message = CONFIG.formatting.discord.death_message.value() + .replace("", player.username) + .replace("", Translate.format(source.getDeathMessage(player))); + NilcordPremain.discord.sendMessageToDiscord(message); } } diff --git a/src/main/java/cc/unilock/nilcord/NilcordPremain.java b/src/main/java/cc/unilock/nilcord/NilcordPremain.java index 81c23fa..17e2b8e 100755 --- a/src/main/java/cc/unilock/nilcord/NilcordPremain.java +++ b/src/main/java/cc/unilock/nilcord/NilcordPremain.java @@ -1,5 +1,7 @@ package cc.unilock.nilcord; +import cc.unilock.nilcord.config.NilcordConfig; +import cc.unilock.nilcord.discord.Discord; import cc.unilock.nilcord.transformer.ClassReaderTransformer; import cc.unilock.nilcord.transformer.DedicatedServerTransformer; import cc.unilock.nilcord.transformer.EntityPlayerTransformer; @@ -7,13 +9,19 @@ import cc.unilock.nilcord.transformer.MinecraftServerTransformer; import cc.unilock.nilcord.transformer.NetServerHandlerTransformer; import cc.unilock.nilcord.transformer.ServerConfigurationManagerTransformer; +import net.minecraft.server.dedicated.DedicatedServer; import nilloader.api.ClassTransformer; import nilloader.api.ModRemapper; import nilloader.api.NilLogger; +import java.nio.file.Paths; + public class NilcordPremain implements Runnable { public static final NilLogger LOGGER = NilLogger.get("Nilcord"); + public static final NilcordConfig CONFIG = NilcordConfig.createToml(Paths.get("config"), "", "nilcord", NilcordConfig.class); + public static Discord discord; public static EventListener listener; + public static DedicatedServer server; @Override public void run() { @@ -32,4 +40,9 @@ public void run() { ClassTransformer.register(new NetServerHandlerTransformer()); ClassTransformer.register(new ServerConfigurationManagerTransformer()); } + + public static void initialize() { + discord = new Discord(); + listener = new EventListener(); + } } diff --git a/src/main/java/cc/unilock/nilcord/config/NilcordConfig.java b/src/main/java/cc/unilock/nilcord/config/NilcordConfig.java index 0630d24..b7ba1fd 100644 --- a/src/main/java/cc/unilock/nilcord/config/NilcordConfig.java +++ b/src/main/java/cc/unilock/nilcord/config/NilcordConfig.java @@ -4,36 +4,24 @@ import folk.sisby.kaleido.lib.quiltconfig.api.annotations.Comment; import folk.sisby.kaleido.lib.quiltconfig.api.values.TrackedValue; -import java.nio.file.Paths; - public class NilcordConfig extends ReflectiveConfig { - public static final NilcordConfig INSTANCE = NilcordConfig.createToml(Paths.get("config"), "", "nilcord", NilcordConfig.class); - @Comment("Settings pertaining to Discord itself") public final Discord discord = new Discord(); public static final class Discord extends Section { @Comment("The Discord bot token to use") - public final TrackedValue bot_token = value("EMPTY"); + public final TrackedValue token = value("EMPTY"); @Comment("The Discord channel ID for the bot to send messages to / receive messages from") - public final TrackedValue bot_channel = value("EMPTY"); + public final TrackedValue channel_id = value("EMPTY"); @Comment("Settings pertaining to the Discord webhook") public final Webhook webhook = new Webhook(); public static final class Webhook extends Section { @Comment("Whether to use a webhook for sending players' chat messages to Discord") - public final TrackedValue webhook_enabled = value(Boolean.FALSE); + public final TrackedValue enabled = value(Boolean.FALSE); @Comment("The webhook URL to use") - public final TrackedValue webhook_url = value("EMPTY"); - - @Comment("The URL to use for the webhook's avatar") - @Comment("Available placeholders: ") - public final TrackedValue webhook_avatar_url = value("https://visage.surgeplay.com/bust/128/"); - - @Comment("The webhook's username") - @Comment("Available placeholders: ") - public final TrackedValue webhook_username = value(""); + public final TrackedValue url = value("EMPTY"); } } @@ -50,7 +38,7 @@ public static final class Minecraft extends Section { public final TrackedValue show_attachments = value(Boolean.TRUE); @Comment("Whether to show messages from other Discord bots in-game") - public final TrackedValue show_bot_message = value(Boolean.FALSE); + public final TrackedValue show_bot_messages = value(Boolean.FALSE); } @Comment("Settings pertaining to message formatting") @@ -82,11 +70,27 @@ public static final class DiscordFormatting extends Section { @Comment("Player achievement messages") @Comment("Additional placeholders: ") - public final TrackedValue achievement_message = value("**** has just earned the achievement **[]**\\n> \\\\> __"); + public final TrackedValue achievement_message = value("**** has just earned the achievement **[]**\n> \\> __"); @Comment("Player death messages") @Comment("Additional placeholders: ") public final TrackedValue death_message = value("** died:** __"); + + @Comment("Settings pertaining to messages sent from the webhook, if enabled") + public final WebhookFormatting webhook = new WebhookFormatting(); + public static final class WebhookFormatting extends Section { + @Comment("The URL to use for the webhook's avatar") + @Comment("Additional placeholders: N/A") + public final TrackedValue avatar_url = value("https://visage.surgeplay.com/bust/128/"); + + @Comment("The webhook's username") + @Comment("Additional placeholders: N/A") + public final TrackedValue username = value(""); + + @Comment("Player chat messages") + @Comment("Additional placeholders: ") + public final TrackedValue chat_message = value(""); + } } @Comment("Settings pertaining to messages visible in Minecraft") diff --git a/src/main/java/cc/unilock/nilcord/discord/Discord.java b/src/main/java/cc/unilock/nilcord/discord/Discord.java new file mode 100644 index 0000000..aedfd2c --- /dev/null +++ b/src/main/java/cc/unilock/nilcord/discord/Discord.java @@ -0,0 +1,190 @@ +package cc.unilock.nilcord.discord; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.IncomingWebhookClient; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageReference; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.WebhookClient; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.events.session.ReadyEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.ChunkingFilter; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.minecraft.entity.player.EntityServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static cc.unilock.nilcord.NilcordPremain.*; + +public class Discord extends ListenerAdapter { + private static final Pattern WEBHOOK_ID_REGEX = Pattern.compile("^https://discord\\.com/api/webhooks/(\\d+)/.+$"); + + private final JDA jda; + private final IncomingWebhookClient webhook; + private final String webhookId; + + public Discord() { + JDABuilder builder = JDABuilder.createDefault(CONFIG.discord.token.value()) + .addEventListeners(this) + .setChunkingFilter(ChunkingFilter.ALL) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT); + + try { + this.jda = builder.build(); + } catch (Exception e) { + throw new RuntimeException("Failed to log into Discord!", e); + } + + if (CONFIG.discord.webhook.enabled.value()) { + try { + this.webhook = WebhookClient.createClient(jda, CONFIG.discord.webhook.url.value()); + Matcher matcher = WEBHOOK_ID_REGEX.matcher(CONFIG.discord.webhook.url.value()); + this.webhookId = matcher.find() ? matcher.group(1) : null; + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid webhook URL!"); + } + } else { + this.webhook = null; + this.webhookId = null; + } + } + + @Override + public void onReady(@NotNull ReadyEvent event) { + LOGGER.info("Bot ready!"); + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + if (!event.isFromType(ChannelType.TEXT)) return; + + User author = event.getAuthor(); + if (!CONFIG.minecraft.show_bot_messages.value() && author.isBot()) return; + if (author.getId().equals(this.jda.getSelfUser().getId()) || author.getId().equals(this.webhookId)) return; + + Message message = event.getMessage(); + MessageReference ref = message.getMessageReference(); + + Member member = message.getMember(); + if (member == null) return; + + StringBuilder attachment_chunk = new StringBuilder(message.getContentDisplay().isEmpty() ? "" : " "); + if (CONFIG.minecraft.show_attachments.value()) { + for (Message.Attachment attachment : message.getAttachments()) { + attachment_chunk.append(CONFIG.formatting.minecraft.attachment_format.value().replace("", attachment.getUrl())); + } + } + + String reply_chunk = ""; + if (ref != null) { + Message refMessage = ref.getMessage() == null ? ref.resolve().complete() : ref.getMessage(); + User refAuthor = refMessage.getAuthor(); + Member refMember = refMessage.getMember(); + if (refMember != null) { + reply_chunk = CONFIG.formatting.minecraft.reply_format.value() + .replace("", refAuthor.getName()) + .replace("", refMember.getEffectiveName()) + .replace("", refMessage.getContentDisplay()) + .replace("", refMessage.getJumpUrl()); + } + } + + String msg = CONFIG.formatting.minecraft.discord_message.value() + .replace("", attachment_chunk.toString()) + .replace("", reply_chunk) + .replace("", CONFIG.formatting.minecraft.username_format.value()) + + .replace("", author.getName()) + .replace("", member.getEffectiveName()) + .replace("", message.getContentDisplay()); + + server.getConfigurationManager().sendChatMsg(msg); + } + + public void onPlayerChatMessage(EntityServerPlayer player, String message) { + String msg = (CONFIG.discord.webhook.enabled.value() ? CONFIG.formatting.discord.webhook.chat_message.value() : CONFIG.formatting.discord.chat_message.value()) + .replace("", player.username) + .replace("", message); + + if (CONFIG.minecraft.enable_everyone_and_here.value()) { + msg = parseEveryoneAndHere(msg); + } + if (CONFIG.minecraft.enable_mentions.value()) { + msg = parseMentions(msg); + } + + sendMessageToDiscord(msg, player); + } + + public void sendMessageToDiscord(String message) { + this.sendMessageToDiscord(message, null); + } + + public void sendMessageToDiscord(String message, @Nullable EntityServerPlayer player) { + if (!CONFIG.discord.webhook.enabled.value() || this.webhook == null || player == null) { + sendBotMessageToDiscord(message); + } else { + sendWebhookMessageToDiscord(message, player); + } + } + + public void sendBotMessageToDiscord(String message) { + TextChannel textChannel = this.jda.getTextChannelById(CONFIG.discord.channel_id.value()); + if (textChannel != null) { + textChannel.sendMessage(message).queue(); + } else { + LOGGER.error("Unable to find channel "+CONFIG.discord.channel_id.value()+"!"); + } + } + + public void sendWebhookMessageToDiscord(String message, EntityServerPlayer player) { + String avatar = CONFIG.formatting.discord.webhook.avatar_url.value() + .replace("", player.username); + + String username = CONFIG.formatting.discord.webhook.username.value() + .replace("", player.username); + + try (MessageCreateData data = new MessageCreateBuilder().setContent(message).build()) { + webhook.sendMessage(data) + .setAvatarUrl(avatar) + .setUsername(username) + .queue(); + } + } + + private static final Pattern EVERYONE_AND_HERE_PATTERN = Pattern.compile("@(?everyone|here)"); + private String parseEveryoneAndHere(String message) { + return EVERYONE_AND_HERE_PATTERN.matcher(message).replaceAll("@\u200B${ping}"); + } + + private String parseMentions(String message) { + String msg = message; + + for (Member member : jda.getTextChannelById(CONFIG.discord.channel_id.value()).getMembers()) { + message = Pattern.compile(Pattern.quote("@" + member.getUser().getName()), Pattern.CASE_INSENSITIVE).matcher(msg).replaceAll(member.getAsMention()); + } + + return message; + } + + public JDA getJda() { + return this.jda; + } + + public void shutdown() { + this.jda.removeEventListener(this); + this.jda.shutdown(); + } +} diff --git a/src/main/java/cc/unilock/nilcord/transformer/DedicatedServerTransformer.java b/src/main/java/cc/unilock/nilcord/transformer/DedicatedServerTransformer.java index e91ffe1..3f3d905 100755 --- a/src/main/java/cc/unilock/nilcord/transformer/DedicatedServerTransformer.java +++ b/src/main/java/cc/unilock/nilcord/transformer/DedicatedServerTransformer.java @@ -1,6 +1,5 @@ package cc.unilock.nilcord.transformer; -import cc.unilock.nilcord.EventListener; import cc.unilock.nilcord.NilcordPremain; import nilloader.api.lib.mini.MiniTransformer; import nilloader.api.lib.mini.PatchContext; @@ -19,8 +18,8 @@ public void patchStartServer(PatchContext ctx) { public static class Hooks { public static void serverStart() { - // Has to be done here, since EntityPlayer doesn't exist during nilmod init - NilcordPremain.listener = new EventListener(); + // Has to be done here, since Minecraft classes don't exist during nilmod init + NilcordPremain.initialize(); NilcordPremain.listener.serverStart(); } } diff --git a/src/main/resources/nilcord.nilmod.css b/src/main/resources/nilcord.nilmod.css index 31d36da..875238e 100755 --- a/src/main/resources/nilcord.nilmod.css +++ b/src/main/resources/nilcord.nilmod.css @@ -7,5 +7,4 @@ entrypoints { premain: "cc.unilock.nilcord.NilcordPremain"; - hijack: "cc.unilock.nilcord.NilcordPremain"; }