diff --git a/.gitignore b/.gitignore index 984aba0..f95b34b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ ### IntelliJ IDEA ### .idea +.kotlin ### Eclipse ### .apt_generated diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c0053..2f7712b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.5.3 +- Unlink users on leave +- Update Dependencies +- Show better error message if FAQ is too long +- Limit the autocomplete suggestion return value length to fix no options showing up +- Added the `/listfaq` command + # 1.5.2 Fix tps command using wrong numbers for calculation diff --git a/README.md b/README.md index 7f40665..4c035a6 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,11 @@ 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 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`. +anybody else. + +The bot can read the token from multiple sources, the easiest to configure being the `token` field in the +main server config file (`astralbot-server.toml`). Alternatively, you can also provide it via 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: `startmc.sh`: diff --git a/build.gradle.kts b/build.gradle.kts index 204799f..908b7b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,14 +10,14 @@ import net.fabricmc.loom.task.RemapJarTask plugins { // This is an Architectury repository, as such the relevant plugins are needed id("architectury-plugin") version "3.4-SNAPSHOT" - id("dev.architectury.loom") version "1.5-SNAPSHOT" apply false + id("dev.architectury.loom") version "1.7-SNAPSHOT" apply false // The shadow plugin is used by the fabric subproject to include dependencies // I'm temporarily using a fork of the original plugin to resolve "Unsupported java classfile major version 65" // see: https://github.com/johnrengelman/shadow/issues/911 - id("io.github.goooler.shadow") version "8.1.7" apply false + id("io.github.goooler.shadow") version "8.1.8" apply false // Since this mod/bot is written in Kotlin and expected to run on Minecraft and as such // the JVM, the Kotlin plugin is needed - kotlin("jvm") version "2.0.0" + kotlin("jvm") version "2.0.10" // For generating documentation based on comments in the code id("org.jetbrains.dokka") version "1.9.10" java @@ -267,7 +267,7 @@ subprojects { 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") + relocate("net.bytebuddy", "dev.erdragh.shadowed.net.bytebuddy") exclude("**/org/slf4j/**") @@ -330,7 +330,7 @@ subprojects { // file. This allows me to keep all previous changes in the file // without having to worry about them being included on new file // uploads. - File("CHANGELOG.md") + File(rootDir, "CHANGELOG.md") .readText(StandardCharsets.UTF_8) .replace(Regex("[^^](#(#|\\n|.)+)|(^#.+)"), "") .trim() diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt index 211ed54..9b243a9 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt @@ -1,6 +1,7 @@ package dev.erdragh.astralbot -import dev.erdragh.astralbot.commands.discord.CommandHandlingListener +import dev.erdragh.astralbot.listeners.CommandHandlingListener +import dev.erdragh.astralbot.listeners.UserEventListener import dev.erdragh.astralbot.config.AstralBotConfig import dev.erdragh.astralbot.handlers.FAQHandler import dev.erdragh.astralbot.handlers.MinecraftHandler @@ -18,6 +19,7 @@ import java.io.File import java.time.Duration import java.time.LocalDateTime import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.absolute import kotlin.properties.Delegates const val MODID = "astralbot" @@ -76,11 +78,11 @@ private fun setupFromJDA(api: JDA) { LOGGER.info("Fetching required data from Discord") updatePresence(0) applicationId = api.retrieveApplicationInfo().submit().get().idLong - if (AstralBotConfig.DISCORD_GUILD.get() < 0) { + if (AstralBotConfig.DISCORD_GUILD.get() <= 0) { LOGGER.warn("No text channel for chat synchronization configured. Chat sync will not be enabled.") return } - if (AstralBotConfig.DISCORD_CHANNEL.get() < 0) { + if (AstralBotConfig.DISCORD_CHANNEL.get() <= 0) { LOGGER.warn("No text channel for chat synchronization configured. Chat sync will not be enabled.") return } @@ -108,7 +110,7 @@ fun startAstralbot(server: MinecraftServer) { startTimestamp = LocalDateTime.now() val env = System.getenv() - baseDirectory = File(server.serverDirectory, MODID) + baseDirectory = File(server.serverDirectory.absoluteFile, MODID) if (baseDirectory!!.mkdir()) { LOGGER.debug("Created $MODID directory") } @@ -127,7 +129,11 @@ fun startAstralbot(server: MinecraftServer) { GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MEMBERS - ).addEventListeners(CommandHandlingListener, minecraftHandler).build() + ).addEventListeners( + CommandHandlingListener, + UserEventListener, + minecraftHandler + ).build() setupJob = GlobalScope.async { launch { diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/DiscordCommands.kt b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/DiscordCommands.kt index 9f78625..e5f4343 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/DiscordCommands.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/DiscordCommands.kt @@ -16,7 +16,8 @@ val allCommands = arrayOf( // Bot management commands ReloadCommand, // Utility Commands - FAQCommand, + FAQCommands, + ListFaqsCommand, // Player related commands LinkCommand, UnlinkCommand, diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FAQCommand.kt b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FAQCommand.kt index caa1d98..0078adc 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FAQCommand.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FAQCommand.kt @@ -1,11 +1,15 @@ package dev.erdragh.astralbot.commands.discord +import dev.erdragh.astralbot.config.AstralBotTextConfig import dev.erdragh.astralbot.handlers.FAQHandler +import net.dv8tion.jda.api.entities.Message import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.interactions.commands.OptionType import net.dv8tion.jda.api.interactions.commands.build.Commands +import net.dv8tion.jda.api.interactions.commands.build.OptionData import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData +import kotlin.math.min /** * This command prints FAQ messages, fetching them from @@ -16,11 +20,11 @@ import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData * * @author Erdragh */ -object FAQCommand : HandledSlashCommand, AutocompleteCommand { +object FAQCommands : HandledSlashCommand, AutocompleteCommand { private const val OPTION_ID = "id" - override val command: SlashCommandData = Commands.slash("faq", "prints a specified FAQ answer") - .addOption(OptionType.STRING, OPTION_ID, "id of the faq", true, true) + override val command: SlashCommandData = Commands.slash("faq", "Prints a specified FAQ answer") + .addOption(OptionType.STRING, OPTION_ID, "ID of the FAQ", true, true) override fun handle(event: SlashCommandInteractionEvent) { // since interacting with the disk may take a while, the reply gets deferred @@ -29,12 +33,46 @@ object FAQCommand : HandledSlashCommand, AutocompleteCommand { val faqId = event.getOption(OPTION_ID)!!.asString val faqResponse = FAQHandler.getFAQForId(faqId) - event.hook.sendMessage(faqResponse).queue() + if (faqResponse.length >= Message.MAX_CONTENT_LENGTH) { + event.hook.sendMessage(AstralBotTextConfig.FAQ_TOO_LONG.get().replace("{{id}}", faqId)).queue() + } else { + event.hook.sendMessage(faqResponse).queue() + } + } override fun autocomplete(event: CommandAutoCompleteInteractionEvent) { if (event.focusedOption.name == OPTION_ID) { - event.replyChoiceStrings(FAQHandler.suggestFAQIds(event.focusedOption.value)).queue() + val faqs = FAQHandler.suggestFAQIds(event.focusedOption.value) + event.replyChoiceStrings(faqs.slice(0.. + val DISCORD_CHANNEL: ForgeConfigSpec.LongValue /** * The ID of the Discord Guild (server) where this bot will be active. * This is used to get the chat sync channel etc. */ - val DISCORD_GUILD: ForgeConfigSpec.ConfigValue + val DISCORD_GUILD: ForgeConfigSpec.LongValue /** * The ID of the Discord role given to linked members */ - val DISCORD_ROLE: ForgeConfigSpec.ConfigValue + val DISCORD_ROLE: ForgeConfigSpec.LongValue /** * If this is set to true the message sent into the Minecraft chat @@ -142,11 +139,11 @@ object AstralBotConfig { DISCORD_LINK = builder.comment("Link to the discord where your users can run the /link command") .define("discordLink", "") DISCORD_CHANNEL = builder.comment("Channel ID where the chat messages are synced") - .define("discordChannel", (-1).toLong()) + .defineInRange("discordChannel", 0L, 0L, Long.MAX_VALUE) DISCORD_GUILD = builder.comment("Guild (server) ID where the chat messages etc. are synced") - .define("discordGuild", (-1).toLong()) + .defineInRange("discordChannel", 0L, 0L, Long.MAX_VALUE) DISCORD_ROLE = builder.comment("ID of the role given to linked users") - .define("discordRole", (-1).toLong()) + .defineInRange("discordChannel", 0L, 0L, Long.MAX_VALUE) CLICKABLE_MESSAGES = builder.comment("Whether to make messages sent into the Minecraft chat open the Discord chat when clicked") @@ -187,7 +184,8 @@ object AstralBotConfig { ENABLED_COMMANDS = builder.comment("Enabled Slash Commands") .defineList("enabledCommands", - allCommands.map { it.command.name }) { + allCommands.map { it.command.name }, + ) { if (it !is String) { LOGGER.warn("$it in enabledCommands is not a String") return@defineList 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 f4eb529..f69ff36 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotTextConfig.kt @@ -9,7 +9,8 @@ object AstralBotTextConfig { val GENERIC_SUCCESS: ForgeConfigSpec.ConfigValue val GENERIC_BLOCKED: ForgeConfigSpec.ConfigValue - val FAQ_ERROR: ForgeConfigSpec.ConfigValue + val FAQ_TOO_LONG: ForgeConfigSpec.ConfigValue + val FAQ_NONE_AVAILABLE: ForgeConfigSpec.ConfigValue val FAQ_NO_REGISTERED: ForgeConfigSpec.ConfigValue val TICK_REPORT: ForgeConfigSpec.ConfigValue @@ -50,8 +51,10 @@ object AstralBotTextConfig { GENERIC_BLOCKED = builder.comment("Generic string that replaces blocked URLs/Links") .define("genericBlocked", "[BLOCKED]") - FAQ_ERROR = builder.comment("Message sent to Discord if an error ocurrs during FAQ loading") - .define(mutableListOf("faq", "error"), "Bot Error (Contact Bot Operator)") + FAQ_TOO_LONG = builder.comment("Message sent to Discord if an FAQ entry is too long. The id is accessible via {{id}}") + .define(mutableListOf("faq", "tooLong"), "FAQ for id `{{id}}` exceeds Discord's message length limit.") + FAQ_NONE_AVAILABLE = builder.comment("Message sent to Discord if there are no FAQs available") + .define(mutableListOf("faq", "noneAvailable"), "No FAQs available") FAQ_NO_REGISTERED = builder.comment( """Message sent to Discord when there is no FAQ for the given id. diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FAQHandler.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FAQHandler.kt index 68b6d4b..7951068 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FAQHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FAQHandler.kt @@ -68,7 +68,7 @@ object FAQHandler { fun getFAQForId(id: String): String { if (!faqDirectory.exists() || !faqDirectory.isDirectory) { LOGGER.error("FAQ directory not specified as directory: ${faqDirectory.absolutePath}") - return AstralBotTextConfig.FAQ_ERROR.get() + return AstralBotTextConfig.GENERIC_ERROR.get() } val faqFiles = faqDirectory.listFiles { file -> file.name == "$id.md" } val faqFile = if (faqFiles?.isNotEmpty() == true) faqFiles[0] else null @@ -81,8 +81,8 @@ object FAQHandler { * Used in the autocomplete implementation of the faq command. * @return a List of all available FAQ ids that start with the given [slug] */ - fun suggestFAQIds(slug: String): List { - return availableFAQIDs.filter { it.startsWith(slug, true) } + fun suggestFAQIds(slug: String?): List { + return availableFAQIDs.filter { it.contains(slug ?: "", true) } } /** diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/CommandHandlingListener.kt b/common/src/main/kotlin/dev/erdragh/astralbot/listeners/CommandHandlingListener.kt similarity index 96% rename from common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/CommandHandlingListener.kt rename to common/src/main/kotlin/dev/erdragh/astralbot/listeners/CommandHandlingListener.kt index 9e2bd28..bae61a1 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/CommandHandlingListener.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/listeners/CommandHandlingListener.kt @@ -1,7 +1,9 @@ -package dev.erdragh.astralbot.commands.discord +package dev.erdragh.astralbot.listeners import dev.erdragh.astralbot.LOGGER import dev.erdragh.astralbot.applicationId +import dev.erdragh.astralbot.commands.discord.AutocompleteCommand +import dev.erdragh.astralbot.commands.discord.getEnabledCommands import dev.erdragh.astralbot.config.AstralBotTextConfig import dev.erdragh.astralbot.guild import dev.erdragh.astralbot.waitForSetup diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/listeners/UserEventListener.kt b/common/src/main/kotlin/dev/erdragh/astralbot/listeners/UserEventListener.kt new file mode 100644 index 0000000..06deee2 --- /dev/null +++ b/common/src/main/kotlin/dev/erdragh/astralbot/listeners/UserEventListener.kt @@ -0,0 +1,11 @@ +package dev.erdragh.astralbot.listeners + +import dev.erdragh.astralbot.handlers.WhitelistHandler +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter + +object UserEventListener : ListenerAdapter() { + override fun onGuildMemberRemove(event: GuildMemberRemoveEvent) { + WhitelistHandler.unWhitelist(event.user) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageFormatting.kt b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageFormatting.kt index eeec9a1..261eac5 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageFormatting.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/util/MessageFormatting.kt @@ -10,6 +10,7 @@ import net.minecraft.network.chat.HoverEvent import net.minecraft.network.chat.MutableComponent import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import net.minecraft.world.item.TooltipFlag import org.commonmark.parser.Parser @@ -82,6 +83,7 @@ fun formatHoverText(text: Component): MessageEmbed { fun formatHoverItems(stack: ItemStack, knownItems: MutableList, player: Player?): MessageEmbed? { if (knownItems.contains(stack)) return null knownItems.add(stack) + // TODO check if context needs fixing val tooltip = stack.getTooltipLines(player, TooltipFlag.Default.NORMAL).map(::formatComponentToMarkdown) return EmbedBuilder() .setTitle("${tooltip[0]} ${if (stack.count > 1) "(${stack.count})" else ""}") diff --git a/fabric/gradle.properties b/fabric/gradle.properties index e69de29..c52e9c1 100644 --- a/fabric/gradle.properties +++ b/fabric/gradle.properties @@ -0,0 +1 @@ +fabric.loom.multiProjectOptimisation=true \ No newline at end of file diff --git a/forge/src/main/kotlin/dev/erdragh/astralbot/forge/BotMod.kt b/forge/src/main/kotlin/dev/erdragh/astralbot/forge/BotMod.kt index 6b7189a..6e58d74 100644 --- a/forge/src/main/kotlin/dev/erdragh/astralbot/forge/BotMod.kt +++ b/forge/src/main/kotlin/dev/erdragh/astralbot/forge/BotMod.kt @@ -26,8 +26,8 @@ import net.minecraftforge.fml.event.config.ModConfigEvent @Mod("astralbot") object BotMod { init { - ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotConfig.SPEC) - ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, AstralBotTextConfig.SPEC, "astralbot-text.toml") + ModLoadingContext.get().activeContainer.registerConfig(ModConfig.Type.SERVER, AstralBotConfig.SPEC) + ModLoadingContext.get().activeContainer.registerConfig(ModConfig.Type.SERVER, AstralBotTextConfig.SPEC, "astralbot-text.toml") MOD_BUS.addListener(::onConfigReloaded) FORGE_BUS.addListener(::onServerStart) diff --git a/gradle.properties b/gradle.properties index 9b23e88..c47f3b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,12 +5,12 @@ org.gradle.jvmargs=-Xmx3G -XX:ThreadStackSize=4096 -XX:CompilerThreadStackSize=4 enabledPlatforms=fabric,forge # Fabric -fabricLoaderVersion=0.15.11 +fabricLoaderVersion=0.16.3 fabricApiVersion=0.77.0 -fabricKotlinVersion=1.11.0+kotlin.2.0.0 +fabricKotlinVersion=1.12.1+kotlin.2.0.20 -version=1.5.2 +version=1.5.3 group=dev.erdragh.astralbot modId=astralbot modAuthor=Erdragh @@ -27,12 +27,12 @@ forgeConfigAPIVersion=4.2.11 nightConfig=3.6.5 # Discord Interactions -jdaVersion=5.0.0-beta.24 +jdaVersion=5.1.0 dcWebhooksVersion=0.8.4 # Database Interactions -exposedVersion=0.51.0 -sqliteJDBCVersion=3.46.0.0 +exposedVersion=0.54.0 +sqliteJDBCVersion=3.46.1.0 # Message parsing commonmarkVersion=0.22.0 \ No newline at end of file