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

View file

@ -2,7 +2,6 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils
import lombok.*
import org.bukkit.Bukkit
import org.bukkit.configuration.Configuration
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.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode
import lombok.RequiredArgsConstructor
import org.bukkit.Bukkit
import java.lang.reflect.Method
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 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
*/
@ -44,16 +52,10 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
*/
val helpText: Array<String> = [],
/**
* 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.
* Aliases for the subcommand that can be used to invoke it in addition to the method name.
*/
val permGroup: String = "", val aliases: Array<String> = []) {
companion object {
/**
* Allowed for OPs only by default
*/
const val MOD_GROUP = "mod"
}
val aliases: Array<String> = [] // TODO
) {
}
@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 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.
* The converter may return null.
@ -96,13 +88,17 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
return false // Unknown command
}
//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 {
dispatcher.execute(results)
} catch (e: CommandSyntaxException) {
sender.sendMessage(e.message)
} 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
@ -342,7 +338,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
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()
/**
@ -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 }
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.Component
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.player.ChromaGamerBase
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
Bukkit.getPluginManager().addPermission(Permission(perm,
PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only
for (method in command.javaClass.methods) {
if (!method.isAnnotationPresent(Subcommand::class.java)) continue
val path = CommandUtils.getCommandPath(method.name, '.')
for (node in getSubcommands(commandNode)) {
if (path.length > 0) {
val subperm = perm + path
if (Bukkit.getPluginManager().getPermission(subperm) == null) //Check needed for plugin reset
Bukkit.getPluginManager().addPermission(Permission(subperm,
PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only
Bukkit.getPluginManager().addPermission(
Permission(
subperm,
PermissionDefault.TRUE
)
) //Allow commands by default, it will check mod-only
}
val pg = permGroup(command, method)
if (pg.length == 0) continue
val pg = permGroup(node.data)
if (pg.isEmpty()) continue
val permGroup = "chroma.$pg"
if (Bukkit.getPluginManager().getPermission(permGroup) == null) //It may occur multiple times
Bukkit.getPluginManager().addPermission(Permission(permGroup,
PermissionDefault.OP)) //Do not allow any commands that belong to a group
Bukkit.getPluginManager().addPermission(
Permission(
permGroup,
PermissionDefault.OP
)
) //Do not allow any commands that belong to a group
}
}
override fun hasPermission(sender: Command2MCSender, command: ICommand2MC, method: Method): Boolean {
return hasPermission(sender.sender, command, method)
override fun hasPermission(context: CommandContext<Command2MCSender>): Boolean {
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 (command == null) return true //Allow viewing the command - it doesn't do anything anyway
var pg: String
var p = true
val cmdperm = "chroma.command." + command.commandPath.replace(' ', '.')
val path = CommandUtils.getCommandPath(method.name, '.')
val cmdperm = "chroma.command.$path"
// TODO: Register a permission for the main command as well - the previous implementation relied on the way the commands were defined
val perms = arrayOf(
if (path.length > 0) cmdperm + path else null,
cmdperm,
cmdperm + path,
if (permGroup(command, method).also { pg = it }.length > 0) "chroma.$pg" else null
)
for (perm in perms) {
if (perm != null) {
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)
} 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
* @return The permission group for the subcommand or empty string
*/
private fun permGroup(command: ICommand2MC, method: Method?): String {
if (method != null) {
val sc = method.getAnnotation(Subcommand::class.java)
if (sc != null && sc.permGroup().length > 0) {
return sc.permGroup()
}
}
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() }, "")
private fun permGroup(data: SubcommandData<ICommand2MC, Command2MCSender>): String {
val group = data.annotations.filterIsInstance<MCCommandSettings>().map {
if (it.permGroup.isEmpty() && it.modOnly) MCCommandSettings.MOD_GROUP else ""
}.firstOrNull()
return group ?: ""
}
/**

View file

@ -3,6 +3,7 @@ package buttondevteam.lib.chat.commands
import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.CoreCommandNode
import buttondevteam.lib.chat.ICommand2
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.tree.CommandNode
import java.util.*
@ -34,4 +35,9 @@ object CommandUtils {
val ret = core<TP, TC, NoOpSubcommandData>()
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.
*/
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) {
/**