diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt index abf958d..320257d 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt @@ -1,10 +1,15 @@ package dev.erdragh.astralbot import dev.erdragh.astralbot.commands.CommandHandlingListener +import dev.erdragh.astralbot.config.AstralBotConfig import dev.erdragh.astralbot.handlers.FAQHandler import dev.erdragh.astralbot.handlers.MinecraftHandler +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.entities.Guild +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel import net.dv8tion.jda.api.requests.GatewayIntent import net.minecraft.server.MinecraftServer import org.slf4j.Logger @@ -14,9 +19,34 @@ import java.io.File const val MODID = "astralbot" val LOGGER: Logger = LoggerFactory.getLogger(MODID) var minecraftHandler: MinecraftHandler? = null -var jda: JDA? = null +var textChannel: TextChannel? = null +var guild: Guild? = null +private var jda: JDA? = null lateinit var baseDirectory: File +private fun setupFromJDA(api: JDA) { + api.awaitReady() + 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) { + LOGGER.warn("No text channel for chat synchronization configured. Chat sync will not be enabled.") + return + } + val g = api.getGuildById(AstralBotConfig.DISCORD_GUILD.get()) + if (g == null) { + LOGGER.warn("Configured Discord Guild (server) ID is not valid.") + return + } + val ch = g.getTextChannelById(AstralBotConfig.DISCORD_CHANNEL.get()) + if (ch == null) { + LOGGER.warn("Configured Discord channel ID is not valid.") + return + } + textChannel = ch +} + fun startAstralbot(server: MinecraftServer) { val env = System.getenv() if (!env.containsKey("DISCORD_TOKEN")) { @@ -29,6 +59,8 @@ fun startAstralbot(server: MinecraftServer) { LOGGER.debug("Created $MODID directory") } + minecraftHandler = MinecraftHandler(server) + FAQHandler.start() jda = JDABuilder.createLight( @@ -36,9 +68,13 @@ fun startAstralbot(server: MinecraftServer) { GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MEMBERS - ).addEventListeners(CommandHandlingListener).build() + ).addEventListeners(CommandHandlingListener, minecraftHandler).build() - minecraftHandler = MinecraftHandler(server, jda) + runBlocking { + launch { + setupFromJDA(jda!!) + } + } // This makes sure that the extra parallel tasks from this // mod/bot combo get shut down even if the Server Shutdown diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/Commands.kt b/common/src/main/kotlin/dev/erdragh/astralbot/commands/Commands.kt index 99e7a40..fda0c50 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/Commands.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/commands/Commands.kt @@ -2,8 +2,13 @@ package dev.erdragh.astralbot.commands +import dev.erdragh.astralbot.config.AstralBotConfig +import dev.erdragh.astralbot.guild import dev.erdragh.astralbot.handlers.FAQHandler import dev.erdragh.astralbot.minecraftHandler +import dev.erdragh.astralbot.textChannel +import kotlinx.coroutines.runBlocking +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel 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 @@ -15,12 +20,7 @@ import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData * This gets used to register the commands. */ val commands = arrayOf( - RefreshCommandsCommand, - FAQCommand, - LinkCommand, - UnlinkCommand, - LinkCheckCommand, - ListCommand + RefreshCommandsCommand, FAQCommand, LinkCommand, UnlinkCommand, LinkCheckCommand, ListCommand, ChatSyncCommand ) /** @@ -130,4 +130,42 @@ object ListCommand : HandledSlashCommand { event.hook.sendMessage("There are no players online currently").queue() } } +} + +object ChatSyncCommand : HandledSlashCommand { + private const val OPTION_CHANNEL = "channel" + override val command: SlashCommandData = + Commands.slash("chatsync", "Configures the Bot to synchronize chat messages").addOption( + OptionType.CHANNEL, + OPTION_CHANNEL, + "The channel where to sync to. If this isn't provided the current channel will be used", + false + ) + + override fun handle(event: SlashCommandInteractionEvent) { + event.deferReply(true).queue() + var success = false + runBlocking { + val eventChannel = event.channel + val g = event.guild + val channel = event.getOptionsByType(OptionType.CHANNEL).findLast { it.name == OPTION_CHANNEL }?.asChannel + textChannel = if (channel is TextChannel) { + channel + } else if (eventChannel is TextChannel) { + eventChannel + } else return@runBlocking + + AstralBotConfig.DISCORD_CHANNEL.set(textChannel!!.idLong) + AstralBotConfig.DISCORD_CHANNEL.save() + + guild = g ?: return@runBlocking + + AstralBotConfig.DISCORD_GUILD.set(guild!!.idLong) + AstralBotConfig.DISCORD_GUILD.save() + success = true + } + event.hook.setEphemeral(true) + .sendMessage(if (success) "Successfully set up chat synchronization" else "Something went wrong while setting up chat sync") + .queue() + } } \ No newline at end of file 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 0120bdc..882aa3d 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/config/AstralBotConfig.kt @@ -25,6 +25,17 @@ object AstralBotConfig { */ val DISCORD_LINK: ForgeConfigSpec.ConfigValue + /** + * The ID of the discord channel where the messages are synchronized + */ + val DISCORD_CHANNEL: ForgeConfigSpec.ConfigValue + + /** + * 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 + init { val builder = ForgeConfigSpec.Builder() @@ -34,6 +45,10 @@ object AstralBotConfig { .define("requireLinkForWhitelist", false) 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()) + DISCORD_GUILD = builder.comment("Guild (server) ID where the chat messages etc. are synced") + .define("discordGuild", (-1).toLong()) SPEC = builder.build() } diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FileWatcher.kt b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FileWatcher.kt index df24f48..b04a805 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FileWatcher.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/FileWatcher.kt @@ -20,13 +20,14 @@ class FileWatcher(private val directoryPath: Path, private val handler: (event: /** * Starts the file system watcher in parallel using Kotlin's coroutines - * and the [Dispatchers.IO] scope. + * in the [GlobalScope] using the [Dispatchers.IO] dispatcher. */ + @OptIn(DelicateCoroutinesApi::class) fun startWatching() { job = GlobalScope.launch(Dispatchers.IO) { watchService = FileSystems.getDefault().newWatchService() directoryPath.register( - watchService, + watchService!!, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE @@ -41,6 +42,7 @@ class FileWatcher(private val directoryPath: Path, private val handler: (event: for (event in key.pollEvents()) { LOGGER.info("Event: {}", event.kind()) // Send the event to the channel + @Suppress("UNCHECKED_CAST") handler(event as WatchEvent) } 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 210ff63..04936f5 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/handlers/MinecraftHandler.kt @@ -1,10 +1,12 @@ package dev.erdragh.astralbot.handlers import com.mojang.authlib.GameProfile -import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.entities.channel.concrete.TextChannel -import net.minecraft.network.chat.ChatType -import net.minecraft.network.chat.PlayerChatMessage +import dev.erdragh.astralbot.guild +import dev.erdragh.astralbot.textChannel +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter +import net.minecraft.network.chat.Component import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerPlayer import java.util.* @@ -15,8 +17,7 @@ import kotlin.jvm.optionals.getOrNull * methods for fetching [GameProfile]s * @author Erdragh */ -class MinecraftHandler(private val server: MinecraftServer, private val api: JDA?) { - private var channel: TextChannel? = null +class MinecraftHandler(private val server: MinecraftServer) : ListenerAdapter() { /** * Fetches all currently online players' [GameProfile]s @@ -60,10 +61,21 @@ class MinecraftHandler(private val server: MinecraftServer, private val api: JDA * @param message the String contents of the message */ fun sendChatToDiscord(player: ServerPlayer, message: String) { - // TODO: Replace with more configurable channel selection - if (channel == null) channel = api?.getTextChannelsByName("chat", true)?.get(0) - - channel?.sendMessage("<${player.name.string}> $message")?.setSuppressedNotifications(true) + textChannel?.sendMessage("<${player.name.string}> $message")?.setSuppressedNotifications(true) ?.setSuppressEmbeds(true)?.queue() } + + private fun sendDiscordToChat(message: Message) { + val color = guild?.getMemberById(message.author.idLong)?.colorRaw + val formattedMessage = + Component.literal(message.author.effectiveName).withStyle { it.withColor(color ?: 0xffffff) }.append(": ") + .append(message.contentDisplay) + server.playerList.broadcastSystemMessage(formattedMessage, false) + } + + override fun onMessageReceived(event: MessageReceivedEvent) { + if (event.channel.idLong == textChannel?.idLong) { + sendDiscordToChat(event.message) + } + } } \ No newline at end of file