Skip to content

Commit

Permalink
Implement rudimentary discord -> minecraft chat sync:
Browse files Browse the repository at this point in the history
- configure channel and guild id via command
  and store it in config file
- register minecraftHandler as an EventListener
  for the message received event
  • Loading branch information
Erdragh committed Dec 5, 2023
1 parent 6a398cc commit 59228bd
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 21 deletions.
42 changes: 39 additions & 3 deletions common/src/main/kotlin/dev/erdragh/astralbot/Bot.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")) {
Expand All @@ -29,16 +59,22 @@ fun startAstralbot(server: MinecraftServer) {
LOGGER.debug("Created $MODID directory")
}

minecraftHandler = MinecraftHandler(server)

FAQHandler.start()

jda = JDABuilder.createLight(
env["DISCORD_TOKEN"],
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
Expand Down
50 changes: 44 additions & 6 deletions common/src/main/kotlin/dev/erdragh/astralbot/commands/Commands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)

/**
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ object AstralBotConfig {
*/
val DISCORD_LINK: ForgeConfigSpec.ConfigValue<String>

/**
* The ID of the discord channel where the messages are synchronized
*/
val DISCORD_CHANNEL: ForgeConfigSpec.ConfigValue<Long>

/**
* 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<Long>

init {
val builder = ForgeConfigSpec.Builder()

Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Path>)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.*
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit 59228bd

Please sign in to comment.