Separated MC-specific command metadata

Implementing permission handling for MC
This commit is contained in:
Norbi Peti 2023-03-07 00:21:50 +01:00
parent be5f9ded60
commit af7d097f9b
7 changed files with 97 additions and 55 deletions

View file

@ -5,7 +5,6 @@ import buttondevteam.core.ComponentManager
import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.exceptions.UnregisteredComponentException import buttondevteam.lib.architecture.exceptions.UnregisteredComponentException
import buttondevteam.lib.chat.ICommand2MC import buttondevteam.lib.chat.ICommand2MC
import lombok.Getter
import org.bukkit.configuration.ConfigurationSection import org.bukkit.configuration.ConfigurationSection
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -24,7 +23,6 @@ abstract class Component<TP : JavaPlugin?> {
val config = IHaveConfig(null) val config = IHaveConfig(null)
@Getter
private val data //TODO private val data //TODO
: IHaveConfig? = null : IHaveConfig? = null

View file

@ -2,7 +2,6 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils import buttondevteam.lib.ChromaUtils
import lombok.*
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.configuration.Configuration import org.bukkit.configuration.Configuration
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask

View file

@ -12,7 +12,6 @@ import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.exceptions.CommandSyntaxException import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode import com.mojang.brigadier.tree.LiteralCommandNode
import lombok.RequiredArgsConstructor
import org.bukkit.Bukkit import org.bukkit.Bukkit
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.function.Function import java.util.function.Function
@ -24,8 +23,17 @@ import java.util.stream.Collectors
* The method name is the subcommand, use underlines (_) to add further subcommands. * The method name is the subcommand, use underlines (_) to add further subcommands.
* The args may be null if the conversion failed and it's optional. * The args may be null if the conversion failed and it's optional.
*/ */
@RequiredArgsConstructor abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> { /**
* The first character in the command line that shows that it's a command.
*/
private val commandChar: Char,
/**
* Whether the command's actual code has to be run on the primary thread.
*/
private val runOnPrimaryThread: Boolean
) {
/** /**
* Parameters annotated with this receive all the remaining arguments * Parameters annotated with this receive all the remaining arguments
*/ */
@ -44,16 +52,10 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
*/ */
val helpText: Array<String> = [], val helpText: Array<String> = [],
/** /**
* The main permission which allows using this command (individual access can be still revoked with "chroma.command.X"). * Aliases for the subcommand that can be used to invoke it in addition to the method name.
* Used to be "tbmc.admin". The [.MOD_GROUP] is provided to use with this.
*/ */
val permGroup: String = "", val aliases: Array<String> = []) { val aliases: Array<String> = [] // TODO
companion object { ) {
/**
* Allowed for OPs only by default
*/
const val MOD_GROUP = "mod"
}
} }
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
@ -66,16 +68,6 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
private val commandHelp = ArrayList<String>() //Mainly needed by Discord private val commandHelp = ArrayList<String>() //Mainly needed by Discord
private val dispatcher = CommandDispatcher<TP>() private val dispatcher = CommandDispatcher<TP>()
/**
* The first character in the command line that shows that it's a command.
*/
private val commandChar = 0.toChar()
/**
* Whether the command's actual code has to be run on the primary thread.
*/
private val runOnPrimaryThread = false
/** /**
* Adds a param converter that obtains a specific object from a string parameter. * Adds a param converter that obtains a specific object from a string parameter.
* The converter may return null. * The converter may return null.
@ -96,13 +88,17 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
return false // Unknown command return false // Unknown command
} }
//Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance) { Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance) { _ ->
try { try {
dispatcher.execute(results) dispatcher.execute(results)
} catch (e: CommandSyntaxException) { } catch (e: CommandSyntaxException) {
sender.sendMessage(e.message) sender.sendMessage(e.message)
} catch (e: Exception) { } catch (e: Exception) {
TBMCCoreAPI.SendException("Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline, e, MainPlugin.Instance) TBMCCoreAPI.SendException(
"Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline,
e,
MainPlugin.Instance
)
} }
} }
return true //We found a method return true //We found a method
@ -342,7 +338,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
invokeCommand.run();*/return 0 invokeCommand.run();*/return 0
} }
abstract fun hasPermission(sender: TP, command: TC, subcommand: Method?): Boolean abstract fun hasPermission(context: CommandContext<TP>): Boolean
val commandsText: Array<String> get() = commandHelp.toTypedArray() val commandsText: Array<String> get() = commandHelp.toTypedArray()
/** /**
@ -393,4 +389,11 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
root.children.removeIf { node -> node.coreExecutable<TP, TC>()?.let { condition.test(it) } ?: false } root.children.removeIf { node -> node.coreExecutable<TP, TC>()?.let { condition.test(it) } ?: false }
for (child in root.children) unregisterCommandIf(condition, child.core()) for (child in root.children) unregisterCommandIf(condition, child.core())
} }
/**
* Get all subcommands of the specified command. Only returns executable nodes.
*/
fun getSubcommands(mainCommand: LiteralCommandNode<TP>): List<CoreCommandNode<TP, TC, SubcommandData<TC, TP>>> {
return dispatcher.root.children.mapNotNull { it.coreExecutable<TP, TC>() } // TODO: Needs more depth
}
} }

