Implement channel handling for Chroma users

- This allows properly implementing Minecraft chat for Discord users instead of creating fake CommandSenders
- Channel access needs to be implemented on all platforms - this could allow automatically managing who has access to a Discord channel for example
- In the future custom commands could be supported without having to connect accounts
- Also any information can be retrieved about the user when they chat, not just their name
This commit is contained in:
Norbi Peti 2023-06-09 00:11:53 +02:00
parent 291fc068fd
commit cd108dc787
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
9 changed files with 80 additions and 52 deletions

View file

@ -57,6 +57,11 @@ class MainPlugin : ButtonPlugin() {
*/ */
val prioritizeCustomCommands = iConfig.getData("prioritizeCustomCommands", false) val prioritizeCustomCommands = iConfig.getData("prioritizeCustomCommands", false)
/**
* The permission group to use for players who are not in the server.
*/ // TODO: Combine the channel access test with command permissions (with a generic implementation)
val externalPlayerPermissionGroup get() = iConfig.getData("externalPlayerPermissionGroup", "default")
public override fun pluginEnable() { public override fun pluginEnable() {
instance = this instance = this
val pdf = description val pdf = description

View file

@ -67,7 +67,7 @@ class PlayerListener(val plugin: MainPlugin) : Listener {
private fun handlePreprocess(sender: CommandSender, message: String, event: Cancellable) { private fun handlePreprocess(sender: CommandSender, message: String, event: Cancellable) {
if (event.isCancelled) return if (event.isCancelled) return
val cg = ChromaGamerBase.getFromSender(sender) val cg = ChromaGamerBase.getFromSender(sender)
val ev = TBMCCommandPreprocessEvent(cg, cg.channel.get(), message, sender) val ev = TBMCCommandPreprocessEvent(cg, cg.channel.get(), message, cg)
Bukkit.getPluginManager().callEvent(ev) Bukkit.getPluginManager().callEvent(ev)
if (ev.isCancelled) event.isCancelled = true //Cancel the original event if (ev.isCancelled) event.isCancelled = true //Cancel the original event
} }
@ -76,13 +76,7 @@ class PlayerListener(val plugin: MainPlugin) : Listener {
fun onTBMCPreprocess(event: TBMCCommandPreprocessEvent) { fun onTBMCPreprocess(event: TBMCCommandPreprocessEvent) {
if (event.isCancelled) return if (event.isCancelled) return
try { try {
val mcuser = event.sender.getAs(TBMCPlayerBase::class.java) val sender = Command2MCSender(event.sender, event.channel, event.permCheck)
if (mcuser == null) { // TODO: The chat should continue to support unconnected accounts.
event.sender.sendMessage("You need to have your Minecraft account connected to send commands.")
event.isCancelled = true
return
}
val sender = Command2MCSender(mcuser, event.channel, event.permCheck)
event.isCancelled = ButtonPlugin.command2MC.handleCommand(sender, event.message) event.isCancelled = ButtonPlugin.command2MC.handleCommand(sender, event.message)
} catch (e: Exception) { } catch (e: Exception) {
TBMCCoreAPI.SendException( TBMCCoreAPI.SendException(

View file

@ -1,15 +1,12 @@
package buttondevteam.core.component.channel package buttondevteam.core.component.channel
import buttondevteam.core.ComponentManager.get import buttondevteam.core.ComponentManager.get
import buttondevteam.core.MainPlugin
import buttondevteam.lib.architecture.ConfigData import buttondevteam.lib.architecture.ConfigData
import buttondevteam.lib.architecture.IHaveConfig import buttondevteam.lib.architecture.IHaveConfig
import buttondevteam.lib.architecture.ListConfigData import buttondevteam.lib.architecture.ListConfigData
import buttondevteam.lib.chat.Color import buttondevteam.lib.chat.Color
import buttondevteam.lib.player.ChromaGamerBase import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import java.util.* import java.util.*
import java.util.function.Function import java.util.function.Function
import java.util.function.Predicate import java.util.function.Predicate
@ -45,7 +42,7 @@ open class Channel
* Only those with access can see the messages. * Only those with access can see the messages.
* If null, everyone has access. * If null, everyone has access.
*/ */
private val filterAndErrorMSG: Function<CommandSender, RecipientTestResult>? private val filterAndErrorMSG: Function<ChromaGamerBase, RecipientTestResult>?
) { ) {
private val config: IHaveConfig? = null // TODO: Use this private val config: IHaveConfig? = null // TODO: Use this
@ -188,28 +185,27 @@ open class Channel
* @param permgroup The group that can access the channel or **null** to only allow OPs. * @param permgroup The group that can access the channel or **null** to only allow OPs.
* @return If has access * @return If has access
*/ */
fun inGroupFilter(permgroup: String?): Function<CommandSender, RecipientTestResult> { @JvmStatic
// TODO: This is Minecraft specific. Change all of this so it supports other ways of checking permissions. fun inGroupFilter(permgroup: String?): Function<ChromaGamerBase, RecipientTestResult> {
// TODO: The commands have to be Minecraft specific, but the channels should be generic. return Function { it.checkChannelInGroup(permgroup) }
// TODO: Implement a way to check permissions for other platforms. Maybe specific strings, like "admin" or "mod"?
return noScoreResult(
{ s ->
s.isOp || s is Player && permgroup?.let { pg ->
MainPlugin.permission.playerInGroup(s, pg)
} ?: false
},
"You need to be a(n) " + (permgroup ?: "OP") + " to use this channel."
)
} }
@JvmStatic
fun noScoreResult( fun noScoreResult(
filter: Predicate<CommandSender>, filter: Predicate<ChromaGamerBase>,
errormsg: String? errormsg: String?
): Function<CommandSender, RecipientTestResult> { ): Function<ChromaGamerBase, RecipientTestResult> {
return Function { s -> return Function { noScoreResult(filter, errormsg, it) }
if (filter.test(s)) RecipientTestResult(SCORE_SEND_OK, GROUP_EVERYONE) }
else RecipientTestResult(errormsg)
} @JvmStatic
fun noScoreResult(
filter: Predicate<ChromaGamerBase>,
errormsg: String?,
user: ChromaGamerBase
): RecipientTestResult {
return if (filter.test(user)) RecipientTestResult(SCORE_SEND_OK, GROUP_EVERYONE)
else RecipientTestResult(errormsg)
} }
lateinit var globalChat: Channel lateinit var globalChat: Channel

View file

@ -2,7 +2,6 @@ package buttondevteam.lib
import buttondevteam.core.component.channel.Channel import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.player.ChromaGamerBase import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.command.CommandSender
import org.bukkit.event.Cancellable import org.bukkit.event.Cancellable
import org.bukkit.event.Event import org.bukkit.event.Event
import org.bukkit.event.HandlerList import org.bukkit.event.HandlerList
@ -17,7 +16,7 @@ class TBMCCommandPreprocessEvent(
val sender: ChromaGamerBase, val sender: ChromaGamerBase,
val channel: Channel, val channel: Channel,
val message: String, val message: String,
val permCheck: CommandSender val permCheck: ChromaGamerBase
) : Event(), Cancellable { ) : Event(), Cancellable {
private var cancelled = false private var cancelled = false
override fun getHandlers(): HandlerList { override fun getHandlers(): HandlerList {

View file

@ -59,7 +59,22 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
} }
override fun hasPermission(sender: Command2MCSender, data: SubcommandData<ICommand2MC, Command2MCSender>): Boolean { override fun hasPermission(sender: Command2MCSender, data: SubcommandData<ICommand2MC, Command2MCSender>): Boolean {
if (sender.sender.isConsole) return true //Always allow the console val defWorld = Bukkit.getWorlds().first().name
val check = if (sender.permCheck !is TBMCPlayerBase) ({
MainPlugin.permission.groupHas(
defWorld,
MainPlugin.instance.externalPlayerPermissionGroup.get(),
it
)
})
else if (sender.permCheck.isConsole) ({ true }) //Always allow the console
else ({ perm: String ->
MainPlugin.permission.playerHas(
sender.permCheck.player?.location?.world?.name ?: defWorld,
sender.permCheck.offlinePlayer,
perm
)
})
var p = true var p = true
val cmdperm = "chroma.command.${data.fullPath.replace(' ', '.')}" val cmdperm = "chroma.command.${data.fullPath.replace(' ', '.')}"
@ -71,11 +86,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
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 = MainPlugin.permission.playerHas( p = check(perm)
sender.sender.player?.location?.world?.name,
sender.sender.offlinePlayer,
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
} }
} }
@ -113,14 +124,18 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
if (original != null) { if (original != null) {
return original return original
} }
// Check Bukkit sender type - TODO: This is no longer the Bukkit sender type val cg = sender.sender
if (senderType.isAssignableFrom(sender.sender.javaClass)) if (senderType.isAssignableFrom(cg.javaClass))
return sender.sender return cg
// Check Bukkit sender type
if (cg is TBMCPlayerBase) {
if (senderType.isAssignableFrom(cg.offlinePlayer.javaClass)) return cg.offlinePlayer
if (cg.player?.javaClass?.let { senderType.isAssignableFrom(it) } == true) return cg.player
}
//The command expects a user of our system //The command expects a user of our system
if (ChromaGamerBase::class.java.isAssignableFrom(senderType)) { if (ChromaGamerBase::class.java.isAssignableFrom(senderType)) {
val cg = sender.sender @Suppress("UNCHECKED_CAST")
if (cg.javaClass == senderType) return sender.sender.getAs(senderType as Class<out ChromaGamerBase>)
return cg
} }
return null return null
} }
@ -187,7 +202,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
val user = ChromaGamerBase.getFromSender(sender) // TODO: Senders should only be used for TBMCPlayerBase classes. val user = ChromaGamerBase.getFromSender(sender) // TODO: Senders should only be used for TBMCPlayerBase classes.
///trim(): remove space if there are no args ///trim(): remove space if there are no args
handleCommand( handleCommand(
Command2MCSender(user as TBMCPlayerBase, user.channel.get(), sender), Command2MCSender(user as TBMCPlayerBase, user.channel.get(), user),
("/${command.name} ${args.joinToString(" ")}").trim { it <= ' ' }, false ("/${command.name} ${args.joinToString(" ")}").trim { it <= ' ' }, false
) )
return true return true

View file

@ -1,10 +1,9 @@
package buttondevteam.lib.chat package buttondevteam.lib.chat
import buttondevteam.core.component.channel.Channel import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.player.TBMCPlayerBase import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.command.CommandSender
class Command2MCSender(val sender: TBMCPlayerBase, val channel: Channel, val permCheck: CommandSender) : Command2Sender { class Command2MCSender(val sender: ChromaGamerBase, val channel: Channel, val permCheck: ChromaGamerBase) : Command2Sender {
// TODO: Remove this class and only use the user classes. // TODO: Remove this class and only use the user classes.
// TODO: The command context should be stored separately. // TODO: The command context should be stored separately.
override fun sendMessage(message: String) { override fun sendMessage(message: String) {

View file

@ -10,9 +10,9 @@ import buttondevteam.lib.TBMCChatEvent
import buttondevteam.lib.TBMCChatPreprocessEvent import buttondevteam.lib.TBMCChatPreprocessEvent
import buttondevteam.lib.TBMCSystemChatEvent import buttondevteam.lib.TBMCSystemChatEvent
import buttondevteam.lib.TBMCSystemChatEvent.BroadcastTarget import buttondevteam.lib.TBMCSystemChatEvent.BroadcastTarget
import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.ChatColor import org.bukkit.ChatColor
import org.bukkit.command.CommandSender
import java.util.function.Supplier import java.util.function.Supplier
object TBMCChatAPI { object TBMCChatAPI {
@ -38,7 +38,7 @@ object TBMCChatAPI {
val rtr = getScoreOrSendError(channel, cm.permCheck) val rtr = getScoreOrSendError(channel, cm.permCheck)
val score = rtr.score val score = rtr.score
if (score == Channel.SCORE_SEND_NOPE || rtr.groupID == null) return@Supplier true if (score == Channel.SCORE_SEND_NOPE || rtr.groupID == null) return@Supplier true
val eventPre = TBMCChatPreprocessEvent(cm.sender, channel, cm.message) val eventPre = TBMCChatPreprocessEvent(cm.user, channel, cm.message)
Bukkit.getPluginManager().callEvent(eventPre) Bukkit.getPluginManager().callEvent(eventPre)
if (eventPre.isCancelled) return@Supplier true if (eventPre.isCancelled) return@Supplier true
cm.message = eventPre.message cm.message = eventPre.message
@ -74,7 +74,7 @@ object TBMCChatAPI {
return callEventAsync(event) return callEventAsync(event)
} }
private fun getScoreOrSendError(channel: Channel, sender: CommandSender): RecipientTestResult { private fun getScoreOrSendError(channel: Channel, sender: ChromaGamerBase): RecipientTestResult {
val result = channel.getRTR(sender) val result = channel.getRTR(sender)
if (result.errormessage != null) sender.sendMessage("${ChatColor.RED}" + result.errormessage) if (result.errormessage != null) sender.sendMessage("${ChatColor.RED}" + result.errormessage)
return result return result

View file

@ -167,6 +167,13 @@ abstract class ChromaGamerBase : Command2Sender {
.findAny().orElseThrow { RuntimeException("Channel $id not found!") } .findAny().orElseThrow { RuntimeException("Channel $id not found!") }
}, { ch -> ch.identifier }) }, { ch -> ch.identifier })
/**
* Check channel access by checking if the user is in the given group. If the group is null, check if the user is OP.
*
* Note that these groups originally come from Minecraft.
*/ // TODO: Allow if a connected account has access
abstract fun checkChannelInGroup(group: String?): Channel.RecipientTestResult
companion object { companion object {
private const val TBMC_PLAYERS_DIR = "TBMC/players/" private const val TBMC_PLAYERS_DIR = "TBMC/players/"
private val senderConverters = ArrayList<Function<CommandSender, out Optional<out ChromaGamerBase>>>() private val senderConverters = ArrayList<Function<CommandSender, out Optional<out ChromaGamerBase>>>()

View file

@ -1,5 +1,7 @@
package buttondevteam.lib.player package buttondevteam.lib.player
import buttondevteam.core.MainPlugin
import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.architecture.IHaveConfig import buttondevteam.lib.architecture.IHaveConfig
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.entity.Player import org.bukkit.entity.Player
@ -45,7 +47,6 @@ abstract class TBMCPlayerBase : ChromaGamerBase() {
} }
override fun sendMessage(message: String) { override fun sendMessage(message: String) {
// TODO: Random senders (Discord) won't receive messages. Including when trying to chat.
player?.sendMessage(message) player?.sendMessage(message)
} }
@ -57,6 +58,18 @@ abstract class TBMCPlayerBase : ChromaGamerBase() {
return playerName.get() return playerName.get()
} }
override fun checkChannelInGroup(group: String?): Channel.RecipientTestResult {
return Channel.noScoreResult(
{ s ->
s is TBMCPlayerBase && (s.offlinePlayer.isOp || s.player != null && group?.let { pg ->
MainPlugin.permission.playerInGroup(s.player, pg)
} ?: false)
},
"You need to be a(n) ${group ?: "OP"} to use this channel.",
this
)
}
companion object { companion object {
/** /**
* Get player as a plugin player. * Get player as a plugin player.