├── gradle.properties ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── vcs.xml ├── gradle.xml ├── misc.xml ├── jarRepositories.xml ├── libraries-with-intellij-classes.xml └── artifacts │ └── BungeeSafeguard_main_jar.xml ├── src └── main │ ├── kotlin │ └── cyou │ │ └── untitled │ │ └── bungeesafeguard │ │ ├── commands │ │ ├── subcommands │ │ │ ├── list │ │ │ │ ├── helpers.kt │ │ │ │ ├── ListAction.kt │ │ │ │ ├── Parsed.kt │ │ │ │ ├── DumpCommand.kt │ │ │ │ ├── OffCommand.kt │ │ │ │ ├── OnCommand.kt │ │ │ │ ├── LazyAddCommand.kt │ │ │ │ ├── LazyRemoveCommand.kt │ │ │ │ ├── ImportCommand.kt │ │ │ │ ├── Base.kt │ │ │ │ ├── RemoveCommand.kt │ │ │ │ └── AddCommand.kt │ │ │ ├── Subcommand.kt │ │ │ ├── BSGSubcommand.kt │ │ │ ├── main │ │ │ │ ├── helpers.kt │ │ │ │ ├── ReloadCommand.kt │ │ │ │ ├── StatusCommand.kt │ │ │ │ ├── DumpCommand.kt │ │ │ │ ├── LoadCommand.kt │ │ │ │ ├── ExportCommand.kt │ │ │ │ ├── MergeCommand.kt │ │ │ │ └── ImportCommand.kt │ │ │ └── SubcommandRegistry.kt │ │ ├── ListCommand.kt │ │ ├── ConfirmCommand.kt │ │ ├── BungeeSafeguard.kt │ │ └── ListCommandImpl.kt │ │ ├── list │ │ ├── helpers.kt │ │ ├── UUIDListImpl.kt │ │ ├── UUIDList.kt │ │ └── ListManager.kt │ │ ├── events │ │ └── BungeeSafeguardEnabledEvent.kt │ │ ├── helpers │ │ ├── UserNotFoundException.kt │ │ ├── BungeeDispatcher.kt │ │ ├── ListChecker.kt │ │ ├── RedirectedLogger.kt │ │ ├── ListDumper.kt │ │ ├── DependencyFixer.kt │ │ ├── TypedJSON.kt │ │ └── UserUUIDHelper.kt │ │ ├── storage │ │ ├── ConfigBackend.kt │ │ ├── FileManager.kt │ │ ├── CachedBackend.kt │ │ ├── Backend.kt │ │ └── YAMLBackend.kt │ │ ├── BungeeSafeguard.kt │ │ ├── Events.kt │ │ ├── UserCache.kt │ │ ├── BungeeSafeguardImpl.kt │ │ └── Config.kt │ └── resources │ ├── plugin.yml │ └── config.yml ├── gradlew.bat ├── gradlew ├── developer.md └── README.md /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'BungeeSafeguard' 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luluno01/BungeeSafeguard/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/helpers.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | fun Array.omitEmpty(): Array { 4 | return map { it.trim() }.filter { it.isNotBlank() }.toTypedArray() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/list/helpers.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.list 2 | 3 | fun List.joinListName(): String { 4 | return this.joinToString { it.name } 5 | } 6 | 7 | fun List.joinLazyListName(): String { 8 | return this.joinToString { it.lazyName } 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/events/BungeeSafeguardEnabledEvent.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.events 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import net.md_5.bungee.api.plugin.Event 5 | 6 | /** 7 | * The event emitted on BungeeSafeguard enabled 8 | */ 9 | class BungeeSafeguardEnabledEvent(val bsg: BungeeSafeguard): Event() -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/ListAction.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | data class ListAction( 4 | val isXBOX: Boolean = false, 5 | val isLazyList: Boolean, 6 | val isAdd: Boolean, 7 | val isImport: Boolean = false, 8 | val isDump: Boolean = false, 9 | val isOn: Boolean = false, 10 | val isOff: Boolean = false 11 | ) -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/UserNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import java.io.IOException 4 | 5 | open class UserNotFoundException : IOException { 6 | constructor() 7 | constructor(message: String?) : super(message) 8 | constructor(message: String?, cause: Throwable?) : super(message, cause) 9 | constructor(cause: Throwable?) : super(cause) 10 | } 11 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: BungeeSafeguard 2 | main: cyou.untitled.bungeesafeguard.BungeeSafeguardImpl 3 | version: "3.1" 4 | author: Untitled 5 | libraries: 6 | - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 7 | - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0 8 | - com.google.code.gson:gson:2.8.7 9 | - io.ktor:ktor-client-core:1.6.0 10 | - io.ktor:ktor-client-core-jvm:1.6.0 # Somehow, BungeeCord misses this 11 | - io.ktor:ktor-client-cio:1.6.0 12 | - io.ktor:ktor-client-cio-jvm:1.6.0 # Somehow, BungeeCord misses this -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/Subcommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands 2 | 3 | import net.md_5.bungee.api.CommandSender 4 | import net.md_5.bungee.api.plugin.Plugin 5 | 6 | abstract class Subcommand( 7 | open val context: Plugin, 8 | open val name: String, 9 | open vararg val aliases: String 10 | ) { 11 | /** 12 | * Execute the subcommand 13 | * @param sender The sender of this command 14 | * @param realArgs Real args for this command 15 | */ 16 | abstract fun execute(sender: CommandSender, realArgs: Array) 17 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/BSGSubcommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.Config 5 | import cyou.untitled.bungeesafeguard.UserCache 6 | 7 | abstract class BSGSubcommand( 8 | override val context: BungeeSafeguard, 9 | name: String, 10 | vararg aliases: String 11 | ): Subcommand(context, name, *aliases) { 12 | protected open val config: Config 13 | get() = context.config 14 | 15 | protected open val userCache: UserCache 16 | get() = context.userCache 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/storage/ConfigBackend.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.storage 2 | 3 | import net.md_5.bungee.api.CommandSender 4 | import net.md_5.bungee.api.plugin.Plugin 5 | import java.io.File 6 | 7 | /** 8 | * The default backend that uses the config file to store the lists 9 | */ 10 | open class ConfigBackend(context: Plugin, configFile: File): YAMLBackend(context, configFile) { 11 | override suspend fun onReloadConfigFile(newConfig: File, commandSender: CommandSender?) { 12 | close(commandSender) 13 | init(newConfig, commandSender) 14 | } 15 | 16 | override fun toString(): String = "ConfigBackend(\"${file?.path ?: ""}\")" 17 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/Parsed.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | data class Parsed(val realArgs: Array, val action: ListAction) { 4 | override fun equals(other: Any?): Boolean { 5 | if (this === other) return true 6 | if (javaClass != other?.javaClass) return false 7 | 8 | other as Parsed 9 | 10 | if (!realArgs.contentEquals(other.realArgs)) return false 11 | if (action != other.action) return false 12 | 13 | return true 14 | } 15 | 16 | override fun hashCode(): Int { 17 | var result = realArgs.contentHashCode() 18 | result = 31 * result + action.hashCode() 19 | return result 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/BungeeDispatcher.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import kotlinx.coroutines.ExecutorCoroutineDispatcher 4 | import kotlinx.coroutines.asCoroutineDispatcher 5 | import kotlinx.coroutines.runBlocking 6 | import kotlinx.coroutines.sync.Mutex 7 | import kotlinx.coroutines.sync.withLock 8 | import net.md_5.bungee.api.plugin.Plugin 9 | 10 | object BungeeDispatcher { 11 | private var dispatcher: ExecutorCoroutineDispatcher? = null 12 | private val lock = Mutex() 13 | 14 | @Suppress("DEPRECATION") 15 | suspend fun getDispatcher(plugin: Plugin): ExecutorCoroutineDispatcher { 16 | lock.withLock { 17 | if (dispatcher == null) { 18 | dispatcher = plugin.executorService.asCoroutineDispatcher() 19 | } 20 | return dispatcher!! 21 | } 22 | } 23 | } 24 | 25 | val Plugin.dispatcher: ExecutorCoroutineDispatcher 26 | get() = runBlocking { BungeeDispatcher.getDispatcher(this@dispatcher) } -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | ######################################### 2 | # BungeeSafeguard Configuration # 3 | # Version: 3.1 # 4 | # Author: Untitled # 5 | ######################################### 6 | 7 | version: "3.1" 8 | whitelist-message: :( You are not whitelisted on this server 9 | blacklist-message: :( We can't let you enter this server 10 | no-uuid-message: :( Name yourself 11 | enable-whitelist: true 12 | # lazy-whitelist: 13 | # - 15 | lazy-whitelist: 16 | # whitelist: 17 | # - 18 | whitelist: 19 | enable-blacklist: false 20 | # lazy-blacklist: 21 | # - 23 | lazy-blacklist: 24 | # blacklist: 25 | # - 26 | blacklist: 27 | # xbl-web-api: 28 | xbl-web-api: https://xbl-api.prouser123.me 29 | # confirm: 30 | confirm: false -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/helpers.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.storage.YAMLBackend 5 | import net.md_5.bungee.api.ChatColor 6 | import net.md_5.bungee.api.CommandSender 7 | import net.md_5.bungee.api.chat.TextComponent 8 | import java.io.File 9 | import java.io.IOException 10 | 11 | suspend fun openYAMLBackend(sender: CommandSender, context: BungeeSafeguard, file: File): YAMLBackend? { 12 | val backend = YAMLBackend(context, file) 13 | return try { 14 | backend.init() 15 | backend 16 | } catch (err: IOException) { 17 | sender.sendMessage(TextComponent("${ChatColor.RED}Cannot open backend file \"${file.path}\": $err")) 18 | err.printStackTrace() 19 | null 20 | } 21 | } 22 | 23 | suspend fun withYAMLBackend(sender: CommandSender, context: BungeeSafeguard, file: File, action: suspend (YAMLBackend) -> T): T? { 24 | val backend = openYAMLBackend(sender, context, file) ?: return null 25 | try { 26 | return action(backend) 27 | } finally { 28 | backend.close(sender) 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/ListChecker.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import cyou.untitled.bungeesafeguard.list.ListManager 4 | import cyou.untitled.bungeesafeguard.list.UUIDList 5 | import net.md_5.bungee.api.ChatColor 6 | import net.md_5.bungee.api.CommandSender 7 | import net.md_5.bungee.api.plugin.Plugin 8 | 9 | object ListChecker { 10 | /** 11 | * Perform sanity check on specified list **without locking** 12 | */ 13 | suspend fun checkLists(context: Plugin, sender: CommandSender?, listMgr: ListManager, listGetter: suspend (UUIDList) -> Set, listNameGetter: (UUIDList) -> String) { 14 | val logger = RedirectedLogger.get(context, sender) 15 | val firstOccurrence = mutableMapOf() 16 | for (list in listMgr.lists) { 17 | for (record in listGetter(list)) { 18 | if (firstOccurrence.contains(record)) { 19 | logger.warning("${ChatColor.AQUA}$record ${ChatColor.RESET}first presented in the ${ChatColor.AQUA}${listNameGetter(firstOccurrence[record]!!)} ${ChatColor.RESET}(higher priority), and then the ${ChatColor.AQUA}${listNameGetter(list)}") 20 | } else { 21 | firstOccurrence[record] = list 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/ReloadCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.coroutineScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | 13 | open class ReloadCommand(context: BungeeSafeguard) : BSGSubcommand(context, "reload") { 14 | override fun execute(sender: CommandSender, realArgs: Array) { 15 | GlobalScope.launch(context.dispatcher) { 16 | coroutineScope { 17 | launch { 18 | try { 19 | config.reload(sender) 20 | sender.sendMessage(TextComponent("${ChatColor.GREEN}BungeeSafeguard reloaded")) 21 | } catch (err: Throwable) { 22 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to reload: $err")) 23 | } 24 | } 25 | launch { 26 | context.userCache.reload() 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/DumpCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.ListDumper 6 | import cyou.untitled.bungeesafeguard.list.ListManager 7 | import cyou.untitled.bungeesafeguard.list.UUIDList 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.CommandSender 11 | import java.util.* 12 | 13 | open class DumpCommand( 14 | context: BungeeSafeguard, 15 | name: ListCommand.Companion.SubcommandName, 16 | listMgr: ListManager, 17 | list: UUIDList 18 | ) : Base(context, name, listMgr, list, false) { 19 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 20 | return Parsed(emptyArray(), ListAction(isLazyList = false, isAdd = false, isDump = true)) 21 | } 22 | 23 | override fun execute(sender: CommandSender, realArgs: Array) { 24 | GlobalScope.launch { 25 | ListDumper.printListStatus( 26 | sender, 27 | listName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, 28 | list.enabled 29 | ) 30 | ListDumper.printListsContent(sender, list.path, list.lazyPath, userCache) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/StatusCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.storage.Backend 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | import java.util.* 13 | 14 | open class StatusCommand(context: BungeeSafeguard) : BSGSubcommand(context, "status") { 15 | override fun execute(sender: CommandSender, realArgs: Array) { 16 | GlobalScope.launch(context.dispatcher) { 17 | sender.sendMessage(TextComponent("${ChatColor.GREEN}Using config file ${ChatColor.AQUA}${config.configInUse}")) 18 | sender.sendMessage(TextComponent("${ChatColor.GREEN}Using backend ${ChatColor.AQUA}${Backend.getBackend()}")) 19 | for (list in context.listMgr.lists) { 20 | sender.sendMessage(TextComponent("${ChatColor.GREEN}${list.name.replaceFirstChar { 21 | if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() 22 | }} ${if (list.enabled) "ENABLED" else "${ChatColor.RED}DISABLED"}")) 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/ListCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.list.ListManager 5 | import cyou.untitled.bungeesafeguard.list.UUIDList 6 | 7 | abstract class ListCommand( 8 | val context: BungeeSafeguard, 9 | protected val listMgr: ListManager, 10 | protected val list: UUIDList, 11 | name: String, permission: String, vararg aliases: String) : ConfirmCommand( 12 | name, 13 | permission, 14 | *aliases 15 | ) { 16 | companion object { 17 | @Suppress("unused") 18 | enum class SubcommandName(val cmdName: String, vararg val aliases: String) { 19 | IMPORT("import"), 20 | ADD("add"), 21 | X_ADD("x-add", "xadd"), 22 | LAZY_ADD("lazy-add", "lazyadd", "ladd"), 23 | REMOVE("remove", "rm"), 24 | X_REMOVE("x-remove", "xremove", "x-rm", "xrm"), 25 | LAZY_REMOVE("lazy-remove", "lazyremove", "lremove", "lrm"), 26 | ON("on"), 27 | OFF("off"), 28 | LIST("list", "ls", "show", "dump"); 29 | 30 | companion object { 31 | /** 32 | * Get `SubcommandName` from its command name 33 | */ 34 | fun fromCmdName(name: String): SubcommandName? { 35 | return values().find { it.cmdName == name } 36 | } 37 | } 38 | 39 | override fun toString(): String = cmdName 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/OffCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.list.ListManager 6 | import cyou.untitled.bungeesafeguard.list.UUIDList 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | import java.util.* 13 | 14 | open class OffCommand( 15 | context: BungeeSafeguard, 16 | name: ListCommand.Companion.SubcommandName, 17 | listMgr: ListManager, 18 | list: UUIDList 19 | ) : Base(context, name, listMgr, list, false) { 20 | /** 21 | * Turn off the list 22 | * @param sender Command sender 23 | */ 24 | open suspend fun off(sender: CommandSender) { 25 | list.off(sender) 26 | sender.sendMessage(TextComponent("${ChatColor.GREEN}${listName.replaceFirstChar { 27 | if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() 28 | }} ${ChatColor.RED}DISABLED")) 29 | } 30 | 31 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 32 | return Parsed(emptyArray(), ListAction(isLazyList = false, isAdd = false, isOff = true)) 33 | } 34 | 35 | override fun execute(sender: CommandSender, realArgs: Array) { 36 | GlobalScope.launch { off(sender) } 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/DumpCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.ListDumper 6 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 7 | import cyou.untitled.bungeesafeguard.storage.Backend 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.util.* 14 | 15 | open class DumpCommand(context: BungeeSafeguard) : BSGSubcommand(context, "dump") { 16 | override fun execute(sender: CommandSender, realArgs: Array) { 17 | GlobalScope.launch(context.dispatcher) { 18 | sender.sendMessage(TextComponent("${ChatColor.GREEN}Using config file ${ChatColor.AQUA}${config.configInUse}")) 19 | sender.sendMessage(TextComponent("${ChatColor.GREEN}Using backend ${ChatColor.AQUA}${Backend.getBackend()}")) 20 | for (list in context.listMgr.lists) { 21 | ListDumper.printListStatus( 22 | sender, 23 | list.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, 24 | list.enabled 25 | ) 26 | ListDumper.printListsContent(sender, list.path, list.lazyPath, userCache) 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/OnCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.list.ListManager 7 | import cyou.untitled.bungeesafeguard.list.UUIDList 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.util.* 14 | 15 | open class OnCommand( 16 | context: BungeeSafeguard, 17 | name: ListCommand.Companion.SubcommandName, 18 | listMgr: ListManager, 19 | list: UUIDList 20 | ) : Base(context, name, listMgr, list, false) { 21 | /** 22 | * Turn on the list 23 | * @param sender Command sender 24 | */ 25 | open suspend fun on(sender: CommandSender) { 26 | list.on(sender) 27 | sender.sendMessage(TextComponent("${ChatColor.GREEN}${listName.replaceFirstChar { 28 | if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() 29 | }} ENABLED")) 30 | } 31 | 32 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 33 | return Parsed(emptyArray(), ListAction(isLazyList = false, isAdd = false, isOn = true)) 34 | } 35 | 36 | override fun execute(sender: CommandSender, realArgs: Array) { 37 | GlobalScope.launch(context.dispatcher) { on(sender) } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/RedirectedLogger.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import net.md_5.bungee.api.ChatColor 4 | import net.md_5.bungee.api.CommandSender 5 | import net.md_5.bungee.api.chat.TextComponent 6 | import net.md_5.bungee.api.plugin.Plugin 7 | 8 | @Suppress("MemberVisibilityCanBePrivate") 9 | abstract class RedirectedLogger private constructor() { 10 | companion object { 11 | class ConsoleLogger(val context: Plugin) : RedirectedLogger() { 12 | override fun info(msg: String) { 13 | context.logger.info(msg) 14 | } 15 | 16 | override fun warning(msg: String) { 17 | context.logger.warning(msg) 18 | } 19 | 20 | override fun severe(msg: String) { 21 | context.logger.severe(msg) 22 | } 23 | } 24 | 25 | class ForkedLogger(val context: Plugin, val sender: CommandSender) : RedirectedLogger() { 26 | override fun info(msg: String) { 27 | context.logger.info(msg) 28 | sender.sendMessage(TextComponent(msg)) 29 | } 30 | 31 | override fun warning(msg: String) { 32 | context.logger.warning(msg) 33 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$msg")) 34 | } 35 | 36 | override fun severe(msg: String) { 37 | context.logger.severe(msg) 38 | sender.sendMessage(TextComponent("${ChatColor.RED}$msg")) 39 | } 40 | } 41 | 42 | fun get(context: Plugin, sender: CommandSender?): RedirectedLogger { 43 | return if (sender == null || sender.name == "CONSOLE") ConsoleLogger(context) 44 | else ForkedLogger(context, sender) 45 | } 46 | } 47 | 48 | abstract fun info(msg: String) 49 | abstract fun warning(msg: String) 50 | abstract fun severe(msg: String) 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/BungeeSafeguard.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard 2 | 3 | import cyou.untitled.bungeesafeguard.commands.ListCommand 4 | import cyou.untitled.bungeesafeguard.list.ListManager 5 | import cyou.untitled.bungeesafeguard.list.UUIDList 6 | import net.md_5.bungee.api.plugin.Plugin 7 | 8 | /** 9 | * Abstract BungeeSafeguard plugin as the interface exposed 10 | * 11 | * Third party can get access to BungeeSafeguard's API via `BungeeSafeguard.Companion.getPlugin` 12 | */ 13 | abstract class BungeeSafeguard: Plugin() { 14 | companion object { 15 | @Volatile 16 | private lateinit var inst: BungeeSafeguard 17 | 18 | /** 19 | * Get the instance of `BungeeSafeguard` 20 | */ 21 | fun getPlugin(): BungeeSafeguard { 22 | return inst 23 | } 24 | 25 | val WHITELIST = arrayOf("whitelist", "main") 26 | val LAZY_WHITELIST = arrayOf("whitelist", "lazy") 27 | const val WHITELIST_NAME = "whitelist" 28 | const val LAZY_WHITELIST_NAME = "lazy-whitelist" 29 | val BLACKLIST = arrayOf("blacklist", "main") 30 | val LAZY_BLACKLIST = arrayOf("blacklist", "lazy") 31 | const val BLACKLIST_NAME = "blacklist" 32 | const val LAZY_BLACKLIST_NAME = "lazy-blacklist" 33 | } 34 | 35 | protected open fun exposeInst() { 36 | inst = this 37 | } 38 | 39 | /** 40 | * The config object 41 | */ 42 | abstract val config: Config 43 | 44 | /** 45 | * User cache 46 | */ 47 | abstract val userCache: UserCache 48 | 49 | /** 50 | * List manager 51 | */ 52 | abstract val listMgr: ListManager 53 | 54 | /** 55 | * The whitelist 56 | */ 57 | abstract val whitelist: UUIDList 58 | 59 | /** 60 | * The blacklist 61 | */ 62 | abstract val blacklist: UUIDList 63 | 64 | /** 65 | * The whitelist command 66 | */ 67 | abstract val whitelistCommand: ListCommand 68 | 69 | /** 70 | * The blacklist command 71 | */ 72 | abstract val blacklistCommand: ListCommand 73 | 74 | /** 75 | * If the plugin is enabled 76 | */ 77 | abstract val enabled: Boolean 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/SubcommandRegistry.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands 2 | 3 | import net.md_5.bungee.api.CommandSender 4 | import net.md_5.bungee.api.plugin.Plugin 5 | 6 | @Suppress("MemberVisibilityCanBePrivate") 7 | open class SubcommandRegistry(val context: Plugin, protected val usage: UsageSender) { 8 | companion object { 9 | interface UsageSender { 10 | fun sendUsage(sender: CommandSender) 11 | } 12 | } 13 | 14 | private val subcommands = mutableListOf() 15 | private val subcommandMap = mutableMapOf() 16 | 17 | open fun registerSubcommand(subcommand: Subcommand): SubcommandRegistry { 18 | assertUnused(subcommand.name) 19 | for (alias in subcommand.aliases) { 20 | assertUnused(alias) 21 | } 22 | subcommands.add(subcommand) 23 | subcommandMap[subcommand.name] = subcommand 24 | for (alias in subcommand.aliases) { 25 | subcommandMap[alias] = subcommand 26 | } 27 | return this 28 | } 29 | 30 | open fun registerSubcommand(vararg subcommand: Subcommand): SubcommandRegistry { 31 | for (cmd in subcommand) { 32 | registerSubcommand(cmd) 33 | } 34 | return this 35 | } 36 | 37 | protected fun assertUnused(name: String) { 38 | assert(!subcommandMap.containsKey(name)) { "Subcommand $name is already registered" } 39 | } 40 | 41 | /** 42 | * Get the instance of a subcommand by name 43 | * 44 | * @param name the name of the subcommand 45 | */ 46 | open fun getSubcommand(name: String): Subcommand? = subcommandMap[name] 47 | 48 | /** 49 | * Get the instance of a subcommand by the first argument 50 | * @param sender 51 | * @param args 52 | */ 53 | open fun getSubcommand(sender: CommandSender, args: Array): Subcommand? { 54 | if (args.isEmpty()) { 55 | return null.also { usage.sendUsage(sender) } 56 | } else { 57 | return subcommandMap[args[0]] ?: return null.also { usage.sendUsage(sender) } 58 | } 59 | } 60 | 61 | /** 62 | * Get a **copy** of list of registered subcommands 63 | */ 64 | open fun getSubcommands(): List { 65 | return subcommands.toList() 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/LoadCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.Config 5 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 6 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | import java.io.File 13 | import java.io.IOException 14 | 15 | open class LoadCommand(context: BungeeSafeguard) : BSGSubcommand(context, "load", "use") { 16 | open fun sendUsage(sender: CommandSender) { 17 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 18 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard load/use ${ChatColor.YELLOW}(must be yml file, the extension \".yml\" can be omitted)")) 19 | } 20 | 21 | override fun execute(sender: CommandSender, realArgs: Array) { 22 | GlobalScope.launch(context.dispatcher) { 23 | if (realArgs.isEmpty()) { 24 | sendUsage(sender) 25 | return@launch 26 | } 27 | var name = realArgs[0] 28 | 29 | /* Start safety check */ 30 | if (Regex("""(^|[/\\])\.\.[/\\]""").find(name) != null) { 31 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Parent folder \"..\" is not allowed in the config path")) 32 | return@launch 33 | } 34 | /* End safety check */ 35 | 36 | if (!name.endsWith(".yml")) name += ".yml" 37 | val configInUseFile = File(context.dataFolder, Config.CONFIG_IN_USE) 38 | try { 39 | configInUseFile.writeText(name) 40 | } catch (err: IOException) { 41 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to update file \"${Config.CONFIG_IN_USE}\", aborting")) 42 | return@launch 43 | } 44 | try { 45 | config.load(sender, name) 46 | } catch (err: Throwable) { 47 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to load config file \"$name\": $err")) 48 | return@launch 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/ConfirmCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands 2 | 3 | import net.md_5.bungee.api.CommandSender 4 | import net.md_5.bungee.api.plugin.Command 5 | import java.util.* 6 | 7 | /** 8 | * A command with confirmation facility 9 | */ 10 | abstract class ConfirmCommand(name: String?, permission: String?, vararg aliases: String?) : 11 | Command(name, permission, *aliases) { 12 | data class PendingTask(val id: Int, val onConfirmed: () -> Unit) 13 | private val pending: MutableMap = mutableMapOf() 14 | private val timer = Timer("timer-confirm") 15 | private var taskId = 0 16 | 17 | /** 18 | * Launch a pending task of confirmation 19 | * @param sender the command sender who is required to confirm the pending command 20 | * @param onConfirmed the job to do upon the confirmation 21 | * @param timeout confirmation timeout, defaults to 10 seconds 22 | */ 23 | protected open fun confirm(sender: CommandSender, onConfirmed: () -> Unit, timeout: Long = 10000L) { 24 | val id: Int 25 | synchronized (pending) { 26 | id = taskId++ 27 | pending[sender] = PendingTask(id, onConfirmed) 28 | } 29 | timer.schedule(object: TimerTask() { 30 | override fun run() { 31 | synchronized (pending) { 32 | val task = pending[sender] 33 | if (task != null && task.id == id) { 34 | pending.remove(sender) 35 | } 36 | } 37 | } 38 | }, timeout) 39 | } 40 | 41 | /** 42 | * Call this method when the sender confirms the last pending command 43 | * @param sender the command sender 44 | * @return true if the pending command is confirmed, or false if the pending command does not exist 45 | */ 46 | protected open fun confirmed(sender: CommandSender): Boolean { 47 | val task: PendingTask? 48 | synchronized (pending) { 49 | task = pending.remove(sender) 50 | } 51 | return if (task == null) { 52 | false 53 | } else { 54 | task.onConfirmed() 55 | true 56 | } 57 | } 58 | 59 | /** 60 | * Clear the queue and stop the timer 61 | */ 62 | open fun destroy() { 63 | synchronized (pending) { 64 | pending.clear() 65 | } 66 | timer.cancel() 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/ListDumper.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import net.md_5.bungee.api.ChatColor 4 | import net.md_5.bungee.api.CommandSender 5 | import net.md_5.bungee.api.chat.TextComponent 6 | import cyou.untitled.bungeesafeguard.UserCache 7 | import cyou.untitled.bungeesafeguard.storage.Backend 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.launch 10 | import java.util.* 11 | 12 | object ListDumper { 13 | fun printLazyListContent(sender: CommandSender, lazyList: Set) { 14 | sender.sendMessage(TextComponent("${ChatColor.GOLD}${lazyList.size} ${ChatColor.GREEN}lazy record(s)")) 15 | for (username in lazyList) { 16 | sender.sendMessage(TextComponent("${ChatColor.AQUA} $username")) 17 | } 18 | } 19 | 20 | private fun getKnownNames(cache: UserCache, userId: UUID): String { 21 | return cache[userId]?.joinToString() ?: "" 22 | } 23 | 24 | fun printListContent(sender: CommandSender, list: Set, cache: UserCache) { 25 | sender.sendMessage(TextComponent("${ChatColor.GOLD}${list.size} ${ChatColor.GREEN}UUID record(s) and the last known names (in reverse chronological order)")) 26 | for (uuid in list) { 27 | sender.sendMessage(TextComponent("${ChatColor.AQUA} $uuid ${ChatColor.YELLOW}${getKnownNames(cache, uuid)}")) 28 | } 29 | } 30 | 31 | suspend fun printListsContent(sender: CommandSender, path: Array, lazyPath: Array, cache: UserCache) { 32 | val backend = Backend.getBackend() 33 | val main = mutableSetOf() 34 | lateinit var lazy: Set 35 | coroutineScope { 36 | launch { 37 | for (rawRecord in backend.get(path)) { 38 | try { 39 | main.add(UUID.fromString(rawRecord)) 40 | } catch (err: IllegalArgumentException) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Record ${ChatColor.RED}\"$rawRecord\" is not a UUID")) 42 | } 43 | } 44 | } 45 | launch { 46 | lazy = backend.get(lazyPath) 47 | } 48 | } 49 | printLazyListContent(sender, lazy) 50 | printListContent(sender, main, cache) 51 | } 52 | 53 | fun printListStatus(sender: CommandSender, name: String, enabled: Boolean) { 54 | sender.sendMessage(TextComponent("${ChatColor.GREEN}$name ${if (enabled) "ENABLED" else "${ChatColor.RED}DISABLED"}")) 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/BungeeSafeguard.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguardImpl 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.SubcommandRegistry 5 | import cyou.untitled.bungeesafeguard.commands.subcommands.list.omitEmpty 6 | import cyou.untitled.bungeesafeguard.commands.subcommands.main.* 7 | import net.md_5.bungee.api.ChatColor 8 | import net.md_5.bungee.api.CommandSender 9 | import net.md_5.bungee.api.chat.TextComponent 10 | import net.md_5.bungee.api.plugin.Command 11 | 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | open class BungeeSafeguard(val context: BungeeSafeguardImpl): Command("bungeesafeguard", "bungeesafeguard.main", "bsg") { 14 | companion object { 15 | open class Usage: SubcommandRegistry.Companion.UsageSender { 16 | override fun sendUsage(sender: CommandSender) { 17 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 18 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard load/use ${ChatColor.YELLOW}(must be a yml file, the extension \".yml\" can be omitted)")) 19 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard reload")) 20 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard status")) 21 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard dump")) 22 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard import ${ChatColor.YELLOW}(must be a yml file)")) 23 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard merge ${ChatColor.YELLOW}(must be a yml file)")) 24 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard export ")) 25 | } 26 | } 27 | } 28 | protected val usage = Usage() 29 | protected val cmdReg = SubcommandRegistry(context, usage) 30 | 31 | init { 32 | cmdReg.registerSubcommand( 33 | LoadCommand(context), 34 | ReloadCommand(context), 35 | StatusCommand(context), 36 | DumpCommand(context), 37 | ImportCommand(context), 38 | MergeCommand(context), 39 | ExportCommand(context) 40 | ) 41 | } 42 | 43 | override fun execute(sender: CommandSender, args: Array) { 44 | val fixedArgs = args.omitEmpty() 45 | cmdReg.getSubcommand(sender, fixedArgs)?.execute(sender, args.sliceArray(IntRange(1, fixedArgs.size - 1))) 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/LazyAddCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.list.ListManager 7 | import cyou.untitled.bungeesafeguard.list.UUIDList 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.util.* 14 | 15 | open class LazyAddCommand( 16 | context: BungeeSafeguard, 17 | name: ListCommand.Companion.SubcommandName, 18 | listMgr: ListManager, 19 | list: UUIDList 20 | ) : Base(context, name, listMgr, list, true) { 21 | /** 22 | * Lazy-add UUID(s) or username(s) to the list 23 | * @param sender Command sender 24 | * @param args Array of UUID(s) or username(s) (can be a mixed array) 25 | */ 26 | open suspend fun lazyAdd(sender: CommandSender, args: Array) { 27 | for (usernameOrUUID in args) { 28 | try { 29 | val uuid = UUID.fromString(usernameOrUUID) 30 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID is a UUID and will be added to the $listName")) 31 | checkBeforeAdd(sender, usernameOrUUID, uuid) 32 | if (list.add(uuid)) { 33 | sender.sendMessage(TextComponent("${ChatColor.GREEN}$uuid added to the $listName")) 34 | } else { 35 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID is already in the $listName")) 36 | } 37 | } catch (e: IllegalArgumentException) { 38 | checkBeforeLazyAdd(sender, usernameOrUUID) 39 | if (list.lazyAdd(usernameOrUUID)) { 40 | sender.sendMessage(TextComponent("${ChatColor.GREEN}$usernameOrUUID added to the $lazyName")) 41 | } else { 42 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID is already in the $lazyName")) 43 | } 44 | } 45 | } 46 | } 47 | 48 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 49 | return if (args.size > 1) { 50 | val realArgs = args.copyOfRange(1, args.size) 51 | Parsed(realArgs, ListAction(isLazyList = true, isAdd = true)) 52 | } else { 53 | null 54 | } 55 | } 56 | 57 | override fun execute(sender: CommandSender, realArgs: Array) { 58 | GlobalScope.launch(context.dispatcher) { lazyAdd(sender, realArgs) } 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/LazyRemoveCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.list.ListManager 7 | import cyou.untitled.bungeesafeguard.list.UUIDList 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.util.* 14 | 15 | open class LazyRemoveCommand( 16 | context: BungeeSafeguard, 17 | name: ListCommand.Companion.SubcommandName, 18 | listMgr: ListManager, 19 | list: UUIDList 20 | ) : Base(context, name, listMgr, list, true) { 21 | /** 22 | * Lazy-remove UUID(s) or username(s) from the list 23 | * @param sender Command sender 24 | * @param args Array of UUID(s) or username(s) (can be a mixed array) 25 | */ 26 | open suspend fun lazyRemove(sender: CommandSender, args: Array) { 27 | for (usernameOrUUID in args) { 28 | try { 29 | val uuid = UUID.fromString(usernameOrUUID) 30 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID is a UUID and will be added to the $listName")) 31 | checkBeforeAdd(sender, usernameOrUUID, uuid) 32 | if (list.remove(uuid)) { 33 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$usernameOrUUID ${ChatColor.YELLOW}removed from the $listName")) 34 | } else { 35 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$usernameOrUUID ${ChatColor.YELLOW}is not in the $listName")) 36 | } 37 | } catch (e: IllegalArgumentException) { 38 | if (list.lazyRemove(usernameOrUUID)) { 39 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID removed from the $lazyName")) 40 | } else { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$usernameOrUUID is not in the $lazyName")) 42 | } 43 | } 44 | } 45 | } 46 | 47 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 48 | return if (args.size > 1) { 49 | val realArgs = args.copyOfRange(1, args.size) 50 | Parsed(realArgs, ListAction(isLazyList = true, isAdd = false)) 51 | } else { 52 | null 53 | } 54 | } 55 | 56 | override fun execute(sender: CommandSender, realArgs: Array) { 57 | GlobalScope.launch(context.dispatcher) { lazyRemove(sender, realArgs) } 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/list/UUIDListImpl.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.list 2 | 3 | import cyou.untitled.bungeesafeguard.storage.Backend 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import net.md_5.bungee.api.CommandSender 7 | import java.util.* 8 | 9 | open class UUIDListImpl( 10 | override val name: String, override val lazyName: String, 11 | override val path: Array, override val lazyPath: Array, 12 | override val behavior: UUIDList.Companion.Behavior, 13 | override var message: String?, 14 | initEnabled: Boolean, 15 | @Suppress("MemberVisibilityCanBePrivate") 16 | protected val onSetEnabled: suspend (Boolean, CommandSender?) -> Unit 17 | ): UUIDList { 18 | override var enabled: Boolean = initEnabled 19 | 20 | @Suppress("MemberVisibilityCanBePrivate") 21 | protected val lock = Mutex() 22 | 23 | @Suppress("MemberVisibilityCanBePrivate") 24 | protected suspend fun getBackend(): Backend = Backend.getBackend() 25 | 26 | override suspend fun moveToListIfInLazyList(username: String, id: UUID): Boolean = getBackend().moveToListIfInLazyList(username, id, path, lazyPath) 27 | 28 | override suspend fun add(id: UUID): Boolean = getBackend().add(path, id.toString()) 29 | 30 | override suspend fun remove(id: UUID): Boolean = getBackend().remove(path, id.toString()) 31 | 32 | override suspend fun has(id: UUID): Boolean = getBackend().has(path, id.toString()) 33 | 34 | override suspend fun lazyAdd(username: String): Boolean = getBackend().add(lazyPath, username) 35 | 36 | override suspend fun lazyRemove(username: String): Boolean = getBackend().remove(lazyPath, username) 37 | 38 | override suspend fun lazyHas(username: String): Boolean = getBackend().has(lazyPath, username) 39 | 40 | override suspend fun get(): Set = getBackend().get(path).mapNotNullTo(mutableSetOf()) { 41 | try { 42 | UUID.fromString(it) 43 | } catch (err: IllegalArgumentException) { 44 | null 45 | } 46 | } 47 | 48 | override suspend fun lazyGet(): Set = getBackend().get(lazyPath).toSet() 49 | 50 | override suspend fun on(commandSender: CommandSender?): Boolean { 51 | lock.withLock { 52 | return if (enabled) { 53 | false 54 | } else { 55 | enabled = true 56 | onSetEnabled(true, commandSender) 57 | true 58 | } 59 | } 60 | } 61 | 62 | override suspend fun off(commandSender: CommandSender?): Boolean { 63 | lock.withLock { 64 | return if (enabled) { 65 | enabled = false 66 | onSetEnabled(false, commandSender) 67 | true 68 | } else { 69 | false 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/storage/FileManager.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.storage 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import java.io.File 6 | 7 | /** 8 | * File manager manages the ownership of files 9 | */ 10 | @Suppress("MemberVisibilityCanBePrivate") 11 | object FileManager { 12 | private data class LockedFile(val file: File) { 13 | val lock = Mutex() 14 | } 15 | 16 | @Suppress("MemberVisibilityCanBePrivate") 17 | val isMac = System.getProperty("os.name").lowercase().startsWith("mac") 18 | 19 | private fun getAbsPath(path: String): String { 20 | val rawAbsPath = File(path).canonicalPath 21 | return if (isMac && !rawAbsPath.startsWith('/')) { 22 | "/$rawAbsPath" 23 | } else { 24 | rawAbsPath 25 | } 26 | } 27 | 28 | private val lock = Mutex() 29 | // Once an entry is created, it cannot be removed 30 | private val ownership: MutableMap = mutableMapOf() 31 | 32 | /** 33 | * Wait for and get the ownership of a file 34 | * 35 | * @param path file path 36 | * @param owner who you are - for debugging purpose 37 | */ 38 | suspend fun get(path: String, owner: Any? = null): File { 39 | val absPath = getAbsPath(path) 40 | val lockedFile: LockedFile? 41 | lock.withLock { 42 | lockedFile = ownership[absPath] 43 | if (lockedFile == null) { 44 | val file = File(absPath) 45 | val newLockedFile = LockedFile(file) 46 | newLockedFile.lock.lock(owner) 47 | ownership[absPath] = newLockedFile 48 | return file 49 | } 50 | } 51 | lockedFile!!.lock.lock(owner) 52 | return lockedFile.file 53 | } 54 | 55 | /** 56 | * Release the ownership of a file 57 | * 58 | * @param path file path 59 | * @param owner who you are - for debugging purpose 60 | */ 61 | suspend fun release(path: String, owner: Any? = null) { 62 | val absPath = getAbsPath(path) 63 | val lockedFile: LockedFile 64 | lock.withLock { 65 | lockedFile = ownership[absPath] ?: throw IllegalStateException("Cannot release unseen file \"$path\"") 66 | lockedFile.lock.unlock(owner) 67 | } 68 | } 69 | 70 | /** 71 | * Get the ownership of a file, do something with the file and release it 72 | * 73 | * @param path file path 74 | * @param owner who you are - for debugging purpose 75 | * @param block job to do with the file 76 | */ 77 | suspend fun withFile(path: String, owner: Any? = null, block: suspend (File) -> T): T { 78 | val file = get(path, owner) 79 | try { 80 | return block(file) 81 | } finally { 82 | release(path, owner) 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/ImportCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.TypedJSON 6 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 7 | import cyou.untitled.bungeesafeguard.list.ListManager 8 | import cyou.untitled.bungeesafeguard.list.UUIDList 9 | import kotlinx.coroutines.GlobalScope 10 | import kotlinx.coroutines.launch 11 | import net.md_5.bungee.api.ChatColor 12 | import net.md_5.bungee.api.CommandSender 13 | import net.md_5.bungee.api.chat.TextComponent 14 | import java.io.File 15 | import java.io.IOException 16 | 17 | open class ImportCommand( 18 | context: BungeeSafeguard, 19 | name: ListCommand.Companion.SubcommandName, 20 | listMgr: ListManager, 21 | list: UUIDList 22 | ) : AddCommand(context, name, listMgr, list, false) { 23 | companion object { 24 | fun loadUUIDsInJSONArray(sender: CommandSender, file: File): Array? { 25 | val json = try { 26 | TypedJSON.fromFileSync(file) 27 | } catch (err: IOException) { 28 | sender.sendMessage(TextComponent("${ChatColor.RED}\"${file.absolutePath}\" does not exists, or is unreadable")) 29 | return null 30 | } 31 | if (!json.json.isJsonArray) { 32 | sender.sendMessage(TextComponent("${ChatColor.RED}The content of \"${file.absolutePath}\" must be a JSON array")) 33 | return null 34 | } 35 | val uuids = arrayListOf() 36 | for ((i, elem) in json.json.asJsonArray.withIndex()) { 37 | val uuid = try { 38 | val obj = elem.asJsonObject 39 | obj.get("uuid").asString 40 | } catch (err: Throwable) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Element with index \"$i\" is invalid, ignored")) 42 | continue 43 | } 44 | uuids.add(uuid) 45 | } 46 | return uuids.toTypedArray() 47 | } 48 | } 49 | 50 | /** 51 | * Import UUIDs from external list and merge with current list 52 | */ 53 | open suspend fun import(sender: CommandSender, path: String) { 54 | val ids = loadUUIDsInJSONArray(sender, File(path)) ?: return 55 | add(sender, ids) 56 | } 57 | 58 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 59 | return if (args.size > 1) { 60 | val realArgs = args.copyOfRange(1, 2) 61 | Parsed(realArgs, ListAction(isLazyList = false, isAdd = false, isImport = true)) 62 | } else { 63 | null 64 | } 65 | } 66 | 67 | override fun execute(sender: CommandSender, realArgs: Array) { 68 | GlobalScope.launch(context.dispatcher) { import(sender, realArgs[0]) } 69 | } 70 | } -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/Base.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 6 | import cyou.untitled.bungeesafeguard.list.ListManager 7 | import cyou.untitled.bungeesafeguard.list.UUIDList 8 | import cyou.untitled.bungeesafeguard.list.joinLazyListName 9 | import cyou.untitled.bungeesafeguard.list.joinListName 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.util.* 14 | 15 | abstract class Base( 16 | context: BungeeSafeguard, 17 | name: ListCommand.Companion.SubcommandName, 18 | protected val listMgr: ListManager, 19 | protected val list: UUIDList, 20 | val confirmable: Boolean = true 21 | ) : BSGSubcommand(context, name.cmdName, *name.aliases) { 22 | /** 23 | * Name of this list 24 | */ 25 | protected open val listName: String 26 | get() = list.name 27 | 28 | /** 29 | * Name of the lazy list 30 | */ 31 | protected open val lazyName: String 32 | get() = list.lazyName 33 | 34 | protected open suspend fun checkBeforeAdd(sender: CommandSender, query: String, uuid: UUID) { 35 | val higher = listMgr.inListsWithHigherPriority(uuid, list) 36 | if (higher.isNotEmpty()) { 37 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$query ${ChatColor.AQUA}($uuid) ${ChatColor.YELLOW}is already in ${higher.joinListName()}, whose priority is higher than $listName")) 38 | } 39 | val lower = listMgr.inListsWithLowerPriority(uuid, list) 40 | if (lower.isNotEmpty()) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$query ${ChatColor.AQUA}($uuid) ${ChatColor.YELLOW}is already in ${lower.joinListName()}, whose priority is lower than $listName")) 42 | } 43 | } 44 | 45 | protected open suspend fun checkBeforeLazyAdd(sender: CommandSender, username: String) { 46 | val higher = listMgr.inLazyListsWithHigherPriority(username, list) 47 | if (higher.isNotEmpty()) { 48 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$username ${ChatColor.YELLOW}is already in ${higher.joinLazyListName()}, whose priority is higher than $listName")) 49 | } 50 | val lower = listMgr.inLazyListsWithLowerPriority(username, list) 51 | if (lower.isNotEmpty()) { 52 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}$username ${ChatColor.YELLOW}is already in ${lower.joinLazyListName()}, whose priority is lower than $listName")) 53 | } 54 | } 55 | 56 | /** 57 | * Parse the raw argument whose format will be currently hardcoded 58 | * @param sender The command sender 59 | * @param args Raw arguments 60 | * @return A `Parsed` object, which contains real args and a list action, 61 | * if the arguments are valid; `null` if the arguments are invalid, 62 | * in which case the usage will be sent back to the command sender by the caller, 63 | * i.e., the parent command 64 | */ 65 | abstract fun parseArgs(sender: CommandSender, args: Array): Parsed? 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/DependencyFixer.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import net.md_5.bungee.api.plugin.Plugin 5 | import java.io.File 6 | import java.net.URL 7 | import java.net.URLClassLoader 8 | 9 | @Suppress("MemberVisibilityCanBePrivate", "unused") 10 | object DependencyFixer { 11 | class RelaxedURLClassLoader(urls: Array?, parent: ClassLoader?) : URLClassLoader(urls, parent) { 12 | fun insertURL(url: URL) { 13 | addURL(url) 14 | } 15 | } 16 | data class FixResult(val newLibraryLoader: RelaxedURLClassLoader, val removedPaths: List) 17 | 18 | /** 19 | * Get artifact ID from its URL 20 | * @param url URL to the artifact, 21 | * e.g., file:///home/abc/kotlin-stdlib-1.0.0.jar 22 | * @return the artifact ID (not guaranteed to be 100% correct) 23 | */ 24 | fun getArtifactIdFromURL(url: URL): String { 25 | val basename = File(url.file).nameWithoutExtension 26 | return """([\w\-]+?)-(\d.+)""".toRegex().matchEntire(basename)!!.groupValues[1] 27 | } 28 | 29 | /** 30 | * Replace the class loader for libraries of given plugin with a custom one 31 | * that prioritizes those libraries of BungeeSafeguard. Specifically, it 32 | * excludes all kotlin*.jar. 33 | * @param plugin the plugin to fix 34 | */ 35 | fun fixLibraryLoader(plugin: Plugin): FixResult { 36 | return fixLibraryLoader(plugin::class.java.classLoader) 37 | } 38 | 39 | /** 40 | * Replace the class loader for libraries of given plugin with a custom one 41 | * that prioritizes those libraries of BungeeSafeguard 42 | * @param pluginClassLoader the class loader to be fixed, retrieved by 43 | * `YourPluginClass::class.java.classLoader` 44 | */ 45 | fun fixLibraryLoader(pluginClassLoader: ClassLoader): FixResult { 46 | val bsgLoader = BungeeSafeguard::class.java.classLoader 47 | val cPluginClassLoader = pluginClassLoader.javaClass 48 | val fLibraryLoader = cPluginClassLoader.getDeclaredField("libraryLoader") 49 | fLibraryLoader.isAccessible = true 50 | try { 51 | val parentLibraryLoader = fLibraryLoader.get(bsgLoader) as URLClassLoader 52 | val oldLibraryLoader = fLibraryLoader.get(pluginClassLoader) as URLClassLoader 53 | val bsgArtifacts = parentLibraryLoader.urLs.map { getArtifactIdFromURL(it) } 54 | val removedPaths = mutableListOf() 55 | val newURLs = oldLibraryLoader.urLs.filter { 56 | !bsgArtifacts.contains(getArtifactIdFromURL(it)).also { remove -> 57 | if (remove) removedPaths.add(it) 58 | } 59 | } 60 | /** 61 | * We must remove duplicated dependencies from the path, otherwise 62 | * they will still be loaded as transitive dependencies by the 63 | * `oldLibraryLoader` 64 | */ 65 | val newLibraryLoader = RelaxedURLClassLoader(newURLs.toTypedArray(), parentLibraryLoader) 66 | fLibraryLoader.set(pluginClassLoader, newLibraryLoader) 67 | return FixResult(newLibraryLoader, removedPaths) 68 | } finally { 69 | fLibraryLoader.isAccessible = false 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/RemoveCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.UserNotFoundException 6 | import cyou.untitled.bungeesafeguard.helpers.UserUUIDHelper 7 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 8 | import cyou.untitled.bungeesafeguard.list.ListManager 9 | import cyou.untitled.bungeesafeguard.list.UUIDList 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import net.md_5.bungee.api.ChatColor 13 | import net.md_5.bungee.api.CommandSender 14 | import net.md_5.bungee.api.chat.TextComponent 15 | 16 | open class RemoveCommand( 17 | context: BungeeSafeguard, 18 | name: ListCommand.Companion.SubcommandName, 19 | listMgr: ListManager, 20 | list: UUIDList, 21 | /** 22 | * Whether a this add command is for XBOX 23 | */ 24 | @Suppress("MemberVisibilityCanBePrivate") 25 | protected val xbox: Boolean 26 | ) : Base(context, name, listMgr, list, true) { 27 | /** 28 | * Remove UUID(s) or username(s) from the list 29 | * @param sender Command sender 30 | * @param args Array of UUID(s) or username(s) (can be a mixed array) 31 | * @param xbox Whether the names in `args` are XBOX tags 32 | */ 33 | open suspend fun remove(sender: CommandSender, args: Array, xbox: Boolean) { 34 | UserUUIDHelper.resolveUUIDs(context, args, xbox) { 35 | when (val err = it.err) { 36 | null -> { 37 | val nameAndUUID = it.result!! 38 | val username = nameAndUUID.name 39 | val uuid = nameAndUUID.id 40 | if (list.remove(uuid)) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}${it.query} ${ChatColor.AQUA}($uuid) ${ChatColor.YELLOW}removed from the $listName")) 42 | } else { 43 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}${it.query} ${ChatColor.AQUA}($uuid) ${ChatColor.YELLOW}is not in the $listName")) 44 | } 45 | if (username != null && !listMgr.inAnyList(uuid)) { 46 | userCache.removeAndSave(uuid) 47 | } 48 | } 49 | is UserNotFoundException -> sender.sendMessage(TextComponent("${ChatColor.RED}User ${it.query} is not found and therefore cannot be removed from the $listName")) 50 | else -> { 51 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to remove ${it.query} from the $listName: $err")) 52 | context.logger.warning("Failed to remove ${it.query} from the $listName:") 53 | err.printStackTrace() 54 | } 55 | } 56 | } 57 | } 58 | 59 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 60 | return if (args.size > 1) { 61 | val realArgs = args.copyOfRange(1, args.size) 62 | Parsed(realArgs, ListAction(isXBOX = xbox, isLazyList = false, isAdd = false)) 63 | } else { 64 | null 65 | } 66 | } 67 | 68 | override fun execute(sender: CommandSender, realArgs: Array) { 69 | GlobalScope.launch(context.dispatcher) { remove(sender, realArgs, xbox) } 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/ExportCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.storage.Backend 7 | import cyou.untitled.bungeesafeguard.storage.FileManager 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.io.File 14 | import java.io.IOException 15 | 16 | open class ExportCommand(context: BungeeSafeguard) : BSGSubcommand(context, "export", "e") { 17 | private fun sendUsage(sender: CommandSender) { 18 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 19 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard export ")) 20 | } 21 | 22 | override fun execute(sender: CommandSender, realArgs: Array) { 23 | if (realArgs.size != 1) return sendUsage(sender) 24 | val dstConf = realArgs[0] 25 | GlobalScope.launch(context.dispatcher) { 26 | doExport(sender, dstConf) 27 | } 28 | } 29 | 30 | protected open suspend fun exportList(sender: CommandSender, dst: Backend, src: Backend, path: Array, lazyPath: Array) { 31 | var ids = 0 32 | for (id in src.get(path)) { 33 | if (dst.add(path, id)) ids++ 34 | } 35 | var names = 0 36 | for (name in src.get(lazyPath)) { 37 | if (dst.add(lazyPath, name)) names++ 38 | } 39 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$ids ${ChatColor.GREEN}UUID(s) and ${ChatColor.AQUA}$names ${ChatColor.GREEN}username(s) exported")) 40 | } 41 | 42 | @Suppress("BlockingMethodInNonBlockingContext") 43 | protected open suspend fun doExport(sender: CommandSender, dstConf: String) { 44 | val src = Backend.getBackend() 45 | val dstFile = File(context.dataFolder, dstConf) 46 | val fail = FileManager.withFile(dstFile.path, "BSG-export") { 47 | return@withFile try { 48 | if (!it.createNewFile()) { 49 | sender.sendMessage(TextComponent("${ChatColor.RED}File \"${dstFile.path}\" already exists, refuse to overwrite")) 50 | true 51 | } else false 52 | } catch (err: IOException) { 53 | sender.sendMessage(TextComponent("${ChatColor.RED}Cannot create file \"${dstFile.path}\" for exporting: $err")) 54 | err.printStackTrace() 55 | true 56 | } 57 | } 58 | if (fail) return 59 | withYAMLBackend(sender, context, File(context.dataFolder, dstConf)) { dst -> 60 | try { 61 | exportList( 62 | sender, 63 | dst, 64 | src, 65 | BungeeSafeguard.WHITELIST, 66 | BungeeSafeguard.LAZY_WHITELIST 67 | ) 68 | exportList( 69 | sender, 70 | dst, 71 | src, 72 | BungeeSafeguard.BLACKLIST, 73 | BungeeSafeguard.LAZY_BLACKLIST 74 | ) 75 | } catch (err: Throwable) { 76 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to export: $err")) 77 | err.printStackTrace() 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/MergeCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.storage.Backend 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | import java.io.File 13 | import java.util.* 14 | 15 | open class MergeCommand(context: BungeeSafeguard) : BSGSubcommand(context, "merge", "m") { 16 | private fun sendUsage(sender: CommandSender) { 17 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 18 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard merge ${ChatColor.YELLOW}(must be a yml file)")) 19 | } 20 | 21 | override fun execute(sender: CommandSender, realArgs: Array) { 22 | if (realArgs.size != 1) return sendUsage(sender) 23 | val oldConf = realArgs[0] 24 | GlobalScope.launch(context.dispatcher) { 25 | doMerge(sender, oldConf) 26 | } 27 | } 28 | 29 | protected open suspend fun mergeList(sender: CommandSender, dst: Backend, src: Backend, name: String, lazyName: String, path: Array, lazyPath: Array) { 30 | var ids = 0 31 | for (rawId in src.get(path)) { 32 | val id = try { UUID.fromString(rawId); rawId } catch (err: IllegalArgumentException) { 33 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Cannot merge non-UUID record \"$rawId\" in the $name, skipping")) 34 | continue 35 | } 36 | if (dst.add(path, id)) ids++ 37 | } 38 | var names = 0 39 | for (username in src.get(lazyPath)) { 40 | if (username.isEmpty()) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Cannot merge empty username in the $name, skipping")) 42 | continue 43 | } 44 | if (dst.add(lazyPath, username)) names++ 45 | } 46 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$ids ${ChatColor.GREEN}UUID(s) merged with current $name")) 47 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$names ${ChatColor.GREEN}username(s) merged with current $lazyName")) 48 | } 49 | 50 | protected open suspend fun doMerge(sender: CommandSender, oldConf: String) { 51 | val dst = Backend.getBackend() 52 | withYAMLBackend(sender, context, File(context.dataFolder, oldConf)) { src -> 53 | try { 54 | mergeList( 55 | sender, 56 | dst, 57 | src, 58 | BungeeSafeguard.WHITELIST_NAME, 59 | BungeeSafeguard.LAZY_WHITELIST_NAME, 60 | BungeeSafeguard.WHITELIST, 61 | BungeeSafeguard.LAZY_WHITELIST 62 | ) 63 | mergeList( 64 | sender, 65 | dst, 66 | src, 67 | BungeeSafeguard.BLACKLIST_NAME, 68 | BungeeSafeguard.LAZY_BLACKLIST_NAME, 69 | BungeeSafeguard.BLACKLIST, 70 | BungeeSafeguard.LAZY_BLACKLIST 71 | ) 72 | } catch (err: Throwable) { 73 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to merge: $err")) 74 | err.printStackTrace() 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/list/UUIDList.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.list 2 | 3 | import net.md_5.bungee.api.CommandSender 4 | import java.util.* 5 | 6 | interface UUIDList { 7 | companion object { 8 | enum class Behavior { 9 | KICK_NOT_MATCHED, 10 | KICK_MATCHED 11 | } 12 | } 13 | 14 | /** 15 | * Human-friendly main list name 16 | */ 17 | val name: String 18 | 19 | /** 20 | * Human-friendly lazy list name 21 | */ 22 | val lazyName: String 23 | 24 | /** 25 | * Main list storage path 26 | */ 27 | val path: Array 28 | 29 | /** 30 | * Lazy list storage path 31 | */ 32 | val lazyPath: Array 33 | 34 | /** 35 | * List behavior 36 | */ 37 | val behavior: Behavior 38 | 39 | /** 40 | * Move record from lazy list to main list if any 41 | * @param username Username 42 | * @param id UUID of the player 43 | * @return If the player is in lazy list 44 | */ 45 | suspend fun moveToListIfInLazyList(username: String, id: UUID): Boolean 46 | 47 | /** 48 | * Add a player to the main list 49 | * 50 | * @param id the record to add to the main list 51 | * @return `true` if the record is added, `false` if the record is already in the list 52 | */ 53 | suspend fun add(id: UUID): Boolean 54 | 55 | /** 56 | * Remove a player from the main list 57 | * 58 | * @param id the record to remove from the main list 59 | * @return `true` if the record is removed, `false` if the record is not in the list in the first place 60 | */ 61 | suspend fun remove(id: UUID): Boolean 62 | 63 | /** 64 | * Check if a player is in the main list 65 | * 66 | * @param id the record to check 67 | * @return `true` if the list contains the record, `false` otherwise 68 | */ 69 | suspend fun has(id: UUID): Boolean 70 | 71 | /** 72 | * Add a player to the lazy list 73 | * 74 | * @param username the record to add to the lazy list 75 | * @return `true` if the record is added, `false` if the record is already in the list 76 | */ 77 | suspend fun lazyAdd(username: String): Boolean 78 | 79 | /** 80 | * Remove a player from the lazy list 81 | * 82 | * @param username the record to remove from the lazy list 83 | * @return `true` if the record is removed, `false` if the record is not in the list in the first place 84 | */ 85 | suspend fun lazyRemove(username: String): Boolean 86 | 87 | /** 88 | * Check if a player is in the lazy list 89 | * 90 | * @param username the record to check 91 | * @return `true` if the list contains the record, `false` otherwise 92 | */ 93 | suspend fun lazyHas(username: String): Boolean 94 | 95 | /** 96 | * Get a readonly copy of the main list 97 | */ 98 | suspend fun get(): Set 99 | 100 | /** 101 | * Get a readonly copy of the lazy list 102 | */ 103 | suspend fun lazyGet(): Set 104 | 105 | /** 106 | * Message to be sent the blocked player 107 | */ 108 | val message: String? 109 | 110 | /** 111 | * Set the enabled state to `true` 112 | * @return `true` if the list was disabled; `false` otherwise 113 | */ 114 | suspend fun on(commandSender: CommandSender?): Boolean 115 | 116 | /** 117 | * Set the enabled state to `false` 118 | * @return `true` if the list was enabled; `false` otherwise 119 | */ 120 | suspend fun off(commandSender: CommandSender?): Boolean 121 | 122 | /** 123 | * If this list is enabled (readonly) 124 | */ 125 | val enabled: Boolean 126 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/list/AddCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.list 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.ListCommand 5 | import cyou.untitled.bungeesafeguard.helpers.UserNotFoundException 6 | import cyou.untitled.bungeesafeguard.helpers.UserUUIDHelper 7 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 8 | import cyou.untitled.bungeesafeguard.list.ListManager 9 | import cyou.untitled.bungeesafeguard.list.UUIDList 10 | import io.ktor.client.* 11 | import kotlinx.coroutines.GlobalScope 12 | import kotlinx.coroutines.launch 13 | import net.md_5.bungee.api.ChatColor 14 | import net.md_5.bungee.api.CommandSender 15 | import net.md_5.bungee.api.chat.TextComponent 16 | 17 | open class AddCommand( 18 | context: BungeeSafeguard, 19 | name: ListCommand.Companion.SubcommandName, 20 | listMgr: ListManager, 21 | list: UUIDList, 22 | /** 23 | * Whether a this add command is for XBOX 24 | */ 25 | @Suppress("MemberVisibilityCanBePrivate") 26 | protected val xbox: Boolean 27 | ) : Base(context, name, listMgr, list, true) { 28 | /** 29 | * Add UUID(s) or username(s) to the main list 30 | * @param sender Command sender 31 | * @param args Array of UUID(s) or username(s) (can be a mixed array) 32 | * @param xbox Whether the names in `args` are XBOX tags 33 | */ 34 | open suspend fun add(sender: CommandSender, args: Array, xbox: Boolean) { 35 | UserUUIDHelper.resolveUUIDs(context, args, xbox) { 36 | when (val err = it.err) { 37 | null -> { 38 | val nameAndUUID = it.result!! 39 | val username = nameAndUUID.name 40 | val uuid = nameAndUUID.id 41 | if (username != null) { 42 | userCache.addAndSave(uuid, username) 43 | } 44 | checkBeforeAdd(sender, it.query, uuid) 45 | if (list.add(uuid)) { 46 | sender.sendMessage(TextComponent("${ChatColor.AQUA}${it.query} ${ChatColor.YELLOW}($uuid) ${ChatColor.AQUA}added to the $listName")) 47 | } else { 48 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}${it.query} ${ChatColor.AQUA}($uuid) ${ChatColor.YELLOW}is already in the $listName")) 49 | } 50 | } 51 | is UserNotFoundException -> sender.sendMessage(TextComponent("${ChatColor.RED}User ${it.query} is not found and therefore cannot be added to the $listName")) 52 | else -> { 53 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to add ${it.query} to the $listName: $err")) 54 | context.logger.warning("Failed to add ${it.query} to the $listName:") 55 | err.printStackTrace() 56 | } 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Add UUID(s) or username(s) to the main list 63 | * @param sender Command sender 64 | * @param args Array of UUID(s) or username(s) (can be a mixed array) 65 | */ 66 | open suspend fun add(sender: CommandSender, args: Array) = add(sender, args, xbox) 67 | 68 | override fun parseArgs(sender: CommandSender, args: Array): Parsed? { 69 | return if (args.size > 1) { 70 | val realArgs = args.copyOfRange(1, args.size) 71 | Parsed(realArgs, ListAction(isXBOX = xbox, isLazyList = false, isAdd = true)) 72 | } else { 73 | null 74 | } 75 | } 76 | 77 | override fun execute(sender: CommandSender, realArgs: Array) { 78 | GlobalScope.launch(context.dispatcher) { add(sender, realArgs, xbox) } 79 | } 80 | } -------------------------------------------------------------------------------- /.idea/artifacts/BungeeSafeguard_main_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/out/artifacts/BungeeSafeguard_main_jar 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/TypedJSON.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonElement 5 | import com.google.gson.JsonParser 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import java.io.File 9 | 10 | @Suppress("MemberVisibilityCanBePrivate", "unused") 11 | open class TypedJSON(val json: JsonElement) { 12 | companion object { 13 | @Suppress("DEPRECATION") 14 | fun fromString(str: String): TypedJSON { 15 | return try { 16 | TypedJSON(JsonParser.parseString(str)) 17 | } catch (err: NoSuchMethodError) { 18 | TypedJSON(JsonParser().parse(str)) 19 | } 20 | } 21 | 22 | suspend fun fromFile(file: File): TypedJSON { 23 | return withContext(Dispatchers.IO) { 24 | fromString(file.readText()) 25 | } 26 | } 27 | 28 | fun fromFileSync(file: File): TypedJSON = fromString(file.readText()) 29 | } 30 | 31 | fun assertPrimitive() { 32 | assertPrimitive() { "JSON primitive expected" } 33 | } 34 | 35 | fun assertPrimitive(lazyMessage: () -> Any) { 36 | assert(json.isJsonPrimitive, lazyMessage) 37 | } 38 | 39 | fun assertString() { 40 | assertString() { "String expected" } 41 | } 42 | 43 | fun assertString(lazyMessage: () -> Any) { 44 | assert(json.isJsonPrimitive && json.asJsonPrimitive.isString, lazyMessage) 45 | } 46 | 47 | fun assertNumber() { 48 | assertNumber() { "Number expected" } 49 | } 50 | 51 | fun assertNumber(lazyMessage: () -> Any) { 52 | assert(json.isJsonPrimitive && json.asJsonPrimitive.isNumber, lazyMessage) 53 | } 54 | 55 | fun assertBoolean() { 56 | assertBoolean() { "Boolean expected" } 57 | } 58 | 59 | fun assertBoolean(lazyMessage: () -> Any) { 60 | assert(json.isJsonPrimitive && json.asJsonPrimitive.isBoolean, lazyMessage) 61 | } 62 | 63 | fun assertNull() { 64 | assertNull() { "Null expected" } 65 | } 66 | 67 | fun assertNull(lazyMessage: () -> Any) { 68 | assert(json.isJsonNull, lazyMessage) 69 | } 70 | 71 | fun assertObject() { 72 | assertObject() { "JSON object expected" } 73 | } 74 | 75 | fun assertObject(lazyMessage: () -> Any) { 76 | assert(json.isJsonObject, lazyMessage) 77 | } 78 | 79 | fun assertArray() { 80 | assertArray() { "Array expected" } 81 | } 82 | 83 | fun assertArray(lazyMessage: () -> Any) { 84 | assert(json.isJsonArray, lazyMessage) 85 | } 86 | 87 | @Suppress("DuplicatedCode") 88 | fun getString(key: String): String? { 89 | if (!json.isJsonObject) return null 90 | val elem = json.asJsonObject.get(key) ?: return null 91 | if (elem.isJsonPrimitive) { 92 | val primitive = elem.asJsonPrimitive 93 | if (primitive.isString) { 94 | return primitive.asString 95 | } else { 96 | throw IllegalStateException("Invalid property type") 97 | } 98 | } else throw IllegalStateException("Invalid property type") 99 | } 100 | 101 | fun getArray(key: String): JsonArray? { 102 | if (!json.isJsonObject) return null 103 | val elem = json.asJsonObject.get(key) ?: return null 104 | return if (elem.isJsonArray) elem.asJsonArray 105 | else null 106 | } 107 | 108 | @Suppress("DuplicatedCode") 109 | fun getLong(key: String): Long? { 110 | if (!json.isJsonObject) return null 111 | val elem = json.asJsonObject.get(key) ?: return null 112 | if (elem.isJsonPrimitive) { 113 | val primitive = elem.asJsonPrimitive 114 | if (primitive.isNumber) { 115 | return primitive.asLong 116 | } else { 117 | throw IllegalStateException("Invalid property type") 118 | } 119 | } else throw IllegalStateException("Invalid property type") 120 | } 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/subcommands/main/ImportCommand.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands.subcommands.main 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.commands.subcommands.BSGSubcommand 5 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 6 | import cyou.untitled.bungeesafeguard.storage.Backend 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import net.md_5.bungee.api.ChatColor 10 | import net.md_5.bungee.api.CommandSender 11 | import net.md_5.bungee.api.chat.TextComponent 12 | import java.io.File 13 | import java.util.* 14 | 15 | open class ImportCommand(context: BungeeSafeguard) : BSGSubcommand(context, "import", "i") { 16 | private fun sendUsage(sender: CommandSender) { 17 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 18 | sender.sendMessage(TextComponent("${ChatColor.AQUA} /bungeesafeguard import ${ChatColor.YELLOW}(must be a yml file)")) 19 | } 20 | 21 | override fun execute(sender: CommandSender, realArgs: Array) { 22 | if (realArgs.size != 1) return sendUsage(sender) 23 | val oldConf = realArgs[0] 24 | GlobalScope.launch(context.dispatcher) { 25 | doImport(sender, oldConf) 26 | } 27 | } 28 | 29 | protected open suspend fun importList(sender: CommandSender, dst: Backend, src: Backend, name: String, lazyName: String, path: Array, lazyPath: Array) { 30 | var ids = 0 31 | for (rawId in src.get(path)) { 32 | val id = try { UUID.fromString(rawId); rawId } catch (err: IllegalArgumentException) { 33 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Cannot import non-UUID record \"$rawId\" in the $name, skipping")) 34 | continue 35 | } 36 | if (dst.add(path, id)) ids++ 37 | } 38 | var names = 0 39 | for (username in src.get(lazyPath)) { 40 | if (username.isEmpty()) { 41 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Cannot import empty username in the $name, skipping")) 42 | continue 43 | } 44 | if (dst.add(lazyPath, username)) names++ 45 | } 46 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$ids ${ChatColor.GREEN}UUID(s) imported to the $name")) 47 | sender.sendMessage(TextComponent("${ChatColor.AQUA}$names ${ChatColor.GREEN}username(s) imported to the $lazyName")) 48 | } 49 | 50 | protected open suspend fun doImport(sender: CommandSender, oldConf: String) { 51 | val dst = Backend.getBackend() 52 | if (dst.getSize(BungeeSafeguard.WHITELIST) > 0 || dst.getSize(BungeeSafeguard.LAZY_WHITELIST) > 0 || dst.getSize(BungeeSafeguard.BLACKLIST) > 0 || dst.getSize(BungeeSafeguard.LAZY_BLACKLIST) > 0) { 53 | sender.sendMessage(TextComponent("${ChatColor.RED}Current backend non-empty, reject importing")) 54 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}If you want to force import, please use /bungeesafeguard merge")) 55 | return 56 | } 57 | withYAMLBackend(sender, context, File(context.dataFolder, oldConf)) { src -> 58 | try { 59 | importList( 60 | sender, 61 | dst, 62 | src, 63 | BungeeSafeguard.WHITELIST_NAME, 64 | BungeeSafeguard.LAZY_WHITELIST_NAME, 65 | BungeeSafeguard.WHITELIST, 66 | BungeeSafeguard.LAZY_WHITELIST 67 | ) 68 | importList( 69 | sender, 70 | dst, 71 | src, 72 | BungeeSafeguard.BLACKLIST_NAME, 73 | BungeeSafeguard.LAZY_BLACKLIST_NAME, 74 | BungeeSafeguard.BLACKLIST, 75 | BungeeSafeguard.LAZY_BLACKLIST 76 | ) 77 | } catch (err: Throwable) { 78 | sender.sendMessage(TextComponent("${ChatColor.RED}Failed to import: $err")) 79 | err.printStackTrace() 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/UserUUIDHelper.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.helpers 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.cio.* 6 | import io.ktor.client.features.* 7 | import io.ktor.client.request.* 8 | import io.ktor.client.statement.* 9 | import io.ktor.http.* 10 | import kotlinx.coroutines.coroutineScope 11 | import kotlinx.coroutines.launch 12 | import java.io.IOException 13 | import java.util.* 14 | 15 | @Suppress("MemberVisibilityCanBePrivate") 16 | object UserUUIDHelper { 17 | data class NameAndUUID(val name: String?, val id: UUID) 18 | data class ResolutionResult( 19 | val err: Throwable?, 20 | val result: NameAndUUID?, 21 | val query: String 22 | ) 23 | 24 | val client = HttpClient(CIO) 25 | 26 | private suspend fun getUUIDFromUsername(username: String, client: HttpClient = this.client): UUID { 27 | val res: HttpResponse = client.get("https://api.mojang.com/users/profiles/minecraft/${username}") 28 | when (val status = res.status) { 29 | HttpStatusCode.OK -> { 30 | val json = TypedJSON.fromString(res.readText()) 31 | json.assertObject() 32 | val id = json.getString("id") ?: throw IOException("Invalid response") 33 | return UUID.fromString( 34 | StringBuilder(id) 35 | .insert(8, '-') 36 | .insert(13, '-') 37 | .insert(18, '-') 38 | .insert(23, '-') 39 | .toString() 40 | ) 41 | } 42 | HttpStatusCode.NoContent -> throw UserNotFoundException("User $username cannot be found from Mojang") 43 | else -> throw IOException("Unable to handle response with status $status") 44 | } 45 | } 46 | 47 | suspend fun getUUIDFromString(usernameOrUUID: String, client: HttpClient = this.client): NameAndUUID { 48 | return try { 49 | NameAndUUID(null, UUID.fromString(usernameOrUUID)) 50 | } catch (e: IllegalArgumentException) { 51 | NameAndUUID(usernameOrUUID, getUUIDFromUsername(usernameOrUUID, client)) 52 | } 53 | } 54 | 55 | fun getUUIDFromXUID(xuid: Long): UUID { 56 | return UUID.fromString( 57 | StringBuilder("00000000-0000-0000-") 58 | .append(xuid.toString(16).padStart(16, '0')) 59 | .insert(23, '-').toString() 60 | ) 61 | } 62 | 63 | private suspend fun doGetUUIDFromXBOXTag(context: BungeeSafeguard, tag: String, client: HttpClient = this.client): UUID { 64 | var xblWebAPIUrl = context.config.xblWebAPIUrl ?: 65 | error("XBL Web API URL must be specified for XUID look up") 66 | if (!xblWebAPIUrl.endsWith('/')) xblWebAPIUrl += '/' 67 | val res: HttpResponse = try { 68 | client.get("${xblWebAPIUrl}xuid/$tag/raw") 69 | } catch (e: ClientRequestException) { 70 | throw UserNotFoundException("User $tag cannot be found from XBOX Live", e) 71 | } 72 | when (val status = res.status) { 73 | HttpStatusCode.OK -> return getUUIDFromXUID(res.readText().toLong()) 74 | else -> throw IOException("Unable to handle response with status $status") 75 | } 76 | } 77 | 78 | suspend fun getUUIDFromXBOXTag(context: BungeeSafeguard, tagOrUUID: String, client: HttpClient = this.client): NameAndUUID { 79 | return try { 80 | NameAndUUID(null, UUID.fromString(tagOrUUID)) 81 | } catch (e: IllegalArgumentException) { 82 | NameAndUUID(tagOrUUID, doGetUUIDFromXBOXTag(context, tagOrUUID, client)) 83 | } 84 | } 85 | 86 | /** 87 | * Resolve UUIDs for given usernames (or UUIDs) 88 | * 89 | * In principle, this method and the `action` should not throw any exception 90 | */ 91 | suspend fun resolveUUIDs(context: BungeeSafeguard, queries: Array, xbox: Boolean, client: HttpClient = this.client, action: suspend (ResolutionResult) -> Unit) { 92 | for (usernameOrUUID in queries) { 93 | coroutineScope { 94 | launch { 95 | try { 96 | val res = if (xbox) getUUIDFromXBOXTag(context, usernameOrUUID, client) 97 | else getUUIDFromString(usernameOrUUID, client) 98 | try { 99 | action(ResolutionResult(null, res, usernameOrUUID)) 100 | } catch (err: Throwable) { 101 | err.printStackTrace() 102 | } 103 | } catch (err: Throwable) { 104 | action(ResolutionResult(err, null, usernameOrUUID)) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/Events.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard 2 | 3 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 4 | import cyou.untitled.bungeesafeguard.list.ListManager 5 | import net.md_5.bungee.api.ChatColor 6 | import net.md_5.bungee.api.chat.TextComponent 7 | import net.md_5.bungee.api.event.LoginEvent 8 | import net.md_5.bungee.api.event.PostLoginEvent 9 | import net.md_5.bungee.api.event.ServerConnectEvent 10 | import net.md_5.bungee.api.plugin.Listener 11 | import net.md_5.bungee.event.EventHandler 12 | import cyou.untitled.bungeesafeguard.list.UUIDList 13 | import kotlinx.coroutines.* 14 | import java.util.* 15 | 16 | @Suppress("MemberVisibilityCanBePrivate") 17 | open class Events(val context: BungeeSafeguard): Listener { 18 | protected val config: Config 19 | get() = context.config 20 | protected val userCache: UserCache 21 | get() = context.userCache 22 | protected val listMgr: ListManager 23 | get() = context.listMgr 24 | 25 | /** 26 | * Update user cache 27 | * 28 | * We only care about the usernames of users in the whitelist or the blacklist 29 | * 30 | * This should be called AFTER `shouldKick`, who might update the two lists, 31 | * because this method will check if the user is in one of the two lists 32 | * 33 | * @param id User's UUID 34 | * @param username Username 35 | */ 36 | open suspend fun updateUserCache(id: UUID, username: String) { 37 | val cache = userCache 38 | if (listMgr.inAnyList(id)) { 39 | cache.addAndSave(id, username) 40 | } else cache.removeAndSave(id) // We don't need to know the username of this user anymore 41 | } 42 | 43 | /** 44 | * Update user cache asynchronously 45 | * 46 | * We only care about the usernames of users in the whitelist or the blacklist 47 | * 48 | * This should be called AFTER `shouldKick`, who might update the two lists, 49 | * because this method will check if the user is in one of the two lists 50 | * 51 | * @param id User's UUID 52 | * @param username Username 53 | */ 54 | @OptIn(DelicateCoroutinesApi::class) 55 | open fun updateUserCacheAsync(id: UUID, username: String) = GlobalScope.launch(context.dispatcher) { 56 | updateUserCache(id, username) 57 | } 58 | 59 | protected open fun logKick(username: String, id: UUID, kicker: UUIDList?) { 60 | when (kicker?.behavior) { 61 | UUIDList.Companion.Behavior.KICK_NOT_MATCHED -> context.logger.info("Player ${ChatColor.AQUA}${username} ${ChatColor.BLUE}(${id})${ChatColor.RESET} blocked for not being in the ${kicker.name}") 62 | UUIDList.Companion.Behavior.KICK_MATCHED -> context.logger.info("Player ${ChatColor.RED}${username} ${ChatColor.BLUE}(${id})${ChatColor.RESET} blocked for being in the ${kicker.name}") 63 | null -> context.logger.info("Player ${ChatColor.RED}${username} ${ChatColor.BLUE}(${id})${ChatColor.RESET} is blocked for safety because no list is enabled/loaded (yet)") 64 | } 65 | } 66 | 67 | protected open suspend fun possiblyKick(username: String, id: UUID, doKick: (UUIDList?) -> Unit) { 68 | val decision = listMgr.shouldKick(username, id) 69 | if (decision.kick) { 70 | val kicker = decision.list 71 | doKick(kicker) 72 | logKick(username, id, kicker) 73 | } 74 | updateUserCacheAsync(id, username) 75 | } 76 | 77 | @EventHandler 78 | open fun onPostLogin(event: PostLoginEvent) { 79 | val player = event.player 80 | val username = player.name 81 | val id = player.uniqueId 82 | runBlocking(context.dispatcher) { 83 | possiblyKick(username, id) { 84 | player.disconnect(TextComponent(it?.message ?: "")) 85 | } 86 | } 87 | updateUserCacheAsync(id, username) 88 | } 89 | 90 | @OptIn(DelicateCoroutinesApi::class) 91 | @EventHandler 92 | fun onLogin(event: LoginEvent) { 93 | val connection = event.connection 94 | val username = connection.name 95 | val id = connection.uniqueId 96 | if (id == null) { 97 | event.setCancelReason(TextComponent(config.noUUIDMessage ?: "")) 98 | event.isCancelled = true 99 | context.logger.info("${ChatColor.YELLOW}Player ${ChatColor.RED}${username} ${ChatColor.YELLOW}has no UUID, blocked for safety") 100 | return 101 | } 102 | event.registerIntent(context) 103 | GlobalScope.launch(context.dispatcher) { 104 | possiblyKick(username, id) { 105 | event.setCancelReason(TextComponent(it?.message ?: "")) 106 | event.isCancelled = true 107 | } 108 | event.completeIntent(context) 109 | } 110 | } 111 | 112 | @EventHandler 113 | fun onServerConnect(event: ServerConnectEvent) { 114 | val player = event.player 115 | val username = player.name 116 | val id = player.uniqueId 117 | runBlocking(context.dispatcher) { 118 | possiblyKick(username, id) { 119 | event.isCancelled = true 120 | player.disconnect(TextComponent(it?.message ?: "")) 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/storage/CachedBackend.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.storage 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import kotlinx.coroutines.* 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.coroutines.sync.withLock 7 | import net.md_5.bungee.api.CommandSender 8 | import net.md_5.bungee.api.plugin.Plugin 9 | import java.io.File 10 | import java.util.* 11 | 12 | /** 13 | * Not-so-smart wrapper backend that caches data for **read** 14 | * 15 | * We can also implement an asynchronous cached backend that synchronize data in the background; 16 | * when failed, dump in memory state and warn the user 17 | */ 18 | @Suppress("MemberVisibilityCanBePrivate") 19 | open class CachedBackend(context: Plugin, val backend: Backend, val allPaths: Array>) : Backend(context) { 20 | protected val lock = Mutex() 21 | protected val lists = mutableMapOf>() 22 | 23 | protected open suspend fun cacheAll() { 24 | // Let's cache all data into the memory 25 | val allLists = coroutineScope { 26 | val jobs = allPaths.map { async { backend.get(it) } } 27 | awaitAll(*jobs.toTypedArray()) 28 | } 29 | allLists.forEachIndexed { index, list -> lists[allPaths[index].joinToString(".")] = list.toMutableSet() } 30 | } 31 | 32 | protected open suspend fun cachePath(path: Array): MutableSet { 33 | val list = backend.get(path).toMutableSet() 34 | lists[path.joinToString(".")] = list 35 | return list 36 | } 37 | 38 | override suspend fun init(commandSender: CommandSender?) = lock.withLock { 39 | backend.init(commandSender) 40 | cacheAll() 41 | } 42 | 43 | override suspend fun close(commandSender: CommandSender?) = backend.close(commandSender) 44 | 45 | override suspend fun reload(commandSender: CommandSender?) = lock.withLock { 46 | backend.reload(commandSender) 47 | lists.clear() 48 | cacheAll() 49 | } 50 | 51 | protected open suspend fun getList(path: Array): MutableSet { 52 | val pathString = path.joinToString(".") 53 | return lists[pathString] ?: cachePath(path) 54 | } 55 | 56 | protected open suspend fun warnInconsistency(call: String, vararg args: Any?) { 57 | val logger = BungeeSafeguard.getPlugin().logger 58 | val prettyCall = StringBuilder() 59 | .append(call, '(', args.joinToString { 60 | try { 61 | when (it) { 62 | is Array<*> -> if (it.isEmpty()) "" else "\"${it.joinToString(".")}\"" 63 | is List<*> -> "[ ${it.joinToString(prefix = "\"", postfix = "\"")} ]" 64 | is Set<*> -> "Set{ ${it.joinToString(prefix = "\"", postfix = "\"")} }" 65 | else -> it.toString() 66 | } 67 | } catch (err: Throwable) { 68 | "" 69 | } 70 | }, ')').toString() 71 | logger.warning( 72 | "$this detects inconsistency when calling $prettyCall\n" + 73 | "If you did not modify the lists (bypassing the interfaces provided by BungeeSafeguard), intentionally or accidentally," + 74 | if (isDefaultBackend()) { 75 | " it is possibly a bug of the default backend implementation.\n" + 76 | "If you believe this is a bug, please report this to https://github.com/Luluno01/BungeeSafeguard/issues" 77 | } else { 78 | " it is possibly a bug of the backend implementation ($backend) your are using.\n" + 79 | "If you believe this is a bug, please report this to its developer." 80 | } 81 | ) 82 | } 83 | 84 | override suspend fun add(path: Array, rawRecord: String): Boolean = lock.withLock { 85 | val list = getList(path) 86 | return if (list.add(rawRecord)) { 87 | backend.add(path, rawRecord).also { if (!it) warnInconsistency("add", path, rawRecord) } 88 | } else { 89 | false 90 | } 91 | } 92 | 93 | override suspend fun remove(path: Array, rawRecord: String): Boolean = lock.withLock { 94 | val list = getList(path) 95 | return if (list.remove(rawRecord)) { 96 | backend.remove(path, rawRecord).also { if (!it) warnInconsistency("remove", path, rawRecord) } 97 | } else { 98 | false 99 | } 100 | } 101 | 102 | override suspend fun has(path: Array, rawRecord: String): Boolean = lock.withLock { 103 | val list = getList(path) 104 | return list.contains(rawRecord) 105 | } 106 | 107 | override suspend fun getSize(path: Array): Int = lock.withLock { 108 | val list = getList(path) 109 | return list.size 110 | } 111 | 112 | override suspend fun get(path: Array): Set = lock.withLock { 113 | return getList(path) 114 | } 115 | 116 | override suspend fun moveToListIfInLazyList( 117 | username: String, 118 | id: UUID, 119 | mainPath: Array, 120 | lazyPath: Array 121 | ): Boolean = lock.withLock { 122 | val lazyList = getList(lazyPath) 123 | return if (lazyList.remove(username)) { 124 | getList(mainPath).add(id.toString()) 125 | backend.moveToListIfInLazyList(username, id, mainPath, lazyPath).also { if (!it) warnInconsistency("moveToListIfInLazyList", username, id, mainPath, lazyPath) } 126 | } else { 127 | false 128 | } 129 | } 130 | 131 | override suspend fun onReloadConfigFile(newConfig: File, commandSender: CommandSender?) { 132 | backend.onReloadConfigFile(newConfig, commandSender) 133 | reload(commandSender) 134 | } 135 | 136 | override fun toString(): String = "CachedBackend($backend)" 137 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/storage/Backend.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.storage 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import net.md_5.bungee.api.ChatColor 7 | import net.md_5.bungee.api.CommandSender 8 | import net.md_5.bungee.api.plugin.Plugin 9 | import java.io.File 10 | import java.util.* 11 | 12 | /** 13 | * Storage backend 14 | * 15 | * * Implementation should be thread/coroutine-safe 16 | * * Backend should be registered with `Backend.Companion.registerBackend` to take effect 17 | * 18 | * Also note that we have the following observations: 19 | * 20 | * * Read is way more frequently than write 21 | */ 22 | abstract class Backend(val context: Plugin) { 23 | companion object { 24 | private var backend: Backend? = null 25 | private var isDefaultBackend = true 26 | private val lock = Mutex() 27 | @Volatile 28 | private var nextId = 0 29 | 30 | /** 31 | * Get backend in use 32 | */ 33 | suspend fun getBackend(): Backend { 34 | lock.withLock { 35 | if (backend == null) throw IllegalStateException("No backend registered yet") 36 | return backend!! 37 | } 38 | } 39 | 40 | /** 41 | * If current backend is the default one 42 | */ 43 | suspend fun isDefaultBackend(): Boolean = lock.withLock { isDefaultBackend } 44 | 45 | /** 46 | * Register backend, this should be called by the external backend (if installed) 47 | * to register itself and replace the default backend exactly once 48 | */ 49 | suspend fun registerBackend(backend: Backend, plugin: Plugin? = null) { 50 | lock.withLock { 51 | val oldBackend = this.backend 52 | when { 53 | oldBackend == null -> { 54 | this.backend = backend 55 | isDefaultBackend = true 56 | // Don't perform sanity check now, the lists are not yet created 57 | } 58 | isDefaultBackend -> { 59 | // Replace default backend 60 | oldBackend.close(null) 61 | this.backend = backend 62 | isDefaultBackend = false 63 | } 64 | else -> throw IllegalStateException("A non-default backend is already set") 65 | } 66 | (plugin?.logger ?: BungeeSafeguard.getPlugin().logger).info("Using storage backend ${ChatColor.AQUA}$backend") 67 | } 68 | } 69 | } 70 | val id = nextId++ 71 | 72 | /** 73 | * Do the initialization 74 | * (e.g., create the underlying file if it does not exist, 75 | * connect to the database) 76 | */ 77 | abstract suspend fun init(commandSender: CommandSender?) 78 | 79 | /** 80 | * Close the backend 81 | * (e.g., close files, database connections) 82 | */ 83 | abstract suspend fun close(commandSender: CommandSender?) 84 | 85 | /** 86 | * Reload from the backend 87 | */ 88 | abstract suspend fun reload(commandSender: CommandSender?) 89 | 90 | /** 91 | * Add a record to the designated storage path 92 | * 93 | * @param path the storage path, e.g., `[ "whitelist", "main" ]` or `[ "whitelist", "lazy" ]` 94 | * @param rawRecord the record to add 95 | * @return `true` if the record is added, `false` if the record is already in the list 96 | */ 97 | abstract suspend fun add(path: Array, rawRecord: String): Boolean 98 | 99 | /** 100 | * Remove a record from the designated storage path 101 | * 102 | * @param path the storage path, e.g., `[ "whitelist", "main" ]` or `[ "whitelist", "lazy" ]` 103 | * @param rawRecord the record to remove 104 | * @return `true` if the record is removed, `false` if the record is not in the list in the first place 105 | */ 106 | abstract suspend fun remove(path: Array, rawRecord: String): Boolean 107 | 108 | /** 109 | * Check if a record is in the list at the designated storage path 110 | * 111 | * @param path the storage path, e.g., `[ "whitelist", "main" ]` or `[ "whitelist", "lazy" ]` 112 | * @param rawRecord the record to check 113 | * @return `true` if the list contains the record, `false` otherwise 114 | */ 115 | abstract suspend fun has(path: Array, rawRecord: String): Boolean 116 | 117 | /** 118 | * Get the size of the list at the designated storage path 119 | * 120 | * @param path the storage path, e.g., `[ "whitelist", "main" ]` or `[ "whitelist", "lazy" ]` 121 | */ 122 | abstract suspend fun getSize(path: Array): Int 123 | 124 | /** 125 | * Get a readonly copy of the list at the designated storage path 126 | * 127 | * @param path the storage path, e.g., `[ "whitelist", "main" ]` or `[ "whitelist", "lazy" ]` 128 | */ 129 | abstract suspend fun get(path: Array): Set 130 | 131 | /** 132 | * Move record from lazy list to main list if any 133 | * 134 | * @param username username 135 | * @param id UUID of the player 136 | * @param mainPath storage path of the main list 137 | * @param lazyPath storage path of the lazy list 138 | * @return if the player is in lazy list 139 | */ 140 | abstract suspend fun moveToListIfInLazyList(username: String, id: UUID, mainPath: Array, lazyPath: Array): Boolean 141 | 142 | /** 143 | * Possibly handle reloading of the main config file; 144 | * 145 | * Invoked by `config.load` with lock acquired 146 | * 147 | * Any exception will be considered as fatal 148 | */ 149 | abstract suspend fun onReloadConfigFile(newConfig: File, commandSender: CommandSender?) 150 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/commands/ListCommandImpl.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.commands 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.Config 5 | import cyou.untitled.bungeesafeguard.commands.ListCommand.Companion.SubcommandName.* 6 | import cyou.untitled.bungeesafeguard.commands.subcommands.SubcommandRegistry 7 | import cyou.untitled.bungeesafeguard.commands.subcommands.list.* 8 | import cyou.untitled.bungeesafeguard.list.ListManager 9 | import cyou.untitled.bungeesafeguard.list.UUIDList 10 | import net.md_5.bungee.api.ChatColor 11 | import net.md_5.bungee.api.CommandSender 12 | import net.md_5.bungee.api.chat.TextComponent 13 | import java.io.File 14 | 15 | open class ListCommandImpl( 16 | context: BungeeSafeguard, 17 | listMgr: ListManager, 18 | list: UUIDList, 19 | name: String, 20 | permission: String, vararg aliases: String 21 | ): 22 | ListCommand(context, listMgr, list, name, permission, *aliases) { 23 | companion object { 24 | open class Usage(val name: String): SubcommandRegistry.Companion.UsageSender { 25 | override fun sendUsage(sender: CommandSender) { 26 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Usage:")) 27 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} /$name import ")) 28 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} For normal Mojang players: /$name ")) 29 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} For XBOX Live players: /$name ")) 30 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} For both Mojang and XBOX players: /$name ")) 31 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} /$name ")) 32 | sender.sendMessage(TextComponent("${ChatColor.YELLOW} /$name ")) 33 | } 34 | } 35 | } 36 | @Suppress("MemberVisibilityCanBePrivate") 37 | protected val cmdReg = SubcommandRegistry(context, Usage(name)) 38 | 39 | init { 40 | cmdReg.registerSubcommand( 41 | ImportCommand(context, IMPORT, listMgr, list), 42 | AddCommand(context, ADD, listMgr, list, false), 43 | AddCommand(context, X_ADD, listMgr, list, true), 44 | LazyAddCommand(context, LAZY_ADD, listMgr, list), 45 | RemoveCommand(context, REMOVE, listMgr, list, false), 46 | RemoveCommand(context, X_REMOVE, listMgr, list, true), 47 | LazyRemoveCommand(context, LAZY_REMOVE, listMgr, list), 48 | OnCommand(context, ON, listMgr, list), 49 | OffCommand(context, OFF, listMgr, list), 50 | DumpCommand(context, LIST, listMgr, list) 51 | ) 52 | } 53 | 54 | protected open val config: Config 55 | get() = context.config 56 | 57 | /** 58 | * Name of this list 59 | */ 60 | protected open val listName: String 61 | get() = list.name 62 | 63 | protected open val lazyName: String 64 | get() = list.lazyName 65 | 66 | /** 67 | * Send confirm message to the command sender 68 | */ 69 | open fun sendConfirmMessage(sender: CommandSender, subcommand: Base, parsed: Parsed) { 70 | val realArgs = parsed.realArgs 71 | val action = parsed.action 72 | if (action.isImport) { 73 | sender.sendMessage( 74 | TextComponent("${ChatColor.YELLOW}Are you sure you want to ${ChatColor.AQUA}${ChatColor.BOLD}import UUIDs " + 75 | "${ChatColor.RESET}${ChatColor.YELLOW}from the following ${ChatColor.AQUA}${ChatColor.BOLD}external JSON file " + 76 | "${ChatColor.RESET}${ChatColor.YELLOW}to the ${ChatColor.AQUA}${ChatColor.BOLD}$listName " + 77 | "${ChatColor.RESET}${ChatColor.YELLOW}in the config file \"${ChatColor.AQUA}${ChatColor.BOLD}${config.configInUse}${ChatColor.RESET}${ChatColor.YELLOW}\"?\n" + 78 | "${ChatColor.AQUA}${ChatColor.BOLD} ${File(realArgs[0]).absolutePath}") 79 | ) 80 | } else { 81 | sender.sendMessage( 82 | TextComponent("${ChatColor.YELLOW}Are you sure you want to ${ChatColor.AQUA}${ChatColor.BOLD}${if (action.isAdd) "add" else "remove"} " + 83 | "${ChatColor.RESET}${ChatColor.YELLOW}the following ${ChatColor.AQUA}${ChatColor.BOLD}${if (action.isXBOX) "XBOX Live" else "Minecraft"} player(s) " + 84 | "${ChatColor.RESET}${ChatColor.YELLOW}${if (action.isAdd) "to" else "from"} the ${ChatColor.AQUA}${ChatColor.BOLD}${if (action.isLazyList) lazyName else listName} " + 85 | "${ChatColor.RESET}${ChatColor.YELLOW}in the config file \"${ChatColor.AQUA}${ChatColor.BOLD}${config.configInUse}${ChatColor.RESET}${ChatColor.YELLOW}\"?\n" + 86 | "${ChatColor.AQUA}${ChatColor.BOLD}" + realArgs.joinToString("\n") { " $it" }) 87 | ) 88 | } 89 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Please use ${ChatColor.AQUA}/$name confirm ${ChatColor.YELLOW}in 10s to confirm")) 90 | } 91 | 92 | protected open fun possiblyDoAfterConfirmation(sender: CommandSender, subcommand: Base, parsed: Parsed) { 93 | if (config.confirm && subcommand.confirmable) { 94 | sendConfirmMessage(sender, subcommand, parsed) 95 | confirm(sender, { subcommand.execute(sender, parsed.realArgs) }) 96 | } else { 97 | subcommand.execute(sender, parsed.realArgs) // Do it now 98 | } 99 | } 100 | 101 | protected open fun onConfirm(sender: CommandSender) { 102 | if (!confirmed(sender)) { 103 | sender.sendMessage(TextComponent("${ChatColor.YELLOW}Nothing to confirm, it might have expired")) 104 | } 105 | } 106 | 107 | override fun execute(sender: CommandSender, args: Array) { 108 | val fixedArgs = args.omitEmpty() 109 | if (fixedArgs.getOrNull(0) == "confirm") { 110 | onConfirm(sender) 111 | } else { 112 | val cmd = cmdReg.getSubcommand(sender, fixedArgs) as Base? ?: return 113 | val parsed = cmd.parseArgs(sender, fixedArgs) ?: return Usage(name).sendUsage(sender) 114 | possiblyDoAfterConfirmation(sender, cmd, parsed) 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/list/ListManager.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.list 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import net.md_5.bungee.api.ChatColor 6 | import net.md_5.bungee.api.CommandSender 7 | import net.md_5.bungee.api.plugin.Plugin 8 | import java.util.* 9 | 10 | /** 11 | * The list manager 12 | */ 13 | open class ListManager(@Suppress("MemberVisibilityCanBePrivate") val context: Plugin) { 14 | companion object { 15 | data class KickDecision( 16 | val kick: Boolean, 17 | val list: UUIDList? 18 | ) 19 | } 20 | 21 | /** 22 | * The lists 23 | * 24 | * Smaller index means higher priority 25 | */ 26 | protected open val mLists: MutableList = mutableListOf() 27 | protected open val lock = Mutex() 28 | open val lists: List 29 | get() = mLists 30 | 31 | open suspend fun withLock(owner: Any? = null, action: suspend () -> T): T { 32 | lock.withLock(owner) { 33 | return action() 34 | } 35 | } 36 | 37 | /** 38 | * Create a new list (list created first has higher priority) 39 | */ 40 | suspend fun createList( 41 | name: String, lazyName: String, 42 | path: Array, lazyPath: Array, 43 | behavior: UUIDList.Companion.Behavior, 44 | message: String?, 45 | initEnabled: Boolean, 46 | onSetEnabled: suspend (Boolean, CommandSender?) -> Unit 47 | ): UUIDListImpl = withLock { UUIDListImpl( 48 | name, lazyName, 49 | path, lazyPath, 50 | behavior, 51 | message, 52 | initEnabled, 53 | onSetEnabled 54 | ).also { mLists.add(it) } } 55 | 56 | /** 57 | * Get the list by its name 58 | */ 59 | open fun forName(name: String): UUIDList? = mLists.find { it.name == name } 60 | 61 | open fun indexOf(name: String): Int { 62 | return mLists.indexOfFirst { it.name == name } 63 | } 64 | 65 | open fun indexOf(list: UUIDList): Int { 66 | return mLists.indexOf(list) 67 | } 68 | 69 | open suspend fun inListsWithHigherPriority(id: UUID, refListIndex: Int): List { 70 | val higher = mutableListOf() 71 | for (i in 0 until refListIndex) { 72 | val list = mLists[i] 73 | if (list.has(id)) { 74 | higher.add(list) 75 | } 76 | } 77 | return higher 78 | } 79 | 80 | open suspend fun inListsWithHigherPriority(id: UUID, refList: UUIDList): List { 81 | return inListsWithHigherPriority(id, indexOf(refList).also { assert(it >= 0) { "Given reference list is not managed by this list manager" } }) 82 | } 83 | 84 | open suspend fun inListsWithLowerPriority(id: UUID, refListIndex: Int): List { 85 | val lower = mutableListOf() 86 | for (i in refListIndex + 1 until mLists.size) { 87 | val list = mLists[i] 88 | if (list.has(id)) { 89 | lower.add(list) 90 | } 91 | } 92 | return lower 93 | } 94 | 95 | open suspend fun inListsWithLowerPriority(id: UUID, refList: UUIDList): List { 96 | return inListsWithLowerPriority(id, indexOf(refList).also { assert(it >= 0) }) 97 | } 98 | 99 | open suspend fun inLazyListsWithHigherPriority(username: String, refListIndex: Int): List { 100 | val higher = mutableListOf() 101 | for (i in 0 until refListIndex) { 102 | val list = mLists[i] 103 | if (list.lazyHas(username)) { 104 | higher.add(list) 105 | } 106 | } 107 | return higher 108 | } 109 | 110 | open suspend fun inLazyListsWithHigherPriority(username: String, refList: UUIDList): List { 111 | return inLazyListsWithHigherPriority(username, indexOf(refList).also { assert(it >= 0) }) 112 | } 113 | 114 | open suspend fun inLazyListsWithLowerPriority(username: String, refListIndex: Int): List { 115 | val lower = mutableListOf() 116 | for (i in refListIndex + 1 until mLists.size) { 117 | val list = mLists[i] 118 | if (list.lazyHas(username)) { 119 | lower.add(list) 120 | } 121 | } 122 | return lower 123 | } 124 | 125 | open suspend fun inLazyListsWithLowerPriority(username: String, refList: UUIDList): List { 126 | return inLazyListsWithLowerPriority(username, indexOf(refList).also { assert(it >= 0) }) 127 | } 128 | 129 | /** 130 | * Determine if given player is in any main list 131 | */ 132 | open suspend fun inAnyList(id: UUID): Boolean = mLists.any { it.has(id) } 133 | 134 | /** 135 | * Check if we should kick a player 136 | * 137 | * Note this method will possibly start a background task to save the config 138 | * 139 | * @param username Player's username 140 | * @param id Player's UUID 141 | * @return The decision 142 | */ 143 | open suspend fun shouldKick(username: String, id: UUID): KickDecision { 144 | if (withLock { lists.isEmpty() }) { 145 | return KickDecision(true, null) 146 | } 147 | var kicker: UUIDList? = null 148 | for (list in lists) { 149 | if (list.moveToListIfInLazyList(username, id)) { 150 | // Just update, don't make decision yet 151 | when (list.behavior) { 152 | UUIDList.Companion.Behavior.KICK_NOT_MATCHED -> { 153 | context.logger.info("${ChatColor.DARK_GREEN}Move player from ${list.lazyName} to ${list.name} ${ChatColor.AQUA}(${username} => ${id})") 154 | } 155 | UUIDList.Companion.Behavior.KICK_MATCHED -> { 156 | context.logger.info("${ChatColor.DARK_PURPLE}Move player from ${list.lazyName} to ${list.name} ${ChatColor.AQUA}(${username} => ${id})") 157 | } 158 | } 159 | } 160 | } 161 | for (list in lists) { 162 | if (list.enabled) { 163 | when (list.behavior) { 164 | UUIDList.Companion.Behavior.KICK_NOT_MATCHED -> { 165 | if (!list.has(id)) { 166 | kicker = list 167 | break 168 | } 169 | } 170 | UUIDList.Companion.Behavior.KICK_MATCHED -> { 171 | if (list.has(id)) { 172 | kicker = list 173 | break 174 | } 175 | } 176 | } 177 | } 178 | } 179 | return KickDecision(kicker != null, kicker) 180 | } 181 | } -------------------------------------------------------------------------------- /developer.md: -------------------------------------------------------------------------------- 1 | # Developing Extension Plugin for BungeeSafeguard 2 | 3 | Start from v3.0, BungeeSafeguard officially announced its Java/Kotlin APIs (I will try to keep them as stable as possible). Including the followings: 4 | 5 | * Lists manipulation 6 | * Custom storage backend for lists 7 | 8 | - [Developing Extension Plugin for BungeeSafeguard](#developing-extension-plugin-for-bungeesafeguard) 9 | - [Get Started](#get-started) 10 | - [Add BungeeSafeguard as a Dependency](#add-bungeesafeguard-as-a-dependency) 11 | - [Library Collision Workaround](#library-collision-workaround) 12 | - [Get the Plugin Instance](#get-the-plugin-instance) 13 | - [Get the Storage Backend](#get-the-storage-backend) 14 | - [Register Custom Storage Backend](#register-custom-storage-backend) 15 | - [Lists Manipulation](#lists-manipulation) 16 | - [Storage Backend](#storage-backend) 17 | 18 | ## Get Started 19 | 20 | *Note: this section assumes you have mastered the basics of Kotlin/Java developing, including the usage of a proper IDE and build tools*. 21 | 22 | To interact with BungeeSafeguard from your plugin (you need to develop your own BungeeCord plugin to invoke the APIs; see [BungeeCord Plugin Development](https://www.spigotmc.org/wiki/bungeecord-plugin-development/) if you are a beginner). 23 | 24 | ### Add BungeeSafeguard as a Dependency 25 | 26 | Gradle (if you download the `BungeeSafeguard.jar` file into the `libs` folder): 27 | 28 | ``` 29 | repositories { 30 | mavenCentral() 31 | flatDir { 32 | dirs 'libs' 33 | } 34 | // ... 35 | } 36 | 37 | dependencies { 38 | compileOnly name: 'BungeeSafeguard' 39 | // ... 40 | } 41 | ``` 42 | 43 | Remember to declare BungeeSafeguard as a hard dependency in your `plugin.yml`. 44 | 45 | ### Library Collision Workaround 46 | 47 | Your extension plugin has to share the same Kotlin runtime and other transitive dependencies with BungeeSafeguard, otherwise a `java.lang.LinkageError: loader constraint violation` exception will occur (see [this issue](https://github.com/SpigotMC/BungeeCord/issues/3139)). 48 | 49 | To fix this problem, call `DependencyFixer.fixLibraryLoader` before interacting with BungeeSafeguard: 50 | 51 | ```Kotlin 52 | import cyou.untitled.bungeesafeguard.helpers.DependencyFixer 53 | 54 | // ... 55 | DependencyFixer.fixLibraryLoader(YourPlugin::class.java.classLoader) 56 | // ... 57 | ``` 58 | 59 | ### Get the Plugin Instance 60 | 61 | ```Kotlin 62 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 63 | 64 | // ... 65 | val bsg = BungeeSafeguard.getPlugin() 66 | bsg.whitelist.add(someId) 67 | // ... 68 | ``` 69 | 70 | Or 71 | 72 | ```Kotlin 73 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 74 | 75 | // ... 76 | // `this` is your plugin instance 77 | val bsg = this.proxy.pluginManager.getPlugin("BungeeSafeguard") as BungeeSafeguard 78 | // ... 79 | ``` 80 | 81 | Or 82 | 83 | ```Kotlin 84 | import net.md_5.bungee.api.plugin.Listener 85 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 86 | import cyou.untitled.bungeesafeguard.events.BungeeSafeguardEnabledEvent 87 | import net.md_5.bungee.event.EventHandler 88 | 89 | class Events(private val context: Plugin): Listener { 90 | @EventHandler 91 | fun onBungeeSafeguardEnabled(event: BungeeSafeguardEnabledEvent) { 92 | // This event will only be captured if you register the listener **before** BungeeSafeguard is enabled 93 | // To determine if you have missed this event, check `bsg.enabled` 94 | val bsg: BungeeSafeguard = event.bsg 95 | // ... 96 | } 97 | } 98 | ``` 99 | 100 | ### Get the Storage Backend 101 | 102 | In most cases you don't need to get the backend instance but in case you have to: 103 | 104 | ```Kotlin 105 | import cyou.untitled.bungeesafeguard.storage.Backend 106 | 107 | // ... 108 | Backend.getBackend().add(arrayOf("whitelist", "lazy"), "someone") 109 | // ... 110 | ``` 111 | 112 | ### Register Custom Storage Backend 113 | 114 | If you implemented your own storage backend, initialize and register it: 115 | 116 | ```Kotlin 117 | import cyou.untitled.bungeesafeguard.storage.Backend 118 | 119 | // ... 120 | val backend = YourBackend() // Optionally, use the CachedBackend wrapper (please refer to the source code of cyou.untitled.bungeesafeguard.storage.Backend.CachedBackend) 121 | backend.init() 122 | Backend.registerBackend(backend, yourPlugin) 123 | // ... 124 | ``` 125 | 126 | ## Lists Manipulation 127 | 128 | To access the lists, do it via `bsg.listMgr`, where `bsg` is the instance of BungeeSafeguard plugin and `listMgr` is a [`ListManager`](./src/main/kotlin/cyou/untitled/bungeesafeguard/list/ListManager.kt) object. 129 | Alternatively, access the lists via the shortcuts `bsg.whitelist` and `bsg.blacklist`. All lists are [`UUIDList`](./src/main/kotlin/cyou/untitled/bungeesafeguard/list/UUIDList.kt) objects and managed by [`ListManager`](./src/main/kotlin/cyou/untitled/bungeesafeguard/list/ListManager.kt). 130 | 131 | For example, to add a new player with UUID "c6526e46-d718-11eb-b8bc-0242ac130003" to the whitelist: 132 | 133 | ```Kotlin 134 | bsg.whitelist.add(UUID.fromString("c6526e46-d718-11eb-b8bc-0242ac130003")) 135 | ``` 136 | 137 | Turn on/off the list: 138 | 139 | ```Kotlin 140 | bsg.whitelist.on() 141 | bsg.whitelist.off() 142 | ``` 143 | 144 | If you want username-to-UUID translation, it is available via [`UserUUIDHelper.resolveUUIDs`](./src/main/kotlin/cyou/untitled/bungeesafeguard/helpers/UserUUIDHelper.kt): 145 | 146 | ```Kotlin 147 | UserUUIDHelper.resolveUUIDs(bsg, arrayOf("user1", "user2"), xbox = false) { 148 | if (it.err == null) { 149 | bsg.whitelist.add(it.result!!.id) 150 | } 151 | } 152 | ``` 153 | 154 | ## Storage Backend 155 | 156 | Firstly, the storage backend is an extra layer that abstracts out the details of the lists storage, i.e., where the list records are stored. 157 | By default, the backend is a [`ConfigBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/ConfigBackend.kt) (which extends [`YAMLBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/YAMLBackend.kt)) wrapped by [`CachedBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/CachedBackend.kt). It caches all list contents for fast read access and stores the contents in the same config file used by BungeeSafeguard. When BungeeSafeguard reloads/loads a new config file, it clears the cache and uses the new config file as backing file. 158 | 159 | Now, suppose you want to implement your custom backend that stores the lists in Redis (so that your multiple BungeeCord networks can share the same lists), what should you do? In general, there are 4 steps: 160 | 161 | 1. Create a standalone BungeeCord plugin 162 | 2. Implement the [`Backend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/Backend.kt) interface 163 | 3. Register an instance of a `Backend` you just implemented (see [here](#register-custom-storage-backend) for example) 164 | 4. Massive tests 165 | 166 | For examples of how to implement `Backend`, see [`YAMLBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/YAMLBackend.kt), [`ConfigBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/ConfigBackend.kt) and [`CachedBackend`](./src/main/kotlin/cyou/untitled/bungeesafeguard/storage/CachedBackend.kt). 167 | -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/UserCache.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard 2 | 3 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 4 | import cyou.untitled.bungeesafeguard.storage.FileManager 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.coroutines.sync.withLock 7 | import kotlinx.coroutines.withContext 8 | import net.md_5.bungee.api.plugin.Plugin 9 | import net.md_5.bungee.config.Configuration 10 | import net.md_5.bungee.config.ConfigurationProvider 11 | import net.md_5.bungee.config.YamlConfiguration 12 | import java.io.File 13 | import java.io.IOException 14 | import java.util.* 15 | 16 | /** 17 | * A map from each player's UUID to at most 10 known usernames 18 | * 19 | * FIXME: `clear` and `remove` cannot be compiled and are temporarily commented out 20 | */ 21 | open class UserCache(val context: Plugin): Map> { 22 | companion object { 23 | const val CACHE_FILE = "usercache.yml" 24 | const val CACHE = "cache" // In case we want to add new entries in the future 25 | const val MAX_KNOWN_NAMES = 10 26 | } 27 | 28 | protected open val mutex = Mutex() 29 | 30 | /** 31 | * Lock the entire cache 32 | */ 33 | protected open suspend fun withLock(owner: Any? = null, action: suspend () -> T): T { 34 | mutex.withLock(owner) { 35 | return action() 36 | } 37 | } 38 | 39 | /** 40 | * The user cache YAML object 41 | */ 42 | protected open var cache: Configuration? = null 43 | 44 | /** 45 | * The underlying map 46 | */ 47 | protected open val map = mutableMapOf>() 48 | 49 | override val size: Int 50 | get() = map.size 51 | 52 | override fun containsKey(key: UUID): Boolean = map.containsKey(key) 53 | 54 | override fun containsValue(value: List): Boolean = map.containsValue(value) 55 | 56 | override fun get(key: UUID): List? = map[key] 57 | 58 | override fun isEmpty(): Boolean = map.isEmpty() 59 | 60 | override val entries: Set>> 61 | get() = map.entries 62 | override val keys: MutableSet 63 | get() = map.keys 64 | override val values: Collection> 65 | get() = map.values 66 | 67 | protected open fun doClear() { 68 | cache?.set(CACHE, null) 69 | map.clear() 70 | } 71 | 72 | // open suspend fun clear() = withLock { doClear() } 73 | 74 | open suspend fun clearAndSave() { 75 | withLock { 76 | doClear() 77 | doSave() 78 | } 79 | } 80 | 81 | protected open fun doRemove(userId: UUID): List? { 82 | cache?.set("$CACHE.$userId", null) 83 | return map.remove(userId) 84 | } 85 | 86 | // open suspend fun remove(userId: UUID): List? = withLock { doRemove(userId) } 87 | 88 | open suspend fun removeAndSave(userId: UUID): List? { 89 | return withLock { 90 | val names = doRemove(userId) 91 | if (names != null) { 92 | doSave() 93 | } 94 | return@withLock names 95 | } 96 | } 97 | 98 | /** 99 | * Add the username for the user ID to the cache if: 100 | * 101 | * 1. It is the first known username of the user, or 102 | * 2. It differs from the last known username of the user 103 | * 104 | * @return `true` if the cache is changed 105 | */ 106 | protected open fun doAdd(userId: UUID, username: String): Boolean { 107 | return if (map.contains(userId)) { 108 | val names = map[userId]!! 109 | if (names.isEmpty() || names.last() != username) { 110 | names.add(username) 111 | if (names.size > MAX_KNOWN_NAMES) { 112 | names.removeFirst() 113 | } 114 | cache?.set("$CACHE.$userId", names) 115 | true 116 | } else { 117 | false 118 | } 119 | } else { 120 | val names = mutableListOf(username) 121 | map[userId] = names 122 | cache?.set("$CACHE.$userId", names) 123 | true 124 | } 125 | } 126 | 127 | // /** 128 | // * Add the username for the user ID to the cache if: 129 | // * 130 | // * 1. It is the first known username of the user, or 131 | // * 2. It differs from the last known username of the user 132 | // * 133 | // * @return `true` if the cache is changed 134 | // */ 135 | // open suspend fun add(userId: UUID, username: String): Boolean { 136 | // return withLock { doAdd(userId, username) } 137 | // } 138 | 139 | open suspend fun addAndSave(userId: UUID, username: String): Boolean { 140 | return withLock { 141 | return@withLock if (doAdd(userId, username)) { 142 | doSave() 143 | true 144 | } else { 145 | false 146 | } 147 | } 148 | } 149 | 150 | protected open val dataFolder: File 151 | get() = context.dataFolder 152 | 153 | /** 154 | * Create new user cache file if it does not exist yet 155 | */ 156 | @Suppress("BlockingMethodInNonBlockingContext") 157 | open suspend fun createNewCache() { 158 | if (!dataFolder.exists()) { 159 | dataFolder.mkdirs() 160 | } 161 | withContext(context.dispatcher) { 162 | File(dataFolder, CACHE_FILE).createNewFile() // Create only if it does not yet exist 163 | } 164 | } 165 | 166 | open suspend fun reload() { 167 | load() 168 | } 169 | 170 | protected open suspend fun doLoad() { 171 | val cache: Configuration 172 | try { 173 | cache = loadCacheFromFile() 174 | this.cache = cache 175 | } catch (err: IOException) { 176 | return 177 | } 178 | val rawCache = cache.getSection(CACHE) 179 | map.clear() 180 | for (uuidStr in rawCache.keys) { 181 | val uuid = try { 182 | UUID.fromString(uuidStr) 183 | } catch (err: IllegalArgumentException) { 184 | continue 185 | } 186 | val names = rawCache.getStringList(uuidStr) 187 | map[uuid] = names 188 | } 189 | } 190 | 191 | open suspend fun load() { 192 | withLock { doLoad() } 193 | } 194 | 195 | @Suppress("BlockingMethodInNonBlockingContext") 196 | open suspend fun loadCacheFromFile(): Configuration { 197 | createNewCache() 198 | val cacheFile = File(dataFolder, CACHE_FILE) 199 | return FileManager.withFile(cacheFile.path, "userCache.loadCacheFromFile") { 200 | return@withFile withContext(context.dispatcher) { 201 | ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(File(dataFolder, CACHE_FILE)) 202 | } 203 | } 204 | } 205 | 206 | @Suppress("BlockingMethodInNonBlockingContext") 207 | protected open suspend fun doSave(): Boolean { 208 | if (cache == null) { 209 | context.logger.warning("Cannot save user cache because it was never successfully loaded") 210 | return false 211 | } 212 | val cacheFile = File(dataFolder, CACHE_FILE) 213 | FileManager.withFile(cacheFile.path, "userCache.doSave") { 214 | withContext(context.dispatcher) { 215 | ConfigurationProvider.getProvider(YamlConfiguration::class.java) 216 | .save(cache, File(dataFolder, CACHE_FILE)) 217 | } 218 | } 219 | return true 220 | } 221 | 222 | open suspend fun save(): Boolean { 223 | return withLock { doSave() } 224 | } 225 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/BungeeSafeguardImpl.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard 2 | 3 | import cyou.untitled.bungeesafeguard.commands.ListCommandImpl 4 | import cyou.untitled.bungeesafeguard.events.BungeeSafeguardEnabledEvent 5 | import cyou.untitled.bungeesafeguard.helpers.ListChecker 6 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 7 | import cyou.untitled.bungeesafeguard.list.ListManager 8 | import cyou.untitled.bungeesafeguard.list.UUIDList 9 | import cyou.untitled.bungeesafeguard.storage.Backend 10 | import cyou.untitled.bungeesafeguard.storage.CachedBackend 11 | import cyou.untitled.bungeesafeguard.storage.ConfigBackend 12 | import kotlinx.coroutines.joinAll 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.runBlocking 15 | import net.md_5.bungee.api.ChatColor 16 | import org.bstats.bungeecord.Metrics 17 | import java.io.File 18 | import cyou.untitled.bungeesafeguard.commands.BungeeSafeguard as BSGCmd 19 | 20 | class BungeeSafeguardImpl: BungeeSafeguard() { 21 | override val config = Config(this) 22 | override val userCache = UserCache(this) 23 | override val listMgr = ListManager(this) 24 | private val events = Events(this) 25 | override lateinit var whitelist: UUIDList 26 | override lateinit var blacklist: UUIDList 27 | override lateinit var whitelistCommand: ListCommandImpl 28 | override lateinit var blacklistCommand: ListCommandImpl 29 | override var enabled: Boolean = false 30 | private set 31 | 32 | override fun onEnable() { 33 | runBlocking(dispatcher) { 34 | lateinit var defaultBackend: Backend 35 | joinAll( 36 | launch { 37 | config.load(null, shouldUpdateLists = false) 38 | 39 | // Init lists which depend on config 40 | // Blacklist has higher priority 41 | blacklist = listMgr.createList( 42 | BLACKLIST_NAME, LAZY_BLACKLIST_NAME, 43 | BLACKLIST, LAZY_BLACKLIST, 44 | UUIDList.Companion.Behavior.KICK_MATCHED, 45 | config.blacklistMessage, 46 | config.enableBlacklist 47 | ) { enabled, commandSender -> 48 | config.enableBlacklist = enabled 49 | config.save(commandSender) 50 | } 51 | 52 | // Whitelist has lower priority 53 | whitelist = listMgr.createList( 54 | WHITELIST_NAME, LAZY_WHITELIST_NAME, 55 | WHITELIST, LAZY_WHITELIST, 56 | UUIDList.Companion.Behavior.KICK_NOT_MATCHED, 57 | config.whitelistMessage, 58 | config.enableWhitelist 59 | ) { enabled, commandSender -> 60 | config.enableWhitelist = enabled 61 | config.save(commandSender) 62 | } 63 | 64 | // Init backend which depends on config and the lists 65 | defaultBackend = CachedBackend( 66 | this@BungeeSafeguardImpl, 67 | ConfigBackend(this@BungeeSafeguardImpl, File(dataFolder, config.configInUse)), 68 | arrayOf(BLACKLIST, LAZY_BLACKLIST, WHITELIST, LAZY_WHITELIST) 69 | ) 70 | defaultBackend.init(null) // Default backend must be loaded after the config because otherwise the config may not be created yet 71 | ListChecker.checkLists(this@BungeeSafeguardImpl, null, listMgr, { defaultBackend.get(it.lazyPath) }, { it.lazyName }) 72 | ListChecker.checkLists(this@BungeeSafeguardImpl, null, listMgr, { defaultBackend.get(it.path) }, { it.name }) 73 | Backend.registerBackend(defaultBackend, this@BungeeSafeguardImpl) 74 | }, 75 | launch { userCache.load() } // User cache does not depend on config or the default backend 76 | ) 77 | } 78 | 79 | // Register events 80 | proxy.pluginManager.registerListener(this, events) 81 | 82 | proxy.pluginManager.registerCommand(this, BSGCmd(this)) 83 | whitelistCommand = ListCommandImpl( 84 | this, listMgr, 85 | whitelist, "whitelist", 86 | "bungeesafeguard.whitelist", "wlist" 87 | ) 88 | proxy.pluginManager.registerCommand(this, whitelistCommand) 89 | blacklistCommand = ListCommandImpl( 90 | this, listMgr, 91 | blacklist, "blacklist", 92 | "bungeesafeguard.blacklist", "blist" 93 | ) 94 | proxy.pluginManager.registerCommand(this, blacklistCommand) 95 | 96 | Metrics(this, 11845) 97 | 98 | exposeInst() 99 | logger.info("${ChatColor.GREEN}BungeeSafeguard enabled") 100 | enabled = true 101 | proxy.pluginManager.callEvent(BungeeSafeguardEnabledEvent(this)) 102 | } 103 | 104 | override fun onDisable() { 105 | whitelistCommand.destroy() 106 | blacklistCommand.destroy() 107 | proxy.pluginManager.unregisterCommands(this) 108 | proxy.pluginManager.unregisterListener(events) 109 | logger.info("Saving configuration") 110 | try { 111 | runBlocking /* No more asynchronous tasks will be executed */ { config.save(null) } 112 | logger.info("Configuration saved") 113 | } catch (err: Throwable) { 114 | logger.severe("Failed to save configuration") 115 | err.printStackTrace() 116 | logger.warning("======== Start dumping name of config file in use for data recovery ========") 117 | logger.warning(config.configInUse) 118 | logger.warning("======== End dumping name of config file in use for data recovery ========") 119 | logger.warning("======== Start dumping whitelist message for data recovery ========") 120 | logger.warning(config.whitelistMessage) 121 | logger.warning("======== End dumping whitelist message for data recovery ========") 122 | logger.warning("======== Start dumping blacklist message for data recovery ========") 123 | logger.warning(config.blacklistMessage) 124 | logger.warning("======== End dumping blacklist message for data recovery ========") 125 | logger.warning("======== Start dumping whitelist enable state for data recovery ========") 126 | logger.warning(whitelist.enabled.toString()) 127 | logger.warning("======== End dumping whitelist enable state for data recovery ========") 128 | logger.warning("======== Start dumping blacklist enable state for data recovery ========") 129 | logger.warning(blacklist.enabled.toString()) 130 | logger.warning("======== End dumping blacklist enable state for data recovery ========") 131 | logger.warning("======== Start dumping XBL Web API URL for data recovery ========") 132 | logger.warning(config.xblWebAPIUrl) 133 | logger.warning("======== End dumping XBL Web API URL for data recovery ========") 134 | logger.warning("======== Start confirmation state for data recovery ========") 135 | logger.warning(config.confirm.toString()) 136 | logger.warning("======== End confirmation state for data recovery ========") 137 | } 138 | runBlocking /* No more asynchronous tasks will be executed */ { 139 | val backend = Backend.getBackend() 140 | val backendDesc = backend.toString() 141 | logger.info("Closing backend $backendDesc") 142 | backend.close(null) 143 | logger.info("Backend $backendDesc closed") 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/storage/YAMLBackend.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard.storage 2 | 3 | import cyou.untitled.bungeesafeguard.BungeeSafeguard 4 | import cyou.untitled.bungeesafeguard.helpers.ListChecker 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.coroutines.sync.withLock 7 | import net.md_5.bungee.api.ChatColor 8 | import net.md_5.bungee.api.CommandSender 9 | import net.md_5.bungee.api.chat.TextComponent 10 | import net.md_5.bungee.api.plugin.Plugin 11 | import net.md_5.bungee.config.Configuration 12 | import net.md_5.bungee.config.ConfigurationProvider 13 | import net.md_5.bungee.config.YamlConfiguration 14 | import java.io.File 15 | import java.util.* 16 | 17 | /** 18 | * The backend that uses a YAML file to store the lists in human-friendly format 19 | * (i.e., the legacy format in older versions) 20 | */ 21 | @Suppress("BlockingMethodInNonBlockingContext", "MemberVisibilityCanBePrivate") 22 | open class YAMLBackend(context: Plugin, initFile: File): Backend(context) { 23 | companion object { 24 | /* YAML entries */ 25 | const val WHITELIST = "whitelist" 26 | const val LAZY_WHITELIST = "lazy-whitelist" 27 | const val BLACKLIST = "blacklist" 28 | const val LAZY_BLACKLIST = "lazy-blacklist" 29 | 30 | val pathTranslations = mapOf( 31 | Pair("whitelist.main", WHITELIST), 32 | Pair("whitelist.lazy", LAZY_WHITELIST), 33 | Pair("blacklist.main", BLACKLIST), 34 | Pair("blacklist.lazy", LAZY_BLACKLIST) 35 | ) 36 | 37 | /** 38 | * Translate the path to legacy config entry 39 | * 40 | * @param path the list path 41 | */ 42 | fun translatePath(path: Array): String { 43 | assert(path.isNotEmpty()) { "Empty path" } 44 | val pathString = path.joinToString(".") 45 | return pathTranslations[pathString] ?: error("Invalid path \"$pathString\"") 46 | } 47 | 48 | protected val yamlConfigProvider = ConfigurationProvider.getProvider(YamlConfiguration::class.java)!! 49 | } 50 | 51 | protected var initialized = false 52 | var file: File = initFile 53 | protected set 54 | 55 | val name = "YAMLBackend-$id" 56 | 57 | protected val lock = Mutex() 58 | 59 | open suspend fun init(file: File, commandSender: CommandSender?) { 60 | lock.withLock { 61 | assert(!initialized) { "Reinitialize without first closing" } 62 | this.file = file 63 | FileManager.withFile(file.path, name) { 64 | // If it does not throw, assume the file is OK 65 | yamlConfigProvider.load(it) 66 | } 67 | initialized = true 68 | commandSender?.sendMessage(TextComponent("${ChatColor.GREEN}YAMLBackend is using \"${file.path}\" as the underlying storage file")) 69 | } 70 | // Sanity check 71 | val bsg = try { 72 | BungeeSafeguard.getPlugin() 73 | } catch (err: UninitializedPropertyAccessException) { return } 74 | val listMgr = bsg.listMgr 75 | ListChecker.checkLists(bsg, null, listMgr, { get(it.lazyPath) }, { it.lazyName }) 76 | ListChecker.checkLists(bsg, null, listMgr, { get(it.path) }, { it.name }) 77 | } 78 | 79 | /** 80 | * Init without logging and checking 81 | */ 82 | open suspend fun init(file: File = this.file) { 83 | lock.withLock { 84 | assert(!initialized) { "Reinitialize without first closing" } 85 | FileManager.withFile(file.path, name) { 86 | // If it does not throw, assume the file is OK 87 | yamlConfigProvider.load(it) 88 | } 89 | } 90 | } 91 | 92 | override suspend fun init(commandSender: CommandSender?) { 93 | init(file, commandSender) 94 | } 95 | 96 | override suspend fun close(commandSender: CommandSender?) { 97 | lock.withLock { 98 | initialized = false 99 | } 100 | } 101 | 102 | override suspend fun reload(commandSender: CommandSender?) { 103 | lock.withLock { 104 | // Do nothing as we don't cache the data here 105 | } 106 | } 107 | 108 | protected suspend fun withEntryAndFile(path: Array, block: suspend (String, File) -> T): T { 109 | lock.withLock { 110 | // First make sure the path is valid 111 | val entry = translatePath(path) 112 | return FileManager.withFile(file.path, name) { block(entry, it) } 113 | } 114 | } 115 | 116 | protected suspend fun withEntryAndConfigFile(path: Array, block: suspend (String, Configuration) -> T): T { 117 | return withEntryAndFile(path) { entry, _ -> 118 | val conf = yamlConfigProvider.load(file) 119 | return@withEntryAndFile block(entry, conf) 120 | } 121 | } 122 | 123 | protected suspend fun withConfigFile(block: suspend (Configuration) -> T): T { 124 | lock.withLock { 125 | return FileManager.withFile(file.path, name) { 126 | val conf = yamlConfigProvider.load(file) 127 | return@withFile block(conf) 128 | } 129 | } 130 | } 131 | 132 | override suspend fun add(path: Array, rawRecord: String): Boolean { 133 | return withEntryAndConfigFile(path) { entry, conf -> 134 | val records = conf.getStringList(entry).toMutableSet() 135 | if (records.add(rawRecord)) { 136 | conf.set(entry, records.toTypedArray()) 137 | yamlConfigProvider.save(conf, file) 138 | true 139 | } else { 140 | false 141 | } 142 | } 143 | } 144 | 145 | override suspend fun remove(path: Array, rawRecord: String): Boolean { 146 | return withEntryAndConfigFile(path) { entry, conf -> 147 | val records = conf.getStringList(entry).toMutableSet() 148 | if (records.remove(rawRecord)) { 149 | conf.set(entry, records.toTypedArray()) 150 | yamlConfigProvider.save(conf, file) 151 | true 152 | } else { 153 | false 154 | } 155 | } 156 | } 157 | 158 | override suspend fun has(path: Array, rawRecord: String): Boolean { 159 | return withEntryAndConfigFile(path) { entry, conf -> 160 | conf.getStringList(entry).contains(rawRecord) 161 | } 162 | } 163 | 164 | override suspend fun getSize(path: Array): Int { 165 | return get(path).size 166 | } 167 | 168 | override suspend fun get(path: Array): Set { 169 | return withEntryAndConfigFile(path) { entry, conf -> 170 | conf.getStringList(entry).toSet() 171 | } 172 | } 173 | 174 | override suspend fun moveToListIfInLazyList( 175 | username: String, 176 | id: UUID, 177 | mainPath: Array, 178 | lazyPath: Array, 179 | ): Boolean { 180 | return withConfigFile { 181 | val lazyEntry = translatePath(lazyPath) 182 | val mainEntry = translatePath(mainPath) 183 | val lazyRecords = it.getStringList(lazyEntry).toMutableSet() 184 | return@withConfigFile if (lazyRecords.remove(username)) { 185 | it.set(lazyEntry, lazyRecords.toTypedArray()) 186 | val mainRecords = it.getStringList(mainEntry).toMutableSet() 187 | mainRecords.add(id.toString()) 188 | it.set(mainEntry, mainRecords.toTypedArray()) 189 | yamlConfigProvider.save(it, file) 190 | true 191 | } else { 192 | false 193 | } 194 | } 195 | } 196 | 197 | override suspend fun onReloadConfigFile(newConfig: File, commandSender: CommandSender?) { 198 | // Do nothing 199 | } 200 | 201 | override fun toString(): String = "YAMLBackend(\"${file.path}\")" 202 | } -------------------------------------------------------------------------------- /src/main/kotlin/cyou/untitled/bungeesafeguard/Config.kt: -------------------------------------------------------------------------------- 1 | package cyou.untitled.bungeesafeguard 2 | 3 | import cyou.untitled.bungeesafeguard.helpers.RedirectedLogger 4 | import cyou.untitled.bungeesafeguard.helpers.dispatcher 5 | import cyou.untitled.bungeesafeguard.list.UUIDListImpl 6 | import cyou.untitled.bungeesafeguard.storage.Backend 7 | import cyou.untitled.bungeesafeguard.storage.FileManager 8 | import kotlinx.coroutines.sync.Mutex 9 | import kotlinx.coroutines.sync.withLock 10 | import kotlinx.coroutines.withContext 11 | import net.md_5.bungee.api.ChatColor 12 | import net.md_5.bungee.api.CommandSender 13 | import net.md_5.bungee.api.plugin.Plugin 14 | import net.md_5.bungee.config.Configuration 15 | import net.md_5.bungee.config.ConfigurationProvider 16 | import net.md_5.bungee.config.YamlConfiguration 17 | import java.io.File 18 | import java.io.IOException 19 | import java.nio.file.Files 20 | 21 | @Suppress("MemberVisibilityCanBePrivate") 22 | open class Config(val context: Plugin) { 23 | companion object { 24 | const val CONFIG_IN_USE = "config-in-use.txt" 25 | const val DEFAULT_CONFIG = "config.yml" 26 | 27 | const val WHITELIST_MESSAGE = "whitelist-message" 28 | const val BLACKLIST_MESSAGE = "blacklist-message" 29 | const val NO_UUID_MESSAGE = "no-uuid-message" 30 | const val ENABLED_WHITELIST = "enable-whitelist" 31 | const val ENABLED_BLACKLIST = "enable-blacklist" 32 | const val XBL_WEB_API = "xbl-web-api" 33 | const val CONFIRM = "confirm" 34 | } 35 | 36 | protected val lock = Mutex() 37 | 38 | /** 39 | * Name of the config file we are currently using (by default we use "config.yml") 40 | */ 41 | @Volatile 42 | open var configInUse: String = DEFAULT_CONFIG 43 | protected set 44 | 45 | @Volatile 46 | open var enableWhitelist: Boolean = true 47 | @Volatile 48 | open var enableBlacklist: Boolean = false 49 | 50 | @Volatile 51 | open var whitelistMessage: String? = null 52 | protected set 53 | @Volatile 54 | open var blacklistMessage: String? = null 55 | protected set 56 | @Volatile 57 | open var noUUIDMessage: String? = null 58 | protected set 59 | 60 | @Volatile 61 | open var xblWebAPIUrl: String? = null 62 | protected set 63 | 64 | @Volatile 65 | open var confirm: Boolean = false 66 | protected set 67 | 68 | protected open val dataFolder: File 69 | get() = context.dataFolder 70 | 71 | @Suppress("BlockingMethodInNonBlockingContext") 72 | open suspend fun saveDefaultConfig(name: String = DEFAULT_CONFIG) { 73 | if (!dataFolder.exists()) { 74 | dataFolder.mkdirs() 75 | } 76 | val conf = File(dataFolder, name) 77 | if (!conf.exists()) { 78 | FileManager.withFile(conf.path, "config.saveDefaultConfig") { 79 | context.getResourceAsStream(DEFAULT_CONFIG).use { input -> Files.copy(input, conf.toPath()) } 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Lock the entire config 86 | */ 87 | protected open suspend fun withLock(owner: Any? = null, action: suspend () -> T): T { 88 | lock.withLock(owner) { 89 | return action() 90 | } 91 | } 92 | 93 | /** 94 | * Load the name of the config in use 95 | */ 96 | protected open suspend fun loadConfigInUse(sender: CommandSender?): String { 97 | val logger = RedirectedLogger.get(context, sender) 98 | val inUseFile = File(dataFolder, CONFIG_IN_USE) 99 | return withContext(context.dispatcher) { 100 | FileManager.withFile(inUseFile.path, "config.loadConfigInUse") { 101 | if (inUseFile.exists() && inUseFile.isFile) { 102 | try { 103 | val name = inUseFile.readText().trim() 104 | val confFile = File(dataFolder, name) 105 | if (confFile.exists() && confFile.isFile) { 106 | name 107 | } else { 108 | logger.warning("Specified file \"$name\" does not exist, fallback to the default config \"$DEFAULT_CONFIG\"") 109 | DEFAULT_CONFIG 110 | } 111 | } catch (err: IOException) { 112 | logger.warning("Cannot read \"$CONFIG_IN_USE\", fallback to the default config \"$DEFAULT_CONFIG\"") 113 | DEFAULT_CONFIG 114 | } 115 | } else { 116 | logger.warning("File \"$CONFIG_IN_USE\" not found, fallback to the default config \"$DEFAULT_CONFIG\"") 117 | try { 118 | File(dataFolder, CONFIG_IN_USE).writeText(DEFAULT_CONFIG) 119 | } catch (err: IOException) { 120 | logger.warning("File \"$CONFIG_IN_USE\" cannot be created!") 121 | } 122 | DEFAULT_CONFIG 123 | } 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Load the config 130 | */ 131 | open suspend fun load(sender: CommandSender?, configName: String? = null, shouldUpdateLists: Boolean = true) { 132 | lock.withLock { 133 | configInUse = configName ?: loadConfigInUse(sender) 134 | assert(configInUse.endsWith(".yml")) { "Config must be a YAML file, got file name \"$configInUse\"" } 135 | saveDefaultConfig(configInUse) 136 | val logger = RedirectedLogger.get(context, sender) 137 | logger.info("Loading config file ${ChatColor.AQUA}$configInUse") 138 | val conf = loadConfigFromFile(configInUse, sender) 139 | whitelistMessage = if (conf.contains(WHITELIST_MESSAGE)) conf.getString(WHITELIST_MESSAGE) else null 140 | blacklistMessage = if (conf.contains(BLACKLIST_MESSAGE)) conf.getString(BLACKLIST_MESSAGE) else null 141 | noUUIDMessage = if (conf.contains(NO_UUID_MESSAGE)) conf.getString(NO_UUID_MESSAGE) else null 142 | enableWhitelist = if (conf.contains(ENABLED_WHITELIST)) conf.getBoolean(ENABLED_WHITELIST) else true 143 | enableBlacklist = if (conf.contains(ENABLED_BLACKLIST)) conf.getBoolean(ENABLED_BLACKLIST) else false 144 | xblWebAPIUrl = if (conf.contains(XBL_WEB_API)) conf.getString(XBL_WEB_API) else null 145 | confirm = if (conf.contains(CONFIRM)) conf.getBoolean(CONFIRM) else false 146 | if (shouldUpdateLists) { 147 | val bsg = BungeeSafeguard.getPlugin() 148 | val whitelist = bsg.whitelist as UUIDListImpl 149 | val blacklist = bsg.blacklist as UUIDListImpl 150 | whitelist.message = whitelistMessage 151 | whitelist.enabled = enableWhitelist 152 | blacklist.message = blacklistMessage 153 | blacklist.enabled = enableBlacklist 154 | } 155 | logger.info("Loaded from config file ${ChatColor.AQUA}$configInUse") 156 | val backend = try { 157 | Backend.getBackend() 158 | } catch (err: IllegalStateException) { 159 | // First time loading, backend is not yet registered 160 | return@withLock 161 | } 162 | try { 163 | backend.onReloadConfigFile(File(context.dataFolder, configInUse), sender) 164 | } catch (err: Throwable) { 165 | logger.warning("Backend $backend failed to handle config reloading: $err") 166 | throw err 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Reload the config 173 | */ 174 | open suspend fun reload(sender: CommandSender?) = load(sender, configInUse, shouldUpdateLists = true) 175 | 176 | @Suppress("BlockingMethodInNonBlockingContext") 177 | protected open suspend fun doLoadConfigFromFile(configName: String = DEFAULT_CONFIG, configFile: File, sender: CommandSender?): Configuration { 178 | val logger = RedirectedLogger.get(context, sender) 179 | if (configFile.createNewFile()) { 180 | logger.info("${ChatColor.AQUA}$configName${ChatColor.RESET} does not exist, created an empty one") 181 | } 182 | return ConfigurationProvider.getProvider(YamlConfiguration::class.java) 183 | .load(File(dataFolder, configName)) 184 | } 185 | 186 | protected open suspend fun loadConfigFromFile(configName: String = DEFAULT_CONFIG, sender: CommandSender?): Configuration { 187 | val configFile = File(dataFolder, configName) 188 | return FileManager.withFile(configFile.path, "config.loadConfigFromFile") { 189 | doLoadConfigFromFile(configName, configFile, sender) 190 | } 191 | } 192 | 193 | /** 194 | * Save the config to the underlying file 195 | */ 196 | @Suppress("BlockingMethodInNonBlockingContext") 197 | open suspend fun save(sender: CommandSender?) { 198 | withLock { 199 | val configFile = File(dataFolder, configInUse) 200 | FileManager.withFile(configFile.path, "config.save") { 201 | val conf = doLoadConfigFromFile(configInUse, configFile, sender) 202 | // Save only changeable entries 203 | conf.set(ENABLED_WHITELIST, enableWhitelist) 204 | conf.set(ENABLED_BLACKLIST, enableBlacklist) 205 | ConfigurationProvider.getProvider(YamlConfiguration::class.java) 206 | .save(conf, File(dataFolder, configInUse)) 207 | } 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BungeeSafeguard 2 | 3 | A blacklist and whitelist BungeeCord plugin with support of UUID look up. 4 | 5 | This plugin was formerly named BungeeGuard. In order not to conflict with the existing plugin BungeeGuard, this plugin is renamed to BungeeSafeguard from v2.0. 6 | 7 | Tested on Waterfall, version: `git:Waterfall-Bootstrap:1.17-R0.1-SNAPSHOT:93773f9:448`. 8 | 9 | - [BungeeSafeguard](#bungeesafeguard) 10 | - [Features](#features) 11 | - [Usage](#usage) 12 | - [New Features of v3.0 and How To Migrate](#new-features-of-v30-and-how-to-migrate) 13 | - [Migrate to v2.0](#migrate-to-v20) 14 | - [XBOX Live Player Support (Bedrock Edition Support)](#xbox-live-player-support-bedrock-edition-support) 15 | - [Replaceable Storage Backend for Lists](#replaceable-storage-backend-for-lists) 16 | - [Optional Extension Plugins](#optional-extension-plugins) 17 | - [Configuration](#configuration) 18 | - [Commands](#commands) 19 | - [Whitelist](#whitelist) 20 | - [whitelist add](#whitelist-add) 21 | - [whitelist x-add](#whitelist-x-add) 22 | - [whitelist lazy-add](#whitelist-lazy-add) 23 | - [whitelist remove](#whitelist-remove) 24 | - [whitelist x-remove](#whitelist-x-remove) 25 | - [whitelist lazy-remove](#whitelist-lazy-remove) 26 | - [whitelist import](#whitelist-import) 27 | - [whitelist on](#whitelist-on) 28 | - [whitelist off](#whitelist-off) 29 | - [whitelist confirm](#whitelist-confirm) 30 | - [whitelist list](#whitelist-list) 31 | - [Blacklist](#blacklist) 32 | - [blacklist add](#blacklist-add) 33 | - [blacklist x-add](#blacklist-x-add) 34 | - [blacklist lazy-add](#blacklist-lazy-add) 35 | - [blacklist remove](#blacklist-remove) 36 | - [blacklist x-remove](#blacklist-x-remove) 37 | - [blacklist lazy-remove](#blacklist-lazy-remove) 38 | - [blacklist import](#blacklist-import) 39 | - [blacklist on](#blacklist-on) 40 | - [blacklist off](#blacklist-off) 41 | - [blacklist confirm](#blacklist-confirm) 42 | - [blacklist list](#blacklist-list) 43 | - [Main Command](#main-command) 44 | - [bungeesafeguard load](#bungeesafeguard-load) 45 | - [bungeesafeguard reload](#bungeesafeguard-reload) 46 | - [bungeesafeguard status](#bungeesafeguard-status) 47 | - [bungeesafeguard dump](#bungeesafeguard-dump) 48 | - [bungeesafeguard import](#bungeesafeguard-import) 49 | - [bungeesafeguard merge](#bungeesafeguard-merge) 50 | - [bungeesafeguard export](#bungeesafeguard-export) 51 | - [Permission Nodes](#permission-nodes) 52 | - [Lazy Lists](#lazy-lists) 53 | - [Operation Confirmation](#operation-confirmation) 54 | - [Important Notes](#important-notes) 55 | 56 | ## Features 57 | 58 | * First **UUID-based** blacklist and whitelist plugin for BungeeCord 59 | * Add and remove players by their **username** or UUID (username-change-proof) 60 | * Add and remove XBOX Live players by their **Gamer Tag** or UUID (Gamertag-change-proof) (from v2.3, see [XBOX Live Player Support](#xbox-live-player-support-bedrock-edition-support) for more details) 61 | * Lazy translation from username to UUID (from v1.2, **offline server friendly**, see [lazy lists](#lazy-lists) for details) 62 | * Switch between multiple configuration files (e.g., a config for maintenance mode which whitelists administrators only; from v2.4) 63 | * Optional confirmation before adding or removing list entries (from v2.4, see [Operation Confirmation](#operation-confirmation) for more details) 64 | * Import from old `whitelist.json` or `banned-players.json` (from v2.5, resolves issue #7; see [whitelist import](#whitelist-import) and [blacklist import](#blacklist-import) for more details) 65 | * API support backed by (possibly) well-structured and documented code base (from v3.0; see [Developing Extension Plugin for BungeeSafeguard](./developer.md) for more details) 66 | * Manage the lists via a GUI Web interface (TODO) 67 | * SQL database storage support (TODO) 68 | * Redis storage support ([Redis-BSG](https://github.com/Luluno01/Redis-BSG)) 69 | 70 | ## Usage 71 | 72 | Download pre-compiled jar file from [release page](https://github.com/Luluno01/BungeeSafeguard/releases). Put downloaded jar file under ``. 73 | 74 | ## New Features of v3.0 and How To Migrate 75 | 76 | If you are a developer who has been using the unannounced API, breaking changes in v3.0: 77 | 78 | * Internal package name (from `vip.untitled.bungeeguard` to `cyou.untitled.bungeesafeguard`) 79 | * Other massive internal refactoring 80 | 81 | Otherwise, v3.0 is a major remastered version with backward compatibility. **You don't need to do anything to upgrade from older versions**. 82 | However, you may want to know some new features brought by v3.0. 83 | 84 | For **normal users**, we have the followings: 85 | 86 | * [Replaceable Storage Backend for Lists](#replaceable-storage-backend-for-lists) 87 | * [Optional Extension Plugins](#optional-extension-plugins) 88 | 89 | For **developers** who **want to interact with BungeeSafeguard** gracefully, please refer to [Developing Extension Plugin for BungeeSafeguard](./developer.md). 90 | 91 | ## Migrate to v2.0 92 | 93 | Breaking changes in v2.0: 94 | 95 | * Plugin name (from BungeeGuard to BungeeSafeguard) 96 | * Internal package name (from `vip.untitled.bungeeguard` to `vip.untitled.bungeesafeguard`) 97 | * Internal class names 98 | * Main command name (from `bungeesafeguard` to `bungeesafeguard`, from `bg` to `bsg`) 99 | * Configuration directory (from `plugins/BungeeGuard` to `plugins/BungeeSafeguard`) 100 | 101 | To migrate to v2.0 from lower versions, do the following: 102 | 103 | 1. Remove old plugin jar file 104 | 2. Install new plugin jar file 105 | 3. Rename old `plugins/BungeeGuard` directory to `plugins/BungeeSafeguard` 106 | 4. Update assigned permissions, change `bungeeguard` to `bungeesafeguard` in permission nodes 107 | 108 | You are now good to go. 109 | 110 | ## XBOX Live Player Support (Bedrock Edition Support) 111 | 112 | Since version `v2.3`, BungeeSafeguard now supports automatic conversion from XBOX Live Gamer Tag to Minecraft-compatible UUID following the conversion rule as defined by [Geyser](https://geysermc.org/). This new feature hopefully resolves the issue #5. 113 | XBOX Live Gamer Tags are now added via the command [`x-add`](#whitelist-x-add) and removed via the command [`x-rm`](#whitelist-x-remove). 114 | There is no need to implement lazy lists for XBOX Live players because current lazy lists are compatible with XBOX Live players. 115 | 116 | Note that you need to specify an [`xbl-web-api`](https://github.com/Prouser123/xbl-web-api) instance (you can either deploy your own or use the public one provided by [`xbl-web-api`](https://xbl-api.prouser123.me/)) by setting its URL as the value of the configuration entry `xbl-web-api` (see section [Configuration](#configuration)). 117 | 118 | ## Replaceable Storage Backend for Lists 119 | 120 | Start from v3.0, BungeeSafeguard supports custom storage backend for the lists. That is, you can store the list records in the config file, the database, or wherever you want. 121 | The use case of custom storage backend is when you have really large lists, or when you want to share lists among multiple networks, you will want a non-toy, dedicated, professional backend. 122 | 123 | Current available storage backend extension plugins: 124 | 125 | | Name | Feature | 126 | | ---- | ------- | 127 | | [Redis-BSG](https://github.com/Luluno01/Redis-BSG) | Store the whitelist/blacklist in a Redis store | 128 | 129 | ## Optional Extension Plugins 130 | 131 | Start from v3.0, BungeeSafeguard exposes a handful of APIs for third-party plugins to manipulate the lists or register custom backend. 132 | For example, you can now implement a standalone plugin that programmatically whitelist or blacklist someone; or a plugin that wraps BungeeSafeguard APIs and exposes them as RESTful API. 133 | 134 | Current available extension plugins (storage backend not included): 135 | 136 | | Name | Feature | 137 | | ---- | ------- | 138 | | [RESTful-BSG](https://github.com/Luluno01/RESTful-BSG) | Access the whitelist/blacklist via RESTful API | 139 | 140 | ## Configuration 141 | 142 | The configuration file for BungeeSafeguard is `plugins/BungeeSafeguard/config.yml`. 143 | 144 | ```yaml 145 | ######################################### 146 | # BungeeSafeguard Configuration # 147 | # Version: 3.1 # 148 | # Author: Untitled # 149 | ######################################### 150 | 151 | # You can safely ignore this 152 | version: "3.1" 153 | 154 | # Message to be sent to the player when that player is blocked for not being whitelisted 155 | whitelist-message: :( You are not whitelisted on this server 156 | 157 | # Message to be sent to the player when that player is blocked for being blacklisted 158 | blacklist-message: :( We can't let you enter this server 159 | 160 | # Message to be sent to the player when that player is blocked for not having a UUID 161 | no-uuid-message: :( Name yourself 162 | 163 | # Whether to use whitelist 164 | enable-whitelist: true 165 | 166 | # Lazy-whitelist (array of usernames) 167 | # lazy-whitelist: 168 | # - 169 | lazy-whitelist: 170 | 171 | # Whitelist (array of UUIDs) 172 | # whitelist: 173 | # - 174 | whitelist: 175 | 176 | # Whether to use blacklist 177 | enable-blacklist: false 178 | 179 | # Lazy-blacklist (array of usernames) 180 | # lazy-blacklist: 181 | # - 182 | lazy-blacklist: 183 | 184 | # Blacklist (array of UUIDs) 185 | # blacklist: 186 | # - 187 | blacklist: 188 | 189 | # xbl-web-api: 190 | xbl-web-api: https://xbl-api.prouser123.me 191 | 192 | # confirm: 193 | confirm: false 194 | ``` 195 | 196 | Note that if you enable both blacklist and whitelist (which is weird, but it is possible to do that), player in both lists will be blocked because blacklist has a higher priority over whitelist. 197 | 198 | ## Commands 199 | 200 | ### Whitelist 201 | 202 | Alias: `wlist`. 203 | 204 | #### whitelist add 205 | 206 | Add player(s) to whitelist: 207 | 208 | ``` 209 | whitelist add 210 | ``` 211 | 212 | Example: 213 | 214 | ``` 215 | whitelist add DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 216 | ``` 217 | 218 | #### whitelist x-add 219 | 220 | Alias: `xadd`. 221 | 222 | Add XBOX Live player(s) to whitelist: 223 | 224 | ``` 225 | whitelist x-add 226 | ``` 227 | 228 | Example: 229 | 230 | ``` 231 | whitelist x-add DummyPlayer0 DummyPlayer1 00000000-0000-0000-852b-afab3ec1e2ff DummyPlayer2 232 | ``` 233 | 234 | #### whitelist lazy-add 235 | 236 | Alias: `whitelist lazyadd` or `whitelist ladd`. 237 | 238 | Add player(s) to lazy-whitelist: 239 | 240 | ``` 241 | whitelist lazy-add 242 | ``` 243 | 244 | Example: 245 | 246 | ``` 247 | whitelist lazy-add DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 248 | ``` 249 | 250 | #### whitelist remove 251 | 252 | Alias: `whitelist rm`. 253 | 254 | Remove player(s) from whitelist: 255 | 256 | ``` 257 | whitelist remove 258 | ``` 259 | 260 | Example: 261 | 262 | ``` 263 | whitelist remove DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 264 | ``` 265 | 266 | #### whitelist x-remove 267 | 268 | Alias: `whitelist xremove`, `whitelist x-rm` or `whitelist xrm`. 269 | 270 | Remove XBOX Live player(s) from whitelist: 271 | 272 | ``` 273 | whitelist x-remove 274 | ``` 275 | 276 | Example: 277 | 278 | ``` 279 | whitelist x-remove DummyPlayer0 DummyPlayer1 00000000-0000-0000-852b-afab3ec1e2ff DummyPlayer2 280 | ``` 281 | 282 | #### whitelist lazy-remove 283 | 284 | Alias: `whitelist lazyremove`, `whitelist lremove` or `whitelist lrm`. 285 | 286 | Remove player(s) from lazy-whitelist: 287 | 288 | ``` 289 | whitelist lazy-remove 290 | ``` 291 | 292 | Example: 293 | 294 | ``` 295 | whitelist lazy-remove DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 296 | ``` 297 | 298 | #### whitelist import 299 | 300 | Import UUID(s) from an existing JSON file, e.g. your old `whitelist.json`, to the whitelist. 301 | 302 | ``` 303 | whitelist import 304 | ``` 305 | 306 | Note that `` could be either an absolute path, e.g. `/home/mc/old-mc-server/whitelist.json`, 307 | or a path relative to the **working directory** of the running BungeeCord process, e.g. `../old-mc-server/whitelist.json`. 308 | 309 | Example: 310 | 311 | ``` 312 | whitelist import whitelist.json 313 | ``` 314 | 315 | *This feature is added as requested by issue #7.* 316 | 317 | #### whitelist on 318 | 319 | Turn on whitelist: 320 | 321 | ``` 322 | whitelist on 323 | ``` 324 | 325 | #### whitelist off 326 | 327 | Turn off whitelist: 328 | 329 | ``` 330 | whitelist off 331 | ``` 332 | 333 | #### whitelist confirm 334 | 335 | Confirm the last issued whitelist command: 336 | 337 | ``` 338 | whitelist confirm 339 | ``` 340 | 341 | #### whitelist list 342 | 343 | Alias: `whitelist ls`, `whitelist show` or `whitelist dump`. 344 | 345 | Dump whitelist and lazy whitelist with at most 10 last known usernames: 346 | 347 | ``` 348 | whitelist list 349 | ``` 350 | 351 | Example output: 352 | 353 | ``` 354 | Whitelist ENABLED 355 | 2 lazy record(s) 356 | foo 357 | bar 358 | 3 UUID record(s) and the last known names (in reverse chronological order) 359 | 00000000-1111-2222-3333-666666666666 LatestName, OldNameLastMonth, OldNameLastYear 360 | ffffffff-1111-2222-3333-666666666666 361 | eeeeeeee-1111-2222-3333-666666666666 LatestName123 362 | ``` 363 | 364 | *This feature is added as requested by issue #8.* 365 | 366 | ### Blacklist 367 | 368 | Alias: `blist`. 369 | 370 | #### blacklist add 371 | 372 | Add player(s) to blacklist: 373 | 374 | ``` 375 | blacklist add 376 | ``` 377 | 378 | Example: 379 | 380 | ``` 381 | blacklist add DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 382 | ``` 383 | 384 | #### blacklist x-add 385 | 386 | Alias: `blacklist xadd`. 387 | 388 | Add XBOX Live player(s) to blacklist: 389 | 390 | ``` 391 | blacklist x-add 392 | ``` 393 | 394 | Example: 395 | 396 | ``` 397 | blacklist x-add DummyPlayer0 DummyPlayer1 00000000-0000-0000-852b-afab3ec1e2ff DummyPlayer2 398 | ``` 399 | 400 | #### blacklist lazy-add 401 | 402 | Alias: `blacklist lazyadd` or `blacklist ladd`. 403 | 404 | Add player(s) to lazy-blacklist: 405 | 406 | ``` 407 | blacklist lazy-add 408 | ``` 409 | 410 | Example: 411 | 412 | ``` 413 | blacklist lazy-add DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 414 | ``` 415 | 416 | #### blacklist remove 417 | 418 | Alias: `blacklist rm`. 419 | 420 | Remove player(s) from blacklist: 421 | 422 | ``` 423 | blacklist remove 424 | ``` 425 | 426 | Example: 427 | 428 | ``` 429 | blacklist remove DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 430 | ``` 431 | 432 | #### blacklist x-remove 433 | 434 | Alias: `blacklist xremove`, `blacklist x-rm`, `blacklist xrm`. 435 | 436 | Remove XBOX Live player(s) from blacklist: 437 | 438 | ``` 439 | blacklist x-remove 440 | ``` 441 | 442 | Example: 443 | 444 | ``` 445 | blacklist x-remove DummyPlayer0 DummyPlayer1 00000000-0000-0000-852b-afab3ec1e2ff DummyPlayer2 446 | ``` 447 | 448 | #### blacklist lazy-remove 449 | 450 | Alias: `blacklist lazyremove`, `blacklist lremove` or `blacklist lrm`. 451 | 452 | Remove player(s) from lazy-blacklist: 453 | 454 | ``` 455 | blacklist lazy-remove 456 | ``` 457 | 458 | Example: 459 | 460 | ``` 461 | blacklist lazy-remove DummyPlayer0 DummyPlayer1 7be767e5-327c-4abd-852b-afab3ec1e2ff DummyPlayer2 462 | ``` 463 | 464 | #### blacklist import 465 | 466 | Import UUID(s) from an existing JSON file, e.g. your old `banned-players.json`, to the blacklist. 467 | 468 | ``` 469 | blacklist import 470 | ``` 471 | 472 | Note that `` could be either an absolute path, e.g. `/home/mc/old-mc-server/banned-players.json`, 473 | or a path relative to the **working directory** of the running BungeeCord process, e.g. `../old-mc-server/banned-players.json`. 474 | 475 | Example: 476 | 477 | ``` 478 | blacklist import banned-players.json 479 | ``` 480 | 481 | *This feature is added as requested by issue #7.* 482 | 483 | #### blacklist on 484 | 485 | Turn on blacklist: 486 | 487 | ``` 488 | blacklist on 489 | ``` 490 | 491 | #### blacklist off 492 | 493 | Turn off blacklist: 494 | 495 | ``` 496 | blacklist off 497 | ``` 498 | 499 | #### blacklist confirm 500 | 501 | Confirm the last issued blacklist command: 502 | 503 | ``` 504 | blacklist confirm 505 | ``` 506 | 507 | #### blacklist list 508 | 509 | Alias: `blacklist ls`, `blacklist show` or `blacklist dump`. 510 | 511 | Dump blacklist and lazy blacklist with at most 10 last known usernames: 512 | 513 | ``` 514 | whitelist list 515 | ``` 516 | 517 | Example output: 518 | 519 | ``` 520 | Blacklist ENABLED 521 | 2 lazy record(s) 522 | foo 523 | bar 524 | 3 UUID record(s) and the last known names (in reverse chronological order) 525 | 00000000-1111-2222-3333-666666666666 LatestName, OldNameLastMonth, OldNameLastYear 526 | ffffffff-1111-2222-3333-666666666666 527 | eeeeeeee-1111-2222-3333-666666666666 LatestName123 528 | ``` 529 | 530 | *This feature is added as requested by issue #8.* 531 | 532 | ### Main Command 533 | 534 | Alias: `bsg`. 535 | 536 | #### bungeesafeguard load 537 | 538 | Alias: `bsg use`. 539 | 540 | Load configuration from a specific `.yml` file under `plugins/BungeeSafeguard/` (the extension `.yml` can be omitted): 541 | 542 | ``` 543 | bungeesafeguard load 544 | ``` 545 | 546 | Example: 547 | 548 | ``` 549 | bungeesafeguard load maintenance-config.yml 550 | ``` 551 | 552 | **Note: enabling [confirmation](#operation-confirmation) is suggested in order not to modify an unexpected configuration file if you are to use multiple configuration files.** 553 | 554 | *This feature is added as requested by issue #6.* 555 | 556 | #### bungeesafeguard reload 557 | 558 | Reload configuration (from file `plugins/BungeeSafeguard/config.yml`): 559 | 560 | ``` 561 | bungeesafeguard reload 562 | ``` 563 | 564 | #### bungeesafeguard status 565 | 566 | Check status of blacklist and whitelist: 567 | 568 | ``` 569 | bungeesafeguard status 570 | ``` 571 | 572 | #### bungeesafeguard dump 573 | 574 | Dump currently loaded blacklist and whitelist: 575 | 576 | ``` 577 | bungeesafeguard dump 578 | ``` 579 | 580 | #### bungeesafeguard import 581 | 582 | Alias: `bsg i`. 583 | 584 | Import all whitelist/blacklist records, including both UUID records and lazy records, from a YAML file: 585 | 586 | ``` 587 | bungeesafeguard import 588 | ``` 589 | 590 | Example: 591 | 592 | ``` 593 | bungeesafeguard import old-config.yml 594 | ``` 595 | 596 | Note this command will refuse to overwrite/merge current lists if current storage backend is non-empty. For list merging, use [`bungeesafeguard merge`](#bungeesafeguard-merge) 597 | 598 | #### bungeesafeguard merge 599 | 600 | Alias: `bsg m`. 601 | 602 | Merge all whitelist/blacklist records, including both UUID records and lazy records, in a YAML file, with current lists in use: 603 | 604 | ``` 605 | bungeesafeguard merge 606 | ``` 607 | 608 | Example: 609 | 610 | ``` 611 | bungeesafeguard merge old-config.yml 612 | ``` 613 | 614 | #### bungeesafeguard export 615 | 616 | Alias: `bsg e`. 617 | 618 | Export all whitelist/blacklist records, including both UUID records and lazy records, to a YAML file: 619 | 620 | ``` 621 | bungeesafeguard export 622 | ``` 623 | 624 | Example: 625 | 626 | ``` 627 | bungeesafeguard export list-backup.yml 628 | ``` 629 | 630 | You can combine this command with [`bungeesafeguard import`](#bungeesafeguard-import) for backend migration. 631 | 632 | ## Permission Nodes 633 | 634 | BungeeSafeGuard uses BungeeCord's built-in permission system. There are 3 permission nodes for the aforementioned 3 category of commands respectively. Only players granted **with** the permission can issue corresponding command **in game** (this restriction does not apply to console). 635 | 636 | | Permission | Commands | 637 | | --------------------------- | ------------- | 638 | | `bungeesafeguard.whitelist` | [`whitelist *`](#whitelist) | 639 | | `bungeesafeguard.blacklist` | [`blacklist *`](#blacklist) | 640 | | `bungeesafeguard.main` | [`bungeesafeguard *`](#main-command) | 641 | 642 | Note that despite that BungeeCord has a built-in permission system, it does not provide a permission manager (or does it?). You will need to install third-party permission plugin so that you can grant permissions to players. 643 | 644 | ## Lazy Lists 645 | 646 | Records are added to/removed from lazy lists via `whitelist lazy-*` and `blacklist lazy-*` commands. 647 | 648 | Lazy-whitelist and lazy-blacklist work in a very similar way. Let's take lazy-whitelist as example, and you will understand how both of them work. Lazy-whitelist is a different list from the plain whitelist you access via `whitelist add` and `whitelist remove`. Upon record addition, username is added to lazy-whitelist rather than translated UUID, which may take some considerable time or even fail to translate. What's more, because the translation requests are sent to Mojang, implicitly requiring that the server is running in online mode (unless you hijack the requests and redirect them to your own authentication server). The workaround (or maybe it is actually a great feature) is not to do the translation immediately but to save the username in a temporary list, i.e. lazy-whitelist. Because server will be told the UUID of the player upon client connection (if I am right), we are able to lazily translate username to UUID without sending HTTP request. In other words, usernames in lazy-whitelist are translated into UUIDs and moved to whitelist (the plain one) once the server knows the corresponding UUID of the username, i.e. when player with the username connect to the server for the first time. 649 | 650 | In this way, offline servers should be able to use this plugin painlessly. 651 | 652 | ## Operation Confirmation 653 | 654 | **By default**, BungeeSafeguard will **NOT** ask for confirmation of records addition/removal commands. If you want to be cautious, set the config entry `confirm` to `true`. Then you will need to use `whitelist confirm` (or `blacklist confirm`) to confirm your last issued `whitelist`-accessing (or `blacklist`-accessing) command in **10 seconds**. 655 | 656 | For example, suppose that you are using the default configuration file `config.yml` (to switch to a different configuration file, use command [bungeesafeguard load](#bungeesafeguard-load)). You just enabled confirmation and want to add the player `DummyPlayer` to the whitelist by executing the command `whitelist add DummyPlayer`. 657 | Then you will be asked: 658 | 659 | Are you sure you want to add the following Minecraft player(s) to the whitelist in the config file config.yml? 660 | DummyPlayer 661 | Please use /whitelist confirm in 10s to confirm 662 | 663 | If you everything looks fine for you, use `whitelist confirm` in 10 seconds and `DummyPlayer` will be added into the whitelist. 664 | 665 | *This feature is added as requested by issue #6.* 666 | 667 | ## Important Notes 668 | 669 | BungeeSafeguard does asynchronous UUID look up when you execute add/remove on the lists. 670 | It's recommended to execute those command only in console, and wait patiently for the completion feedback from the command before executing other commands of BungeeSafeguard. 671 | 672 | Offline servers should be able to use this plugin by using lazy lists or supplying BungeeSafeguard with players' UUIDs rather than their usernames. However, offline servers are still suffering from UUID abuse if they have no authentication plugin installed or have no external authentication mechanism. Offline server owners need to fully understand whitelist and blacklist is **NOT** a prevention of UUID abuse. 673 | 674 | Last but not least, you should always be prepared for the worst situation, for example, when BungeeCord or BungeeSafeguard somehow, magically, fail to protect your servers. Backup is a good way to counter Murphy's law. 675 | --------------------------------------------------------------------------------