From 2d2e5d3dae9336b9639843db132d152515243c56 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 12:30:06 +0200 Subject: [PATCH 01/12] Configure common project to be server only --- common/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 8a1ee68..e256258 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.spongepowered.gradle.vanilla.repository.MinecraftPlatform + plugins { idea java @@ -10,6 +12,7 @@ val modId: String by project minecraft { version(minecraftVersion) + platform(MinecraftPlatform.SERVER) if (file("src/main/resources/${modId}.accesswidener").exists()) accessWideners(file("src/main/resources/${modId}.accesswidener")) } From 8a56bc5d978b431b9334d8ff37f91f26adbdce95 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 17:22:29 +0200 Subject: [PATCH 02/12] Implement webhook based chat synchronization --- CHANGELOG.md | 3 + build.gradle.kts | 3 + .../main/kotlin/dev/erdragh/astralbot/Bot.kt | 2 +- .../astralbot/config/AstralBotConfig.kt | 30 ++++++ .../astralbot/handlers/MinecraftHandler.kt | 99 +++++++++++++++---- .../astralbot/util/MessageSenderLookup.kt | 45 +++++++++ .../dev/erdragh/astralbot/fabric/BotMod.kt | 7 ++ gradle.properties | 1 + .../dev/erdragh/astralbot/neoforge/BotMod.kt | 11 +++ 9 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a861f0..b4830f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# indev +- Introduce webhook chat sync, allowing for user imitation + # 1.4.0 - Port to 1.20.4 and NeoForge - Removed Forge as a supported Platform for Minecraft > 1.20.1 diff --git a/build.gradle.kts b/build.gradle.kts index b6c8696..96625b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,6 +71,7 @@ subprojects { // Bot dependencies val jdaVersion: String by project + val dcWebhooksVersion: String by project val exposedVersion: String by project val sqliteJDBCVersion: String by project val commonmarkVersion: String by project @@ -102,6 +103,8 @@ subprojects { arrayOf( // Library used to communicate with Discord, see https://jda.wiki "net.dv8tion:JDA:$jdaVersion", + // Library used for sending messages via Discord Webhooks + "club.minnced:discord-webhooks:$dcWebhooksVersion", // Library to interact with the SQLite database, // see: https://github.com/JetBrains/Exposed diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt index 8c38944..acbed12 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt @@ -28,7 +28,7 @@ var minecraftHandler: MinecraftHandler? = null var textChannel: TextChannel? = null var guild: Guild? = null -private var jda: JDA? = null +var jda: JDA? = null var applicationId by Delegates.notNull() var baseDirectory: File? = null diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt index f79246d..7329cef 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt @@ -20,6 +20,25 @@ object AstralBotConfig { */ val DISCORD_TOKEN: ModConfigSpec.ConfigValue + /** + * Used for sending more appealing messages + */ + val WEBHOOK_URL: ModConfigSpec.ConfigValue + /** + * Whether the configured [WEBHOOK_URL] should actually be used, + * useful in case somebody wants to temporarily disable using + * the webhook without removing the URL + */ + val WEBHOOK_ENABLED: ModConfigSpec.BooleanValue + + /** + * Whether the chat messages sent via the webhook should + * imitate the sender's Discord account or their Minecraft + * account. If this is on, the linked Discord account will + * be used. + */ + val WEBHOOK_USE_LINKED: ModConfigSpec.BooleanValue + /** * Whether the default whitelisting process is respected or ignored. * Setting this to `true` will *force* every user who wants to join @@ -101,6 +120,13 @@ object AstralBotConfig { DISCORD_TOKEN = builder.comment("Discord token for the bot. Can also be supplied via DISCORD_TOKEN environment variable") .define("token", "") + WEBHOOK_URL = builder.comment("URL to the webhook where the messages will be sent from") + .define(listOf("webhook", "url"), "") + WEBHOOK_ENABLED = builder.comment("Whether to use the configured webhook for sending messages") + .define(listOf("webhook", "enabled"), true) + WEBHOOK_USE_LINKED = builder.comment("Whether to imitate user's linked Discord accounts when sending messages from MC to DC") + .define(listOf("webhook", "useLinked"), false) + REQUIRE_LINK_FOR_WHITELIST = builder.comment("Whether to require being linked to be whitelisted") .define("requireLinkForWhitelist", false) DISCORD_LINK = builder.comment("Link to the discord where your users can run the /link command") @@ -190,4 +216,8 @@ object AstralBotConfig { } return true } + + fun useWebhooks(): Boolean { + return WEBHOOK_ENABLED.get() && WEBHOOK_URL.get() != "" + } } \ No newline at end of file diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt index 32b65f9..52cef4c 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt @@ -1,5 +1,9 @@ package dev.erdragh.astralbot.handlers +import club.minnced.discord.webhook.WebhookClient +import club.minnced.discord.webhook.send.WebhookEmbed +import club.minnced.discord.webhook.send.WebhookEmbedBuilder +import club.minnced.discord.webhook.send.WebhookMessageBuilder import com.mojang.authlib.GameProfile import dev.erdragh.astralbot.* import dev.erdragh.astralbot.config.AstralBotConfig @@ -9,7 +13,10 @@ import net.dv8tion.jda.api.entities.Member import net.dv8tion.jda.api.entities.Message import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.exceptions.ErrorResponseException import net.dv8tion.jda.api.hooks.ListenerAdapter +import net.dv8tion.jda.api.requests.ErrorResponse +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder import net.minecraft.ChatFormatting import net.minecraft.network.chat.ClickEvent import net.minecraft.network.chat.Component @@ -24,6 +31,7 @@ import java.util.* import kotlin.jvm.optionals.getOrNull import kotlin.math.min + /** * Wrapper class around the [MinecraftServer] to provide convenience * methods for fetching [GameProfile]s, sending Messages, acting @@ -33,7 +41,18 @@ import kotlin.math.min class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() { private val playerNames = HashSet(server.maxPlayers); private val notchPlayer = byName("Notch")?.let { ServerPlayer(this.server, this.server.allLevels.elementAt(0), it, ClientInformation.createDefault()) } + private var webhookClient: WebhookClient? = AstralBotConfig.WEBHOOK_URL.get().let { if (it != "") WebhookClient.withUrl(it) else null } + /** + * Method that maybe creates a new webhook client if one wasn't configured before. + * Gets called when the config is reloaded. + */ + fun updateWebhookClient() { + val url = AstralBotConfig.WEBHOOK_URL.get() + if (webhookClient == null || url != webhookClient?.url) { + webhookClient = url.let { if (it != "") WebhookClient.withUrl(it) else null } + } + } companion object { private val numberFormat = DecimalFormat("###.##") @@ -149,18 +168,41 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() } } + val messageSenderInfo = if(AstralBotConfig.useWebhooks()) MessageSenderLookup.getMessageSenderInfo(player, AstralBotConfig.WEBHOOK_USE_LINKED.get()) else null val escape = { it: String -> it.replace("_", "\\_") } - textChannel?.sendMessage( - if (player != null) - AstralBotTextConfig.PLAYER_MESSAGE.get() - .replace("{{message}}", formatComponentToMarkdown(message)) - .replace("{{fullName}}", escape(player.displayName?.string ?: player.name.string)) - .replace("{{name}}", escape(player.name.string)) - else escape(message.string) - ) - ?.addEmbeds(formattedEmbeds) - ?.setSuppressedNotifications(true) - ?.queue() + val content = if (messageSenderInfo != null) { + formatComponentToMarkdown(message) + } else if (player != null) { + AstralBotTextConfig.PLAYER_MESSAGE.get() + .replace("{{message}}", formatComponentToMarkdown(message)) + .replace("{{fullName}}", escape(player.displayName?.string ?: player.name.string)) + .replace("{{name}}", escape(player.name.string)) + } else { + formatComponentToMarkdown(message) + } + + if (!AstralBotConfig.useWebhooks() || webhookClient == null || messageSenderInfo == null) { + val createdMessage = MessageCreateBuilder() + .addEmbeds(formattedEmbeds) + .setContent(content) + .setSuppressedNotifications(true) + .build() + textChannel?.sendMessage(createdMessage)?.queue() + } else { + val webhookMessage = WebhookMessageBuilder() + .addEmbeds(formattedEmbeds.map { embed -> + WebhookEmbedBuilder().let { builder -> embed.title?.let { title -> builder.setTitle(WebhookEmbed.EmbedTitle(title, null)) }; builder } + .setDescription(embed.description) + .setColor(embed.colorRaw) + .build() + }) + .setContent(content) + .setUsername(messageSenderInfo.name) + .setAvatarUrl(messageSenderInfo.avatar) + .build() + + webhookClient!!.send(webhookMessage) + } } /** @@ -229,18 +271,28 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() messageContents.append(formatEmbeds(message)) } - val referencedAuthor = message.referencedMessage?.author?.id + val respondeeId = message.referencedMessage?.author?.id + val respondeeName = message.referencedMessage?.author?.effectiveName waitForSetup() - if (referencedAuthor != null) { - // This fetches the Member from the ID in a blocking manner - guild?.retrieveMemberById(referencedAuthor)?.submit()?.whenComplete { repliedMember, error -> + if (respondeeId != null) { + guild?.retrieveMemberById(respondeeId)?.submit()?.whenComplete { repliedMember, error -> if (error != null) { - LOGGER.error("Failed to get member with id: $referencedAuthor", error) + if (error !is ErrorResponseException || error.errorResponse.code != ErrorResponse.UNKNOWN_USER.code) { + // if the error is because the user was unknown, the log shouldn't be spammed. + // This is becausane webhook messages will produce this error. + LOGGER.error("Failed to get member with id: $respondeeId", error) + } + if (respondeeName != null) { + message.member?.let { + comp.append(formatMessageWithUsers(messageContents, it, Component.literal(respondeeName).withStyle(ChatFormatting.WHITE))) + send(comp) + } + } return@whenComplete } else if (repliedMember == null) { - LOGGER.error("Failed to get member with id: $referencedAuthor") + LOGGER.error("Failed to get member with id: $respondeeId") return@whenComplete } message.member?.let { @@ -248,6 +300,11 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() send(comp) } } + } else if (respondeeName != null) { + message.member?.let { + comp.append(formatMessageWithUsers(messageContents, it, Component.literal(respondeeName).withStyle(ChatFormatting.WHITE))) + send(comp) + } } else { message.member?.let { comp.append(formatMessageWithUsers(messageContents, it, null)) @@ -256,7 +313,7 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() } } - private fun formatMessageWithUsers(message: MutableComponent, author: Member, replied: Member?): MutableComponent { + private fun formatMessageWithUsers(message: MutableComponent, author: Member, replied: Component?): MutableComponent { val formatted = Component.empty() val templateSplitByUser = AstralBotTextConfig.DISCORD_MESSAGE.get().split("{{user}}") @@ -289,7 +346,7 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() reply.append(formattedLiteral(value)) if (index + 1 < replyTemplateSplit.size) { - reply.append(formattedUser(it)) + reply.append(it) } } } @@ -317,6 +374,10 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() return formatted } + private fun formatMessageWithUsers(message: MutableComponent, author: Member, replied: Member): MutableComponent { + return formatMessageWithUsers(message, author, formattedUser(replied)) + } + /** * Formats the attachments and embeds on a Discord [Message] into * a comma separated list. diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt new file mode 100644 index 0000000..a818818 --- /dev/null +++ b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt @@ -0,0 +1,45 @@ +package dev.erdragh.astralbot.util + +import dev.erdragh.astralbot.LOGGER +import dev.erdragh.astralbot.handlers.WhitelistHandler +import dev.erdragh.astralbot.jda +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.player.Player + +object MessageSenderLookup { + private val minecraftSkins: MutableMap = mutableMapOf() + private val discordAvatars: MutableMap = mutableMapOf() + + fun getMessageSenderInfo(player: ServerPlayer?, discord: Boolean = false): ResolvedSenderInfo? { + if (player == null) return null + + if (discord) { + val cached = discordAvatars[player] + if (cached != null) return cached + + val discordId = WhitelistHandler.checkWhitelist(player.gameProfile.id) + ?: return ResolvedSenderInfo("Unlinked Account (${(player.displayName ?: player.name).string})", null) + val discordUser = jda?.getUserById(discordId) + if (discordUser == null) { + // Lazily load the user info if it's not cached + jda?.retrieveUserById(discordId)?.submit()?.whenComplete { user, error -> + if (error == null) { + discordAvatars[player] = ResolvedSenderInfo(user.effectiveName, user.avatarUrl) + } else { + LOGGER.error("Failed to retrieve user: $discordId for chat synchronization", error) + } + } + return ResolvedSenderInfo("Uncached User (${(player.displayName ?: player.name).string})", null) + } + + return discordAvatars.computeIfAbsent(player) { + ResolvedSenderInfo(discordUser.effectiveName, discordUser.avatarUrl) + } + } + return minecraftSkins.computeIfAbsent(player) { + ResolvedSenderInfo((player.displayName ?: player.name).string, "https://mc-heads.net/head/${player.gameProfile.id}") + } + } + + data class ResolvedSenderInfo(val name: String, val avatar: String?) +} \ No newline at end of file diff --git a/fabric/src/main/kotlin/dev/erdragh/astralbot/fabric/BotMod.kt b/fabric/src/main/kotlin/dev/erdragh/astralbot/fabric/BotMod.kt index 890ed3e..2f58238 100644 --- a/fabric/src/main/kotlin/dev/erdragh/astralbot/fabric/BotMod.kt +++ b/fabric/src/main/kotlin/dev/erdragh/astralbot/fabric/BotMod.kt @@ -6,11 +6,13 @@ import dev.erdragh.astralbot.config.AstralBotConfig import dev.erdragh.astralbot.config.AstralBotTextConfig import dev.erdragh.astralbot.handlers.DiscordMessageComponent import fuzs.forgeconfigapiport.fabric.api.neoforge.v4.NeoForgeConfigRegistry +import fuzs.forgeconfigapiport.fabric.api.neoforge.v4.NeoForgeModConfigEvents import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.fabricmc.fabric.api.message.v1.ServerMessageEvents import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.minecraft.client.resources.SkinManager import net.minecraft.server.level.ServerPlayer import net.neoforged.fml.config.ModConfig @@ -19,6 +21,11 @@ object BotMod : ModInitializer { NeoForgeConfigRegistry.INSTANCE.register(MODID, ModConfig.Type.SERVER, AstralBotConfig.SPEC) NeoForgeConfigRegistry.INSTANCE.register(MODID, ModConfig.Type.SERVER, AstralBotTextConfig.SPEC, "astralbot-text.toml") + NeoForgeModConfigEvents.reloading(MODID).register { + // Updates the webhook client if the URL changed + minecraftHandler?.updateWebhookClient() + } + ServerLifecycleEvents.SERVER_STARTED.register { LOGGER.info("Starting AstralBot on Fabric") startAstralbot(it) diff --git a/gradle.properties b/gradle.properties index b74b46a..72393c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,6 +33,7 @@ forgeConfigAPIVersion=20.4.3 # Discord Interactions jdaVersion=5.0.0-beta.24 +dcWebhooksVersion=0.8.4 # Database Interactions exposedVersion=0.50.1 diff --git a/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt b/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt index 39c80df..ff4e562 100644 --- a/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt +++ b/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt @@ -11,6 +11,7 @@ import net.minecraft.server.level.ServerPlayer import net.neoforged.fml.ModLoadingContext import net.neoforged.fml.common.Mod import net.neoforged.fml.config.ModConfig +import net.neoforged.fml.event.config.ModConfigEvent import net.neoforged.neoforge.event.RegisterCommandsEvent import net.neoforged.neoforge.event.ServerChatEvent import net.neoforged.neoforge.event.entity.player.PlayerEvent @@ -23,6 +24,8 @@ object BotMod { init { ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotConfig.SPEC) ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotTextConfig.SPEC, "astralbot-text.toml") + FORGE_BUS.addListener(::onConfigReloaded) + FORGE_BUS.addListener(::onServerStart) FORGE_BUS.addListener(::onServerStop) FORGE_BUS.addListener(::onChatMessage) @@ -34,6 +37,14 @@ object BotMod { FORGE_BUS.addListener(::onPlayerLeave) } + // Unused parameter suppressed to keep type + // information about Server stop event. + @Suppress("UNUSED_PARAMETER") + private fun onConfigReloaded(event: ModConfigEvent.Reloading) { + // Updates the webhook client if the URL changed + minecraftHandler?.updateWebhookClient() + } + private fun onServerStart(event: ServerStartedEvent) { LOGGER.info("AstralBot starting on NeoForge") startAstralbot(event.server) From c32dc213bee16396b53560fe530a68efaf73597f Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 18:00:18 +0200 Subject: [PATCH 03/12] Fix mc link command not returning on error --- .../erdragh/astralbot/commands/minecraft/MinecraftCommands.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/minecraft/MinecraftCommands.kt b/common/src/main/kotlin/dev/erdragh/astralbot/commands/minecraft/MinecraftCommands.kt index 71777d4..8f07dfa 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/minecraft/MinecraftCommands.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/commands/minecraft/MinecraftCommands.kt @@ -18,6 +18,7 @@ object LinkCommand : Command { val caller = context?.source?.playerOrException!! if (WhitelistHandler.checkWhitelist(caller.uuid) != null) { context.source.sendFailure(Component.literal(AstralBotTextConfig.LINK_COMMAND_ALREADY_LINKED.get())) + return 1 } val whitelistCode = WhitelistHandler.getOrGenerateWhitelistCode(caller.uuid) context.source.sendSuccess({ From cb13c693cac9f48a03b50c15f6f24f8feefd0cad Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 18:00:39 +0200 Subject: [PATCH 04/12] Make webhook chat sync more configurable --- .../astralbot/config/AstralBotConfig.kt | 7 +++ .../astralbot/config/AstralBotTextConfig.kt | 11 ++++- .../astralbot/handlers/MinecraftHandler.kt | 6 ++- .../astralbot/util/MessageSenderLookup.kt | 43 +++++++++++++------ 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt index 7329cef..a4bfffe 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt @@ -31,6 +31,11 @@ object AstralBotConfig { */ val WEBHOOK_ENABLED: ModConfigSpec.BooleanValue + /** + * URL template for getting avatars from Minecraft users + */ + val WEBHOOK_MC_AVATAR_URL: ModConfigSpec.ConfigValue + /** * Whether the chat messages sent via the webhook should * imitate the sender's Discord account or their Minecraft @@ -126,6 +131,8 @@ object AstralBotConfig { .define(listOf("webhook", "enabled"), true) WEBHOOK_USE_LINKED = builder.comment("Whether to imitate user's linked Discord accounts when sending messages from MC to DC") .define(listOf("webhook", "useLinked"), false) + WEBHOOK_MC_AVATAR_URL = builder.comment("API that returns images based on Minecraft users. {{uuid}} and {{name}} can be used") + .define(listOf("webhook", "mcAvatarUrl"), "https://mc-heads.net/head/{{uuid}}") REQUIRE_LINK_FOR_WHITELIST = builder.comment("Whether to require being linked to be whitelisted") .define("requireLinkForWhitelist", false) diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt index b052323..6b600f0 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt @@ -16,6 +16,8 @@ object AstralBotTextConfig { val PLAYER_MESSAGE: ModConfigSpec.ConfigValue + val WEBHOOK_NAME_TEMPLATE: ModConfigSpec.ConfigValue + val DISCORD_MESSAGE: ModConfigSpec.ConfigValue val DISCORD_REPLY: ModConfigSpec.ConfigValue val DISCORD_EMBEDS: ModConfigSpec.ConfigValue @@ -63,12 +65,19 @@ object AstralBotTextConfig { .define("tickReport", "Average Tick Time: {{mspt}} MSPT (TPS: {{tps}})") PLAYER_MESSAGE = - builder.comment("""Template for how Minecraft chat messages are sent to Discord. + builder.comment("""Template for how Minecraft chat messages are sent to Discord if webhooks aren't used The player's name can be accessed via {{name}} and its name with pre- and suffix via {{fullName}}. The message itself is accessed via {{message}}. """.replace(whitespaceRegex, "\n")) .define(mutableListOf("messages", "minecraft"), "<{{fullName}}> {{message}}") + WEBHOOK_NAME_TEMPLATE = + builder.comment("""Template for how chat synchronization using Webhooks formats + the message author's name. + The player's primary name can be accessed via {{primary}} and the secondary name via {{secondary}}. + """.replace(whitespaceRegex, "\n")) + .define(mutableListOf("webhook", "name"), "{{primary}} ({{secondary}})") + DISCORD_MESSAGE = builder.comment("""Template for how Discord messages are synchronized to Minecraft. The sender is referenced by {{user}}. The optional response is accessed by {{reply}}. diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt index 52cef4c..253bf3c 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt @@ -197,7 +197,11 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() .build() }) .setContent(content) - .setUsername(messageSenderInfo.name) + .setUsername( + AstralBotTextConfig.WEBHOOK_NAME_TEMPLATE.get() + .replace("{{primary}}", messageSenderInfo.primaryName) + .replace("{{secondary}}", messageSenderInfo.secondaryName ?: "Unlinked") + ) .setAvatarUrl(messageSenderInfo.avatar) .build() diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt index a818818..f3d29b9 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageSenderLookup.kt @@ -1,8 +1,10 @@ package dev.erdragh.astralbot.util import dev.erdragh.astralbot.LOGGER +import dev.erdragh.astralbot.config.AstralBotConfig import dev.erdragh.astralbot.handlers.WhitelistHandler import dev.erdragh.astralbot.jda +import net.dv8tion.jda.api.entities.User import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Player @@ -13,33 +15,50 @@ object MessageSenderLookup { fun getMessageSenderInfo(player: ServerPlayer?, discord: Boolean = false): ResolvedSenderInfo? { if (player == null) return null - if (discord) { - val cached = discordAvatars[player] - if (cached != null) return cached - - val discordId = WhitelistHandler.checkWhitelist(player.gameProfile.id) - ?: return ResolvedSenderInfo("Unlinked Account (${(player.displayName ?: player.name).string})", null) - val discordUser = jda?.getUserById(discordId) + val discordId = WhitelistHandler.checkWhitelist(player.gameProfile.id) + var discordUser: User? = null + if (discordId != null) { + discordUser = jda?.getUserById(discordId) if (discordUser == null) { // Lazily load the user info if it's not cached jda?.retrieveUserById(discordId)?.submit()?.whenComplete { user, error -> if (error == null) { - discordAvatars[player] = ResolvedSenderInfo(user.effectiveName, user.avatarUrl) + discordAvatars[player] = ResolvedSenderInfo(user.effectiveName, (player.displayName ?: player.name).string, user.avatarUrl) + minecraftSkins[player] = ResolvedSenderInfo( + (player.displayName ?: player.name).string, + user.effectiveName, + AstralBotConfig.WEBHOOK_MC_AVATAR_URL.get() + .replace("{{uuid}}", player.gameProfile.id.toString()) + .replace("{{name}}", player.gameProfile.name) + ) } else { LOGGER.error("Failed to retrieve user: $discordId for chat synchronization", error) } } - return ResolvedSenderInfo("Uncached User (${(player.displayName ?: player.name).string})", null) + } + } + + if (discord) { + val cached = discordAvatars[player] + if (cached != null) return cached + if (discordUser == null) { + return ResolvedSenderInfo("Uncached", (player.displayName ?: player.name).string, null) } return discordAvatars.computeIfAbsent(player) { - ResolvedSenderInfo(discordUser.effectiveName, discordUser.avatarUrl) + ResolvedSenderInfo(discordUser.effectiveName, (player.displayName ?: player.name).string, discordUser.avatarUrl) } } return minecraftSkins.computeIfAbsent(player) { - ResolvedSenderInfo((player.displayName ?: player.name).string, "https://mc-heads.net/head/${player.gameProfile.id}") + ResolvedSenderInfo( + (player.displayName ?: player.name).string, + if (discordId == null) null else (discordUser?.effectiveName ?: "Uncached"), + AstralBotConfig.WEBHOOK_MC_AVATAR_URL.get() + .replace("{{uuid}}", player.gameProfile.id.toString()) + .replace("{{name}}", player.gameProfile.name) + ) } } - data class ResolvedSenderInfo(val name: String, val avatar: String?) + data class ResolvedSenderInfo(val primaryName: String, val secondaryName: String?, val avatar: String?) } \ No newline at end of file From 9c924a8a3d85c0d1219b14b77d0735ccb91176f6 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 18:02:19 +0200 Subject: [PATCH 05/12] Update README to reflect changes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17cf69f..265709b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ while allowing true complexity for power users. These features are the core of what this bot will do. See [the Status section](#status) to see how much is already implemented. - Discord and Minecraft account linking, optionally requiring this to be whitelisted -- Discord and Minecraft chat synchronization +- Discord and Minecraft chat synchronization, optionally via webhook for improved readability - FAQ commands using Markdown files without needing to restart the server ## Dependencies @@ -45,10 +45,10 @@ The following things will be configurable: - [x] Reading Markdown files - [x] Updating suggestions without restart - [ ] Management/Creation via commands -- [ ] Chat synchronization +- [x] Chat synchronization - [x] Minecraft to Discord - [x] Discord to Minecraft - - [ ] User imitation on Discord + - [x] User imitation on Discord. Either uses Minecraft avatars or Discord avatars ## Running There is no public instance of this bot/mod available. To use it, create a new Application From 3f6f1a412f41aa3ed3b7c75fbecabaefebed7d31 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 18:06:44 +0200 Subject: [PATCH 06/12] Add warn message when webhook URL not set --- common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt index acbed12..540e129 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt @@ -117,6 +117,10 @@ fun startAstralbot(server: MinecraftServer) { return } + if (AstralBotConfig.WEBHOOK_ENABLED.get() && AstralBotConfig.WEBHOOK_URL.get() == "") { + LOGGER.warn("Webhooks enabled, but no URL provided, chat synchronization will fallback to default implementation.") + } + minecraftHandler = MinecraftHandler(server) jda = JDABuilder.createLight( From 3e0c814d59735516280d8f8216567d9b29b0ca3b Mon Sep 17 00:00:00 2001 From: Erdragh Date: Tue, 28 May 2024 18:09:26 +0200 Subject: [PATCH 07/12] Update README instructions --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 265709b..9ec7276 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following things will be configurable: 2. (If possible) Minecraft user imitation (The Minecraft usernames and heads will be used for messages) 3. (Only available if requiring linking for whitelist) Discord user imitation (The messages will be written under the linked Discord name) - Default: No user imitation + Default: Minecraft user imitation (if webhook URL is set, otherwise no imitation) - Managing FAQs through a command interface (default: `off`) - Database connection. Uses an SQLite database by default @@ -56,9 +56,9 @@ on the [Discord Developer Portal](https://discord.com/developers/applications) a to have the three privileged gateway intents: `PRESENCE`, `SERVER MEMBERS` and `MESSAGE CONTENT`. Copy the bot token and store it somewhere safe (like a Password Manager) and never show it to -anybody else. To make sure the token gets read by the bot, it has to be in an [Environment Variable](https://en.wikipedia.org/wiki/Environment_variable) -`DISCORD_TOKEN` where the running Minecraft server can access it. You could for example modify a `start.sh` script -on a Unix-like system to `export` it or start the shell script with it set directly: +anybody else. To make sure the token gets read by the bot, it has to be in either an [Environment Variable](https://en.wikipedia.org/wiki/Environment_variable) +`DISCORD_TOKEN` where the running Minecraft server can access it or in the config file under the key `token`. +You could for example modify a `start.sh` script on a Unix-like system to `export` it or start the shell script with it set directly: `startmc.sh`: ```shell @@ -75,4 +75,8 @@ DISCORD_TOKEN= startmc.sh # Start the script with the env vari After starting the server, you can go into the OAuth2 URL builder on the Discord Developer Portal and generate a URL with the `bot` and `applications.command` permissions. -Use the generated URL to have the bot join your server. \ No newline at end of file +Use the generated URL to have the bot join your server. + +To be able to use user imitation, which makes the chat synchronization more readable, you will have to add a +webhook to your Discord server that uses the same chat you configured for the normal chat synchronization. Copy its +URL and add it to the `webhook.url` key in the config. \ No newline at end of file From 38dad35df7e3ecc61d0250ce625856037b4f98b2 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Thu, 30 May 2024 19:05:08 +0200 Subject: [PATCH 08/12] Simplify shadowJar configuration --- build.gradle.kts | 45 +++++++++++++++++++++++++++++++++++++++ fabric/build.gradle.kts | 20 ----------------- neoforge/build.gradle.kts | 23 -------------------- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 96625b4..da4248a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.jetbrains.kotlin.gradle.utils.extendsFrom import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat @@ -112,6 +113,10 @@ subprojects { "org.jetbrains.exposed:exposed-dao:$exposedVersion", "org.jetbrains.exposed:exposed-jdbc:$exposedVersion", ).forEach { + implementation(it) { + exclude(module = "opus-java") + exclude(group = "org.slf4j") + } runtimeLib(it) { exclude(module = "opus-java") exclude(group = "org.slf4j") @@ -125,6 +130,7 @@ subprojects { // on JDA and Exposed, but is already provided by the // respective Kotlin implementation of the mod loaders exclude(group = "org.jetbrains.kotlin") + exclude(group = "org.jetbrains.kotlinx") // Minecraft already ships with a logging system exclude(group = "org.slf4j") } @@ -197,9 +203,48 @@ subprojects { } if (!isCommon) { + apply(plugin = "io.github.goooler.shadow") + dependencies { implementation(project(":common")) } + + tasks.named("shadowJar") { + // The shadowBotDep configuration was explicitly made to be shaded in, this is where that happens + configurations.clear() + configurations = listOf(shadowBotDep) + + // This transforms the service files to make relocated Exposed work (see: https://github.com/JetBrains/Exposed/issues/1353) + mergeServiceFiles() + + // Forge restricts loading certain classes for security reasons. + // Luckily, shadow can relocate them to a different package. + relocate("org.apache.commons.collections4", "dev.erdragh.shadowed.org.apache.commons.collections4") + + // Relocating Exposed somewhere different so other mods not doing that don't run into issues (e.g. Ledger) + relocate("org.jetbrains.exposed", "dev.erdragh.shadowed.org.jetbrains.exposed") + + // Relocating jackson to prevent incompatibilities with other mods also bundling it (e.g. GroovyModLoader on Forge) + relocate("com.fasterxml.jackson", "dev.erdragh.shadowed.com.fasterxml.jackson") + + // relocate discord interaction stuff to maybe allow other discord integrations mods to work + relocate("club.minnced.discord", "dev.erdragh.shadowed.club.minnced.discord") + relocate("net.dv8tion.jda", "dev.erdragh.shadowed.net.dv8tion.jda") + + // relocate dependencies of discord stuff + relocate("okhttp3", "dev.erdragh.shadowed.okhttp3") + relocate("okio", "dev.erdragh.shadowed.okio") + relocate("gnu.trove", "dev.erdragh.shadowed.gnu.trove") + relocate("com.iwebpp.crypto", "dev.erdragh.shadowed.com.iwebpp.crypto") + relocate("com.neovisionaries.ws", "dev.erdragh.shadowed.com.neovisionaries.ws") + relocate("org.json", "dev.erdragh.shadowed.org.json") + relocate("net.bytebuddy", "dev.erdragh.net.bytebuddy") + + exclude("**/org/slf4j/**") + + exclude("**/org/jetbrains/annotations/*") + exclude("**/org/intellij/**") + } } // Disables Gradle's custom module metadata from being published to maven. The diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 8407bb6..40e5e36 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -76,26 +76,6 @@ tasks { shadowJar { archiveClassifier.set("dev-shadow") - - configurations = listOf(shadowBotDep) - - // This transforms the service files to make relocated Exposed work (see: https://github.com/JetBrains/Exposed/issues/1353) - mergeServiceFiles() - - // Relocating Exposed somewhere different so other mods not doing that don't run into issues (e.g. Ledger) - relocate("org.jetbrains.exposed", "dev.erdragh.shadowed.org.jetbrains.exposed") - - // Relocating jackson to prevent incompatibilities with other mods also bundling it (e.g. GroovyModLoader on Forge) - relocate("com.fasterxml.jackson", "dev.erdragh.shadowed.com.fasterxml.jackson") - - exclude(".cache/**") //Remove datagen cache from jar. - exclude("**/astralbot/datagen/**") //Remove data gen code from jar. - exclude("**/org/slf4j/**") - - exclude("kotlinx/**") - exclude("_COROUTINE/**") - exclude("**/org/jetbrains/annotations/*") - exclude("**/org/intellij/**") } remapJar { diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 12d3812..2a2f620 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -92,29 +92,6 @@ tasks { shadowJar { archiveClassifier = null - configurations = listOf(shadowBotDep) - - // This transforms the service files to make relocated Exposed work (see: https://github.com/JetBrains/Exposed/issues/1353) - mergeServiceFiles() - - // Forge restricts loading certain classes for security reasons. - // Luckily, shadow can relocate them to a different package. - relocate("org.apache.commons.collections4", "dev.erdragh.shadowed.org.apache.commons.collections4") - - // Relocating Exposed somewhere different so other mods not doing that don't run into issues (e.g. Ledger) - relocate("org.jetbrains.exposed", "dev.erdragh.shadowed.org.jetbrains.exposed") - - // Relocating jackson to prevent incompatibilities with other mods also bundling it (e.g. GroovyModLoader on Forge) - relocate("com.fasterxml.jackson", "dev.erdragh.shadowed.com.fasterxml.jackson") - - exclude(".cache/**") //Remove datagen cache from jar. - exclude("**/astralbot/datagen/**") //Remove data gen code from jar. - exclude("**/org/slf4j/**") - - exclude("kotlinx/**") - exclude("_COROUTINE/**") - exclude("**/org/jetbrains/annotations/*") - exclude("**/org/intellij/**") } jarJar.configure { From 2bafa967badbdd386f60d9b8afd7d2b8d7212e90 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Thu, 30 May 2024 19:10:20 +0200 Subject: [PATCH 09/12] Fix listening for Config Reload event on wrong bus on Neo --- .../kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt b/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt index ff4e562..1bbd833 100644 --- a/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt +++ b/neoforge/src/main/kotlin/dev/erdragh/astralbot/neoforge/BotMod.kt @@ -1,12 +1,15 @@ package dev.erdragh.astralbot.neoforge -import dev.erdragh.astralbot.* +import dev.erdragh.astralbot.LOGGER import dev.erdragh.astralbot.commands.minecraft.registerMinecraftCommands import dev.erdragh.astralbot.config.AstralBotConfig import dev.erdragh.astralbot.config.AstralBotTextConfig -import dev.erdragh.astralbot.neoforge.event.SystemMessageEvent import dev.erdragh.astralbot.handlers.DiscordMessageComponent +import dev.erdragh.astralbot.minecraftHandler import dev.erdragh.astralbot.neoforge.event.CommandMessageEvent +import dev.erdragh.astralbot.neoforge.event.SystemMessageEvent +import dev.erdragh.astralbot.startAstralbot +import dev.erdragh.astralbot.stopAstralbot import net.minecraft.server.level.ServerPlayer import net.neoforged.fml.ModLoadingContext import net.neoforged.fml.common.Mod @@ -18,13 +21,14 @@ import net.neoforged.neoforge.event.entity.player.PlayerEvent import net.neoforged.neoforge.event.server.ServerStartedEvent import net.neoforged.neoforge.event.server.ServerStoppingEvent import thedarkcolour.kotlinforforge.neoforge.forge.FORGE_BUS +import thedarkcolour.kotlinforforge.neoforge.forge.MOD_BUS @Mod("astralbot") object BotMod { init { ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotConfig.SPEC) ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotTextConfig.SPEC, "astralbot-text.toml") - FORGE_BUS.addListener(::onConfigReloaded) + MOD_BUS.addListener(::onConfigReloaded) FORGE_BUS.addListener(::onServerStart) FORGE_BUS.addListener(::onServerStop) From ee26f4246e46d1d0a75a61127110bc0ba7cad1d6 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Thu, 30 May 2024 19:34:29 +0200 Subject: [PATCH 10/12] Improve whitelist message handling --- .../astralbot/mixins/PlayerListMixin.java | 2 +- .../astralbot/config/AstralBotTextConfig.kt | 9 +++++++++ .../astralbot/handlers/WhitelistHandler.kt | 19 +++++++++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/dev/erdragh/astralbot/mixins/PlayerListMixin.java b/common/src/main/java/dev/erdragh/astralbot/mixins/PlayerListMixin.java index 451d880..43eaa5c 100644 --- a/common/src/main/java/dev/erdragh/astralbot/mixins/PlayerListMixin.java +++ b/common/src/main/java/dev/erdragh/astralbot/mixins/PlayerListMixin.java @@ -20,6 +20,6 @@ public class PlayerListMixin { @Inject(method = "canPlayerLogin", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;)Lnet/minecraft/network/chat/MutableComponent;"), cancellable = true) private void astralbot$returnWhiteListMessage(SocketAddress socketAddress, GameProfile gameProfile, CallbackInfoReturnable cir) { - cir.setReturnValue(WhitelistHandler.INSTANCE.writeWhitelistMessage(gameProfile)); + WhitelistHandler.INSTANCE.writeWhitelistMessage(gameProfile).ifPresent(cir::setReturnValue); } } diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt index 6b600f0..7de58db 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt @@ -25,6 +25,8 @@ object AstralBotTextConfig { val RELOAD_ERROR: ModConfigSpec.ConfigValue val RELOAD_SUCCESS: ModConfigSpec.ConfigValue + val WHITELIST_LINKED_NOT_ALLOWED: ModConfigSpec.ConfigValue + val LINK_NO_MINECRAFT: ModConfigSpec.ConfigValue val LINK_MINECRAFT_TAKEN: ModConfigSpec.ConfigValue val LINK_DISCORD_TAKEN: ModConfigSpec.ConfigValue @@ -101,6 +103,13 @@ object AstralBotTextConfig { RELOAD_SUCCESS = builder.comment("Message sent to Discord after a successful reload") .define(mutableListOf("reload", "success"), "Reloaded commands for guild") + WHITELIST_LINKED_NOT_ALLOWED = builder.comment(""" + The message the user gets shown if they are already linked, but have not + yet been whitelisted by another means (i.e. being an operator, being on the vanilla whitelist, etc.) + The Minecraft username is accessible via {{name}} + """.replace(whitespaceRegex, "\n")) + .define(listOf("whitelist", "linkedNotAllowed"), "Hi {{mc}}! You're already linked, but not yet whitelisted.") + LINK_NO_MINECRAFT = builder.comment(""" Message for when there's no Minecraft account associated with the given Link code. diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/WhitelistHandler.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/WhitelistHandler.kt index b9033ee..18c5b60 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/WhitelistHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/WhitelistHandler.kt @@ -4,7 +4,9 @@ import com.mojang.authlib.GameProfile import dev.erdragh.astralbot.LOGGER import dev.erdragh.astralbot.baseDirectory import dev.erdragh.astralbot.config.AstralBotConfig +import dev.erdragh.astralbot.config.AstralBotTextConfig import net.dv8tion.jda.api.entities.User +import net.minecraft.network.chat.ClickEvent import net.minecraft.network.chat.Component import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq @@ -250,10 +252,19 @@ object WhitelistHandler { * - `{{DISCORD}}` with the link to the Discord server * configured in the [AstralBotConfig] */ - fun writeWhitelistMessage(user: GameProfile): Component { + fun writeWhitelistMessage(user: GameProfile): Optional { + // This value represents whether the user is whitelisted in the DB. If this is the + // case the config to require linking is on and the user hasn't yet been whitelisted + // by another means. + val isDbWhitelisted = checkWhitelist(user.id) != null + + if (isDbWhitelisted) return Optional.of(Component.literal(AstralBotTextConfig.WHITELIST_LINKED_NOT_ALLOWED.get().replace("{{name}}", user.name))) + + val linkCode = getWhitelistCode(user.id) ?: return Optional.empty() + val template = whitelistTemplate.readText() .replace("{{USER}}", user.name) - .replace("{{CODE}}", getWhitelistCode(user.id).toString()) + .replace("{{CODE}}", linkCode.toString()) .split("{{DISCORD}}") // Throws an error if the template is malformed @@ -266,9 +277,9 @@ object WhitelistHandler { component.append(template[i]) if (discordLink.isNotEmpty() && i + 1 < template.size) { // TODO: Make this clickable. Using `withStyle` and a ClickEvent did not seem to work - component.append(Component.literal(discordLink)) + component.append(Component.literal(discordLink).withStyle {it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, discordLink))}) } } - return component + return Optional.of(component) } } \ No newline at end of file From cdb20e3e0729efd903f5cd452994b8e5b7989bb5 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Thu, 30 May 2024 20:18:35 +0200 Subject: [PATCH 11/12] Try to automatically create a webhook for AstralBot chat sync --- CHANGELOG.md | 3 ++ .../main/kotlin/dev/erdragh/astralbot/Bot.kt | 7 ++-- .../astralbot/config/AstralBotConfig.kt | 19 +++++----- .../astralbot/handlers/MinecraftHandler.kt | 36 ++++++++++++++++--- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4830f7..2583541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # indev - Introduce webhook chat sync, allowing for user imitation + + **IMPORTANT INFO**: If you don't want to manually set the webhook URL, the + bot needs to have the MANAGE_WEBHOOKS permission # 1.4.0 - Port to 1.20.4 and NeoForge diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt index 540e129..211ed54 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt @@ -97,6 +97,9 @@ private fun setupFromJDA(api: JDA) { textChannel = ch guild = g + // Text channel was fetched, now the minecraftHandler can fetch its webhook stuff + minecraftHandler?.updateWebhookClient() + ch.sendMessage("Server Started!").queue() } @@ -117,10 +120,6 @@ fun startAstralbot(server: MinecraftServer) { return } - if (AstralBotConfig.WEBHOOK_ENABLED.get() && AstralBotConfig.WEBHOOK_URL.get() == "") { - LOGGER.warn("Webhooks enabled, but no URL provided, chat synchronization will fallback to default implementation.") - } - minecraftHandler = MinecraftHandler(server) jda = JDABuilder.createLight( diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt index a4bfffe..0fe9467 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt @@ -3,7 +3,10 @@ package dev.erdragh.astralbot.config import dev.erdragh.astralbot.LOGGER import dev.erdragh.astralbot.commands.discord.allCommands import net.neoforged.neoforge.common.ModConfigSpec +import java.net.URI import java.net.URL +import java.net.URLDecoder +import java.nio.charset.StandardCharsets /** * Config for the AstralBot mod. This uses Forge's config system @@ -169,15 +172,15 @@ object AstralBotConfig { ) ) { if (it !is String) { - LOGGER.warn("$it in URL blocklist is not a String") + LOGGER.warn("$it in URI blocklist is not a String") return@defineList false } // TODO: Replace with better way to check for URL try { - URL(it) + URI(it) return@defineList true } catch (e: Exception) { - LOGGER.warn("Failed to parse URL on blocklist: $it", e) + LOGGER.warn("Failed to parse URI on blocklist: $it", e) return@defineList false } } @@ -213,18 +216,14 @@ object AstralBotConfig { fun urlAllowed(url: String?): Boolean { if (url == null) return true try { - val parsedURL = URL(url) + val parsedURL = URI(url) for (blockedURL in URL_BLOCKLIST.get()) { - if (parsedURL.host == URL(blockedURL).host) return false + if (parsedURL.host == URI(blockedURL).host) return false } } catch (e: Exception) { - LOGGER.warn("URL $url", e) + LOGGER.warn("URI $url", e) return false } return true } - - fun useWebhooks(): Boolean { - return WEBHOOK_ENABLED.get() && WEBHOOK_URL.get() != "" - } } \ No newline at end of file diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt index 253bf3c..029ef64 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt @@ -1,6 +1,7 @@ package dev.erdragh.astralbot.handlers import club.minnced.discord.webhook.WebhookClient +import club.minnced.discord.webhook.external.JDAWebhookClient import club.minnced.discord.webhook.send.WebhookEmbed import club.minnced.discord.webhook.send.WebhookEmbedBuilder import club.minnced.discord.webhook.send.WebhookMessageBuilder @@ -39,9 +40,9 @@ import kotlin.math.min * @author Erdragh */ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() { - private val playerNames = HashSet(server.maxPlayers); + private val playerNames = HashSet(server.maxPlayers) private val notchPlayer = byName("Notch")?.let { ServerPlayer(this.server, this.server.allLevels.elementAt(0), it, ClientInformation.createDefault()) } - private var webhookClient: WebhookClient? = AstralBotConfig.WEBHOOK_URL.get().let { if (it != "") WebhookClient.withUrl(it) else null } + private var webhookClient: WebhookClient? = null /** * Method that maybe creates a new webhook client if one wasn't configured before. @@ -52,6 +53,29 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() if (webhookClient == null || url != webhookClient?.url) { webhookClient = url.let { if (it != "") WebhookClient.withUrl(it) else null } } + if (webhookClient == null) { + textChannel?.retrieveWebhooks()?.submit()?.whenComplete { webhooks, error -> + if (error != null) { + LOGGER.error("Failed to fetch webhooks for channel: ${textChannel!!.id} (${textChannel!!.name})", error) + } else { + if (webhooks.size > 1) { + LOGGER.warn("Multiple webhooks available for channel: ${textChannel!!.id} (${textChannel!!.name}). AstralBot will use the first one. If you want to use a specific one, set the webhook.url config option.") + webhookClient = JDAWebhookClient.from(webhooks[0]) + } else if (webhooks.size == 1) { + webhookClient = JDAWebhookClient.from(webhooks[0]) + } else { + LOGGER.info("No webhook found in channel: ${textChannel!!.id} (${textChannel!!.name}). AstralBot will try to create one.") + textChannel!!.createWebhook("AstralBot Chat synchronization").submit().whenComplete { webhook, error -> + if (error != null) { + LOGGER.error("Failed to create webhook for channel: ${textChannel!!.id} (${textChannel!!.name})", error) + } else { + webhookClient = JDAWebhookClient.from(webhook) + } + } + } + } + } + } } companion object { @@ -168,7 +192,7 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() } } - val messageSenderInfo = if(AstralBotConfig.useWebhooks()) MessageSenderLookup.getMessageSenderInfo(player, AstralBotConfig.WEBHOOK_USE_LINKED.get()) else null + val messageSenderInfo = if(useWebhooks()) MessageSenderLookup.getMessageSenderInfo(player, AstralBotConfig.WEBHOOK_USE_LINKED.get()) else null val escape = { it: String -> it.replace("_", "\\_") } val content = if (messageSenderInfo != null) { formatComponentToMarkdown(message) @@ -181,7 +205,7 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() formatComponentToMarkdown(message) } - if (!AstralBotConfig.useWebhooks() || webhookClient == null || messageSenderInfo == null) { + if (!useWebhooks() || webhookClient == null || messageSenderInfo == null) { val createdMessage = MessageCreateBuilder() .addEmbeds(formattedEmbeds) .setContent(content) @@ -462,4 +486,8 @@ class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() } return comp } + + private fun useWebhooks(): Boolean { + return AstralBotConfig.WEBHOOK_ENABLED.get() && webhookClient != null + } } \ No newline at end of file From 63b34dda6e0e5c04fc1e2a20559b3866649a5400 Mon Sep 17 00:00:00 2001 From: Erdragh Date: Fri, 31 May 2024 19:19:19 +0200 Subject: [PATCH 12/12] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ec7276..3703c77 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ After starting the server, you can go into the OAuth2 URL builder on the Discord Developer Portal and generate a URL with the `bot` and `applications.command` permissions. Use the generated URL to have the bot join your server. -To be able to use user imitation, which makes the chat synchronization more readable, you will have to add a -webhook to your Discord server that uses the same chat you configured for the normal chat synchronization. Copy its -URL and add it to the `webhook.url` key in the config. \ No newline at end of file +To be able to use user imitation, which makes the chat synchronization more readable, the bot will try and create a webhook +in the configured chat synchronization channel. This means it will need to have the permission to manage webhooks. If you +don't want that you can manually create a webhook in the channel where you want the messages synchronized and set it in the +`webhook.url` option in the config for the mod. \ No newline at end of file