View file

@ -5,6 +5,8 @@ import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ButtonPlugin import buttondevteam.lib.architecture.ButtonPlugin
import buttondevteam.lib.architecture.Component import buttondevteam.lib.architecture.Component
import buttondevteam.lib.chat.commands.CommandUtils import buttondevteam.lib.chat.commands.CommandUtils
import buttondevteam.lib.chat.commands.CommandUtils.subcommandPath
import buttondevteam.lib.chat.commands.MCCommandSettings
import buttondevteam.lib.chat.commands.SubcommandData import buttondevteam.lib.chat.commands.SubcommandData
import buttondevteam.lib.player.ChromaGamerBase import buttondevteam.lib.player.ChromaGamerBase
import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.arguments.StringArgumentType
@ -57,44 +59,52 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset
Bukkit.getPluginManager().addPermission(Permission(perm, Bukkit.getPluginManager().addPermission(Permission(perm,
PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only
for (method in command.javaClass.methods) { for (node in getSubcommands(commandNode)) {
if (!method.isAnnotationPresent(Subcommand::class.java)) continue
val path = CommandUtils.getCommandPath(method.name, '.')
if (path.length > 0) { if (path.length > 0) {
val subperm = perm + path val subperm = perm + path
if (Bukkit.getPluginManager().getPermission(subperm) == null) //Check needed for plugin reset if (Bukkit.getPluginManager().getPermission(subperm) == null) //Check needed for plugin reset
Bukkit.getPluginManager().addPermission(Permission(subperm, Bukkit.getPluginManager().addPermission(
PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only Permission(
subperm,
PermissionDefault.TRUE
)
) //Allow commands by default, it will check mod-only
} }
val pg = permGroup(command, method) val pg = permGroup(node.data)
if (pg.length == 0) continue if (pg.isEmpty()) continue
val permGroup = "chroma.$pg" val permGroup = "chroma.$pg"
if (Bukkit.getPluginManager().getPermission(permGroup) == null) //It may occur multiple times if (Bukkit.getPluginManager().getPermission(permGroup) == null) //It may occur multiple times
Bukkit.getPluginManager().addPermission(Permission(permGroup, Bukkit.getPluginManager().addPermission(
PermissionDefault.OP)) //Do not allow any commands that belong to a group Permission(
permGroup,
PermissionDefault.OP
)
) //Do not allow any commands that belong to a group
} }
} }
override fun hasPermission(sender: Command2MCSender, command: ICommand2MC, method: Method): Boolean { override fun hasPermission(context: CommandContext<Command2MCSender>): Boolean {
return hasPermission(sender.sender, command, method) return hasPermission(context.source.sender, context.subcommandPath)
} }
fun hasPermission(sender: CommandSender, command: ICommand2MC?, method: Method): Boolean { fun hasPermission(sender: CommandSender, path: String): Boolean {
if (sender is ConsoleCommandSender) return true //Always allow the console if (sender is ConsoleCommandSender) return true //Always allow the console
if (command == null) return true //Allow viewing the command - it doesn't do anything anyway
var pg: String var pg: String
var p = true var p = true
val cmdperm = "chroma.command." + command.commandPath.replace(' ', '.') val cmdperm = "chroma.command.$path"
val path = CommandUtils.getCommandPath(method.name, '.') // TODO: Register a permission for the main command as well - the previous implementation relied on the way the commands were defined
val perms = arrayOf( val perms = arrayOf(
if (path.length > 0) cmdperm + path else null, cmdperm + path,
cmdperm,
if (permGroup(command, method).also { pg = it }.length > 0) "chroma.$pg" else null if (permGroup(command, method).also { pg = it }.length > 0) "chroma.$pg" else null
) )
for (perm in perms) { for (perm in perms) {
if (perm != null) { if (perm != null) {
if (p) { //Use OfflinePlayer to avoid fetching player data if (p) { //Use OfflinePlayer to avoid fetching player data
p = if (sender is OfflinePlayer) MainPlugin.permission.playerHas(if (sender is Player) sender.location.world.name else null, sender as OfflinePlayer, perm) else false //Use sender's method p = if (sender is OfflinePlayer) MainPlugin.permission.playerHas(
if (sender is Player) sender.location.world.name else null,
sender as OfflinePlayer,
perm
) else false //Use sender's method
if (!p) p = sender.hasPermission(perm) if (!p) p = sender.hasPermission(perm)
} else break //If any of the permissions aren't granted then don't allow } else break //If any of the permissions aren't granted then don't allow
} }
@ -108,14 +118,11 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
* @param method The subcommand to check * @param method The subcommand to check
* @return The permission group for the subcommand or empty string * @return The permission group for the subcommand or empty string
*/ */
private fun permGroup(command: ICommand2MC, method: Method?): String { private fun permGroup(data: SubcommandData<ICommand2MC, Command2MCSender>): String {
if (method != null) { val group = data.annotations.filterIsInstance<MCCommandSettings>().map {
val sc = method.getAnnotation(Subcommand::class.java) if (it.permGroup.isEmpty() && it.modOnly) MCCommandSettings.MOD_GROUP else ""
if (sc != null && sc.permGroup().length > 0) { }.firstOrNull()
return sc.permGroup() return group ?: ""
}
}
return if (getAnnForValue(command.javaClass, CommandClass::class.java, Function { obj: CommandClass -> obj.modOnly() }, false)) Subcommand.MOD_GROUP else getAnnForValue(command.javaClass, CommandClass::class.java, Function<CommandClass, String> { obj: CommandClass -> obj.permGroup() }, "")
} }
/** /**

View file

@ -3,6 +3,7 @@ package buttondevteam.lib.chat.commands
import buttondevteam.lib.chat.Command2Sender import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.CoreCommandNode import buttondevteam.lib.chat.CoreCommandNode
import buttondevteam.lib.chat.ICommand2 import buttondevteam.lib.chat.ICommand2
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.CommandNode
import java.util.* import java.util.*
@ -34,4 +35,9 @@ object CommandUtils {
val ret = core<TP, TC, NoOpSubcommandData>() val ret = core<TP, TC, NoOpSubcommandData>()
return if (ret.data is SubcommandData<*, *>) ret.core() else null return if (ret.data is SubcommandData<*, *>) ret.core() else null
} }
val <TP : Command2Sender> CommandContext<TP>.subcommandPath
get(): String {
TODO("Return command path")
}
} }

View file

@ -0,0 +1,25 @@
package buttondevteam.lib.chat.commands
import java.lang.annotation.Inherited
@Inherited
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MCCommandSettings(
/**
* The main permission which allows using this command (individual access can be still revoked with "chroma.command.X").
* Used to be "tbmc.admin". The [.MOD_GROUP] is provided to use with this.
*/
val permGroup: String = "",
/**
* Whether the (sub)command is mod only. This means it requires the chroma.mod permission.
* This is just a shorthand for providing MOD_GROUP for permGroup.
*/
val modOnly: Boolean = false
) {
companion object {
/**
* Allowed for OPs only by default
*/
const val MOD_GROUP = "mod"
}
}

View file

@ -41,7 +41,11 @@ class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
/** /**
* A function that determines whether the user has permission to run this subcommand. * A function that determines whether the user has permission to run this subcommand.
*/ */
private val permissionCheck: (TP) -> Boolean private val permissionCheck: (TP) -> Boolean,
/**
* All annotations implemented by the method that executes the command. Can be used to add custom metadata when implementing a platform.
*/
val annotations: Array<Annotation>
) : NoOpSubcommandData(helpTextGetter) { ) : NoOpSubcommandData(helpTextGetter) {
/** /**