Skip to content

Commit

Permalink
Discord bot impl
Browse files Browse the repository at this point in the history
the rest of the owl!!!

Signed-off-by: unilock <[email protected]>
  • Loading branch information
unilock committed Mar 7, 2024
1 parent 376e669 commit 1494570
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 33 deletions.
52 changes: 41 additions & 11 deletions src/main/java/cc/unilock/nilcord/EventListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("<username>", 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("<username>", 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("<username>", player.username)
.replace("<achievement_title>", Translate.format(achievement.statName))
.replace("<achievement_description>", 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("<username>", player.username)
.replace("<death_message>", Translate.format(source.getDeathMessage(player)));
NilcordPremain.discord.sendMessageToDiscord(message);
}
}
13 changes: 13 additions & 0 deletions src/main/java/cc/unilock/nilcord/NilcordPremain.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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;
import cc.unilock.nilcord.transformer.EntityServerPlayerTransformer;
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() {
Expand All @@ -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();
}
}
40 changes: 22 additions & 18 deletions src/main/java/cc/unilock/nilcord/config/NilcordConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> bot_token = value("EMPTY");
public final TrackedValue<String> token = value("EMPTY");

@Comment("The Discord channel ID for the bot to send messages to / receive messages from")
public final TrackedValue<String> bot_channel = value("EMPTY");
public final TrackedValue<String> 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<Boolean> webhook_enabled = value(Boolean.FALSE);
public final TrackedValue<Boolean> enabled = value(Boolean.FALSE);

@Comment("The webhook URL to use")
public final TrackedValue<String> webhook_url = value("EMPTY");

@Comment("The URL to use for the webhook's avatar")
@Comment("Available placeholders: <username> <uuid>")
public final TrackedValue<String> webhook_avatar_url = value("https://visage.surgeplay.com/bust/128/<uuid>");

@Comment("The webhook's username")
@Comment("Available placeholders: <username>")
public final TrackedValue<String> webhook_username = value("<username>");
public final TrackedValue<String> url = value("EMPTY");
}
}

Expand All @@ -50,7 +38,7 @@ public static final class Minecraft extends Section {
public final TrackedValue<Boolean> show_attachments = value(Boolean.TRUE);

@Comment("Whether to show messages from other Discord bots in-game")
public final TrackedValue<Boolean> show_bot_message = value(Boolean.FALSE);
public final TrackedValue<Boolean> show_bot_messages = value(Boolean.FALSE);
}

@Comment("Settings pertaining to message formatting")
Expand Down Expand Up @@ -82,11 +70,27 @@ public static final class DiscordFormatting extends Section {

@Comment("Player achievement messages")
@Comment("Additional placeholders: <achievement_description> <achievement_title>")
public final TrackedValue<String> achievement_message = value("**<username>** has just earned the achievement **[<achievement_title>]**\\n> \\\\> _<achievement_description>_");
public final TrackedValue<String> achievement_message = value("**<username>** has just earned the achievement **[<achievement_title>]**\n> \\> _<achievement_description>_");

@Comment("Player death messages")
@Comment("Additional placeholders: <death_message>")
public final TrackedValue<String> death_message = value("**<username> died:** _<death_message>_");

@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<String> avatar_url = value("https://visage.surgeplay.com/bust/128/<username>");

@Comment("The webhook's username")
@Comment("Additional placeholders: N/A")
public final TrackedValue<String> username = value("<username>");

@Comment("Player chat messages")
@Comment("Additional placeholders: <message>")
public final TrackedValue<String> chat_message = value("<message>");
}
}

@Comment("Settings pertaining to messages visible in Minecraft")
Expand Down
190 changes: 190 additions & 0 deletions src/main/java/cc/unilock/nilcord/discord/Discord.java
Original file line number Diff line number Diff line change
@@ -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_url>", 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("<reply_username>", refAuthor.getName())
.replace("<reply_nickname>", refMember.getEffectiveName())
.replace("<reply_message>", refMessage.getContentDisplay())
.replace("<reply_url>", refMessage.getJumpUrl());
}
}

String msg = CONFIG.formatting.minecraft.discord_message.value()
.replace("<attachment_format>", attachment_chunk.toString())
.replace("<reply_format>", reply_chunk)
.replace("<username_format>", CONFIG.formatting.minecraft.username_format.value())

.replace("<username>", author.getName())
.replace("<nickname>", member.getEffectiveName())
.replace("<message>", 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("<username>", player.username)
.replace("<message>", 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("<username>", player.username);

String username = CONFIG.formatting.discord.webhook.username.value()
.replace("<username>", 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("@(?<ping>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();
}
}
Loading

0 comments on commit 1494570

Please sign in to comment.