From 935daedab5e6cad4322823392897cef7412c9e09 Mon Sep 17 00:00:00 2001 From: DiamondIceNS Date: Sun, 10 Dec 2017 13:14:08 -0600 Subject: [PATCH 1/5] Update Kotlin version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7c8c60..9fb60cc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ import org.apache.tools.ant.filters.ReplaceTokens buildscript { - ext.kotlin_version = '1.1.51' + ext.kotlin_version = '1.2.0' repositories { mavenCentral() From 1ab20a097343f378f96a629e620761640a26a3f7 Mon Sep 17 00:00:00 2001 From: DiamondIceNS Date: Sun, 10 Dec 2017 14:20:37 -0600 Subject: [PATCH 2/5] Implement rudimentary command forwarding commands in Discord will now send to Minecraft and execute as expected. Works with any command added by any plugin that can be executed as server. Command forwarding is not very robust, does not give feedback, and will attempt to send a command before checking if it exists or not. Will work on fixing those in future commits. Also removed some console spam. --- build.gradle | 2 +- .../commands/controllers/BotControllerManager.kt | 8 +++++++- .../gg/obsidian/discordbridge/minecraft/EventListener.kt | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 9fb60cc..d29f7b1 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { apply plugin: 'kotlin' group = 'gg.obsidian' -version = '3.0.1' +version = '3.1.0-dev1' description = """Bridge chat between Minecraft and Discord""" ext.url = 'https://github.com/the-obsidian/DiscordBridge' diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt index 2a40453..74a8a6a 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt @@ -11,6 +11,7 @@ import gg.obsidian.discordbridge.utils.UtilFunctions.noSpace import gg.obsidian.discordbridge.utils.UtilFunctions.stripColor import gg.obsidian.discordbridge.utils.UtilFunctions.toDiscordChatMessage import gg.obsidian.discordbridge.utils.UtilFunctions.toMinecraftChatMessage +import org.bukkit.Bukkit import java.lang.reflect.Method import java.util.* import java.util.logging.Level @@ -110,6 +111,12 @@ class BotControllerManager(val plugin: Plugin) { val command = commands[commandName] if (command == null) { + // Attempt to run as a server command + val serverCommandSuccess = Bukkit.getServer().dispatchCommand(Bukkit.getServer().consoleSender, args.joinToString(" ").substring(Config.COMMAND_PREFIX.length)) + if (serverCommandSuccess) + return true + + commandNotFound(event, commandName) return false } @@ -361,7 +368,6 @@ class BotControllerManager(val plugin: Plugin) { * message is not relayed to Minecraft */ private fun relay(event: IEventWrapper, logIgnore: Boolean) { - plugin.logger.info("Entered relay. event: ${event.message}") when (event) { is AsyncPlayerChatEventWrapper -> { var worldname = event.event.player.world.name diff --git a/src/main/kotlin/gg/obsidian/discordbridge/minecraft/EventListener.kt b/src/main/kotlin/gg/obsidian/discordbridge/minecraft/EventListener.kt index 72c1c13..04495ac 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/minecraft/EventListener.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/minecraft/EventListener.kt @@ -73,7 +73,6 @@ class EventListener(val plugin: Plugin): Listener { plugin.server.scheduler.runTaskAsynchronously(plugin, { controllerManager.dispatchMessage(wrapper) }) event.message = MarkdownToMinecraftSeralizer().toMinecraft(plugin.pegDownProc.parseMarkdown(event.message.toCharArray())) - plugin.logger.info("Seralized! (synchronous)") } /** From 68258c2b011af37980476b24811affc6e450363c Mon Sep 17 00:00:00 2001 From: DiamondIceNS Date: Wed, 13 Dec 2017 14:47:05 -0600 Subject: [PATCH 3/5] Fully implement command forwarding Added: + Command forwarding is now possible using either prefix or @tag syntax + Command forwarding works with any command from any plugin that can be run from server console + Command output is sent back to Discord Modified: * Refactored some of the icky duplicate code in dispatchMessage into a standalone function --- .../commands/DiscordCommandSender.kt | 96 +++++++ .../commands/annotations/BotCommand.kt | 4 +- .../controllers/BotControllerManager.kt | 249 +++++++++++++----- .../controllers/FunCommandsController.kt | 10 +- 4 files changed, 292 insertions(+), 67 deletions(-) create mode 100644 src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt b/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt new file mode 100644 index 0000000..8658bcf --- /dev/null +++ b/src/main/kotlin/gg/obsidian/discordbridge/commands/DiscordCommandSender.kt @@ -0,0 +1,96 @@ +package gg.obsidian.discordbridge.commands + +import net.dv8tion.jda.core.entities.MessageChannel +import org.bukkit.Bukkit +import org.bukkit.Server +import org.bukkit.command.CommandSender +import org.bukkit.command.ConsoleCommandSender +import org.bukkit.command.RemoteConsoleCommandSender +import org.bukkit.permissions.Permission +import org.bukkit.permissions.PermissionAttachment +import org.bukkit.permissions.PermissionAttachmentInfo +import org.bukkit.plugin.Plugin + +class DiscordCommandSender(val channel: MessageChannel) : RemoteConsoleCommandSender { + + private val sender:ConsoleCommandSender = Bukkit.getServer().consoleSender + + init { + + } + + override fun sendMessage(message: String?) { + channel.sendMessage(message).queue() + } + + override fun sendMessage(messages: Array?) { + if (messages != null) + for (m in messages) channel.sendMessage(m) + } + + override fun spigot(): CommandSender.Spigot { + return sender.spigot() + } + + override fun addAttachment(plugin: Plugin?): PermissionAttachment { + return sender.addAttachment(plugin) + } + + override fun addAttachment(plugin: Plugin?, ticks: Int): PermissionAttachment { + return sender.addAttachment(plugin, ticks) + } + + override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean): PermissionAttachment { + return sender.addAttachment(plugin, name, value) + } + + override fun addAttachment(plugin: Plugin?, name: String?, value: Boolean, ticks: Int): PermissionAttachment { + return sender.addAttachment(plugin, name, value, ticks) + } + + override fun getEffectivePermissions(): MutableSet { + return sender.effectivePermissions + } + + override fun getName(): String { + return sender.name + } + + override fun getServer(): Server { + return sender.server + } + + override fun hasPermission(name: String?): Boolean { + return sender.hasPermission(name) + } + + override fun hasPermission(perm: Permission?): Boolean { + return sender.hasPermission(perm) + } + + override fun isOp(): Boolean { + return sender.isOp + } + + override fun isPermissionSet(name: String?): Boolean { + return sender.isPermissionSet(name) + } + + override fun isPermissionSet(perm: Permission?): Boolean { + return sender.isPermissionSet(perm) + } + + override fun recalculatePermissions() { + return sender.recalculatePermissions() + } + + override fun removeAttachment(attachment: PermissionAttachment?) { + return sender.removeAttachment(attachment) + } + + override fun setOp(value: Boolean) { + return sender.setOp(value) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt b/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt index ed1de34..766751f 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/commands/annotations/BotCommand.kt @@ -7,9 +7,9 @@ package gg.obsidian.discordbridge.commands.annotations * @param description a short string that describes the command's function * @param name an optional field to override the command's access name if it is not the same as the method name * @param relayTriggerMessage whether the message used to trigger this command should be relayed - * @param ignoreExcessArguments if false, this command will fail if the invoker provides too many arguments + * @param squishExcessArgs if true, this command will put all extra args passed to it into a single string */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class BotCommand(val usage: String, val description: String, val name: String = "", - val relayTriggerMessage: Boolean = true, val ignoreExcessArguments: Boolean = true) + val relayTriggerMessage: Boolean = true, val squishExcessArgs: Boolean = false) diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt index 74a8a6a..7b00d05 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/BotControllerManager.kt @@ -11,6 +11,7 @@ import gg.obsidian.discordbridge.utils.UtilFunctions.noSpace import gg.obsidian.discordbridge.utils.UtilFunctions.stripColor import gg.obsidian.discordbridge.utils.UtilFunctions.toDiscordChatMessage import gg.obsidian.discordbridge.utils.UtilFunctions.toMinecraftChatMessage +import net.dv8tion.jda.core.Permission import org.bukkit.Bukkit import java.lang.reflect.Method import java.util.* @@ -76,7 +77,7 @@ class BotControllerManager(val plugin: Plugin) { val isTagged: Boolean = method.getAnnotation(TaggedResponse::class.java) != null val isPrivate: Boolean = method.getAnnotation(PrivateResponse::class.java) != null val command = Command(commandName, usage, annotation.description, parameters, annotation.relayTriggerMessage, - annotation.ignoreExcessArguments, isTagged, isPrivate, controllerClass, method) + annotation.squishExcessArgs, isTagged, isPrivate, controllerClass, method) commands.put(command.name, command) } @@ -95,66 +96,62 @@ class BotControllerManager(val plugin: Plugin) { commandNotFound(event, event.command.name) return true } - val inputArguments = event.rawMessage.split("\\s+".toRegex(), command.parameters.size).toTypedArray() - return invokeCommand(command, controllers, event, inputArguments) + return invokeCommand(command, controllers, event, event.args.asList().toTypedArray()) } // Short circuit scripted responses if (scriptedResponse(event)) return true - val args = event.rawMessage.trim().split("\\s+".toRegex(), 2).toTypedArray() - // command - if (Config.COMMAND_PREFIX.isNotBlank() && args[0].startsWith(Config.COMMAND_PREFIX)) { - val commandName = args[0].substring(Config.COMMAND_PREFIX.length).toLowerCase() - if (commandName == "") return true - val command = commands[commandName] - - if (command == null) { - // Attempt to run as a server command - val serverCommandSuccess = Bukkit.getServer().dispatchCommand(Bukkit.getServer().consoleSender, args.joinToString(" ").substring(Config.COMMAND_PREFIX.length)) - if (serverCommandSuccess) - return true + if (Config.COMMAND_PREFIX.isNotBlank() && event.rawMessage.startsWith(Config.COMMAND_PREFIX)) { + val split = event.rawMessage.replaceFirst(Config.COMMAND_PREFIX, "").trim().split("\\s+".toRegex()).toTypedArray() + return parseCommand(event, split, false) + } + // @ command from Minecraft + if (event is AsyncPlayerChatEventWrapper && event.rawMessage.startsWith("@${Config.USERNAME.noSpace()} ")) { + val split = event.rawMessage.replaceFirst("@${Config.USERNAME.noSpace()} ", "").trim().split("\\s+".toRegex()).toTypedArray() + return parseCommand(event, split, true) + } - commandNotFound(event, commandName) - return false - } + // @ command from Discord + if (event is MessageWrapper && event.rawMessage.startsWith(Connection.JDA.selfUser.asMention + " ")) { + val split = event.rawMessage.replaceFirst(Connection.JDA.selfUser.asMention + " ", "").trim().split("\\s+".toRegex()).toTypedArray() + return parseCommand(event, split, true) + } - val inputArguments = if (args.size == 1) arrayOf() - else args[1].split("\\s+".toRegex(), command.parameters.size).toTypedArray() + // Just relay the message if it is neither + relay(event, true) + return true + } - return invokeCommand(command, controllers, event, inputArguments) - } + /** + * Attempt to parse and execute a command from an input string + * + * @param event the original event object + * @param args the input string broken up into an array of words, with the command as the first element + * @param defaultToTalk if true, failure to find a command to execute will execute Talk with Cleverbot using the + * full string as an argument. If false, failure will do nothing and the method call will return false. + */ + private fun parseCommand(event: IEventWrapper, args: Array, defaultToTalk: Boolean): Boolean { + val commandName = args[0].toLowerCase() + if (commandName == "") return true + var command = commands[commandName] - // @ command - if ((event is AsyncPlayerChatEventWrapper && args[0] == "@"+Config.USERNAME.noSpace() || - args[0] == Connection.JDA.selfUser.asMention) && args.count() == 2) { - val args2 = args[1].split("\\s+".toRegex(), 2).toTypedArray() - val commandName = args2[0].toLowerCase() - if (commandName == "") return true - var params = if (args2.size > 1) args2[1] else "" - var command = commands[commandName] + if (command == null) { + // Attempt to run as a server command if sent from Discord + if (event is MessageWrapper && event.originalMessage.member.hasPermission(Permission.ADMINISTRATOR)) + if (serverCommand(event, args)) return true + if (defaultToTalk) command = commands["talk"] if (command == null) { - // Assume user wants to talk to Cleverbot - command = commands["talk"] - if (command == null) { - commandNotFound(event, commandName) - return false - } - params = args[1] + commandNotFound(event, commandName) + return false } - - val inputArguments = if (params == "") arrayOf() - else params.split("\\s+".toRegex(), command.parameters.size).toTypedArray() - - return invokeCommand(command, controllers, event, inputArguments) } - // Just relay the message if it is neither - relay(event, true) - return true + val slicedArgs = if (args.size > 1) args.slice(1 until args.size).toTypedArray() else arrayOf() + return invokeCommand(command, controllers, event, slicedArgs) } /** @@ -221,14 +218,16 @@ class BotControllerManager(val plugin: Plugin) { * @param command the command to invoke * @param instances a map of IBotController instances accessed by their Java classes * @param event the incoming event object - * @param inputArguments an array of String arguments to pass to the command method + * @param args an array of String arguments to pass to the command method * @return false if the command invocation has invalid arguments, true otherwise */ private fun invokeCommand(command: Command, instances: Map, IBotController>, - event: IEventWrapper, inputArguments: Array): Boolean { + event: IEventWrapper, args: Array): Boolean { // Relay the trigger if applicable if (command.relayTriggerMessage) relay(event, false) + var squishedArgs = args + when { // Check for permission event is AsyncPlayerChatEventWrapper && !event.event.player.hasPermission("discordbridge.${command.name}") -> { @@ -255,15 +254,19 @@ class BotControllerManager(val plugin: Plugin) { } } - // Fail if not enough arguments - inputArguments.size < command.parameters.size -> { - commandWrongParameterCount(event, command.name, command.usage, inputArguments.size, command.parameters.size) - return false + // If command is squishy, pack excess args into final string + command.squishExcessArgs -> { + if (args.size == 1) + squishedArgs = arrayOf(args.joinToString(" ")) + else { + squishedArgs = args.sliceArray(0 until command.parameters.size) + squishedArgs[command.parameters.size-1] = args.sliceArray(command.parameters.size until args.size).joinToString(" ") + } } - // Fail if command forces exact argument count AND the supplied argument count does not match expected - inputArguments.size != command.parameters.size && !command.ignoreExcessArguments -> { - commandWrongParameterCount(event, command.name, command.usage, inputArguments.size, command.parameters.size) + // Fail if wrong number of arguments + squishedArgs.size != command.parameters.size -> { + commandWrongParameterCount(event, command.name, command.usage, squishedArgs.size, command.parameters.size) return false } } @@ -271,15 +274,15 @@ class BotControllerManager(val plugin: Plugin) { // Package arguments to send to method val arguments = arrayOfNulls(command.parameters.size + 1) arguments[0] = event - val paramRange = if (command.ignoreExcessArguments && inputArguments.size > command.parameters.size) + val paramRange = if (command.squishExcessArgs && squishedArgs.size > command.parameters.size) IntRange(0, command.parameters.size - 1) else command.parameters.indices for (i in paramRange) { val parameterClass = command.parameters[i] try { - arguments[i + 1] = parseArgument(parameterClass, inputArguments[i]) + arguments[i + 1] = parseArgument(parameterClass, squishedArgs[i]) } catch (ignored: IllegalArgumentException) { - commandWrongParameterType(event, command.name, command.usage, i, inputArguments[i].javaClass, parameterClass) + commandWrongParameterType(event, command.name, command.usage, i, squishedArgs[i].javaClass, parameterClass) return false } } @@ -290,7 +293,7 @@ class BotControllerManager(val plugin: Plugin) { respond(event, command, response) return true } catch (e: IllegalArgumentException) { - commandWrongParameterCount(event, command.name, command.usage, inputArguments.size, command.parameters.size) + commandWrongParameterCount(event, command.name, command.usage, squishedArgs.size, command.parameters.size) return false } catch (e: Exception) { commandException(event, e) @@ -299,6 +302,46 @@ class BotControllerManager(val plugin: Plugin) { } + /** + * Attempts to run a default or installed chat command as server + * + * @param event the incoming event object + * @param args the arguments associated with the command, space-delimited + */ + private fun serverCommand(event: IEventWrapper, args: Array): Boolean { + val sender = DiscordCommandSender(event.channel) + val commandName = args[0].toLowerCase() + when { + DefaultCommands.minecraft.contains(commandName) -> { + plugin.logDebug("Discord user ${event.senderName} invoked Minecraft command '${args.joinToString(" ")}'") + Bukkit.getServer().dispatchCommand(sender, args.joinToString(" ")) + return true + } + + DefaultCommands.bukkit.contains(commandName) -> { + plugin.logDebug("Discord user ${event.senderName} invoked Bukkit command '${args.joinToString(" ")}'") + Bukkit.getServer().dispatchCommand(sender, args.joinToString(" ")) + return true + } + + DefaultCommands.spigot.contains(commandName) -> { + plugin.logDebug("Discord user ${event.senderName} invoked Spigot command '${args.joinToString(" ")}'") + Bukkit.getServer().dispatchCommand(sender, args.joinToString(" ")) + return true + } + + else -> { + val pluginCommand = Bukkit.getServer().getPluginCommand(commandName) + if (pluginCommand != null) { + plugin.logDebug("Discord user ${event.senderName} invoked ${pluginCommand.plugin.name} command '${args.joinToString(" ")}'") + pluginCommand.execute(sender, commandName, args.sliceArray(1 until args.size)) + return true + } + } + } + return false + } + /** * Sends the response of a successful command invocation to its respective medium * @@ -587,7 +630,7 @@ class BotControllerManager(val plugin: Plugin) { * @param description a description of the command's function * @param parameters a List of the command arguments' expected Java class types * @param relayTriggerMessage whether the message that triggered this command should be relayed - * @param ignoreExcessArguments whether this command should ignore excess arguments + * @param squishExcessArgs whether this command should combine excess arguments into one * @param isTagged if the output of this command should be prepended with "@invokerName | " * @param isPrivate if the output of this command should be sent via DM/PM to the invoker * @param controllerClass the Java class type of the IBotController that defines this command @@ -599,10 +642,96 @@ class BotControllerManager(val plugin: Plugin) { val description: String, val parameters: List>, val relayTriggerMessage: Boolean, - val ignoreExcessArguments: Boolean, + val squishExcessArgs: Boolean, val isTagged: Boolean, val isPrivate: Boolean, val controllerClass: Class<*>, val commandMethod: Method ) + + private object DefaultCommands { + val minecraft : List = listOf( + "advancement", + "ban", + "blockdata", + "clear", + "clone", + //"data", + //"datapack", + "debug", + "defaultgamemode", + "deop", + "difficulty", + "effect", + "enchant", + "entitydata", + //"experience", + "execute", + "fill", + "function", + "gamemode", + "gamerule", + "give", + "help", + "kick", + "kill", + "list", + "locate", + "me", + "op", + "pardon", + "particle", + "playsound", + "publish", + "recipe", + "reload", + "replaceitem", + "save", + "say", + "scoreboard", + "seed", + "setblock", + "setidletimeout", + "setmaxplayers", + "setworldspawn", + "spawnpoint", + "spreadplayers", + "stats", + "stop", + "stopsound", + "summon", + "teleport", + "tell", + //"tag", + //"team", + "tellraw", + "testfor", + "testforblock", + "testforblocks", + "tickingarea", + "time", + "title", + "toggledownfall", + "tp", + "transferserver", + "trigger", + "weather", + "whitelist", + "worldborder", + "wsserver" + ) + + val bukkit : List = listOf( + "version", + "plugins", + "help", + "reload", + "timings" + ) + + val spigot:List = listOf( + "restart", + "tps" + ) + } } diff --git a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/FunCommandsController.kt b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/FunCommandsController.kt index 1214aa5..836d9ce 100644 --- a/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/FunCommandsController.kt +++ b/src/main/kotlin/gg/obsidian/discordbridge/commands/controllers/FunCommandsController.kt @@ -29,7 +29,7 @@ class FunCommandsController(val plugin: Plugin) : IBotController { * @param event the incoming event object * @return the response string */ - @BotCommand(name = "8ball", usage = "", description = "Consult the Magic 8 Ball") + @BotCommand(name = "8ball", usage = "", description = "Consult the Magic 8 Ball", squishExcessArgs = true) @TaggedResponse private fun eightBall(event: IEventWrapper): String { plugin.logDebug("user ${event.senderName} consults the Magic 8-Ball") @@ -46,7 +46,7 @@ class FunCommandsController(val plugin: Plugin) : IBotController { * @return the response string */ @BotCommand(name = "choose", usage = "