├── 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 |
4 |
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 |
5 |
6 |
7 |
8 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
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 |
--------------------------------------------------------------------------------