Skip to content

Commit b173d01

Browse files
committed
feat: add formatted agx command help
1 parent 184b701 commit b173d01

6 files changed

Lines changed: 266 additions & 1 deletion

File tree

‎authgatewayx-paper/src/main/kotlin/pl/syntaxdevteam/authgatewayx/paper/AuthGatewayXPaper.kt‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ import pl.syntaxdevteam.authgatewayx.paper.command.AccountInfoCommandText
5656
import pl.syntaxdevteam.authgatewayx.paper.command.MigrationAdminCommandController
5757
import pl.syntaxdevteam.authgatewayx.paper.command.MigrationAdminCommandText
5858
import pl.syntaxdevteam.authgatewayx.paper.command.MutableMigrationAdminCommandGateway
59+
import pl.syntaxdevteam.authgatewayx.paper.command.MutableAdminHelpCommandGateway
60+
import pl.syntaxdevteam.authgatewayx.paper.command.AdminHelpCommandController
61+
import pl.syntaxdevteam.authgatewayx.paper.command.AdminHelpCommandText
5962
import pl.syntaxdevteam.authgatewayx.paper.command.PasswordCommandRegistrar
6063
import pl.syntaxdevteam.authgatewayx.paper.isolation.PreAuthAdmission
6164
import pl.syntaxdevteam.authgatewayx.paper.isolation.PreAuthEntryListener
@@ -107,6 +110,7 @@ class AuthGatewayXPaper : JavaPlugin() {
107110
private val multiAccountCommandGateway = MutableMultiAccountCommandGateway()
108111
private val accountInfoCommandGateway = MutableAccountInfoCommandGateway()
109112
private val migrationAdminCommandGateway = MutableMigrationAdminCommandGateway()
113+
private val adminHelpCommandGateway = MutableAdminHelpCommandGateway()
110114
private var runtime: RuntimeComponents? = null
111115

112116
override fun onEnable() {
@@ -118,6 +122,7 @@ class AuthGatewayXPaper : JavaPlugin() {
118122
multiAccountCommandGateway,
119123
accountInfoCommandGateway,
120124
migrationAdminCommandGateway,
125+
adminHelpCommandGateway,
121126
canSuggestOnlinePlayers = { sender ->
122127
sender is ConsoleCommandSender ||
123128
sender is Player &&
@@ -177,6 +182,25 @@ class AuthGatewayXPaper : JavaPlugin() {
177182
behaviorGate: ConnectionBehaviorGate,
178183
cheapGuard: PaperLoginCheapGuard,
179184
) {
185+
adminHelpCommandGateway.delegate = AdminHelpCommandController(
186+
AdminHelpCommandText(
187+
header = messages.stringMessageToComponentNoPrefix("help", "header"),
188+
playerSection = messages.stringMessageToComponentNoPrefix("help", "player_section"),
189+
adminSection = messages.stringMessageToComponentNoPrefix("help", "admin_section"),
190+
changePassword = messages.stringMessageToComponentNoPrefix("help", "change_password"),
191+
logout = messages.stringMessageToComponentNoPrefix("help", "logout"),
192+
info = messages.stringMessageToComponentNoPrefix("help", "info"),
193+
alts = messages.stringMessageToComponentNoPrefix("help", "alts"),
194+
setPassword = messages.stringMessageToComponentNoPrefix("help", "set_password"),
195+
migrateStatus = messages.stringMessageToComponentNoPrefix("help", "migrate_status"),
196+
migrateInspect = messages.stringMessageToComponentNoPrefix("help", "migrate_inspect"),
197+
migrateRetry = messages.stringMessageToComponentNoPrefix("help", "migrate_retry"),
198+
migrateRecover = messages.stringMessageToComponentNoPrefix("help", "migrate_recover"),
199+
hover = messages.stringMessageToComponentNoPrefix("help", "hover"),
200+
footer = messages.stringMessageToComponentNoPrefix("help", "footer"),
201+
),
202+
description.version,
203+
)
180204
val scheduler = PaperPlatformScheduler(this)
181205
val sessions = InMemorySessionRegistry()
182206
val storageExecutor = BoundedTaskExecutor(positive("executors.storage-threads"), positive("executors.storage-queue"), "authgatewayx-storage")
@@ -696,6 +720,7 @@ class AuthGatewayXPaper : JavaPlugin() {
696720
multiAccountCommandGateway.delegate = null
697721
accountInfoCommandGateway.delegate = null
698722
migrationAdminCommandGateway.delegate = null
723+
adminHelpCommandGateway.delegate = null
699724
runtime?.close()
700725
runtime = null
701726
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package pl.syntaxdevteam.authgatewayx.paper.command
2+
3+
import net.kyori.adventure.text.Component
4+
import net.kyori.adventure.text.TextReplacementConfig
5+
import net.kyori.adventure.text.event.ClickEvent
6+
import org.bukkit.command.CommandSender
7+
import org.bukkit.command.ConsoleCommandSender
8+
import org.bukkit.entity.Player
9+
10+
fun interface AdminHelpCommandGateway {
11+
fun show(sender: CommandSender)
12+
}
13+
14+
class MutableAdminHelpCommandGateway : AdminHelpCommandGateway {
15+
@Volatile var delegate: AdminHelpCommandGateway? = null
16+
17+
override fun show(sender: CommandSender) = delegate?.show(sender) ?: Unit
18+
}
19+
20+
data class AdminHelpCommandText(
21+
val header: Component,
22+
val playerSection: Component,
23+
val adminSection: Component,
24+
val changePassword: Component,
25+
val logout: Component,
26+
val info: Component,
27+
val alts: Component,
28+
val setPassword: Component,
29+
val migrateStatus: Component,
30+
val migrateInspect: Component,
31+
val migrateRetry: Component,
32+
val migrateRecover: Component,
33+
val hover: Component,
34+
val footer: Component,
35+
)
36+
37+
class AdminHelpCommandController(
38+
private val text: AdminHelpCommandText,
39+
private val version: String,
40+
) : AdminHelpCommandGateway {
41+
override fun show(sender: CommandSender) {
42+
val playerEntries = buildList {
43+
if (sender is Player && sender.hasPermission(CHANGE_PASSWORD_PERMISSION)) {
44+
add(text.changePassword to "/changepassword")
45+
}
46+
if (sender is Player && sender.hasPermission(LOGOUT_PERMISSION)) {
47+
add(text.logout to "/logout")
48+
}
49+
}
50+
val adminEntries = buildList {
51+
if (allowed(sender, INFO_PERMISSION)) add(text.info to "/agx info ")
52+
if (allowed(sender, ALTS_PERMISSION)) add(text.alts to "/agx alts ")
53+
if (sender is Player && sender.hasPermission(SET_PASSWORD_PERMISSION)) {
54+
add(text.setPassword to "/agx setpassword ")
55+
}
56+
if (allowed(sender, MigrationAdminCommandController.VIEW_PERMISSION)) {
57+
add(text.migrateStatus to "/agx migrate status ")
58+
add(text.migrateInspect to "/agx migrate inspect ")
59+
}
60+
if (allowed(sender, MigrationAdminCommandController.EXECUTE_PERMISSION)) {
61+
add(text.migrateRetry to "/agx migrate retry ")
62+
}
63+
if (allowed(sender, MigrationAdminCommandController.RECOVER_PERMISSION)) {
64+
add(text.migrateRecover to "/agx migrate recover ")
65+
}
66+
}
67+
68+
sender.sendMessage(text.header.withText("{version}", version))
69+
if (playerEntries.isNotEmpty()) {
70+
sender.sendMessage(text.playerSection)
71+
playerEntries.forEach { (component, suggestion) ->
72+
sender.sendMessage(interactive(sender, component, suggestion))
73+
}
74+
}
75+
if (adminEntries.isNotEmpty()) {
76+
sender.sendMessage(text.adminSection)
77+
adminEntries.forEach { (component, suggestion) ->
78+
sender.sendMessage(interactive(sender, component, suggestion))
79+
}
80+
}
81+
sender.sendMessage(text.footer)
82+
}
83+
84+
private fun interactive(sender: CommandSender, component: Component, suggestion: String): Component =
85+
if (sender is Player) {
86+
component
87+
.clickEvent(ClickEvent.suggestCommand(suggestion))
88+
.hoverEvent(text.hover)
89+
} else {
90+
component
91+
}
92+
93+
private fun allowed(sender: CommandSender, permission: String): Boolean =
94+
sender is ConsoleCommandSender || sender.hasPermission(permission)
95+
96+
private fun Component.withText(placeholder: String, value: String): Component = replaceText(
97+
TextReplacementConfig.builder().matchLiteral(placeholder).replacement(Component.text(value)).build(),
98+
)
99+
100+
companion object {
101+
const val CHANGE_PASSWORD_PERMISSION = "authgatewayx.command.changepassword"
102+
const val LOGOUT_PERMISSION = "authgatewayx.command.logout"
103+
const val INFO_PERMISSION = "authgatewayx.admin.info"
104+
const val ALTS_PERMISSION = "authgatewayx.admin.alts"
105+
const val SET_PASSWORD_PERMISSION = "authgatewayx.admin.password"
106+
}
107+
}

‎authgatewayx-paper/src/main/kotlin/pl/syntaxdevteam/authgatewayx/paper/command/PasswordCommandRegistrar.kt‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class PasswordCommandRegistrar(
3838
private val multiAccountGateway: MultiAccountCommandGateway,
3939
private val accountInfoGateway: AccountInfoCommandGateway,
4040
private val migrationGateway: MigrationAdminCommandGateway,
41+
private val helpGateway: AdminHelpCommandGateway,
4142
private val canSuggestOnlinePlayers: (CommandSender) -> Boolean,
4243
) {
4344
private fun onlinePlayerArgument(name: String) =
@@ -73,6 +74,14 @@ class PasswordCommandRegistrar(
7374
)
7475
event.registrar().register(
7576
Commands.literal("authgatewayx")
77+
.executes { context ->
78+
helpGateway.show(context.source.sender)
79+
1
80+
}
81+
.then(Commands.literal("help").executes { context ->
82+
helpGateway.show(context.source.sender)
83+
1
84+
})
7685
.then(Commands.literal("info")
7786
.requires { it.sender is ConsoleCommandSender || it.sender.hasPermission("authgatewayx.admin.info") }
7887
.then(Commands.argument("username", StringArgumentType.word()).executes { context ->

‎authgatewayx-paper/src/main/resources/lang/messages_pl.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
prefix: "<gradient:#55ffff:#5555ff>[<b>AuthGatewayX</b>]</gradient>"
2+
help:
3+
header: '<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</dark_gray><newline><gradient:#55ffff:#5555ff><b>AuthGatewayX</b></gradient> <gray>v{version}</gray> <dark_gray>•</dark_gray> <white>Lista komend</white>'
4+
player_section: '<yellow><b>Komendy gracza</b></yellow>'
5+
admin_section: '<gold><b>Administracja</b></gold>'
6+
change_password: '<dark_gray>›</dark_gray> <aqua>/changepassword</aqua> <dark_gray>—</dark_gray> <gray>zmiana hasła konta offline</gray>'
7+
logout: '<dark_gray>›</dark_gray> <aqua>/logout</aqua> <dark_gray>—</dark_gray> <gray>bezpieczne zakończenie sesji</gray>'
8+
info: '<dark_gray>›</dark_gray> <aqua>/agx info ‹nick›</aqua> <dark_gray>—</dark_gray> <gray>pełny raport konta gracza</gray>'
9+
alts: '<dark_gray>›</dark_gray> <aqua>/agx alts ‹nick›</aqua> <dark_gray>—</dark_gray> <gray>powiązania kont przez wspólne IP</gray>'
10+
set_password: '<dark_gray>›</dark_gray> <aqua>/agx setpassword ‹nick›</aqua> <dark_gray>—</dark_gray> <gray>administracyjne ustawienie hasła</gray>'
11+
migrate_status: '<dark_gray>›</dark_gray> <aqua>/agx migrate status ‹nick›</aqua> <dark_gray>—</dark_gray> <gray>stan ostatniej migracji UUID</gray>'
12+
migrate_inspect: '<dark_gray>›</dark_gray> <aqua>/agx migrate inspect ‹nick› [stare-uuid]</aqua> <dark_gray>—</dark_gray> <gray>diagnostyka danych przed migracją</gray>'
13+
migrate_retry: '<dark_gray>›</dark_gray> <aqua>/agx migrate retry ‹nick›</aqua> <dark_gray>—</dark_gray> <gray>ponowienie istniejącej migracji</gray>'
14+
migrate_recover: '<dark_gray>›</dark_gray> <aqua>/agx migrate recover ‹nick› [stare-uuid]</aqua> <dark_gray>—</dark_gray> <gray>bezpieczne recovery migracji</gray>'
15+
hover: '<gray>Kliknij, aby wstawić komendę do czatu.</gray>'
16+
footer: '<dark_gray>Alias: /authgatewayx • Pomoc: /agx help</dark_gray><newline><dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</dark_gray>'
17+
218
auth:
319
login_title: "<aqua>Logowanie konta dla <white>{username}</white></aqua>"
420
registration_title: "<aqua>Rejestracja konta dla <white>{username}</white></aqua>"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package pl.syntaxdevteam.authgatewayx.paper.command
2+
3+
import net.kyori.adventure.text.Component
4+
import net.kyori.adventure.text.event.ClickEvent
5+
import org.bukkit.command.CommandSender
6+
import org.bukkit.command.ConsoleCommandSender
7+
import org.bukkit.entity.Player
8+
import java.lang.reflect.Proxy
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertFalse
12+
import kotlin.test.assertTrue
13+
14+
class AdminHelpCommandControllerTest {
15+
private val text = AdminHelpCommandText(
16+
header = Component.text("header {version}"),
17+
playerSection = Component.text("player"),
18+
adminSection = Component.text("admin"),
19+
changePassword = Component.text("change"),
20+
logout = Component.text("logout"),
21+
info = Component.text("info"),
22+
alts = Component.text("alts"),
23+
setPassword = Component.text("setpassword"),
24+
migrateStatus = Component.text("status"),
25+
migrateInspect = Component.text("inspect"),
26+
migrateRetry = Component.text("retry"),
27+
migrateRecover = Component.text("recover"),
28+
hover = Component.text("hover"),
29+
footer = Component.text("footer"),
30+
)
31+
32+
@Test
33+
fun `player sees only commands allowed by permissions and entries are clickable`() {
34+
val replies = mutableListOf<Component>()
35+
val player = sender(
36+
Player::class.java,
37+
setOf(
38+
AdminHelpCommandController.LOGOUT_PERMISSION,
39+
AdminHelpCommandController.ALTS_PERMISSION,
40+
),
41+
replies,
42+
)
43+
AdminHelpCommandController(text, "1.2.3").show(player)
44+
45+
assertEquals(6, replies.size)
46+
assertTrue(replies[0].toString().contains("1.2.3"))
47+
assertEquals(text.playerSection, replies[1])
48+
assertEquals(ClickEvent.suggestCommand("/logout"), replies[2].clickEvent())
49+
assertEquals(text.adminSection, replies[3])
50+
assertEquals(ClickEvent.suggestCommand("/agx alts "), replies[4].clickEvent())
51+
assertEquals(text.footer, replies[5])
52+
assertFalse(replies.any { it == text.info || it == text.setPassword })
53+
}
54+
55+
@Test
56+
fun `console sees console-capable administration commands but not player-only commands`() {
57+
val replies = mutableListOf<Component>()
58+
val console = sender(ConsoleCommandSender::class.java, emptySet(), replies)
59+
AdminHelpCommandController(text, "1.0.0").show(console)
60+
61+
assertEquals(text.adminSection, replies[1])
62+
assertTrue(replies.contains(text.info))
63+
assertTrue(replies.contains(text.alts))
64+
assertTrue(replies.contains(text.migrateStatus))
65+
assertTrue(replies.contains(text.migrateInspect))
66+
assertTrue(replies.contains(text.migrateRetry))
67+
assertTrue(replies.contains(text.migrateRecover))
68+
assertFalse(replies.contains(text.changePassword))
69+
assertFalse(replies.contains(text.logout))
70+
assertFalse(replies.contains(text.setPassword))
71+
assertEquals(text.footer, replies.last())
72+
}
73+
74+
@Test
75+
fun `player without plugin permissions receives only frame`() {
76+
val replies = mutableListOf<Component>()
77+
val player = sender(Player::class.java, emptySet(), replies)
78+
AdminHelpCommandController(text, "1.0.0").show(player)
79+
80+
assertEquals(2, replies.size)
81+
assertEquals(text.footer, replies.last())
82+
}
83+
84+
private fun <T : CommandSender> sender(
85+
type: Class<T>,
86+
permissions: Set<String>,
87+
replies: MutableList<Component>,
88+
): T = Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, args ->
89+
when (method.name) {
90+
"hasPermission" -> (args?.firstOrNull() as? String) in permissions
91+
"sendMessage" -> {
92+
args?.filterIsInstance<Component>()?.let(replies::addAll)
93+
null
94+
}
95+
"isOnline" -> true
96+
"toString" -> "help-test-sender"
97+
"hashCode" -> 1
98+
"equals" -> false
99+
else -> null
100+
}
101+
} as T
102+
}

‎docs/wiki/Komendy.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Poniższe komendy działają na serwerze gry. Używa się ich po zalogowaniu, be
88
| --- | --- | --- |
99
| `/changepassword` | Zalogowane konto offline | Otwiera okno zmiany własnego hasła |
1010
| `/logout` | Zalogowane konto offline | Kończy sesję i rozłącza gracza |
11+
| `/agx`, `/agx help` | Zalogowany gracz lub konsola | Pokazuje sformatowaną listę komend dostępnych dla bieżącego nadawcy |
12+
| `/authgatewayx info <nick>` | Zalogowany administrator lub konsola | Pokazuje raport konta, sesji, bezpieczeństwa i danych dostępnych dla uprawnień administratora |
1113
| `/authgatewayx alts <nick>` | Zalogowany administrator lub konsola | Pokazuje podejrzane powiązania kont offline przez historię wspólnych adresów |
1214
| `/authgatewayx setpassword <nick>` | Administrator z odpowiednim uprawnieniem | Otwiera okno ustawienia nowego hasła konta offline |
1315
| `/authgatewayx migrate status <nick>` | Administrator lub konsola | Pokazuje ostatni trwały ticket migracji UUID |
@@ -16,7 +18,11 @@ Poniższe komendy działają na serwerze gry. Używa się ich po zalogowaniu, be
1618
| `/authgatewayx migrate recover <nick> [stare-uuid]` | Administrator lub konsola | Tworzy recovery dla konta już przełączonego przez starszą wersję AGX, wyłącznie po znalezieniu śladów starego UUID |
1719

1820
Komenda administracyjna `/authgatewayx` ma alias `/agx`; podkomendy, argumenty i
19-
uprawnienia są identyczne, np. `/agx migrate status Alex`.
21+
uprawnienia są identyczne, np. `/agx migrate status Alex`. Samo `/agx` oraz
22+
`/agx help` wyświetla czytelną, kolorową listę komend. Lista jest filtrowana według
23+
uprawnień nadawcy, więc zwykły gracz nie zobaczy komend administracyjnych, których nie
24+
może wykonać. W grze pozycje listy są klikalne i wstawiają odpowiednią komendę do pola
25+
czatu bez automatycznego jej wykonywania.
2026

2127
Dla `/authgatewayx alts <nick>` (także `/agx alts <nick>`) klient Minecraft
2228
podpowiada nazwy aktualnie połączonych graczy podczas wpisywania argumentu. Podpowiedzi

0 commit comments

Comments
 (0)