diff --git a/README.md b/README.md index fcaee9c..a58e3f1 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,16 @@ settings: password: 'password' debug: false relay_cancelled_messages: true + messages: + join: true + leave: true + death: false templates: discord: chat_message: '<%u> %m' player_join: '%u joined the server' player_leave: '%u left the server' + player_death: '%r' minecraft: chat_message: '<%u&b(discord)&r> %m' ``` @@ -42,15 +47,34 @@ settings: * `password` is the Discord password of your bot user * `debug` enables more verbose logging * `relay_cancelled_messages` will relay chat messages even if they are cancelled -* `templates` - customize the message text - `%u` will be replaced with the username and `%m` will be replaced with the message. Color codes, prefixed with `&`, will be translated on the Minecraft end. +* `messages` enables or disables certain kinds of messages +* `templates` - customize the message text + +**Templates** + +- `%u` will be replaced with the username +- '%d' will be replaced with the user's display name +- `%m` will be replaced with the message +- `%w` will be replaced with the world name +- `%r` will be replaced with the death reason +- Color codes, prefixed with `&`, will be translated on the Minecraft end ## Features * Anything said in Minecraft chat will be sent to your chosen Discord channel * Anything said in your chosen Discord channel will be sent to your Minecraft chat (with a `(discord)` suffix added to usernames) * Join / leave messages are sent to Discord +* Death messages can optionally be sent to Discord * Message templates are customized +## Permissions + +- `discordbridge.reload` - ability to reload config and reconnect the Discord connection + +## Commands + +- `/discord reload` - reloads config and reconnects to Discord + ## Upcoming Features * Deeper integration into Minecraft chat (like supporting chat channels inside Minecraft) diff --git a/pom.xml b/pom.xml index 93cdcae..527de9b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ gg.obsidian DiscordBridge - 1.3.0 + 1.4.0 Bridge chat between Discord and Minecraft https://github.com/the-obsidian/DiscordBridge diff --git a/src/main/kotlin/gg/obsidian/discordbridge/CommandHandler.kt b/src/main/kotlin/gg/obsidian/discordbridge/CommandHandler.kt new file mode 100644 index 0000000..695e4ad --- /dev/null +++ b/src/main/kotlin/gg/obsidian/discordbridge/CommandHandler.kt @@ -0,0 +1,36 @@ +package gg.obsidian.discordbridge + +import org.bukkit.ChatColor +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +class CommandHandler(val plugin: Plugin): CommandExecutor { + + override fun onCommand(player: CommandSender, cmd: Command, alias: String?, args: Array?): Boolean { + if (player is Player && !Permissions.reload.has(player)) return true + + val isConsole = (player is Player) + + if (cmd.name != "discord") return true + + if (args == null || args.size != 1 || !args[0].equals("reload")) { + sendMessage("&eUsage: /discord reload", player, isConsole) + return true + } + + sendMessage("&eReloading Discord Bridge...", player, isConsole) + plugin.reload() + return true + } + + private fun sendMessage(message: String, player: CommandSender, isConsole: Boolean) { + val formattedMessage = ChatColor.translateAlternateColorCodes('&', message) + if (isConsole) { + plugin.server.consoleSender.sendMessage(formattedMessage) + } else { + player.sendMessage(formattedMessage) + } + } +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/Configuration.kt b/src/main/kotlin/gg/obsidian/discordbridge/Configuration.kt index d1557a2..7609f8e 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/Configuration.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/Configuration.kt @@ -9,9 +9,19 @@ class Configuration(val plugin: Plugin) { var PASSWORD: String = "" var DEBUG: Boolean = false var RELAY_CANCELLED_MESSAGES = true + + // Toggle message types + var MESSAGES_JOIN = true + var MESSAGES_LEAVE = true + var MESSAGES_DEATH = false + + // Discord message templates var TEMPLATES_DISCORD_CHAT_MESSAGE = "" var TEMPLATES_DISCORD_PLAYER_JOIN = "" var TEMPLATES_DISCORD_PLAYER_LEAVE = "" + var TEMPLATES_DISCORD_PLAYER_DEATH = "" + + // Minecraft message templates var TEMPLATES_MINECRAFT_CHAT_MESSAGE = "" fun load() { @@ -23,11 +33,17 @@ class Configuration(val plugin: Plugin) { EMAIL = plugin.config.getString("settings.email") PASSWORD = plugin.config.getString("settings.password") DEBUG = plugin.config.getBoolean("settings.debug", false) - RELAY_CANCELLED_MESSAGES = plugin.config.getBoolean("settings.relay_cancelled_messages", true); + RELAY_CANCELLED_MESSAGES = plugin.config.getBoolean("settings.relay_cancelled_messages", true) + + MESSAGES_JOIN = plugin.config.getBoolean("settings.messages.join", true) + MESSAGES_LEAVE = plugin.config.getBoolean("settings.messages.leave", true) + MESSAGES_DEATH = plugin.config.getBoolean("settings.messages.death", false) TEMPLATES_DISCORD_CHAT_MESSAGE = plugin.config.getString("settings.templates.discord.chat_message", "<%u> %m") TEMPLATES_DISCORD_PLAYER_JOIN = plugin.config.getString("settings.templates.discord.player_join", "%u joined the server") TEMPLATES_DISCORD_PLAYER_LEAVE = plugin.config.getString("settings.templates.discord.player_leave", "%u left the server") + TEMPLATES_DISCORD_PLAYER_DEATH = plugin.config.getString("settings.templates.discord.player_death", "%r") + TEMPLATES_MINECRAFT_CHAT_MESSAGE = plugin.config.getString("settings.templates.minecraft.chat_message", "<%u&b(discord)&r> %m") } } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/DiscordConnection.kt b/src/main/kotlin/gg/obsidian/discordbridge/DiscordConnection.kt index 48f36d9..dd4decd 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/DiscordConnection.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/DiscordConnection.kt @@ -6,12 +6,13 @@ import net.dv8tion.jda.entities.TextChannel class DiscordConnection(val plugin: Plugin) : Runnable { var api = JDABuilder(plugin.configuration.EMAIL, plugin.configuration.PASSWORD).build() + var listener = DiscordListener(plugin, api, this) var server: Guild? = null var channel: TextChannel? = null override fun run() { try { - api.addEventListener(DiscordListener(plugin, api)) + api.addEventListener(listener) } catch (e: Exception) { plugin.logger.severe("Error connecting to Discord: " + e) } @@ -28,6 +29,14 @@ class DiscordConnection(val plugin: Plugin) : Runnable { channel!!.sendMessage(message) } + fun reconnect() { + api.removeEventListener(listener) + api.shutdown(false) + api = JDABuilder(plugin.configuration.EMAIL, plugin.configuration.PASSWORD).build() + listener = DiscordListener(plugin, api, this) + api.addEventListener(listener) + } + private fun getServerById(id: String): Guild? { for (server in api.guilds) if (server.id.equals(id, true)) diff --git a/src/main/kotlin/gg/obsidian/discordbridge/DiscordListener.kt b/src/main/kotlin/gg/obsidian/discordbridge/DiscordListener.kt index 32b341d..9b2ae72 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/DiscordListener.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/DiscordListener.kt @@ -2,11 +2,12 @@ package gg.obsidian.discordbridge import com.neovisionaries.ws.client.WebSocket import com.neovisionaries.ws.client.WebSocketException +import com.neovisionaries.ws.client.WebSocketFrame import net.dv8tion.jda.JDA import net.dv8tion.jda.events.message.MessageReceivedEvent import net.dv8tion.jda.hooks.ListenerAdapter -class DiscordListener(val plugin: Plugin, val api: JDA) : ListenerAdapter() { +class DiscordListener(val plugin: Plugin, val api: JDA, val connection: DiscordConnection) : ListenerAdapter() { override fun onMessageReceived(event: MessageReceivedEvent) { plugin.logDebug("Received message ${event.message.id} from Discord") @@ -35,4 +36,9 @@ class DiscordListener(val plugin: Plugin, val api: JDA) : ListenerAdapter() { fun onUnexpectedError(ws: WebSocket, wse: WebSocketException) { plugin.logger.severe("Unexpected error from DiscordBridge: ${wse.message}") } + + fun onDisconnected(webSocket: WebSocket, serverCloseFrame: WebSocketFrame, clientCloseFrame: WebSocketFrame, closedByServer: Boolean) { + plugin.logDebug("Discord disconnected - attempting to reconnect") + connection.reconnect() + } } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt b/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt new file mode 100644 index 0000000..797c681 --- /dev/null +++ b/src/main/kotlin/gg/obsidian/discordbridge/EventListener.kt @@ -0,0 +1,88 @@ +package gg.obsidian.discordbridge + +import org.bukkit.ChatColor +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.AsyncPlayerChatEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent + +class EventListener(val plugin: Plugin): Listener { + + @EventHandler(priority = EventPriority.MONITOR) + fun onChat(event: AsyncPlayerChatEvent) { + plugin.logDebug("Received a chat event from ${event.player.name}: ${event.message}") + if (!event.isCancelled || plugin.configuration.RELAY_CANCELLED_MESSAGES) { + val username = ChatColor.stripColor(event.player.name) + val formattedMessage = Util.formatMessage( + plugin.configuration.TEMPLATES_DISCORD_CHAT_MESSAGE, + mapOf( + "%u" to username, + "%m" to ChatColor.stripColor(event.message), + "%d" to ChatColor.stripColor(event.player.displayName), + "%w" to event.player.world.name + ) + ) + + plugin.sendToDiscord(formattedMessage) + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun onPlayerJoin(event: PlayerJoinEvent) { + if (!plugin.configuration.MESSAGES_JOIN) return + + val username = ChatColor.stripColor(event.player.name) + plugin.logDebug("Received a join event for $username") + + val formattedMessage = Util.formatMessage( + plugin.configuration.TEMPLATES_DISCORD_PLAYER_JOIN, + mapOf( + "%u" to username, + "%d" to ChatColor.stripColor(event.player.displayName) + ) + ) + + plugin.sendToDiscord(formattedMessage) + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun onPlayerQuit(event: PlayerQuitEvent) { + if (!plugin.configuration.MESSAGES_LEAVE) return + + val username = ChatColor.stripColor(event.player.name) + plugin.logDebug("Received a leave event for $username") + + val formattedMessage = Util.formatMessage( + plugin.configuration.TEMPLATES_DISCORD_PLAYER_LEAVE, + mapOf( + "%u" to username, + "%d" to ChatColor.stripColor(event.player.displayName) + ) + ) + + plugin.sendToDiscord(formattedMessage) + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun onPlayerDeath(event: PlayerDeathEvent) { + if (!plugin.configuration.MESSAGES_DEATH) return + + val username = ChatColor.stripColor(event.entity.name) + plugin.logDebug("Received a death event for $username") + + val formattedMessage = Util.formatMessage( + plugin.configuration.TEMPLATES_DISCORD_PLAYER_DEATH, + mapOf( + "%u" to username, + "%d" to ChatColor.stripColor(event.entity.displayName), + "%r" to event.deathMessage, + "%w" to event.entity.world.name + ) + ) + + plugin.sendToDiscord(formattedMessage) + } +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/Permissions.kt b/src/main/kotlin/gg/obsidian/discordbridge/Permissions.kt new file mode 100644 index 0000000..4fa57d1 --- /dev/null +++ b/src/main/kotlin/gg/obsidian/discordbridge/Permissions.kt @@ -0,0 +1,11 @@ +package gg.obsidian.discordbridge + +import org.bukkit.entity.Player + +enum class Permissions(val node: String) { + reload("discordbridge.reload"); + + fun has(player: Player): Boolean { + return player.hasPermission(node) + } +} diff --git a/src/main/kotlin/gg/obsidian/discordbridge/Plugin.kt b/src/main/kotlin/gg/obsidian/discordbridge/Plugin.kt index 1a1c8d6..bd96815 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/Plugin.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/Plugin.kt @@ -1,15 +1,8 @@ package gg.obsidian.discordbridge -import org.bukkit.ChatColor -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority -import org.bukkit.event.Listener -import org.bukkit.event.player.AsyncPlayerChatEvent -import org.bukkit.event.player.PlayerJoinEvent -import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.plugin.java.JavaPlugin -class Plugin : JavaPlugin(), Listener { +class Plugin : JavaPlugin() { val configuration = Configuration(this) var connection: DiscordConnection? = null @@ -20,39 +13,14 @@ class Plugin : JavaPlugin(), Listener { this.connection = DiscordConnection(this) server.scheduler.runTaskAsynchronously(this, connection) - server.pluginManager.registerEvents(this, this) + server.pluginManager.registerEvents(EventListener(this), this) + getCommand("discord").executor = CommandHandler(this) } - // Event Handlers - - @EventHandler(priority = EventPriority.MONITOR) - fun onChat(event: AsyncPlayerChatEvent) { - logDebug("Received a chat event from ${event.player.name}: ${event.message}") - if (!event.isCancelled || configuration.RELAY_CANCELLED_MESSAGES) { - val username = ChatColor.stripColor(event.player.name) - val formattedMessage = configuration.TEMPLATES_DISCORD_CHAT_MESSAGE - .replace("%u", username) - .replace("%m", event.message) - sendToDiscord(formattedMessage) - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - fun onPlayerJoin(event: PlayerJoinEvent) { - val username = ChatColor.stripColor(event.player.name) - logDebug("Received a join event for $username") - val formattedMessage = configuration.TEMPLATES_DISCORD_PLAYER_JOIN - .replace("%u", username) - sendToDiscord(formattedMessage) - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - fun onPlayerQuit(event: PlayerQuitEvent) { - val username = ChatColor.stripColor(event.player.name) - logDebug("Received a leave event for $username") - val formattedMessage = configuration.TEMPLATES_DISCORD_PLAYER_LEAVE - .replace("%u", username) - sendToDiscord(formattedMessage) + fun reload() { + reloadConfig() + configuration.load() + connection?.reconnect() } // Message senders @@ -63,10 +31,15 @@ class Plugin : JavaPlugin(), Listener { } fun sendToMinecraft(username: String, message: String) { - val formattedMessage = ChatColor.translateAlternateColorCodes('&', - configuration.TEMPLATES_MINECRAFT_CHAT_MESSAGE - .replace("%u", username) - .replace("%m", message)) + val formattedMessage = Util.formatMessage( + configuration.TEMPLATES_MINECRAFT_CHAT_MESSAGE, + mapOf( + "%u" to username, + "%m" to message + ), + colors = true + ) + server.broadcastMessage(formattedMessage) } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/Util.kt b/src/main/kotlin/gg/obsidian/discordbridge/Util.kt new file mode 100644 index 0000000..62871c7 --- /dev/null +++ b/src/main/kotlin/gg/obsidian/discordbridge/Util.kt @@ -0,0 +1,16 @@ +package gg.obsidian.discordbridge + +import org.bukkit.ChatColor + +object Util { + fun formatMessage(message: String, replacements: Map, colors: Boolean = false): String { + var formattedString = message + for ((token, replacement) in replacements) { + formattedString = formattedString.replace(token, replacement) + } + + if (colors) formattedString = ChatColor.translateAlternateColorCodes('&', formattedString) + + return formattedString + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e6e5cfb..851d4aa 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -6,10 +6,15 @@ settings: password: 'password' debug: false relay_cancelled_messages: true + messages: + join: true + leave: true + death: false templates: discord: chat_message: '<%u> %m' player_join: '%u joined the server' player_leave: '%u left the server' + player_death: '%r' minecraft: chat_message: '<%u&b(discord)&r> %m' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 26ae9b8..6735681 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,3 +7,12 @@ website: ${project.url} main: gg.obsidian.discordbridge.Plugin +permissions: + discordbridge.reload: + description: Reload the Discord Bridge + default: op + +commands: + discord: + description: Reload the Discord Bridge + usage: /discord reload