From 049937d254b0e31e242fb5fabc8c24b9adc96d79 Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Sat, 14 Nov 2026 11:09:36 -0700 Subject: [PATCH 1/2] Rename the plugin to Sheriff (0.5.0) D194. The owner asked that no old names be kept, so this also drops the folder copy and test left from the earlier rename. Package, plugin, jar, console command and permission are all Sheriff. A server installing it renames its plugin folder and LuckPerms node first; RELEASE_NOTES.md lists the steps. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 4 +- CLAUDE.md | 4 +- README.md | 59 ++++++------- RELEASE_NOTES.md | 23 +++-- build.gradle.kts | 12 +-- docs/decisions.md | 4 + docs/kids-first.md | 12 +-- docs/paper-26-audit.md | 6 +- docs/test-audit.md | 15 ++-- settings.gradle.kts | 2 +- .../{usher => sheriff}/AuditLog.java | 2 +- .../{usher => sheriff}/ChangeService.java | 34 ++++---- .../{usher => sheriff}/ConsoleCommands.java | 28 +++--- .../{usher => sheriff}/GroupStore.java | 2 +- .../{usher => sheriff}/JoinAttempts.java | 8 +- .../{usher => sheriff}/LatchkeyHook.java | 50 +++++------ .../{usher => sheriff}/LuckPermsStore.java | 2 +- .../{usher => sheriff}/NameResolver.java | 6 +- .../{usher => sheriff}/PickerMenus.java | 6 +- .../extantape/{usher => sheriff}/Planner.java | 14 +-- .../{usher => sheriff}/PlayerCommands.java | 14 +-- .../extantape/{usher => sheriff}/Profile.java | 2 +- .../{usher => sheriff}/Settings.java | 4 +- .../SheriffPlugin.java} | 38 +++----- .../{usher => sheriff}/WorldAccess.java | 6 +- .../api/JoinAttemptEvent.java | 2 +- .../api/ProfilesChangedEvent.java | 4 +- .../api/SheriffProblemEvent.java} | 6 +- .../java/com/extantape/usher/Migration.java | 48 ---------- src/main/resources/config.yml | 8 +- src/main/resources/paper-plugin.yml | 14 +-- .../{usher => sheriff}/AuditLogTest.java | 2 +- .../ConsoleCommandsTest.java | 6 +- .../{usher => sheriff}/FakeStore.java | 2 +- .../{usher => sheriff}/LatchkeyHookTest.java | 6 +- .../{usher => sheriff}/PlannerTest.java | 4 +- .../{usher => sheriff}/SettingsTest.java | 2 +- .../{usher => sheriff}/TextTest.java | 2 +- .../com/extantape/usher/MigrationTest.java | 59 ------------- test/acceptance.mjs | 87 +++++-------------- test/harness.mjs | 54 ++++++------ test/package-lock.json | 4 +- test/package.json | 2 +- .../com/extantape/usher/spy/SpyPlugin.java | 12 +-- test/spy/resources/paper-plugin.yml | 8 +- 46 files changed, 257 insertions(+), 434 deletions(-) rename src/main/java/com/extantape/{usher => sheriff}/AuditLog.java (99%) rename src/main/java/com/extantape/{usher => sheriff}/ChangeService.java (93%) rename src/main/java/com/extantape/{usher => sheriff}/ConsoleCommands.java (95%) rename src/main/java/com/extantape/{usher => sheriff}/GroupStore.java (97%) rename src/main/java/com/extantape/{usher => sheriff}/JoinAttempts.java (96%) rename src/main/java/com/extantape/{usher => sheriff}/LatchkeyHook.java (72%) rename src/main/java/com/extantape/{usher => sheriff}/LuckPermsStore.java (99%) rename src/main/java/com/extantape/{usher => sheriff}/NameResolver.java (94%) rename src/main/java/com/extantape/{usher => sheriff}/PickerMenus.java (99%) rename src/main/java/com/extantape/{usher => sheriff}/Planner.java (90%) rename src/main/java/com/extantape/{usher => sheriff}/PlayerCommands.java (92%) rename src/main/java/com/extantape/{usher => sheriff}/Profile.java (98%) rename src/main/java/com/extantape/{usher => sheriff}/Settings.java (97%) rename src/main/java/com/extantape/{usher/UsherPlugin.java => sheriff/SheriffPlugin.java} (87%) rename src/main/java/com/extantape/{usher => sheriff}/WorldAccess.java (97%) rename src/main/java/com/extantape/{usher => sheriff}/api/JoinAttemptEvent.java (96%) rename src/main/java/com/extantape/{usher => sheriff}/api/ProfilesChangedEvent.java (95%) rename src/main/java/com/extantape/{usher/api/UsherProblemEvent.java => sheriff/api/SheriffProblemEvent.java} (86%) delete mode 100644 src/main/java/com/extantape/usher/Migration.java rename src/test/java/com/extantape/{usher => sheriff}/AuditLogTest.java (98%) rename src/test/java/com/extantape/{usher => sheriff}/ConsoleCommandsTest.java (94%) rename src/test/java/com/extantape/{usher => sheriff}/FakeStore.java (98%) rename src/test/java/com/extantape/{usher => sheriff}/LatchkeyHookTest.java (94%) rename src/test/java/com/extantape/{usher => sheriff}/PlannerTest.java (98%) rename src/test/java/com/extantape/{usher => sheriff}/SettingsTest.java (99%) rename src/test/java/com/extantape/{usher => sheriff}/TextTest.java (97%) delete mode 100644 src/test/java/com/extantape/usher/MigrationTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63b0fc9..0104316 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,5 +28,5 @@ jobs: - run: ./gradlew build - uses: actions/upload-artifact@v4 with: - name: Usher + name: Sheriff path: build/libs/*.jar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8f4ccf..0f7502a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,9 +27,9 @@ jobs: want="${GITHUB_REF_NAME#v}" have="$(./gradlew -q properties | sed -n 's/^version: //p')" [ "$want" = "$have" ] || { echo "Tag $GITHUB_REF_NAME doesn't match version $have in build.gradle.kts"; exit 1; } - head -1 RELEASE_NOTES.md | grep -qx "# Usher $want" || { echo "RELEASE_NOTES.md is for another version. Its first line must be: # Usher $want"; exit 1; } + head -1 RELEASE_NOTES.md | grep -qx "# Sheriff $want" || { echo "RELEASE_NOTES.md is for another version. Its first line must be: # Sheriff $want"; exit 1; } - run: ./gradlew build - name: Attach the jar to a GitHub Release env: GH_TOKEN: ${{ github.token }} - run: gh release create "$GITHUB_REF_NAME" build/libs/Usher-*.jar --title "Usher $GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md --verify-tag + run: gh release create "$GITHUB_REF_NAME" build/libs/Sheriff-*.jar --title "Sheriff $GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md --verify-tag diff --git a/CLAUDE.md b/CLAUDE.md index fc8207f..b3be83a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# Usher +# Sheriff -Registered in the owner's ops process (`~/Code/ops`). Area: `minecraft-plugins` (lead: Minecraft: Plugins Lead). Added 2026-11-09 from the ops starter kit; the repo itself dates from 2026-11-07 (as Stationmaster). +Registered in the owner's ops process (`~/Code/ops`). Area: `minecraft-plugins` (lead: Minecraft: Plugins Lead). Added 2026-11-09 from the ops starter kit; the repo itself dates from 2026-11-07. **Before doing anything here, run `~/Code/ops/scripts/repo-context`.** It shows who leads this repo, whether that session is active, what's pending, and which briefs apply. If it says the repo has no active owner, tell the director ("Helm") before starting work. diff --git a/README.md b/README.md index 0308241..c8d348b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Usher +# Sheriff -A Paper plugin for whitelisted servers. Admins approve a new player and choose their profiles, and Usher sets their whitelist entry and LuckPerms groups. Nobody types LuckPerms commands. +A Paper plugin for whitelisted servers. Admins approve a new player and choose their profiles, and Sheriff sets their whitelist entry and LuckPerms groups. Nobody types LuckPerms commands. **Status:** pre-release. It passes its bot tests on throwaway Paper 26.1.2, 26.2 and 26.3 servers with LuckPerms and Multiverse. It hasn't been used on a real server or seen in a real game client yet. See [Testing](#testing). - Paper 26.1 or newer, on Java 25. There's no support for 1.21.x. -- Needs LuckPerms. Without it, Usher still loads but refuses every change and says why. +- Needs LuckPerms. Without it, Sheriff still loads but refuses every change and says why. - Works with Multiverse and Multiverse-Inventories, but doesn't need them. ## What it does @@ -15,7 +15,7 @@ A profile is a LuckPerms group, such as "Builders", plus the worlds that group o - Adds or removes groups through LuckPerms' API. The change applies at once and survives restarts. - Saves the groups first, then adds the person to the whitelist. If LuckPerms fails, they aren't whitelisted. - Sends anyone standing in a world they just lost to the lobby. It's an ordinary teleport, so Multiverse-Inventories keeps their things with the world they left. -- Logs who changed what and when, in `plugins/Usher/changes.log`. +- Logs who changed what and when, in `plugins/Sheriff/changes.log`. Approving someone with no profile gives them whatever LuckPerms' default group allows. @@ -23,24 +23,24 @@ There are three ways to do it: - **In the game:** `/approve ` opens a chest menu. Click profiles on or off, then press Approve. - **From Leaf Juice or a script:** through the server console. See [Console commands](#console-commands). -- **From Discord or another plugin:** Usher fires Paper events that a bridge plugin can listen to. See [Events](#events). +- **From Discord or another plugin:** Sheriff fires Paper events that a bridge plugin can listen to. See [Events](#events). ### Safety rules -- Only `usher.approve` can approve or change profiles. Players without it don't see the commands at all. +- Only `sheriff.approve` can approve or change profiles. Players without it don't see the commands at all. - Nobody can approve themselves or change their own profiles, even with the permission. The console can. - `restricted-groups` are never added or removed. A profile is refused, with a warning in the log, if its group: - is on that list, in any capitals, - inherits a group on that list, - grants every permission (`*`), or - - grants `usher.approve` or LuckPerms editing. + - grants `sheriff.approve` or LuckPerms editing. These checks run at startup and again on every change, so they still hold if a group changes after a menu opens. - Adding a profile that puts someone in two chat circles at once (see `circle` in the config) asks for a yes first. A profile with a `confirm` question, such as an adults-only world, does too. A yes to one question doesn't cover another. - Changes run one at a time. Two admins editing the same person both get their change. If both add the same profile, it's added once. - Nothing is changed until Approve or Save is pressed. Closing the menu cancels. Every click in the menu is cancelled, so no item can be taken out. - The menu says whether the account has played here, has tried to join, or has never tried. An account that has never tried gets a warning, so a mistyped name that belongs to a stranger is easy to spot. -Children play on the servers this was built for. [docs/kids-first.md](docs/kids-first.md) says how Usher protects them and what it leaves to the people running it. Owner decisions are recorded in [docs/decisions.md](docs/decisions.md). +Children play on the servers this was built for. [docs/kids-first.md](docs/kids-first.md) says how Sheriff protects them and what it leaves to the people running it. Owner decisions are recorded in [docs/decisions.md](docs/decisions.md). ### The menu @@ -48,11 +48,11 @@ Each profile shows its state in words with a symbol: ✔ On, + Will be added, ## Install -1. Put `Usher-.jar` in `plugins/`, next to LuckPerms. -2. Start the server once, then edit `plugins/Usher/config.yml`. The profiles in it are examples. +1. Put `Sheriff-.jar` in `plugins/`, next to LuckPerms. +2. Start the server once, then edit `plugins/Sheriff/config.yml`. The profiles in it are examples. 3. Create each profile's group in LuckPerms, and give it its worlds, for example `multiverse.access.`. -4. Run `usher reload` in the console. -5. Grant `usher.approve` to the people who approve, and nobody else. +4. Run `sheriff reload` in the console. +5. Grant `sheriff.approve` to the people who approve, and nobody else. The lobby must be open to everyone, since people are sent there. If Multiverse's `enforce-access` is on, the default group needs `multiverse.access.`. @@ -62,7 +62,7 @@ The lobby must be open to everyone, since people are sent there. If Multiverse's | --- | --- | | `lobby-world` | Where people go when a world closes to them. Blank means the main world. | | `name-lookup` | How typed names become accounts. `auto` asks Mojang in online mode. `mojang` always asks, which is right behind a proxy. `offline` never asks. | -| `restricted-groups` | Groups Usher never touches. | +| `restricted-groups` | Groups Sheriff never touches. | | `profiles..group` | The LuckPerms group. It must exist. | | `profiles..worlds` | Worlds this profile opens. `*` matches anything, so `explore*` covers `explore_nether`. | | `profiles..circle` | The chat circle it joins, for the two-circles warning. | @@ -70,26 +70,17 @@ The lobby must be open to everyone, since people are sent there. If Multiverse's | `profiles..toggle` | `true` puts it in the menu's lower rows, as an extra. | | `profiles..granted` | What the player sees when it's added. | -A config that can't be read at startup makes Usher refuse every change until it's fixed. A broken file on `reload` keeps the settings already in use. +A config that can't be read at startup makes Sheriff refuse every change until it's fixed. A broken file on `reload` keeps the settings already in use. ### Names -`/approve` and the console take a name that has never joined, in any capitals, and find the real account. Usher checks, in this order: +`/approve` and the console take a name that has never joined, in any capitals, and find the real account. Sheriff checks, in this order: 1. Players the server already knows. 2. People the whitelist recently turned away. 3. Mojang's public profile lookup, in online or `mojang` mode. -A typo fails with "No Minecraft account is called …". If Mojang can't be reached, Usher says that instead of guessing, and nothing changes. In `offline` mode, capitals matter for names the server hasn't seen, because offline accounts are built from the exact name. - -## Upgrading from Stationmaster - -Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing else changed in how it works. - -- Remove `Stationmaster-0.2.1.jar` and add `Usher-0.3.0.jar`, with the server stopped. -- On its first start, Usher copies `plugins/Stationmaster/` to `plugins/Usher/` if `plugins/Usher/` doesn't exist yet: your config, `changes.log` and `join-attempts.json` come across unchanged. The old folder is left as it was; delete it once you're happy. If `plugins/Usher/` already exists, nothing is copied. -- The old names went in 0.4.0, as 0.3.x said: the permission `stationmaster.approve` and the console command `stationmaster` (also `smaster`) no longer work. Grant `usher.approve` instead: `lp group permission set usher.approve true`. -- The API package is `com.extantape.usher.api` (`UsherProblemEvent` was `StationmasterProblemEvent`). +A typo fails with "No Minecraft account is called …". If Mojang can't be reached, Sheriff says that instead of guessing, and nothing changes. In `offline` mode, capitals matter for names the server hasn't seen, because offline accounts are built from the exact name. ## Commands @@ -98,10 +89,10 @@ Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing els | `/approve ` | approvers | Opens the menu to whitelist someone and choose their profiles. | | `/profile ` | approvers, console | Shows someone's profiles, with a button to change them. | | `/profile edit` | approvers | Opens the menu to change their profiles. | -| `/usher reload` | approvers, console | Re-reads `config.yml`. | -| `usher …` | console | The commands below, for Leaf Juice, scripts and people. | +| `/sheriff reload` | approvers, console | Re-reads `config.yml`. | +| `sheriff …` | console | The commands below, for Leaf Juice, scripts and people. | -Approvers need `usher.approve`. It's `false` by default, operators included. +Approvers need `sheriff.approve`. It's `false` by default, operators included. ## Console commands @@ -131,19 +122,19 @@ How `apply` works: Example: ``` -> usher apply 069a79f4-44e9-4726-a5be-fca90e38aaf5 Notch builders none true leaf-juice --json r42 +> sheriff apply 069a79f4-44e9-4726-a5be-fca90e38aaf5 Notch builders none true leaf-juice --json r42 ea-result r42 {"id":"r42","ok":true,"status":"ok","code":"applied","message":"Notch is approved and can join now, with Builders.", ...} ``` ## Events -Usher sends nothing over the network itself, apart from name lookups to Mojang. To post to Discord, listen to these events from another plugin. All fire on the main thread. They carry names and profiles, which can show who is a child, so post them only to a channel that just the admins can read. +Sheriff sends nothing over the network itself, apart from name lookups to Mojang. To post to Discord, listen to these events from another plugin. All fire on the main thread. They carry names and profiles, which can show who is a child, so post them only to a channel that just the admins can read. | Event | When | | --- | --- | -| `com.extantape.usher.api.ProfilesChangedEvent` | Someone was approved or their profiles changed. It has who, by whom, how, what was added and removed, and any warnings said yes to. | -| `com.extantape.usher.api.JoinAttemptEvent` | Someone not on the whitelist tried to join. At most once every ten minutes per account. | -| `com.extantape.usher.api.UsherProblemEvent` | LuckPerms missing or failing, a config problem, a refused restricted group, self-approval, or a change cut off by a crash. | +| `com.extantape.sheriff.api.ProfilesChangedEvent` | Someone was approved or their profiles changed. It has who, by whom, how, what was added and removed, and any warnings said yes to. | +| `com.extantape.sheriff.api.JoinAttemptEvent` | Someone not on the whitelist tried to join. At most once every ten minutes per account. | +| `com.extantape.sheriff.api.SheriffProblemEvent` | LuckPerms missing or failing, a config problem, a refused restricted group, self-approval, or a change cut off by a crash. | ## If the server stops mid-change @@ -153,7 +144,7 @@ Each change writes a `started` line to `changes.log` before LuckPerms is touched `./gradlew build` runs the unit tests: config parsing, the safety rules, the warnings and the change log's crash handling. -The bot tests in `test/` build fresh Paper servers in a temp folder, on 127.0.0.1 port 25615. Each has LuckPerms, Multiverse-Core, Multiverse-Inventories, made-up worlds and groups, and mineflayer bots as players. Every result that says "blocked" has a check beside it showing the same thing works when it should. See the top of `test/harness.mjs` for setup, then run `node test/acceptance.mjs`. It runs one server at a time and deletes it when the run ends. Set `USHER_TEST_KEEP=1` to keep it for a look. LuckPerms uses YAML storage unless `USHER_TEST_STORAGE=h2`. +The bot tests in `test/` build fresh Paper servers in a temp folder, on 127.0.0.1 port 25615. Each has LuckPerms, Multiverse-Core, Multiverse-Inventories, made-up worlds and groups, and mineflayer bots as players. Every result that says "blocked" has a check beside it showing the same thing works when it should. See the top of `test/harness.mjs` for setup, then run `node test/acceptance.mjs`. It runs one server at a time and deletes it when the run ends. Set `SHERIFF_TEST_KEEP=1` to keep it for a look. LuckPerms uses YAML storage unless `SHERIFF_TEST_STORAGE=h2`. **Passed on Paper 26.1.2, 26.2 and 26.3 (2026-11-07), 100 checks each:** diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 35c573c..552f780 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,20 +1,17 @@ -# Usher 0.4.1 +# Sheriff 0.5.0 -A fix to how `attempts` pages, so it matches the shared command format. Nothing else changes. +A new name (D194): the sheriff decides who's welcome in town. Nothing else changes in how it works. -This is an early 0.x release. It has passed bot tests on throwaway servers, but it hasn't been used on a real server or seen in a real game client. +## Installing over the previous version -## Fixed +With the server stopped: +1. Rename its plugin folder to `plugins/Sheriff`. +2. Rename its permission in LuckPerms to `sheriff.approve`. +3. Use `/sheriff` for the console commands. +4. Swap in `Sheriff-0.5.0.jar`. -- **`usher attempts --json` answers in the spec's list shape:** `items`, and `next` as a string cursor, or `null` on the last page. 0.4.0 sent the list under `attempts`, with a numeric `next`, which Leaf Juice found. The cursor is still a position, written as a string. One that Usher didn't hand out is refused with `bad-argument`. -- `describe` gives the cursor's type as `string`. -- Signalbox 0.3.0, whose `Answer.page` makes this shape the default. - -## Upgrading from 0.4.0 - -Swap the jar with the server stopped. Scripts that read `attempts` from the answer read `items` instead. +Leaf Juice needs its matching update to find Sheriff. ## Tested -- 51 unit tests pass, including the cursor rules. -- The bot suite passes 121/121 on throwaway Paper 26.1.2, 26.2 and 26.3, including a check of the list's shape. +Unit tests pass. The rename was checked on a throwaway server. diff --git a/build.gradle.kts b/build.gradle.kts index 93a4a19..388d012 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "com.extantape" -version = "0.4.1" +version = "0.5.0" java { toolchain.languageVersion.set(JavaLanguageVersion.of(25)) @@ -48,17 +48,17 @@ tasks.test { } // Signalbox goes inside the jar, relocated, so it can never clash with another plugin's copy. The shaded jar is the -// only one built, so the release workflow's Usher-*.jar finds exactly one. +// only one built, so the release workflow's Sheriff-*.jar finds exactly one. tasks.shadowJar { - archiveBaseName.set("Usher") + archiveBaseName.set("Sheriff") archiveClassifier.set("") from("LICENSE") - relocate("com.extantape.signalbox", "com.extantape.usher.libs.signalbox") + relocate("com.extantape.signalbox", "com.extantape.sheriff.libs.signalbox") } tasks.jar { enabled = false } tasks.assemble { dependsOn(tasks.shadowJar) } -// Test only: a tiny plugin that logs Usher's events, for the bot tests in test/. Never shipped. +// Test only: a tiny plugin that logs Sheriff's events, for the bot tests in test/. Never shipped. val spy: SourceSet = sourceSets.create("spy") { java.srcDir("test/spy/java") resources.srcDir("test/spy/resources") @@ -67,6 +67,6 @@ val spy: SourceSet = sourceSets.create("spy") { dependencies { "spyCompileOnly"(paperApi) } tasks.register("spyJar") { from(spy.output) - archiveFileName.set("UsherSpy.jar") + archiveFileName.set("SheriffSpy.jar") destinationDirectory.set(layout.buildDirectory.dir("test-spy")) } diff --git a/docs/decisions.md b/docs/decisions.md index 594cac1..f8a9b2d 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -37,3 +37,7 @@ Decisions the owner made for Stationmaster, with dates. Follow them without aski - **Invites (2026-11-09, owner):** when a Latchkey invite is redeemed, the username is whitelisted at once and Usher, if installed, applies the profile the invite names, through its usual safety rules. To be built with Latchkey 0.2.0 (crossing `latchkey-usher`). - **Profiles on invites (2026-11-09, owner):** a Latchkey invite carries a list of Usher profiles. Usher runs its safety checks when the invite is created, so a bad combination is refused before anyone arrives, and applies the profiles when it's redeemed. An invite with no profiles gives lobby-only access until an admin adds more. Usher's commands get a machine-readable JSON form and `/usher describe`, through the shared command library. Waystation and HouseRules need nothing new: their platforms and circles are already permissions a profile can carry. - **Console format and the old names (2026-11-10, owner through Helm):** 0.4.0 moves the console commands to the shared Signalbox format (`--json `, `ea-result`, `describe`) and removes `stationmaster`, `smaster` and `stationmaster.approve`, as 0.3.x promised. Leaf Juice's asks (crossing plugins-leafjuice, ledger 10f277027b09): `none` is reserved and can't be a profile name, and `describe` marks optional arguments (Signalbox 0.2.0). + +## 2026-11-14 + +- **Name (owner, D194):** the plugin is renamed Sheriff. The owner chose "Sheriff (Recommended)", and asked that no old names be kept: no aliases, no old permissions and no folder copy. Released as 0.5.0. diff --git a/docs/kids-first.md b/docs/kids-first.md index 9a9d010..db0f75f 100644 --- a/docs/kids-first.md +++ b/docs/kids-first.md @@ -1,20 +1,20 @@ # Kids first -Children play on the servers Usher is built for, and it's the plugin that adds people to kids' groups. This note says how it protects them and what it leaves to the people running it. Reviewed 2026-11-07. +Children play on the servers Sheriff is built for, and it's the plugin that adds people to kids' groups. This note says how it protects them and what it leaves to the people running it. Reviewed 2026-11-07. -## What Usher does +## What Sheriff does -- **Nobody gets in by accident.** The whitelist stays on. Approving someone takes an admin with `usher.approve` or the console, and nobody can approve themselves. +- **Nobody gets in by accident.** The whitelist stays on. Approving someone takes an admin with `sheriff.approve` or the console, and nobody can approve themselves. - **The right account.** Before approving, the menu says whether the account has played here, has tried to join (and when), or has never come near the server. An account that has never tried to join gets a ⚠ warning to check it's the right person. `resolve` gives Leaf Juice the same facts (`playedBefore`, `triedToJoin`, `lastTried`), so it can warn too. A mistyped name that happens to belong to a stranger is the main way a stranger could end up in a kids' world. This makes that mistake visible before it happens. - **Kids' and adults' chat stay apart unless an admin says yes.** A profile can name its chat circle. Any change that would put someone in two circles at once stops and asks first, in the menu and from Leaf Juice. A yes to one question never covers another. - **Adult-only extras ask first.** A profile with a `confirm` question can't be added without a yes. - **No admin powers through a profile.** Restricted groups, groups that inherit them, and groups granting `*`, the approve permission or LuckPerms editing are refused. This holds even if the config is edited to offer them. - **Leaving a world is safe.** Someone who loses a world is sent to the lobby with a kind message. Their things stay in that world, ready if it's given back. -- **Little data, kept on the server.** Usher stores names, account IDs, profiles, times and who made each change. That's in `changes.log` and `join-attempts.json`. It stores no IP addresses, chat or locations, and sends nothing anywhere except name lookups to Mojang. +- **Little data, kept on the server.** Sheriff stores names, account IDs, profiles, times and who made each change. That's in `changes.log` and `join-attempts.json`. It stores no IP addresses, chat or locations, and sends nothing anywhere except name lookups to Mojang. ## What it leaves to the people running it - **Where events go.** `JoinAttemptEvent` and `ProfilesChangedEvent` carry names and which profiles someone has, which can show who is a child. A Discord bridge should post them only to a channel that just the admins can read. -- **Group permissions.** Usher adds and removes groups. Which worlds and chat a group opens is set in LuckPerms. The kids' groups should open only the kids' worlds. +- **Group permissions.** Sheriff adds and removes groups. Which worlds and chat a group opens is set in LuckPerms. The kids' groups should open only the kids' worlds. - **Setting circles.** The two-circles warning only works for profiles that have `circle` set. Every kids' and adults' profile should have one. -- **Who holds the permission.** Grant `usher.approve` to the trusted adults who approve, and nobody else. +- **Who holds the permission.** Grant `sheriff.approve` to the trusted adults who approve, and nobody else. diff --git a/docs/paper-26-audit.md b/docs/paper-26-audit.md index e802378..5a4f507 100644 --- a/docs/paper-26-audit.md +++ b/docs/paper-26-audit.md @@ -1,13 +1,13 @@ # Paper 26.x audit -Usher targets Paper 26.1 and newer on Java 25, and is tested on 26.1.2, 26.2 and 26.3. This note says what uses modern Paper APIs, what doesn't, and why. Each API's status was checked against Paper's documentation (docs.papermc.io) on 2026-11-07, and against the `paper-api` 26.1.2 jar it compiles with. +Sheriff targets Paper 26.1 and newer on Java 25, and is tested on 26.1.2, 26.2 and 26.3. This note says what uses modern Paper APIs, what doesn't, and why. Each API's status was checked against Paper's documentation (docs.papermc.io) on 2026-11-07, and against the `paper-api` 26.1.2 jar it compiles with. ## Moved to modern APIs | What | Now | Why | | --- | --- | --- | | Plugin descriptor | `paper-plugin.yml`, with LuckPerms as an optional server dependency that joins the classpath | The owner's rule: Paper only. Paper's docs still mark Paper plugins as experimental. That's accepted, and every test runs this way. | -| Commands | `BasicCommand`, registered in the `COMMANDS` lifecycle event, instead of `plugin.yml` commands | Paper plugins can't use the old `commands` section. `permission()` hides `/approve` and `/profile` from players without `usher.approve`, so they never see them. | +| Commands | `BasicCommand`, registered in the `COMMANDS` lifecycle event, instead of `plugin.yml` commands | Paper plugins can't use the old `commands` section. `permission()` hides `/approve` and `/profile` from players without `sheriff.approve`, so they never see them. | | Menu items | Data components (`ITEM_NAME`, `LORE`, `ENCHANTMENT_GLINT_OVERRIDE`, `PROFILE`) instead of `ItemMeta` and `SkullMeta` | Shorter, and no meta copies. `ITEM_NAME` is never italic, so names don't need un-italicising. Paper warns that this API can change between versions. That's acceptable for a menu, and the tests cover all three versions. | | Text | Adventure components everywhere | No legacy colour codes anywhere. | | Java | Java 25: records, pattern matching and unnamed variables (`_`) where a lambda ignores its arguments | Only where they read more plainly. | @@ -21,7 +21,7 @@ Usher targets Paper 26.1 and newer on Java 25, and is tested on 26.1.2, 26.2 and | A chest inventory instead of `MenuType` | A plain 54-slot chest with a holder does the job, and `MenuType` would add nothing to it. | | A chest confirmation screen instead of a dialog | The Dialog API would suit a yes/no question. But the chest screen is the same surface the admin is already in, the bots can drive it on every version, and the warnings already fit. Worth a look after real admins try the menu. | | `AsyncPlayerPreLoginEvent` for join attempts | Not deprecated in 26.x (the build has deprecation warnings on and shows none), and it gives the account and name before the whitelist check. | -| No bootstrapper | Usher registers nothing that needs one. Paper's docs call bootstrappers highly experimental. | +| No bootstrapper | Sheriff registers nothing that needs one. Paper's docs call bootstrappers highly experimental. | | No NMS | Nothing needs it. | ## Removed diff --git a/docs/test-audit.md b/docs/test-audit.md index edbd26e..b4a1d5e 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -4,19 +4,17 @@ Kept by this repo's owning session (Minecraft: Plugins Lead), per `~/Code/CLAUDE ## 2026-11-10: after 0.3.1, 0.4.0 and 0.4.1 -What changed since the first audit: profiles on Latchkey invites (0.3.1), the console on Signalbox format 1 with `describe` and the Stationmaster names removed (0.4.0), and attempts in pages (0.4.1). The first audit's table still holds, except its last row: the old names are gone, as 0.3.x said they would be. | Promise | Covered by | | --- | --- | -| A Latchkey invite's profiles are checked when it's made: unknown and admin-granting profiles refused, a second circle asked as a question, only its own yes answers it | unit `LatchkeyHookTest` (8 tests); live in Latchkey's own bot test, which runs with Usher | -| **A profile that turns unsafe between the invite and its use isn't applied, and the log says so** | **new**, live in Latchkey's bot test ("a profile made unsafe after the invite is not applied when it's used"). Usher re-checks through the same `ChangeService` as every other change | +| A Latchkey invite's profiles are checked when it's made: unknown and admin-granting profiles refused, a second circle asked as a question, only its own yes answers it | unit `LatchkeyHookTest` (8 tests); live in Latchkey's own bot test, which runs with Sheriff | +| **A profile that turns unsafe between the invite and its use isn't applied, and the log says so** | **new**, live in Latchkey's bot test ("a profile made unsafe after the invite is not applied when it's used"). Sheriff re-checks through the same `ChangeService` as every other change | | Without LuckPerms, no invite profile is allowed | unit `withoutLuckPermsNothingIsAllowed` | | One answer line per console request, format 1, with stable codes (`unknown-command`, `bad-argument`, a bad id) | unit `ConsoleCommandsTest`; live "an unknown command still gets one answer", "bad arguments answer bad-argument", "a bad request id is answered in plain words" | | `describe` lists every command and marks only the optional arguments | unit `describeListsEveryCommand`, `describeMarksTheOptionalArgumentsAndOnlyThose`; live | -| Attempts come in pages; a cursor Usher didn't hand out is refused | unit `cursorsAreOnlyOnesUsherHandedOut`; live "the list pages the spec's way" | +| Attempts come in pages; a cursor Sheriff didn't hand out is refused | unit `cursorsAreOnlyOnesSheriffHandedOut`; live "the list pages the spec's way" | | "none" can't be a profile name, since it means "no profiles" | unit `SettingsTest`, `ConsoleCommandsTest` | | **Console commands stay console-only, even for an approver** | **new** live "an approver typing a console command in game is refused, and told what to use", "and P_build is unchanged" | -| The old `stationmaster.*` nodes no longer let anyone approve | live "the old node stationmaster.approve no longer lets anyone approve" | **Gaps added this audit:** the two in bold. The bot test passed 123/123 on Paper 26.1.2. The changes don't depend on the version, so it wasn't rerun on 26.2 and 26.3, where 0.4.1 passed 121/121. @@ -24,11 +22,11 @@ What changed since the first audit: profiles on Latchkey invites (0.3.1), the co ## 2026-11-09: first full audit (at 0.3.0) -Usher adds people to kids' groups, so the promises in `docs/kids-first.md` come first. "Live" is `test/acceptance.mjs` on throwaway Paper servers with bots and LuckPerms; "unit" is `src/test`. +Sheriff adds people to kids' groups, so the promises in `docs/kids-first.md` come first. "Live" is `test/acceptance.mjs` on throwaway Paper servers with bots and LuckPerms; "unit" is `src/test`. | Promise | Covered by | | --- | --- | -| Nobody gets in by accident: whitelist on, approving needs `usher.approve` or the console | live "a stranger is turned away", "a player without the permission gets no menu", "every command is hidden from them", "strangers still can't" | +| Nobody gets in by accident: whitelist on, approving needs `sheriff.approve` or the console | live "a stranger is turned away", "a player without the permission gets no menu", "every command is hidden from them", "strangers still can't" | | Nobody approves themselves, in any capitals | live "self-approval and self-editing are refused, in any capitals" | | **The right account: an account that has never tried to join gets a ⚠ warning in the menu** | **new** live "…gets the ⚠ warning in the menu", and "an account that did try to join gets no never-tried warning"; `resolve` facts: live "resolve tells Leaf Juice who tried to join and who never did" | | Kids' and adults' circles stay apart unless an admin says yes; a yes never covers another question | unit `bridgingCirclesNeedsAYes`, `bridgingWhenBothAreAddedAtOnce`, `adultToggleAsksForAYesAndAYesForOneThingDoesNotCoverAnother`; live warning screen, Go back, Yes | @@ -40,10 +38,9 @@ Usher adds people to kids' groups, so the promises in `docs/kids-first.md` come | One answer line per console request; repeats not applied twice | live "every request gets exactly one result line", "a repeated request ID is answered again but not applied twice" | | Broken config refuses every change; a broken reload keeps the old settings | unit `brokenYamlRefusesEverything`, `brokenSettingsRefuse`; live | | Without LuckPerms: loads, refuses, says why | live "LuckPerms missing" | -| Upgrade from Stationmaster: folder copied, old names work for one release | unit `MigrationTest`; live "The rename from Stationmaster" | **Gaps added this audit:** the two in bold. Both passed on Paper 26.1.2 and 26.3 (119/119 each). **Skipped, with reasons:** - "Sends nothing anywhere except name lookups to Mojang": the only network call in the code is `NameResolver`'s Mojang lookup (checked by reading the code at this audit). A test that watches all network traffic would cost more than it proves. The bots run with offline lookups. -- How Usher looks to a person (menu colours, wording): needs a real client. It's on the real-client list. +- How Sheriff looks to a person (menu colours, wording): needs a real client. It's on the real-client list. diff --git a/settings.gradle.kts b/settings.gradle.kts index 7542907..2698efc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "Usher" +rootProject.name = "Sheriff" diff --git a/src/main/java/com/extantape/usher/AuditLog.java b/src/main/java/com/extantape/sheriff/AuditLog.java similarity index 99% rename from src/main/java/com/extantape/usher/AuditLog.java rename to src/main/java/com/extantape/sheriff/AuditLog.java index 4916204..3c68605 100644 --- a/src/main/java/com/extantape/usher/AuditLog.java +++ b/src/main/java/com/extantape/sheriff/AuditLog.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import com.google.gson.Gson; import com.google.gson.JsonObject; diff --git a/src/main/java/com/extantape/usher/ChangeService.java b/src/main/java/com/extantape/sheriff/ChangeService.java similarity index 93% rename from src/main/java/com/extantape/usher/ChangeService.java rename to src/main/java/com/extantape/sheriff/ChangeService.java index 49d4dfa..9554e0c 100644 --- a/src/main/java/com/extantape/usher/ChangeService.java +++ b/src/main/java/com/extantape/sheriff/ChangeService.java @@ -1,7 +1,7 @@ -package com.extantape.usher; +package com.extantape.sheriff; -import com.extantape.usher.api.ProfilesChangedEvent; -import com.extantape.usher.api.UsherProblemEvent; +import com.extantape.sheriff.api.ProfilesChangedEvent; +import com.extantape.sheriff.api.SheriffProblemEvent; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import org.bukkit.Bukkit; @@ -30,9 +30,9 @@ */ public final class ChangeService { - public static final String PERMISSION = "usher.approve"; + public static final String PERMISSION = "sheriff.approve"; - /** True for approvers. The old Stationmaster node stopped working in 0.4.0, as 0.3.x said it would. */ + /** True for approvers. */ public static boolean mayApprove(org.bukkit.permissions.Permissible who) { return who.hasPermission(PERMISSION); } @@ -51,14 +51,14 @@ public record Result(String status, String code, String message, List { - Thread t = new Thread(r, "Usher changes"); + Thread t = new Thread(r, "Sheriff changes"); t.setDaemon(true); return t; }); - ChangeService(UsherPlugin plugin) { this.plugin = plugin; } + ChangeService(SheriffPlugin plugin) { this.plugin = plugin; } public CompletableFuture submit(Request request) { return CompletableFuture.supplyAsync(() -> { @@ -66,7 +66,7 @@ public CompletableFuture submit(Request request) { return run(request); } catch (RuntimeException e) { plugin.getLogger().log(Level.SEVERE, "A change for " + request.targetName() + " failed unexpectedly", e); - return result(request, "error", "internal", "Something went wrong inside Usher, so the change stopped. See the server log.", List.of(), null, false, false); + return result(request, "error", "internal", "Something went wrong inside Sheriff, so the change stopped. See the server log.", List.of(), null, false, false); } }, worker); } @@ -77,7 +77,7 @@ private Result run(Request r) { Settings settings = plugin.settings(); GroupStore store = plugin.store(); AuditLog audit = plugin.audit(); - if (audit == null) return result(r, "error", "log_failed", "Usher can't open its change log, so nothing was changed.", List.of(), null, false, false); + if (audit == null) return result(r, "error", "log_failed", "Sheriff can't open its change log, so nothing was changed.", List.of(), null, false, false); if (r.requestId() != null) { JsonObject previous = audit.resultFor(r.requestId()); @@ -93,7 +93,7 @@ private Result run(Request r) { // Checks that need the main thread: permissions and the whitelist. String[] pre = callMain(() -> { - if (!plugin.settings().ok()) return new String[]{"config_broken", "Usher's config is broken, so nothing was changed. " + plugin.settings().problems().get(0)}; + if (!plugin.settings().ok()) return new String[]{"config_broken", "Sheriff's config is broken, so nothing was changed. " + plugin.settings().problems().get(0)}; if (!plugin.store().ready()) return new String[]{"luckperms_missing", "LuckPerms isn't running, so nothing was changed."}; if (!r.actor().isConsole()) { Player admin = Bukkit.getPlayer(r.actor().uuid()); @@ -130,10 +130,10 @@ private Result run(Request r) { audit.write(entry(r, seq, "started", plan, settings, null)); } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Couldn't write changes.log", e); - return problem(r, "log_failed", "Usher couldn't write its change log, so nothing was changed."); + return problem(r, "log_failed", "Sheriff couldn't write its change log, so nothing was changed."); } - pause("usher.test.pause-before-groups"); + pause("sheriff.test.pause-before-groups"); GroupStore.UserState after; try { after = plan.addGroups().isEmpty() && plan.removeGroups().isEmpty() ? state @@ -141,7 +141,7 @@ private Result run(Request r) { } catch (Exception e) { return luckPermsFailed(r, seq, e); } - pause("usher.test.pause-after-groups"); + pause("sheriff.test.pause-after-groups"); List added = ids(settings, plan.addGroups()), removed = ids(settings, plan.removeGroups()); // Found by name, so the whitelist entry carries the name and not just the UUID. Only used if it's the same account. @@ -192,7 +192,7 @@ private Result luckPermsFailed(Request r, Long seq, Exception e) { } private void fireProblem(String code, String message, Request r) { - plugin.runMain(() -> Bukkit.getPluginManager().callEvent(new UsherProblemEvent(code, message, r.target(), r.actor().name()))); + plugin.runMain(() -> Bukkit.getPluginManager().callEvent(new SheriffProblemEvent(code, message, r.target(), r.actor().name()))); } private void log(AuditLog audit, JsonObject entry) { @@ -244,7 +244,7 @@ private static String doneMessage(Request r, List added, List re sb.append(name).append(" now has ").append(labels(s, added)).append("."); } if (!removed.isEmpty()) sb.append(sb.isEmpty() ? "" : " ").append(labels(s, removed)).append(removed.size() == 1 ? " was" : " were").append(" taken off ").append(name).append("."); - if (!plan.keptByInheritance().isEmpty()) sb.append(" They still get ").append(labels(s, plan.keptByInheritance())).append(" through another group, which Usher can't change."); + if (!plan.keptByInheritance().isEmpty()) sb.append(" They still get ").append(labels(s, plan.keptByInheritance())).append(" through another group, which Sheriff can't change."); return sb.toString().trim(); } @@ -252,7 +252,7 @@ private static String nothingMessage(Request r, Planner.Plan plan, Settings s) { StringBuilder sb = new StringBuilder("Nothing to change for ").append(r.targetName()).append("."); if (!plan.alreadyHad().isEmpty()) sb.append(" They already have ").append(labels(s, plan.alreadyHad())).append("."); if (!plan.didNotHave().isEmpty()) sb.append(" They didn't have ").append(labels(s, plan.didNotHave())).append("."); - if (!plan.keptByInheritance().isEmpty()) sb.append(" They get ").append(labels(s, plan.keptByInheritance())).append(" through another group, which Usher can't change."); + if (!plan.keptByInheritance().isEmpty()) sb.append(" They get ").append(labels(s, plan.keptByInheritance())).append(" through another group, which Sheriff can't change."); return sb.toString(); } diff --git a/src/main/java/com/extantape/usher/ConsoleCommands.java b/src/main/java/com/extantape/sheriff/ConsoleCommands.java similarity index 95% rename from src/main/java/com/extantape/usher/ConsoleCommands.java rename to src/main/java/com/extantape/sheriff/ConsoleCommands.java index 8d69405..f03cebc 100644 --- a/src/main/java/com/extantape/usher/ConsoleCommands.java +++ b/src/main/java/com/extantape/sheriff/ConsoleCommands.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import com.extantape.signalbox.Answer; import com.extantape.signalbox.Args; @@ -31,7 +31,7 @@ import java.util.regex.Pattern; /** - * /usher: console commands for Leaf Juice, scripts and people. With {@code --json } each answers with exactly one + * /sheriff: console commands for Leaf Juice, scripts and people. With {@code --json } each answers with exactly one * Signalbox line, {@code ea-result }, which may come a moment later; without it, in plain words. Changes that * need a yes answer with the questions and change nothing; send them again with {@code --yes id,id}. See the README. */ @@ -40,9 +40,9 @@ final class ConsoleCommands implements BasicCommand { private static final Pattern VIA = Pattern.compile("[a-z0-9-]{1,32}"); static final int MAX_LIMIT = 100; - private final UsherPlugin plugin; + private final SheriffPlugin plugin; - ConsoleCommands(UsherPlugin plugin) { this.plugin = plugin; } + ConsoleCommands(SheriffPlugin plugin) { this.plugin = plugin; } /** The console, and approvers in the game (who only get reload). Hidden from everyone else. */ @Override @@ -79,7 +79,7 @@ public void execute(CommandSourceStack source, String[] args) { List t = r.tokens(); if (t.isEmpty()) { if (r.json()) fail(sender, r.id(), Errors.UNKNOWN_COMMAND, "Give a command: " + String.join(", ", SUBS) + "."); - else sender.sendMessage("Usher " + plugin.getPluginMeta().getVersion() + ": " + String.join(", ", SUBS) + ". See the README for details."); + else sender.sendMessage("Sheriff " + plugin.getPluginMeta().getVersion() + ": " + String.join(", ", SUBS) + ". See the README for details."); return; } String sub = t.get(0).toLowerCase(Locale.ROOT); @@ -105,8 +105,8 @@ public void execute(CommandSourceStack source, String[] args) { case "log" -> log(sender, r, rest); case "describe" -> { none(rest); - if (r.json()) send(sender, Answer.ok(r.id(), Describe.body("Usher", plugin.getPluginMeta().getVersion(), described()))); - else for (Describe.Command d : described()) sender.sendMessage("usher " + d.name()); + if (r.json()) send(sender, Answer.ok(r.id(), Describe.body("Sheriff", plugin.getPluginMeta().getVersion(), described()))); + else for (Describe.Command d : described()) sender.sendMessage("sheriff " + d.name()); } default -> fail(sender, r.id(), Errors.UNKNOWN_COMMAND, "Unknown: " + sub + ". Try " + String.join(", ", SUBS) + "."); } @@ -161,7 +161,7 @@ private void profiles(CommandSender sender, Request r) { // --- resolve and show --- private void resolve(CommandSender sender, Request r, List rest) throws Request.Bad, Refusal { - if (rest.size() != 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: usher resolve "); + if (rest.size() != 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: sheriff resolve "); String typed = Args.player(rest.get(0)); plugin.names().resolve(typed, plugin.settings().lookup()).whenComplete((who, e) -> { if (e != null) { nameFail(sender, r, e, typed); return; } @@ -190,7 +190,7 @@ private void nameFail(CommandSender sender, Request r, Throwable e, String typed } private void show(CommandSender sender, Request r, List rest) throws Request.Bad, Refusal { - if (rest.size() != 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: usher show "); + if (rest.size() != 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: sheriff show "); if (!plugin.store().ready()) throw new Refusal("luckperms-missing", "LuckPerms isn't running."); String typed = Args.player(rest.get(0)); UUID id = typed.contains("-") ? UUID.fromString(typed) : null; @@ -233,7 +233,7 @@ record ApplyArgs(UUID uuid, String name, Set add, Set remove, bo static ApplyArgs parseApply(List rest) throws Request.Bad, Refusal { if (rest.size() < 4 || rest.size() > 6) - throw new Refusal(Errors.BAD_ARGUMENT, "Use: usher apply [whitelist true|false] [via]"); + throw new Refusal(Errors.BAD_ARGUMENT, "Use: sheriff apply [whitelist true|false] [via]"); UUID id = Args.uuid(rest.get(0)); String name = rest.get(1); if (!NameResolver.possibleName(name)) throw new Refusal(Errors.BAD_ARGUMENT, "\"" + name + "\" can't be a Minecraft name."); @@ -260,7 +260,7 @@ private void apply(CommandSender sender, Request r, List rest) throws Re Set confirmed = LatchkeyHook.confirmedCodes(plugin.settings(), r.yes()); var request = new ChangeService.Request(r.id(), ChangeService.Actor.console(a.via()), a.uuid(), a.name(), a.add(), a.remove(), confirmed, a.whitelist()); plugin.changes().submit(request).whenComplete((result, e) -> plugin.runMain(() -> { - if (e != null || result == null) { fail(sender, r.id(), Errors.INTERNAL, "Something went wrong inside Usher. See the server log."); return; } + if (e != null || result == null) { fail(sender, r.id(), Errors.INTERNAL, "Something went wrong inside Sheriff. See the server log."); return; } answer(sender, r, result); })); } @@ -290,7 +290,7 @@ private void answer(CommandSender sender, Request r, ChangeService.Result result // --- attempts and log --- private void attempts(CommandSender sender, Request r, List rest) throws Request.Bad, Refusal { - if (rest.size() > 2) throw new Refusal(Errors.BAD_ARGUMENT, "Use: usher attempts [limit] [cursor]"); + if (rest.size() > 2) throw new Refusal(Errors.BAD_ARGUMENT, "Use: sheriff attempts [limit] [cursor]"); int limit = rest.isEmpty() ? 50 : Args.integer(rest.get(0)); int cursor = rest.size() < 2 ? 0 : cursorOf(rest.get(1)); if (limit < 1 || limit > MAX_LIMIT || cursor < 0) throw new Refusal(Errors.BAD_ARGUMENT, "limit is 1 to " + MAX_LIMIT + " and cursor 0 or more."); @@ -316,7 +316,7 @@ private void attempts(CommandSender sender, Request r, List rest) throws } private void log(CommandSender sender, Request r, List rest) throws Request.Bad, Refusal { - if (rest.size() > 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: usher log [limit]"); + if (rest.size() > 1) throw new Refusal(Errors.BAD_ARGUMENT, "Use: sheriff log [limit]"); int count = rest.isEmpty() ? 20 : Args.integer(rest.get(0)); if (count < 1 || count > MAX_LIMIT) throw new Refusal(Errors.BAD_ARGUMENT, "limit is 1 to " + MAX_LIMIT + "."); if (plugin.audit() == null) throw new Refusal("log-failed", "changes.log can't be opened."); @@ -336,7 +336,7 @@ private void log(CommandSender sender, Request r, List rest) throws Requ /** The cursor a page handed out: a string to the caller, a position here. Anything else is refused. */ static int cursorOf(String token) throws Refusal { - if (!token.matches("\\d{1,9}")) throw new Refusal(Errors.BAD_ARGUMENT, "That cursor isn't one Usher handed out. Use the next value from the last page."); + if (!token.matches("\\d{1,9}")) throw new Refusal(Errors.BAD_ARGUMENT, "That cursor isn't one Sheriff handed out. Use the next value from the last page."); return Integer.parseInt(token); } diff --git a/src/main/java/com/extantape/usher/GroupStore.java b/src/main/java/com/extantape/sheriff/GroupStore.java similarity index 97% rename from src/main/java/com/extantape/usher/GroupStore.java rename to src/main/java/com/extantape/sheriff/GroupStore.java index d8ed9fe..e21247e 100644 --- a/src/main/java/com/extantape/usher/GroupStore.java +++ b/src/main/java/com/extantape/sheriff/GroupStore.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import java.util.Set; import java.util.UUID; diff --git a/src/main/java/com/extantape/usher/JoinAttempts.java b/src/main/java/com/extantape/sheriff/JoinAttempts.java similarity index 96% rename from src/main/java/com/extantape/usher/JoinAttempts.java rename to src/main/java/com/extantape/sheriff/JoinAttempts.java index ca8e035..454aca2 100644 --- a/src/main/java/com/extantape/usher/JoinAttempts.java +++ b/src/main/java/com/extantape/sheriff/JoinAttempts.java @@ -1,6 +1,6 @@ -package com.extantape.usher; +package com.extantape.sheriff; -import com.extantape.usher.api.JoinAttemptEvent; +import com.extantape.sheriff.api.JoinAttemptEvent; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; @@ -43,11 +43,11 @@ public static final class Attempt { private static final long EVENT_GAP_MS = 10 * 60 * 1000L; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private final UsherPlugin plugin; + private final SheriffPlugin plugin; private final Path file; private final Map attempts = new LinkedHashMap<>(); - JoinAttempts(UsherPlugin plugin, Path file) { + JoinAttempts(SheriffPlugin plugin, Path file) { this.plugin = plugin; this.file = file; try { diff --git a/src/main/java/com/extantape/usher/LatchkeyHook.java b/src/main/java/com/extantape/sheriff/LatchkeyHook.java similarity index 72% rename from src/main/java/com/extantape/usher/LatchkeyHook.java rename to src/main/java/com/extantape/sheriff/LatchkeyHook.java index cb89266..d9c294f 100644 --- a/src/main/java/com/extantape/usher/LatchkeyHook.java +++ b/src/main/java/com/extantape/sheriff/LatchkeyHook.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.bukkit.Bukkit; import org.bukkit.event.Event; @@ -18,11 +18,11 @@ import java.util.logging.Level; /** - * Latchkey invites (owner, 2026-11-09; crossing latchkey-usher). When an admin makes an invite naming profiles, Usher - * runs its usual checks then, so a bad combination is refused before anyone arrives. When the invite is used, Usher + * Latchkey invites (owner, 2026-11-09; crossing latchkey-sheriff). When an admin makes an invite naming profiles, Sheriff + * runs its usual checks then, so a bad combination is refused before anyone arrives. When the invite is used, Sheriff * applies the profiles through the same change path as any approval, with the yeses given when the invite was made. * - *

Latchkey is optional and Usher doesn't build against it: its events are found by name at startup and read through + *

Latchkey is optional and Sheriff doesn't build against it: its events are found by name at startup and read through * their public methods, which are the agreed contract. If Latchkey isn't there, or its events don't match, this does * nothing and says so once. */ @@ -31,7 +31,7 @@ final class LatchkeyHook implements Listener { static final String REDEEMED = "com.extantape.latchkey.api.InviteRedeemedEvent"; static final String ACTOR = "Latchkey invite"; - /** What Usher says about an invite's profiles: a refusal, or questions keyed by their command-line id. */ + /** What Sheriff says about an invite's profiles: a refusal, or questions keyed by their command-line id. */ record Check(String refusalCode, String refusalText, Map questions) { boolean refused() { return refusalCode != null; } } @@ -39,11 +39,11 @@ record Check(String refusalCode, String refusalText, Map questio private LatchkeyHook() {} /** - * Usher's checks for a new player. Invites are for people who haven't arrived, so the plan starts from no groups; + * Sheriff's checks for a new player. Invites are for people who haven't arrived, so the plan starts from no groups; * when the invite is used the real plan runs again against their actual groups. */ static Check check(Settings settings, GroupStore store, Collection profiles, Set yesIds) { - if (!store.ready()) return new Check("luckperms-missing", "LuckPerms isn't running, so Usher can't check these profiles.", Map.of()); + if (!store.ready()) return new Check("luckperms-missing", "LuckPerms isn't running, so Sheriff can't check these profiles.", Map.of()); Set confirmed = confirmedCodes(settings, yesIds); Planner.Plan plan = Planner.plan(settings, store, new GroupStore.UserState(Set.of(), Set.of()), new LinkedHashSet<>(profiles), Set.of(), confirmed); @@ -54,7 +54,7 @@ static Check check(Settings settings, GroupStore store, Collection profi } /** - * Usher's warning codes as command-line ids, which allow only {@code a-z 0-9 -}: {@code confirm:grown_sky} becomes + * Sheriff's warning codes as command-line ids, which allow only {@code a-z 0-9 -}: {@code confirm:grown_sky} becomes * {@code confirm-grown-sky}. */ static String toYesId(String code) { return code.replace(':', '-').replace('_', '-'); } @@ -75,7 +75,7 @@ static Set confirmedCodes(Settings settings, Set yesIds) { } /** Hooks into Latchkey's events if Latchkey is installed. */ - static void register(UsherPlugin usher) { + static void register(SheriffPlugin sheriff) { Plugin latchkey = Bukkit.getPluginManager().getPlugin("Latchkey"); if (latchkey == null) return; try { @@ -94,15 +94,15 @@ static void register(UsherPlugin usher) { try { @SuppressWarnings("unchecked") List profiles = (List) getProfiles.invoke(event); @SuppressWarnings("unchecked") Set yes = (Set) getConfirmed.invoke(event); - Check c = check(usher.settings(), usher.store(), profiles, yes); - checkedBy.invoke(event, "Usher"); + Check c = check(sheriff.settings(), sheriff.store(), profiles, yes); + checkedBy.invoke(event, "Sheriff"); if (c.refused()) refuse.invoke(event, c.refusalCode(), c.refusalText()); else for (var q : c.questions().entrySet()) need.invoke(event, q.getKey(), q.getValue()); } catch (ReflectiveOperationException | RuntimeException e) { - usher.getLogger().log(Level.WARNING, "Couldn't check a Latchkey invite's profiles", e); - try { refuse.invoke(event, "internal", "Usher couldn't check these profiles. See the server log."); } catch (ReflectiveOperationException ignored) {} + sheriff.getLogger().log(Level.WARNING, "Couldn't check a Latchkey invite's profiles", e); + try { refuse.invoke(event, "internal", "Sheriff couldn't check these profiles. See the server log."); } catch (ReflectiveOperationException ignored) {} } - }, usher); + }, sheriff); Bukkit.getPluginManager().registerEvent(redeemedClass, owner, EventPriority.MONITOR, (l, event) -> { if (!redeemedClass.isInstance(event)) return; @@ -111,33 +111,33 @@ static void register(UsherPlugin usher) { String name = (String) rName.invoke(event); @SuppressWarnings("unchecked") List profiles = (List) rProfiles.invoke(event); @SuppressWarnings("unchecked") Set yes = (Set) rConfirmed.invoke(event); - apply(usher, uuid, name, profiles, yes); + apply(sheriff, uuid, name, profiles, yes); } catch (ReflectiveOperationException | RuntimeException e) { - usher.getLogger().log(Level.WARNING, "Couldn't read a used Latchkey invite, so its profiles weren't applied", e); + sheriff.getLogger().log(Level.WARNING, "Couldn't read a used Latchkey invite, so its profiles weren't applied", e); } - }, usher); - usher.getLogger().info("Checking and applying the profiles on Latchkey invites."); + }, sheriff); + sheriff.getLogger().info("Checking and applying the profiles on Latchkey invites."); } catch (ReflectiveOperationException | ClassCastException e) { - usher.getLogger().warning("Latchkey is installed but its invite events don't match what Usher expects, so invite profiles " + sheriff.getLogger().warning("Latchkey is installed but its invite events don't match what Sheriff expects, so invite profiles " + "won't be applied. Update both plugins. (" + e.getClass().getSimpleName() + ")"); } } /** Applies an invite's profiles as a change made by the admin who made the invite, with their earlier yeses. */ - static void apply(UsherPlugin usher, UUID uuid, String name, List profiles, Set yesIds) { + static void apply(SheriffPlugin sheriff, UUID uuid, String name, List profiles, Set yesIds) { if (profiles.isEmpty()) { - usher.getLogger().info(name + " joined through an invite with no profiles, so they have lobby-only access for now."); + sheriff.getLogger().info(name + " joined through an invite with no profiles, so they have lobby-only access for now."); return; } - Set confirmed = confirmedCodes(usher.settings(), yesIds); + Set confirmed = confirmedCodes(sheriff.settings(), yesIds); var request = new ChangeService.Request(null, new ChangeService.Actor(ACTOR, null, "latchkey"), uuid, name, new LinkedHashSet<>(profiles), Set.of(), confirmed, false); - usher.changes().submit(request).thenAccept(result -> { + sheriff.changes().submit(request).thenAccept(result -> { if (result.ok()) return; List asks = new ArrayList<>(); for (Planner.Warning w : result.warnings()) asks.add(w.text()); - usher.getLogger().warning("The profiles on " + name + "'s invite weren't applied: " + result.message() - + (asks.isEmpty() ? "" : " " + String.join(" ", asks)) + " Apply them with /usher once that's sorted."); + sheriff.getLogger().warning("The profiles on " + name + "'s invite weren't applied: " + result.message() + + (asks.isEmpty() ? "" : " " + String.join(" ", asks)) + " Apply them with /sheriff once that's sorted."); }); } } diff --git a/src/main/java/com/extantape/usher/LuckPermsStore.java b/src/main/java/com/extantape/sheriff/LuckPermsStore.java similarity index 99% rename from src/main/java/com/extantape/usher/LuckPermsStore.java rename to src/main/java/com/extantape/sheriff/LuckPermsStore.java index 5010ba4..1ab4561 100644 --- a/src/main/java/com/extantape/usher/LuckPermsStore.java +++ b/src/main/java/com/extantape/sheriff/LuckPermsStore.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import net.luckperms.api.LuckPerms; import net.luckperms.api.model.group.Group; diff --git a/src/main/java/com/extantape/usher/NameResolver.java b/src/main/java/com/extantape/sheriff/NameResolver.java similarity index 94% rename from src/main/java/com/extantape/usher/NameResolver.java rename to src/main/java/com/extantape/sheriff/NameResolver.java index 42939e6..f193ed0 100644 --- a/src/main/java/com/extantape/usher/NameResolver.java +++ b/src/main/java/com/extantape/sheriff/NameResolver.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -32,7 +32,7 @@ public static final class NameProblem extends RuntimeException { } private static final Pattern VALID = Pattern.compile("[A-Za-z0-9_]{1,16}"); - static final String MOJANG = System.getProperty("usher.mojang-url", "https://api.mojang.com/users/profiles/minecraft/"); + static final String MOJANG = System.getProperty("sheriff.mojang-url", "https://api.mojang.com/users/profiles/minecraft/"); private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(8)).build(); private final Executor async; @@ -69,7 +69,7 @@ private Resolved askMojang(String typed) { HttpResponse response; try { response = http.send(HttpRequest.newBuilder(URI.create(MOJANG + typed)).timeout(Duration.ofSeconds(10)) - .header("User-Agent", "Usher").GET().build(), HttpResponse.BodyHandlers.ofString()); + .header("User-Agent", "Sheriff").GET().build(), HttpResponse.BodyHandlers.ofString()); } catch (Exception e) { if (e instanceof InterruptedException) Thread.currentThread().interrupt(); throw new NameProblem("lookup_failed", "Couldn't reach Mojang to look up " + typed + ". Nothing was changed. Try again in a minute."); diff --git a/src/main/java/com/extantape/usher/PickerMenus.java b/src/main/java/com/extantape/sheriff/PickerMenus.java similarity index 99% rename from src/main/java/com/extantape/usher/PickerMenus.java rename to src/main/java/com/extantape/sheriff/PickerMenus.java index b5d0312..9ede540 100644 --- a/src/main/java/com/extantape/usher/PickerMenus.java +++ b/src/main/java/com/extantape/sheriff/PickerMenus.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import io.papermc.paper.datacomponent.DataComponentTypes; import io.papermc.paper.datacomponent.item.ItemLore; @@ -75,10 +75,10 @@ final class Menu implements InventoryHolder { Set removes() { Set s = new LinkedHashSet<>(startOn); s.removeAll(wanted); return s; } } - private final UsherPlugin plugin; + private final SheriffPlugin plugin; private final Map open = new HashMap<>(); - PickerMenus(UsherPlugin plugin) { this.plugin = plugin; } + PickerMenus(SheriffPlugin plugin) { this.plugin = plugin; } void open(Player admin, NameResolver.Resolved target, boolean approving, GroupStore.UserState state) { if (!ChangeService.mayApprove(admin)) return; diff --git a/src/main/java/com/extantape/usher/Planner.java b/src/main/java/com/extantape/sheriff/Planner.java similarity index 90% rename from src/main/java/com/extantape/usher/Planner.java rename to src/main/java/com/extantape/sheriff/Planner.java index 6363083..ed1d1ee 100644 --- a/src/main/java/com/extantape/usher/Planner.java +++ b/src/main/java/com/extantape/sheriff/Planner.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -10,7 +10,7 @@ public final class Planner { /** Permissions a profile's group must never carry, since they'd let the person approve or edit permissions. */ - static final List ADMIN_PERMISSIONS = List.of("usher.approve", "stationmaster.approve", "luckperms.user.parent.add", "luckperms.editor"); + static final List ADMIN_PERMISSIONS = List.of("sheriff.approve", "luckperms.user.parent.add", "luckperms.editor"); public record Warning(String code, String text) {} @@ -32,7 +32,7 @@ private Planner() {} public static Plan plan(Settings settings, GroupStore store, GroupStore.UserState state, Set addIds, Set removeIds, Set confirmed) { - if (!settings.ok()) return Plan.refuse("config_broken", "Usher's config is broken, so nothing was changed."); + if (!settings.ok()) return Plan.refuse("config_broken", "Sheriff's config is broken, so nothing was changed."); for (String id : addIds) if (removeIds.contains(id)) return Plan.refuse("conflict", "You asked to add and remove " + id + " at once, so nothing was changed."); Set all = new LinkedHashSet<>(addIds); @@ -75,17 +75,17 @@ public static Plan plan(Settings settings, GroupStore store, GroupStore.UserStat return new Plan(null, null, List.copyOf(warnings), unconfirmed, add, remove, had, notHad, kept); } - /** Why a profile may never be granted or removed by Usher, or null if it's fine. */ + /** Why a profile may never be granted or removed by Sheriff, or null if it's fine. */ public static String unsafe(Settings settings, GroupStore store, Profile p) { if (settings.restricted().contains(p.group())) return "The " + p.label() + " group can only be changed from the server console by an owner, so nothing was changed."; Set lineage = store.lineage(p.group()); if (lineage.isEmpty()) return "The group for " + p.label() + " (" + p.group() + ") doesn't exist in LuckPerms, so nothing was changed."; for (String g : lineage) if (settings.restricted().contains(g)) - return p.label() + " inherits the restricted group " + g + ", so Usher won't touch it. Nothing was changed."; - if (store.grants(p.group(), "*")) return p.label() + " grants every permission, so Usher won't touch it. Nothing was changed."; + return p.label() + " inherits the restricted group " + g + ", so Sheriff won't touch it. Nothing was changed."; + if (store.grants(p.group(), "*")) return p.label() + " grants every permission, so Sheriff won't touch it. Nothing was changed."; for (String perm : ADMIN_PERMISSIONS) if (store.grants(p.group(), perm)) - return p.label() + " grants " + perm + ", an admin permission, so Usher won't touch it. Nothing was changed."; + return p.label() + " grants " + perm + ", an admin permission, so Sheriff won't touch it. Nothing was changed."; return null; } diff --git a/src/main/java/com/extantape/usher/PlayerCommands.java b/src/main/java/com/extantape/sheriff/PlayerCommands.java similarity index 92% rename from src/main/java/com/extantape/usher/PlayerCommands.java rename to src/main/java/com/extantape/sheriff/PlayerCommands.java index 1f9c675..b9c9125 100644 --- a/src/main/java/com/extantape/usher/PlayerCommands.java +++ b/src/main/java/com/extantape/sheriff/PlayerCommands.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -18,14 +18,14 @@ import java.util.function.Consumer; /** - * /approve <name> and /profile <name> [edit], for admins in the game. Both need usher.approve, so + * /approve <name> and /profile <name> [edit], for admins in the game. Both need sheriff.approve, so * players without it don't see them at all. */ final class PlayerCommands { - private final UsherPlugin plugin; + private final SheriffPlugin plugin; - PlayerCommands(UsherPlugin plugin) { this.plugin = plugin; } + PlayerCommands(SheriffPlugin plugin) { this.plugin = plugin; } /** One of the two commands, as Paper's BasicCommand. */ final class Command implements BasicCommand { @@ -55,11 +55,11 @@ private void run(CommandSender sender, boolean approve, String[] args) { } boolean edit = approve || args.length == 2; if (edit && !(sender instanceof Player)) { - sender.sendMessage(plain("The menu is for players. From the console, use: usher apply +profile -profile")); + sender.sendMessage(plain("The menu is for players. From the console, use: sheriff apply +profile -profile")); return; } - if (!plugin.settings().ok()) { sender.sendMessage(bad("Usher's config is broken, so it can't change anything right now. " + plugin.settings().problems().get(0))); return; } - if (!plugin.store().ready()) { sender.sendMessage(bad("LuckPerms isn't running, so Usher can't change anyone's profiles right now.")); return; } + if (!plugin.settings().ok()) { sender.sendMessage(bad("Sheriff's config is broken, so it can't change anything right now. " + plugin.settings().problems().get(0))); return; } + if (!plugin.store().ready()) { sender.sendMessage(bad("LuckPerms isn't running, so Sheriff can't change anyone's profiles right now.")); return; } sender.sendMessage(plain("Looking up " + args[0] + "…")); resolve(sender, args[0], who -> { diff --git a/src/main/java/com/extantape/usher/Profile.java b/src/main/java/com/extantape/sheriff/Profile.java similarity index 98% rename from src/main/java/com/extantape/usher/Profile.java rename to src/main/java/com/extantape/sheriff/Profile.java index df0ba08..97465c4 100644 --- a/src/main/java/com/extantape/usher/Profile.java +++ b/src/main/java/com/extantape/sheriff/Profile.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import java.util.List; diff --git a/src/main/java/com/extantape/usher/Settings.java b/src/main/java/com/extantape/sheriff/Settings.java similarity index 97% rename from src/main/java/com/extantape/usher/Settings.java rename to src/main/java/com/extantape/sheriff/Settings.java index 735103b..0bb6f30 100644 --- a/src/main/java/com/extantape/usher/Settings.java +++ b/src/main/java/com/extantape/sheriff/Settings.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; @@ -63,7 +63,7 @@ public static Settings from(ConfigurationSection config) { ConfigurationSection p = section.getConfigurationSection(key); if (p == null) { problems.add("Profile " + key + " isn't a section, so it's left out."); continue; } if (!ID.matcher(id).matches()) { problems.add("Profile " + key + " needs an id of letters, digits, - or _, so it's left out."); continue; } - // "none" stands for an empty list in commands (usher apply, latchkey invite create), so it can't be a profile. + // "none" stands for an empty list in commands (sheriff apply, latchkey invite create), so it can't be a profile. if (id.equals("none")) { problems.add("Profile none can't be called that: \"none\" means no profiles in commands. Rename it; it's left out."); continue; } String group = p.getString("group", "").trim().toLowerCase(Locale.ROOT); if (!GROUP.matcher(group).matches()) { problems.add("Profile " + id + " has no usable group, so it's left out."); continue; } diff --git a/src/main/java/com/extantape/usher/UsherPlugin.java b/src/main/java/com/extantape/sheriff/SheriffPlugin.java similarity index 87% rename from src/main/java/com/extantape/usher/UsherPlugin.java rename to src/main/java/com/extantape/sheriff/SheriffPlugin.java index 4fbc7bc..6461a94 100644 --- a/src/main/java/com/extantape/usher/UsherPlugin.java +++ b/src/main/java/com/extantape/sheriff/SheriffPlugin.java @@ -1,6 +1,6 @@ -package com.extantape.usher; +package com.extantape.sheriff; -import com.extantape.usher.api.UsherProblemEvent; +import com.extantape.sheriff.api.SheriffProblemEvent; import com.google.gson.JsonObject; import net.luckperms.api.LuckPerms; import org.bukkit.Bukkit; @@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; -public final class UsherPlugin extends JavaPlugin { +public final class SheriffPlugin extends JavaPlugin { private volatile Settings settings = Settings.broken("not loaded yet"); private volatile GroupStore store = new LuckPermsStore.Missing(); @@ -32,7 +32,6 @@ public final class UsherPlugin extends JavaPlugin { @Override public void onEnable() { - migrateFromStationmaster(); saveDefaultConfig(); settings = readSettings(); report(settings); @@ -40,7 +39,7 @@ public void onEnable() { try { audit = new AuditLog(getDataFolder().toPath().resolve("changes.log")); } catch (IOException e) { - getLogger().log(Level.SEVERE, "Can't open changes.log, so Usher will refuse every change", e); + getLogger().log(Level.SEVERE, "Can't open changes.log, so Sheriff will refuse every change", e); settings = Settings.broken("changes.log can't be opened"); audit = null; } @@ -66,7 +65,7 @@ public void onEnable() { var commands = event.registrar(); commands.register("approve", "Let someone in and choose their profiles.", players.approve()); commands.register("profile", "See or change someone's profiles.", players.profile()); - commands.register("usher", "Console commands for Leaf Juice and scripts, and reload.", console); + commands.register("sheriff", "Console commands for Leaf Juice and scripts, and reload.", console); }); // After startup, so listeners from other plugins hear about problems. @@ -74,7 +73,7 @@ public void onEnable() { checkProfilesAgainstLuckPerms(); if (!settings.ok()) callProblem("config_broken", settings.problems().get(0)); else for (String p : settings.problems()) callProblem("config_problem", p); - if (!store.ready()) callProblem("luckperms_missing", "LuckPerms isn't running, so Usher can't change anyone's profiles."); + if (!store.ready()) callProblem("luckperms_missing", "LuckPerms isn't running, so Sheriff can't change anyone's profiles."); if (audit != null) recoverUnfinished(); }); } @@ -87,7 +86,7 @@ public void onDisable() { private GroupStore connectLuckPerms() { if (!getServer().getPluginManager().isPluginEnabled("LuckPerms")) { - getLogger().severe("LuckPerms isn't running. Usher will load, but it will refuse every change until LuckPerms works."); + getLogger().severe("LuckPerms isn't running. Sheriff will load, but it will refuse every change until LuckPerms works."); return new LuckPermsStore.Missing(); } try { @@ -96,7 +95,7 @@ private GroupStore connectLuckPerms() { } catch (Throwable e) { getLogger().log(Level.SEVERE, "Couldn't reach LuckPerms' API", e); } - getLogger().severe("LuckPerms is loaded but its API isn't available, so Usher will refuse every change."); + getLogger().severe("LuckPerms is loaded but its API isn't available, so Sheriff will refuse every change."); return new LuckPermsStore.Missing(); } @@ -115,7 +114,7 @@ List reload() { if (!fresh.ok()) { report(fresh); callProblem("config_broken", fresh.problems().get(0)); - return List.of("Couldn't reload: " + fresh.problems().get(0), settings.ok() ? "Still using the settings from before." : "Usher still refuses every change until config.yml is fixed."); + return List.of("Couldn't reload: " + fresh.problems().get(0), settings.ok() ? "Still using the settings from before." : "Sheriff still refuses every change until config.yml is fixed."); } settings = fresh; report(fresh); @@ -128,7 +127,7 @@ List reload() { } private void report(Settings s) { - if (!s.ok()) getLogger().severe(s.problems().get(0) + " Usher will refuse every change until it's fixed."); + if (!s.ok()) getLogger().severe(s.problems().get(0) + " Sheriff will refuse every change until it's fixed."); else for (String p : s.problems()) getLogger().warning(p); } @@ -141,7 +140,7 @@ private List checkProfilesAgainstLuckPerms() { String why = Planner.unsafe(settings, store, p); if (why == null) { keep.put(p.id(), p); continue; } String line = "Profile " + p.id() + " is left out: " + why.replace(" Nothing was changed.", "").replace(", so nothing was changed.", ".") - + (why.contains("doesn't exist") ? " Create the group, then run usher reload." : ""); + + (why.contains("doesn't exist") ? " Create the group, then run sheriff reload." : ""); problems.add(line); getLogger().warning(line); callProblem(why.contains("doesn't exist") ? "config_problem" : "restricted", line); @@ -197,7 +196,7 @@ private void recoverUnfinished() { } void callProblem(String code, String message) { - Bukkit.getPluginManager().callEvent(new UsherProblemEvent(code, message, null, null)); + Bukkit.getPluginManager().callEvent(new SheriffProblemEvent(code, message, null, null)); } void runMain(Runnable r) { @@ -215,17 +214,4 @@ void runMain(Runnable r) { ChangeService changes() { return changes; } NameResolver names() { return names; } PickerMenus menus() { return menus; } - - - /** - * The plugin was called Stationmaster before 0.3.0. If plugins/Usher doesn't exist yet and plugins/Stationmaster - * does, copy it across (config, changes.log, join-attempts.json) so nothing is lost. The old folder is left as it - * was, as a backup. Nothing is overwritten: if plugins/Usher exists, this does nothing. - */ - private void migrateFromStationmaster() { - Path now = getDataFolder().toPath(); - Path old = now.resolveSibling("Stationmaster"); - String done = Migration.copyIfMissing(old, now); - if (done != null) getLogger().warning(done); - } } diff --git a/src/main/java/com/extantape/usher/WorldAccess.java b/src/main/java/com/extantape/sheriff/WorldAccess.java similarity index 97% rename from src/main/java/com/extantape/usher/WorldAccess.java rename to src/main/java/com/extantape/sheriff/WorldAccess.java index 58b50c6..8ac1635 100644 --- a/src/main/java/com/extantape/usher/WorldAccess.java +++ b/src/main/java/com/extantape/sheriff/WorldAccess.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -23,9 +23,9 @@ */ public final class WorldAccess implements Listener { - private final UsherPlugin plugin; + private final SheriffPlugin plugin; - WorldAccess(UsherPlugin plugin) { this.plugin = plugin; } + WorldAccess(SheriffPlugin plugin) { this.plugin = plugin; } public static boolean closed(Settings settings, Set groups, String world) { boolean listed = false; diff --git a/src/main/java/com/extantape/usher/api/JoinAttemptEvent.java b/src/main/java/com/extantape/sheriff/api/JoinAttemptEvent.java similarity index 96% rename from src/main/java/com/extantape/usher/api/JoinAttemptEvent.java rename to src/main/java/com/extantape/sheriff/api/JoinAttemptEvent.java index 8d46083..eff67fc 100644 --- a/src/main/java/com/extantape/usher/api/JoinAttemptEvent.java +++ b/src/main/java/com/extantape/sheriff/api/JoinAttemptEvent.java @@ -1,4 +1,4 @@ -package com.extantape.usher.api; +package com.extantape.sheriff.api; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/com/extantape/usher/api/ProfilesChangedEvent.java b/src/main/java/com/extantape/sheriff/api/ProfilesChangedEvent.java similarity index 95% rename from src/main/java/com/extantape/usher/api/ProfilesChangedEvent.java rename to src/main/java/com/extantape/sheriff/api/ProfilesChangedEvent.java index bcbb760..73f5fbb 100644 --- a/src/main/java/com/extantape/usher/api/ProfilesChangedEvent.java +++ b/src/main/java/com/extantape/sheriff/api/ProfilesChangedEvent.java @@ -1,4 +1,4 @@ -package com.extantape.usher.api; +package com.extantape.sheriff.api; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; @@ -10,7 +10,7 @@ /** * Fired on the main thread after a change is saved: someone was approved, or their profiles changed. Listen to it to - * post to Discord or anywhere else. Usher itself sends nothing over the network. + * post to Discord or anywhere else. Sheriff itself sends nothing over the network. */ public final class ProfilesChangedEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); diff --git a/src/main/java/com/extantape/usher/api/UsherProblemEvent.java b/src/main/java/com/extantape/sheriff/api/SheriffProblemEvent.java similarity index 86% rename from src/main/java/com/extantape/usher/api/UsherProblemEvent.java rename to src/main/java/com/extantape/sheriff/api/SheriffProblemEvent.java index 673ecaf..5243eed 100644 --- a/src/main/java/com/extantape/usher/api/UsherProblemEvent.java +++ b/src/main/java/com/extantape/sheriff/api/SheriffProblemEvent.java @@ -1,4 +1,4 @@ -package com.extantape.usher.api; +package com.extantape.sheriff.api; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; @@ -11,7 +11,7 @@ * Fired on the main thread when something went wrong or was refused for safety: LuckPerms missing or failing, a broken * config, a restricted group, self-approval, or a change cut off by a crash. */ -public final class UsherProblemEvent extends Event { +public final class SheriffProblemEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); private final String code; @@ -19,7 +19,7 @@ public final class UsherProblemEvent extends Event { private final @Nullable UUID target; private final @Nullable String actor; - public UsherProblemEvent(String code, String message, @Nullable UUID target, @Nullable String actor) { + public SheriffProblemEvent(String code, String message, @Nullable UUID target, @Nullable String actor) { this.code = code; this.message = message; this.target = target; this.actor = actor; } diff --git a/src/main/java/com/extantape/usher/Migration.java b/src/main/java/com/extantape/usher/Migration.java deleted file mode 100644 index f9287e3..0000000 --- a/src/main/java/com/extantape/usher/Migration.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.extantape.usher; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.stream.Stream; - -/** Carries a server's files over from the old plugin folder after the rename from Stationmaster (2026-11-09). */ -final class Migration { - private Migration() {} - - /** - * Copies {@code old} to {@code now} if {@code now} doesn't exist and {@code old} is a folder. Copies into a - * temporary sibling first and renames it into place, so a crash halfway leaves no half-filled folder. - * - * @return a line for the log saying what happened, or null if there was nothing to do - */ - static String copyIfMissing(Path old, Path now) { - if (Files.exists(now) || !Files.isDirectory(old)) return null; - Path tmp = now.resolveSibling(now.getFileName() + ".migrating"); - try { - if (Files.exists(tmp)) deleteTree(tmp); - try (Stream all = Files.walk(old)) { - all.forEach(p -> { - Path to = tmp.resolve(old.relativize(p).toString()); - try { - if (Files.isDirectory(p)) Files.createDirectories(to); - else Files.copy(p, to, StandardCopyOption.COPY_ATTRIBUTES); - } catch (IOException e) { throw new UncheckedIOException(e); } - }); - } - Files.move(tmp, now, StandardCopyOption.ATOMIC_MOVE); - return "Copied plugins/" + old.getFileName() + " to plugins/" + now.getFileName() - + " (Stationmaster is now called Usher). The old folder was left untouched; delete it once you're happy."; - } catch (IOException | UncheckedIOException e) { - return "Couldn't copy plugins/" + old.getFileName() + " to plugins/" + now.getFileName() + ": " + e.getMessage() - + ". Copy it by hand while the server is stopped, or Usher starts with a fresh config."; - } - } - - private static void deleteTree(Path root) throws IOException { - try (Stream all = Files.walk(root)) { - for (Path p : all.sorted(java.util.Comparator.reverseOrder()).toList()) Files.delete(p); - } - } -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 773954c..a5f3332 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,5 +1,5 @@ -# Usher settings. The profiles below are examples: replace them with your own groups and worlds. -# After editing, run "usher reload" in the console or /usher reload in the game. +# Sheriff settings. The profiles below are examples: replace them with your own groups and worlds. +# After editing, run "sheriff reload" in the console or /sheriff reload in the game. # Where people are sent when a world closes to them. Blank means the server's main world. lobby-world: "" @@ -10,9 +10,9 @@ lobby-world: "" # offline: never ask Mojang. Only right for offline-mode servers, where capitals in a name matter. name-lookup: auto -# Groups Usher never adds or removes, whatever the profiles say. Change these from the console. +# Groups Sheriff never adds or removes, whatever the profiles say. Change these from the console. # A profile whose group is listed here, or inherits one listed here, is refused. So is any profile whose -# group grants every permission ("*"), usher.approve or LuckPerms editing. +# group grants every permission ("*"), sheriff.approve or LuckPerms editing. restricted-groups: [admin, owner, moderator] # Each profile adds the person to one LuckPerms group, which should grant that profile's worlds diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index de58bea..9fddb24 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,22 +1,22 @@ -name: Usher +name: Sheriff version: '${version}' -main: com.extantape.usher.UsherPlugin +main: com.extantape.sheriff.SheriffPlugin api-version: '26.1' description: Approve new players and choose their profiles, without typing LuckPerms commands. -website: https://github.com/ExtantApe/Usher +website: https://github.com/ExtantApe/Sheriff dependencies: server: - # Optional on purpose: without LuckPerms, Usher still loads and refuses every change, saying why. + # Optional on purpose: without LuckPerms, Sheriff still loads and refuses every change, saying why. LuckPerms: load: BEFORE required: false join-classpath: true - # Optional: with Latchkey, Usher checks an invite's profiles when it's made and applies them when it's used. - # Usher reads Latchkey's events by name, so it builds and runs without it. + # Optional: with Latchkey, Sheriff checks an invite's profiles when it's made and applies them when it's used. + # Sheriff reads Latchkey's events by name, so it builds and runs without it. Latchkey: load: BEFORE required: false permissions: - usher.approve: + sheriff.approve: description: Approve people and change their profiles, by command or menu. Nobody can use it on themselves. default: false diff --git a/src/test/java/com/extantape/usher/AuditLogTest.java b/src/test/java/com/extantape/sheriff/AuditLogTest.java similarity index 98% rename from src/test/java/com/extantape/usher/AuditLogTest.java rename to src/test/java/com/extantape/sheriff/AuditLogTest.java index e61fcf0..67bf820 100644 --- a/src/test/java/com/extantape/usher/AuditLogTest.java +++ b/src/test/java/com/extantape/sheriff/AuditLogTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import com.google.gson.JsonObject; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/extantape/usher/ConsoleCommandsTest.java b/src/test/java/com/extantape/sheriff/ConsoleCommandsTest.java similarity index 94% rename from src/test/java/com/extantape/usher/ConsoleCommandsTest.java rename to src/test/java/com/extantape/sheriff/ConsoleCommandsTest.java index a4be3eb..5a8d096 100644 --- a/src/test/java/com/extantape/usher/ConsoleCommandsTest.java +++ b/src/test/java/com/extantape/sheriff/ConsoleCommandsTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import com.extantape.signalbox.Request; import com.google.gson.JsonParser; @@ -56,14 +56,14 @@ static ConsoleCommands.ApplyArgs apply(String line) throws Exception { } @Test void describeMarksTheOptionalArgumentsAndOnlyThose() { - String line = com.extantape.signalbox.Answer.ok("d1", com.extantape.signalbox.Describe.body("Usher", "0.4.0", ConsoleCommands.described())); + String line = com.extantape.signalbox.Answer.ok("d1", com.extantape.signalbox.Describe.body("Sheriff", "0.4.0", ConsoleCommands.described())); assertTrue(line.contains("{\"name\":\"whitelist\",\"type\":\"bool\",\"optional\":true}"), line); assertTrue(line.contains("{\"name\":\"via\",\"type\":\"string\",\"optional\":true}"), line); assertTrue(line.contains("{\"name\":\"uuid\",\"type\":\"uuid\"}"), "required ones carry no optional key"); assertTrue(line.contains("{\"name\":\"limit\",\"type\":\"int\",\"optional\":true}")); } - @Test void cursorsAreOnlyOnesUsherHandedOut() throws Exception { + @Test void cursorsAreOnlyOnesSheriffHandedOut() throws Exception { assertEquals(50, ConsoleCommands.cursorOf("50")); assertEquals(0, ConsoleCommands.cursorOf("0")); for (String bad : List.of("-1", "abc", "1.5", "9999999999")) assertThrows(ConsoleCommands.Refusal.class, () -> ConsoleCommands.cursorOf(bad), bad); diff --git a/src/test/java/com/extantape/usher/FakeStore.java b/src/test/java/com/extantape/sheriff/FakeStore.java similarity index 98% rename from src/test/java/com/extantape/usher/FakeStore.java rename to src/test/java/com/extantape/sheriff/FakeStore.java index 6518155..b33f763 100644 --- a/src/test/java/com/extantape/usher/FakeStore.java +++ b/src/test/java/com/extantape/sheriff/FakeStore.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import java.util.HashMap; import java.util.HashSet; diff --git a/src/test/java/com/extantape/usher/LatchkeyHookTest.java b/src/test/java/com/extantape/sheriff/LatchkeyHookTest.java similarity index 94% rename from src/test/java/com/extantape/usher/LatchkeyHookTest.java rename to src/test/java/com/extantape/sheriff/LatchkeyHookTest.java index 191c451..acb4b8f 100644 --- a/src/test/java/com/extantape/usher/LatchkeyHookTest.java +++ b/src/test/java/com/extantape/sheriff/LatchkeyHookTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.*; -/** Usher's checks on Latchkey invites, without a server. The reflection wiring is covered by the bot tests. */ +/** Sheriff's checks on Latchkey invites, without a server. The reflection wiring is covered by the bot tests. */ class LatchkeyHookTest { Settings settings; FakeStore store; @@ -48,7 +48,7 @@ class LatchkeyHookTest { for (String id : c.questions().keySet()) assertTrue(id.matches("[a-z0-9-]{1,64}"), id); } - @Test void yesIdsMapBackToUshersCodes() { + @Test void yesIdsMapBackToSheriffsCodes() { assertEquals("confirm-grown-sky", LatchkeyHook.toYesId("confirm:grown_sky")); assertEquals(Set.of("confirm:grown_sky", "bridge"), LatchkeyHook.confirmedCodes(settings, Set.of("confirm-grown-sky", "bridge"))); assertEquals(Set.of(), LatchkeyHook.confirmedCodes(settings, Set.of("confirm-nothing", "made-up"))); diff --git a/src/test/java/com/extantape/usher/PlannerTest.java b/src/test/java/com/extantape/sheriff/PlannerTest.java similarity index 98% rename from src/test/java/com/extantape/usher/PlannerTest.java rename to src/test/java/com/extantape/sheriff/PlannerTest.java index e9c4e48..62e9910 100644 --- a/src/test/java/com/extantape/usher/PlannerTest.java +++ b/src/test/java/com/extantape/sheriff/PlannerTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -101,7 +101,7 @@ void aProfileThatInheritsARestrictedGroupIsRefused() { @Test void aProfileThatGrantsAdminPowersIsRefused() { - store.perm("builders", "usher.approve"); + store.perm("builders", "sheriff.approve"); assertEquals("restricted", plan(store.user(), Set.of("builders"), Set.of(), Set.of()).refusal()); store.perms.get("builders").clear(); store.perm("default", "*"); diff --git a/src/test/java/com/extantape/usher/SettingsTest.java b/src/test/java/com/extantape/sheriff/SettingsTest.java similarity index 99% rename from src/test/java/com/extantape/usher/SettingsTest.java rename to src/test/java/com/extantape/sheriff/SettingsTest.java index 10292e2..f96492b 100644 --- a/src/test/java/com/extantape/usher/SettingsTest.java +++ b/src/test/java/com/extantape/sheriff/SettingsTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/extantape/usher/TextTest.java b/src/test/java/com/extantape/sheriff/TextTest.java similarity index 97% rename from src/test/java/com/extantape/usher/TextTest.java rename to src/test/java/com/extantape/sheriff/TextTest.java index 54d6bdd..f20fbfb 100644 --- a/src/test/java/com/extantape/usher/TextTest.java +++ b/src/test/java/com/extantape/sheriff/TextTest.java @@ -1,4 +1,4 @@ -package com.extantape.usher; +package com.extantape.sheriff; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/extantape/usher/MigrationTest.java b/src/test/java/com/extantape/usher/MigrationTest.java deleted file mode 100644 index af81ee4..0000000 --- a/src/test/java/com/extantape/usher/MigrationTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.extantape.usher; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.*; - -class MigrationTest { - - @Test - void copiesTheOldFolderWhenTheNewOneIsMissing(@TempDir Path plugins) throws Exception { - Path old = plugins.resolve("Stationmaster"); - Files.createDirectories(old.resolve("sub")); - Files.writeString(old.resolve("config.yml"), "profiles: {}\n"); - Files.writeString(old.resolve("changes.log"), "{\"a\":1}\n"); - Files.writeString(old.resolve("sub/x.json"), "[]"); - - String msg = Migration.copyIfMissing(old, plugins.resolve("Usher")); - - assertNotNull(msg); - assertEquals("profiles: {}\n", Files.readString(plugins.resolve("Usher/config.yml"))); - assertEquals("{\"a\":1}\n", Files.readString(plugins.resolve("Usher/changes.log"))); - assertEquals("[]", Files.readString(plugins.resolve("Usher/sub/x.json"))); - assertTrue(Files.exists(old.resolve("config.yml")), "the old folder is left as a backup"); - assertFalse(Files.exists(plugins.resolve("Usher.migrating"))); - } - - @Test - void neverOverwritesAnExistingNewFolder(@TempDir Path plugins) throws Exception { - Files.createDirectories(plugins.resolve("Stationmaster")); - Files.writeString(plugins.resolve("Stationmaster/config.yml"), "old"); - Files.createDirectories(plugins.resolve("Usher")); - Files.writeString(plugins.resolve("Usher/config.yml"), "new"); - - assertNull(Migration.copyIfMissing(plugins.resolve("Stationmaster"), plugins.resolve("Usher"))); - assertEquals("new", Files.readString(plugins.resolve("Usher/config.yml"))); - } - - @Test - void doesNothingOnAFreshServer(@TempDir Path plugins) { - assertNull(Migration.copyIfMissing(plugins.resolve("Stationmaster"), plugins.resolve("Usher"))); - assertFalse(Files.exists(plugins.resolve("Usher"))); - } - - @Test - void clearsAHalfFinishedCopyFromACrash(@TempDir Path plugins) throws Exception { - Files.createDirectories(plugins.resolve("Stationmaster")); - Files.writeString(plugins.resolve("Stationmaster/config.yml"), "real"); - Files.createDirectories(plugins.resolve("Usher.migrating")); - Files.writeString(plugins.resolve("Usher.migrating/junk"), "half"); - - assertNotNull(Migration.copyIfMissing(plugins.resolve("Stationmaster"), plugins.resolve("Usher"))); - assertEquals("real", Files.readString(plugins.resolve("Usher/config.yml"))); - assertFalse(Files.exists(plugins.resolve("Usher/junk"))); - } -} diff --git a/test/acceptance.mjs b/test/acceptance.mjs index 996fa3d..6cbefe0 100644 --- a/test/acceptance.mjs +++ b/test/acceptance.mjs @@ -15,8 +15,8 @@ const ACCESS = { young: ['young_explore'], grown: ['grown_explore'], builders: [ const bots = [] const join = async name => { const b = await bot(name); bots.push(b); return b } const quit = async b => { if (!b) return; b.quit(); await sleep(800) } -const changesLog = () => existsSync(path.join(dir, 'plugins/Usher/changes.log')) - ? readFileSync(path.join(dir, 'plugins/Usher/changes.log'), 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l) } catch { return { unreadable: l } } }) : [] +const changesLog = () => existsSync(path.join(dir, 'plugins/Sheriff/changes.log')) + ? readFileSync(path.join(dir, 'plugins/Sheriff/changes.log'), 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l) } catch { return { unreadable: l } } }) : [] const spy = (from, re) => s.log.slice(from).filter(l => /SPY /.test(l) && re.test(l)) const whitelist = () => JSON.parse(readFileSync(path.join(dir, 'whitelist.json'), 'utf8')) const offlineUuid = name => { @@ -91,11 +91,11 @@ try { for (const [g, ws] of Object.entries(ACCESS)) for (const w of ws) await s.ask(`lp group ${g} permission set multiverse.access.${w} true`, 300) for (const perm of ['multiverse.access.world', 'multiverse.teleport.self.w', 'multiverse.teleport.self.*']) await s.ask('lp group default permission set ' + perm + ' true', 300) for (const a of ['AdminA', 'AdminB', 'Pleb']) await s.ask('whitelist add ' + a, 300) - const reload = (await s.ask('usher reload', 1200)).join(' ') + const reload = (await s.ask('sheriff reload', 1200)).join(' ') check('setup: all five example profiles load once their groups exist', /Reloaded: 5 profiles/.test(reload), reload.slice(0, 200)) const adminA = await join('AdminA'), adminB = await join('AdminB'), pleb = await join('Pleb') - for (const a of ['AdminA', 'AdminB']) await s.ask(`lp user ${a} permission set usher.approve true`, 500) + for (const a of ['AdminA', 'AdminB']) await s.ask(`lp user ${a} permission set sheriff.approve true`, 500) await sleep(1000) // ---------- Console interface ---------- @@ -105,7 +105,7 @@ try { && prof.profiles.find(p => p.id === 'grown_sky')?.confirm && prof.profiles.find(p => p.id === 'young')?.circle === 'young', JSON.stringify(prof).slice(0, 200)) check('every request gets exactly one result line', prof.hits === 1) check('restricted groups are listed', same(prof.restricted, ['admin', 'owner', 'moderator'])) - const badReq = (await s.ask('usher profiles --json Has!Bad', 800)).join(' ') + const badReq = (await s.ask('sheriff profiles --json Has!Bad', 800)).join(' ') check('a bad request id is answered in plain words, not silence', /request id must be/i.test(badReq), badReq.slice(0, 160)) // The 0.4.0 format itself: one Signalbox line, straight after the log header, with dashed codes. check('answers are Signalbox lines: ea-result {"id":…,"ok":…}', /\]: ea-result t[\w-]+ \{"id":"t[\w-]+","ok":true/.test(prof.line), prof.line.slice(0, 120)) @@ -113,10 +113,10 @@ try { check('an unknown command still gets one answer, with code unknown-command', unknownSub.raw.error?.code === 'unknown-command' && unknownSub.hits === 1) // Sent as typed, without the helper, which would fill in the missing arguments. const badArgFrom = s.log.length - s.send('usher apply 00000000-0000-0000-0000-00000000000a --json badarg-1') + s.send('sheriff apply 00000000-0000-0000-0000-00000000000a --json badarg-1') const badArgLine = await s.waitFor(/\]: ea-result badarg-1 /, 15000, badArgFrom) const badArg = { raw: JSON.parse(badArgLine.slice(badArgLine.indexOf('{'))) } - check('bad arguments answer bad-argument, with how to use it', badArg.raw.error?.code === 'bad-argument' && /usher apply /.test(badArg.raw.error.message)) + check('bad arguments answer bad-argument, with how to use it', badArg.raw.error?.code === 'bad-argument' && /sheriff apply /.test(badArg.raw.error.message)) const desc = await s.sm('describe') check('describe lists every command, format 1', desc.raw.format === 1 && ['profiles', 'resolve', 'show', 'apply', 'attempts', 'log', 'reload', 'describe'].every(n => desc.raw.commands.some(c => c.name === n)), JSON.stringify(desc.raw).slice(0, 160)) @@ -186,7 +186,7 @@ try { let opened = false const onOpen = () => { opened = true } pleb.on('windowOpen', onOpen) - for (const c of ['/approve Someone', '/profile P_build', '/profile P_build edit', '/usher apply 00000000-0000-0000-0000-000000000000 Someone young none', '/usher reload', '/usher profiles --json x1']) { + for (const c of ['/approve Someone', '/profile P_build', '/profile P_build edit', '/sheriff apply 00000000-0000-0000-0000-000000000000 Someone young none', '/sheriff reload', '/sheriff profiles --json x1']) { pleb.chat(c); await sleep(700) } pleb.removeListener('windowOpen', onOpen) @@ -215,7 +215,7 @@ try { check('viewing your own profiles is fine', adminA.chat_log.some(m => /AdminA is approved/.test(m)), adminA.chat_log.join(' | ')) // Console commands stay console-only, even for an approver: in game, changes go through the menu and its warnings. adminA.chat_log.length = 0 - adminA.chat('/usher apply ' + offlineUuid('P_build') + ' P_build builders,young'); await sleep(1500) + adminA.chat('/sheriff apply ' + offlineUuid('P_build') + ' P_build builders,young'); await sleep(1500) check('an approver typing a console command in game is refused, and told what to use', adminA.chat_log.some(m => /console commands/.test(m)), adminA.chat_log.join(' | ')) check('and P_build is unchanged', same(await profilesOf('P_build'), ['builders'])) @@ -284,10 +284,10 @@ try { // ---------- Restricted groups ---------- console.log('\n# Restricted groups') - const cfg = path.join(dir, 'plugins/Usher/config.yml') + const cfg = path.join(dir, 'plugins/Sheriff/config.yml') const goodCfg = readFileSync(cfg, 'utf8') await s.ask('lp creategroup helpers', 400); await s.ask('lp group helpers parent add moderator', 400) - await s.ask('lp creategroup approvers', 400); await s.ask('lp group approvers permission set usher.approve true', 400) + await s.ask('lp creategroup approvers', 400); await s.ask('lp group approvers permission set sheriff.approve true', 400) await s.ask('lp creategroup everything', 400); await s.ask('lp group everything permission set * true', 400) writeFileSync(cfg, goodCfg + ` sneaky1: {label: "Sneaky one", group: Moderator} @@ -297,7 +297,7 @@ try { sneaky5: {label: "Sneaky five", group: owner} `) from = s.log.length - const rl = (await s.ask('usher reload', 1500)).join(' ') + const rl = (await s.ask('sheriff reload', 1500)).join(' ') const prof2 = await s.sm('profiles') check('config tricks are refused: restricted name in other capitals, inheriting one, granting approve, granting *, a child of admin', prof2.profiles.length === 5 && ['sneaky1', 'sneaky2', 'sneaky3', 'sneaky4', 'sneaky5'].every(id => prof2.problems.some(p => p.includes(id))), prof2.problems.join(' | ').slice(0, 300)) @@ -321,7 +321,7 @@ try { && adminA.chat_log.some(m => /inherits the restricted group admin/.test(m)), adminA.chat_log.slice(-1)[0]) await s.ask('lp group grown parent remove admin', 600) writeFileSync(cfg, goodCfg) - await s.ask('usher reload', 1500) + await s.ask('sheriff reload', 1500) // ---------- Bridging the chat circles ---------- console.log('\n# Bridging the chat circles') @@ -410,7 +410,7 @@ try { // ---------- Broken config ---------- console.log('\n# Broken config') writeFileSync(cfg, goodCfg.replace('profiles:', 'profiles: [oops')) - const brk = (await s.ask('usher reload', 1500)).join(' ') + const brk = (await s.ask('sheriff reload', 1500)).join(' ') check('reloading a broken config says so and keeps the old settings', /Couldn't reload/.test(brk) && /Still using the settings from before/.test(brk) && (await s.sm('profiles')).profiles.length === 5) await s.stop(); await sleep(1000) s = new Server(dir); await s.start() @@ -418,16 +418,16 @@ try { check('starting with a broken config refuses every change, clearly', brk2.status === 'refused' && brk2.code === 'config_broken' && !whitelist().some(e => e.name === 'P_cfg'), brk2.message) check('and says so in the log at startup', s.log.some(l => /config.yml can't be read/.test(l))) writeFileSync(cfg, goodCfg) - await s.ask('usher reload', 1500) + await s.ask('sheriff reload', 1500) check('control: fixed and reloaded, it works again', (await s.sm('apply', offlineUuid('P_cfg'), 'P_cfg', '+young')).status === 'ok') // ---------- kill -9 in the middle of a change ---------- console.log('\n# kill -9 in the middle of a change') for (const [when, prop] of [['after groups, before the whitelist', 'pause-after-groups'], ['before groups', 'pause-before-groups']]) { await s.stop(); await sleep(1000) - s = new Server(dir, { props: [`-Dusher.test.${prop}=15000`] }); await s.start() + s = new Server(dir, { props: [`-Dsheriff.test.${prop}=15000`] }); await s.start() const name = prop === 'pause-after-groups' ? 'P_kill1' : 'P_kill2' - s.send(`usher apply ${offlineUuid(name)} ${name} builders none --json k-${name.toLowerCase().replace('_', '-')}`) + s.send(`sheriff apply ${offlineUuid(name)} ${name} builders none --json k-${name.toLowerCase().replace('_', '-')}`) const end = Date.now() + 10000 while (Date.now() < end && !changesLog().some(e => e.event === 'started' && e.target === name)) await sleep(100) await sleep(prop === 'pause-after-groups' ? 3000 : 500) @@ -462,56 +462,11 @@ try { check('control: once LuckPerms can save again, the same change works', (await s.sm('apply', offlineUuid('P_lpfail'), 'P_lpfail', '+builders')).status === 'ok') } - // ---------- Rename from Stationmaster (the old names went in 0.4.0; the folder still comes across) ---------- - console.log('\n# The rename from Stationmaster') - { - await s.ask('whitelist add OldNode', 400) - const oldBot = await join('OldNode') - await sleep(800) - const reply = async () => { oldBot.chat_log.length = 0; oldBot.chat('/usher reload'); await sleep(1200); return oldBot.chat_log.join(' ') } - check('control: the OldNode bot is connected and its chat works', /Unknown or incomplete command|Unknown command/i.test(await reply())) - const before = await openMenu(oldBot, '/profile P_young edit').then(() => true, () => false) - check('control: without either node, OldNode can\'t open the menu', !before) - from = s.log.length - await s.ask('lp user OldNode permission set stationmaster.approve true', 800) - await sleep(500) - const opened = await openMenu(oldBot, '/profile P_young edit').then(() => true, () => false) - if (oldBot.currentWindow) oldBot.closeWindow(oldBot.currentWindow) - check('the old node stationmaster.approve no longer lets anyone approve (gone in 0.4.0, as 0.3.x said)', !opened) - await s.ask('lp user OldNode permission unset stationmaster.approve', 800) - await quit(oldBot) - for (const old of ['stationmaster', 'smaster']) { - const r = (await s.ask(old + ' reload', 1500)).join(' ') - check(`"${old}" is gone`, /Unknown or incomplete command|Unknown command/i.test(r) && !/Reloaded/.test(r), r.slice(0, 160)) - } - const r = await s.sm('profiles') - check('the console answer line is ea-result, and usher-result is gone', r.status === 'ok' && r.hits === 1 && !s.log.some(l => /usher-result /.test(l))) - - // A server upgrading from Stationmaster: only plugins/Stationmaster exists. - await s.stop(); await sleep(1000) - const now = path.join(dir, 'plugins/Usher'), old = path.join(dir, 'plugins/Stationmaster') - const sha = f => createHash('sha256').update(readFileSync(f)).digest('hex') - const cfgSha = sha(path.join(now, 'config.yml')), logSha = sha(path.join(now, 'changes.log')) - renameSync(now, old) - s = new Server(dir); await s.start(); await sleep(1500) - check('upgrade: plugins/Stationmaster is copied to plugins/Usher on first start', - existsSync(now) && sha(path.join(now, 'config.yml')) === cfgSha && sha(path.join(now, 'changes.log')) === logSha) - check('upgrade: the old folder is left as it was', existsSync(path.join(old, 'config.yml')) && sha(path.join(old, 'changes.log')) === logSha) - check('upgrade: the log says what was copied', s.log.some(l => /Copied plugins\/Stationmaster to plugins\/Usher/.test(l))) - const reload2 = (await s.ask('usher reload', 1500)).join(' ') - check('upgrade: the copied config is the one in use', /Reloaded: 5 profiles/.test(reload2), reload2.slice(0, 160)) - writeFileSync(path.join(old, 'config.yml'), 'broken: [') - await s.stop(); await sleep(1000) - s = new Server(dir); await s.start(); await sleep(1500) - check('upgrade: a second start never copies again', !s.log.some(l => /Copied plugins\/Stationmaster/.test(l)) && sha(path.join(now, 'config.yml')) === cfgSha) - rmSync(old, { recursive: true, force: true }) - } - // ---------- Stored data ---------- console.log('\n# Stored data') { // kids-first.md: "It stores no IP addresses, chat or locations." The bots all came from 127.0.0.1. - const stored = ['changes.log', 'join-attempts.json'].map(f => path.join(dir, 'plugins/Usher', f)).filter(existsSync).map(f => readFileSync(f, 'utf8')).join('\n') + const stored = ['changes.log', 'join-attempts.json'].map(f => path.join(dir, 'plugins/Sheriff', f)).filter(existsSync).map(f => readFileSync(f, 'utf8')).join('\n') check('control: there is stored data to look at', stored.length > 100 && /CaseTest|P_young/.test(stored)) check('no IP address is stored', !/\b\d{1,3}(\.\d{1,3}){3}\b/.test(stored), (stored.match(/\b\d{1,3}(\.\d{1,3}){3}\b/) || [''])[0]) } @@ -524,9 +479,9 @@ try { s = new Server(dir); await s.start(); await sleep(1500) const miss = await s.sm('apply', offlineUuid('P_nolp'), 'P_nolp', '+builders') check('without LuckPerms: refused clearly, and nobody whitelisted', miss.status === 'refused' && miss.code === 'luckperms_missing' && !whitelist().some(e => e.name === 'P_nolp'), miss.message) - check('Usher still loads and says why', s.log.some(l => /LuckPerms isn't running/.test(l)) && s.log.some(l => /SPY problem code=luckperms_missing/.test(l))) + check('Sheriff still loads and says why', s.log.some(l => /LuckPerms isn't running/.test(l)) && s.log.some(l => /SPY problem code=luckperms_missing/.test(l))) const unexpected = s.errors().filter(l => !/LuckPerms isn't running/.test(l)) - check('no other errors from Usher in the log', unexpected.length === 0, unexpected[0]) + check('no other errors from Sheriff in the log', unexpected.length === 0, unexpected[0]) await s.stop() renameSync(lpJar + '.off', lpJar) } catch (e) { @@ -536,7 +491,7 @@ try { for (const b of bots) try { b.quit() } catch {} try { if (s.proc?.exitCode === null) await s.stop() } catch {} // One throwaway server at a time, and none left behind: delete it as soon as the run ends. - if (process.env.USHER_TEST_KEEP !== '1') rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 }) + if (process.env.SHERIFF_TEST_KEEP !== '1') rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 }) } summary() console.log(`(${results.length} checks on Paper ${PAPER})`) diff --git a/test/harness.mjs b/test/harness.mjs index c639e41..73d7890 100644 --- a/test/harness.mjs +++ b/test/harness.mjs @@ -1,13 +1,13 @@ // Test harness: builds a fresh throwaway Paper server in a temp folder, on 127.0.0.1 only, and drives it from the // console with mineflayer bots as players. It never copies or touches a real server. // -// USHER_TEST_JARS folder with paper-.jar, LuckPerms-Bukkit-*.jar, multiverse-core-*.jar, +// SHERIFF_TEST_JARS folder with paper-.jar, LuckPerms-Bukkit-*.jar, multiverse-core-*.jar, // multiverse-inventories-*.jar, ViaVersion-*.jar and ViaBackwards-*.jar -// USHER_TEST_EULA an eula.txt the owner already accepted. It's copied, never written. -// USHER_TEST_RUN where servers are built. Must be under a temp folder. -// USHER_TEST_PAPER 26.1.2 (default), 26.2 or 26.3 -// USHER_TEST_JAVA a Java 25 to run Paper with -// USHER_TEST_STORAGE LuckPerms storage: yaml (default) or h2 +// SHERIFF_TEST_EULA an eula.txt the owner already accepted. It's copied, never written. +// SHERIFF_TEST_RUN where servers are built. Must be under a temp folder. +// SHERIFF_TEST_PAPER 26.1.2 (default), 26.2 or 26.3 +// SHERIFF_TEST_JAVA a Java 25 to run Paper with +// SHERIFF_TEST_STORAGE LuckPerms storage: yaml (default) or h2 import { spawn } from 'node:child_process' import { existsSync, readFileSync, readdirSync, writeFileSync, copyFileSync, mkdirSync, rmSync } from 'node:fs' import path from 'node:path' @@ -15,24 +15,24 @@ import os from 'node:os' import mineflayer from 'mineflayer' export const PORT = 25615 -export const PAPER = process.env.USHER_TEST_PAPER ?? '26.1.2' +export const PAPER = process.env.SHERIFF_TEST_PAPER ?? '26.1.2' // mineflayer speaks up to 26.1, so on newer servers the bots join through ViaVersion and ViaBackwards. export const BOT_VERSION = '26.1' export const VIA = !PAPER.startsWith('26.1') -export const JAVA = process.env.USHER_TEST_JAVA ?? 'java' -export const STORAGE = process.env.USHER_TEST_STORAGE ?? 'yaml' -if (!['yaml', 'h2'].includes(STORAGE)) throw new Error('USHER_TEST_STORAGE must be yaml or h2, not ' + STORAGE) -const JARS = must('USHER_TEST_JARS') -const EULA = must('USHER_TEST_EULA') -export const RUN = path.resolve(must('USHER_TEST_RUN')) +export const JAVA = process.env.SHERIFF_TEST_JAVA ?? 'java' +export const STORAGE = process.env.SHERIFF_TEST_STORAGE ?? 'yaml' +if (!['yaml', 'h2'].includes(STORAGE)) throw new Error('SHERIFF_TEST_STORAGE must be yaml or h2, not ' + STORAGE) +const JARS = must('SHERIFF_TEST_JARS') +const EULA = must('SHERIFF_TEST_EULA') +export const RUN = path.resolve(must('SHERIFF_TEST_RUN')) const temp = [os.tmpdir(), '/tmp', '/private/tmp', '/var/folders'].map(p => path.resolve(p)) -if (!temp.some(t => RUN.startsWith(t + path.sep))) throw new Error('USHER_TEST_RUN must be inside a temp folder, not ' + RUN) +if (!temp.some(t => RUN.startsWith(t + path.sep))) throw new Error('SHERIFF_TEST_RUN must be inside a temp folder, not ' + RUN) if (!/^eula=true$/m.test(readFileSync(EULA, 'utf8'))) throw new Error('That eula.txt isn\'t accepted. Accepting it is the owner\'s call.') const ROOT = new URL('../', import.meta.url).pathname const LIBS = path.join(ROOT, 'build/libs/') -const built = existsSync(LIBS) ? readdirSync(LIBS).filter(f => /^Usher-.*\.jar$/.test(f)) : [] -if (built.length !== 1) throw new Error(`Run ./gradlew clean build first: expected one Usher jar in build/libs, found ${built.length}`) +const built = existsSync(LIBS) ? readdirSync(LIBS).filter(f => /^Sheriff-.*\.jar$/.test(f)) : [] +if (built.length !== 1) throw new Error(`Run ./gradlew clean build first: expected one Sheriff jar in build/libs, found ${built.length}`) export const JAR = path.join(LIBS, built[0]) function must(name) { @@ -63,7 +63,7 @@ export function freshServer(name = 'server', { luckperms = true, storage = STORA writeFileSync(path.join(dir, 'server.properties'), [ 'server-ip=127.0.0.1', `server-port=${PORT}`, 'online-mode=false', 'white-list=true', 'enforce-whitelist=true', 'level-type=minecraft\\:flat', 'generate-structures=false', 'spawn-protection=0', 'enable-query=false', 'enable-rcon=false', - 'view-distance=4', 'simulation-distance=4', 'difficulty=peaceful', 'spawn-monsters=false', 'motd=Usher test'].join('\n') + '\n') + 'view-distance=4', 'simulation-distance=4', 'difficulty=peaceful', 'spawn-monsters=false', 'motd=Sheriff test'].join('\n') + '\n') if (luckperms) { mkdirSync(path.join(dir, 'plugins/LuckPerms'), { recursive: true }) writeFileSync(path.join(dir, 'plugins/LuckPerms/config.yml'), `server: global\nstorage-method: ${storage}\nwatch-files: false\n`) @@ -71,12 +71,12 @@ export function freshServer(name = 'server', { luckperms = true, storage = STORA return dir } -export const SPY = path.join(ROOT, 'build/test-spy/UsherSpy.jar') +export const SPY = path.join(ROOT, 'build/test-spy/SheriffSpy.jar') export function installJar(dir) { - copyFileSync(JAR, path.join(dir, 'plugins', 'Usher.jar')) + copyFileSync(JAR, path.join(dir, 'plugins', 'Sheriff.jar')) if (!existsSync(SPY)) throw new Error('Run ./gradlew spyJar first') - copyFileSync(SPY, path.join(dir, 'plugins', 'UsherSpy.jar')) + copyFileSync(SPY, path.join(dir, 'plugins', 'SheriffSpy.jar')) } /** The shipped config with test settings: offline lookups and the lobby. */ @@ -86,8 +86,8 @@ export function writeConfig(dir, edit = t => t) { if (!text.includes(from)) throw new Error('The default config no longer has: ' + from) text = text.replace(from, to) } - mkdirSync(path.join(dir, 'plugins/Usher'), { recursive: true }) - writeFileSync(path.join(dir, 'plugins/Usher/config.yml'), edit(text)) + mkdirSync(path.join(dir, 'plugins/Sheriff'), { recursive: true }) + writeFileSync(path.join(dir, 'plugins/Sheriff/config.yml'), edit(text)) } export class Server { @@ -115,7 +115,7 @@ export class Server { if (this.proc.exitCode !== null) { const last = this.log.slice(-5).map(strip).join('\n ') throw new Error(`Server exited (code ${this.proc.exitCode}) while waiting for ${re}. Its last lines:\n ${last}\n` + - ' Paper 26 needs Java 25. Set USHER_TEST_JAVA if `java` is older.') + ' Paper 26 needs Java 25. Set SHERIFF_TEST_JAVA if `java` is older.') } await sleep(50) } @@ -130,7 +130,7 @@ export class Server { return this.log.slice(from).map(strip) } /** - * Runs a usher console command in the Signalbox format (0.4.0: `usher [--yes ids] --json `) and + * Runs a sheriff console command in the Signalbox format (0.4.0: `sheriff [--yes ids] --json `) and * returns the answer, mapped back to the fields the checks read: status (ok, warn, refused, error), code with * underscores, message, and warnings for questions. The raw line and answer stay on .line and .raw. * Arguments may use the old shorthand (+profile, -profile, confirm=, whitelist=, via=); they're translated here. @@ -139,13 +139,13 @@ export class Server { /** Same, with a request id chosen by the caller, for replays. */ async smAs(req, sub, ...args) { const from = this.log.length - this.send(['usher', sub, ...toSignalbox(sub, args), '--json', req].join(' ')) + this.send(['sheriff', sub, ...toSignalbox(sub, args), '--json', req].join(' ')) const line = await this.waitFor(new RegExp('\\]: ea-result ' + req.replace(/[.]/g, '\\.') + ' '), 30000, from) const hits = this.log.slice(from).filter(l => l.includes('ea-result ' + req + ' ')).length const raw = JSON.parse(line.slice(line.indexOf('{'))) return { req, hits, line, raw, ...fromSignalbox(raw) } } - errors() { return this.log.filter(l => /Usher/.test(l) && /(ERROR|Exception|SEVERE)/.test(l)) } + errors() { return this.log.filter(l => /Sheriff/.test(l) && /(ERROR|Exception|SEVERE)/.test(l)) } async stop() { this.send('stop') const done = await Promise.race([this.exited.then(() => true), sleep(90000).then(() => false)]) @@ -174,7 +174,7 @@ function toSignalbox(sub, args) { return yes.length ? [...out, '--yes', yes.join(',')] : out } -// Codes Usher answers as "error" (something failed) rather than "refused" (a rule said no). +// Codes Sheriff answers as "error" (something failed) rather than "refused" (a rule said no). const ERRORS = new Set(['luckperms_failed', 'internal', 'log_failed', 'server_busy', 'whitelist_failed', 'busy']) /** A Signalbox answer in the shape the checks were written for. */ function fromSignalbox(raw) { diff --git a/test/package-lock.json b/test/package-lock.json index 97c77be..cd0172b 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -1,10 +1,10 @@ { - "name": "usher-tests", + "name": "sheriff-tests", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "usher-tests", + "name": "sheriff-tests", "dependencies": { "mineflayer": "^4.39.0" } diff --git a/test/package.json b/test/package.json index 4d5e3c4..388d745 100644 --- a/test/package.json +++ b/test/package.json @@ -1 +1 @@ -{ "name": "usher-tests", "private": true, "type": "module", "dependencies": { "mineflayer": "^4.39.0" } } +{ "name": "sheriff-tests", "private": true, "type": "module", "dependencies": { "mineflayer": "^4.39.0" } } diff --git a/test/spy/java/com/extantape/usher/spy/SpyPlugin.java b/test/spy/java/com/extantape/usher/spy/SpyPlugin.java index 700c6ce..050244d 100644 --- a/test/spy/java/com/extantape/usher/spy/SpyPlugin.java +++ b/test/spy/java/com/extantape/usher/spy/SpyPlugin.java @@ -1,14 +1,14 @@ -package com.extantape.usher.spy; +package com.extantape.sheriff.spy; -import com.extantape.usher.api.JoinAttemptEvent; -import com.extantape.usher.api.ProfilesChangedEvent; -import com.extantape.usher.api.UsherProblemEvent; +import com.extantape.sheriff.api.JoinAttemptEvent; +import com.extantape.sheriff.api.ProfilesChangedEvent; +import com.extantape.sheriff.api.SheriffProblemEvent; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; -/** Test only: prints every Usher event as one SPY line, the way a Discord bridge would see it. */ +/** Test only: prints every Sheriff event as one SPY line, the way a Discord bridge would see it. */ public final class SpyPlugin extends JavaPlugin implements Listener { @Override public void onEnable() { getServer().getPluginManager().registerEvents(this, this); } @@ -22,7 +22,7 @@ public final class SpyPlugin extends JavaPlugin implements Listener { getLogger().info("SPY attempt name=" + e.getName() + " uuid=" + e.getAccount() + " count=" + e.getAttempts() + " main=" + Bukkit.isPrimaryThread()); } - @EventHandler public void on(UsherProblemEvent e) { + @EventHandler public void on(SheriffProblemEvent e) { getLogger().info("SPY problem code=" + e.getCode() + " main=" + Bukkit.isPrimaryThread() + " message=" + e.getMessage()); } } diff --git a/test/spy/resources/paper-plugin.yml b/test/spy/resources/paper-plugin.yml index 3eaae88..3a447b5 100644 --- a/test/spy/resources/paper-plugin.yml +++ b/test/spy/resources/paper-plugin.yml @@ -1,11 +1,11 @@ -name: UsherSpy +name: SheriffSpy version: '1' -main: com.extantape.usher.spy.SpyPlugin +main: com.extantape.sheriff.spy.SpyPlugin api-version: '26.1' -description: Test only. Logs Usher's events to the console. +description: Test only. Logs Sheriff's events to the console. dependencies: server: - Usher: + Sheriff: load: BEFORE required: true join-classpath: true From 0f4fe83f4fe975ca6933329779270a0b8747447b Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Sat, 14 Nov 2026 12:39:01 -0700 Subject: [PATCH 2/2] Hook WelcomeWagon's invite events by their new names D197 renamed the invite plugin WelcomeWagon, so the hook, its test, the plugin dependency and docs follow, along with Mayor and BlueMapAuth in the text. Co-Authored-By: Claude Opus 5.5 --- RELEASE_NOTES.md | 2 +- docs/test-audit.md | 4 +-- .../extantape/sheriff/ConsoleCommands.java | 4 +-- .../java/com/extantape/sheriff/Settings.java | 2 +- .../com/extantape/sheriff/SheriffPlugin.java | 2 +- ...atchkeyHook.java => WelcomeWagonHook.java} | 36 +++++++++---------- src/main/resources/paper-plugin.yml | 6 ++-- ...ookTest.java => WelcomeWagonHookTest.java} | 28 +++++++-------- 8 files changed, 42 insertions(+), 42 deletions(-) rename src/main/java/com/extantape/sheriff/{LatchkeyHook.java => WelcomeWagonHook.java} (83%) rename src/test/java/com/extantape/sheriff/{LatchkeyHookTest.java => WelcomeWagonHookTest.java} (62%) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 552f780..37a6141 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,7 +10,7 @@ With the server stopped: 3. Use `/sheriff` for the console commands. 4. Swap in `Sheriff-0.5.0.jar`. -Leaf Juice needs its matching update to find Sheriff. +Install WelcomeWagon 0.6.0 at the same time: Sheriff hooks its invite events by their new names. Leaf Juice needs its matching update to find Sheriff. ## Tested diff --git a/docs/test-audit.md b/docs/test-audit.md index b4a1d5e..c443a0e 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -7,8 +7,8 @@ Kept by this repo's owning session (Minecraft: Plugins Lead), per `~/Code/CLAUDE | Promise | Covered by | | --- | --- | -| A Latchkey invite's profiles are checked when it's made: unknown and admin-granting profiles refused, a second circle asked as a question, only its own yes answers it | unit `LatchkeyHookTest` (8 tests); live in Latchkey's own bot test, which runs with Sheriff | -| **A profile that turns unsafe between the invite and its use isn't applied, and the log says so** | **new**, live in Latchkey's bot test ("a profile made unsafe after the invite is not applied when it's used"). Sheriff re-checks through the same `ChangeService` as every other change | +| A WelcomeWagon invite's profiles are checked when it's made: unknown and admin-granting profiles refused, a second circle asked as a question, only its own yes answers it | unit `WelcomeWagonHookTest` (8 tests); live in WelcomeWagon's own bot test, which runs with Sheriff | +| **A profile that turns unsafe between the invite and its use isn't applied, and the log says so** | **new**, live in WelcomeWagon's bot test ("a profile made unsafe after the invite is not applied when it's used"). Sheriff re-checks through the same `ChangeService` as every other change | | Without LuckPerms, no invite profile is allowed | unit `withoutLuckPermsNothingIsAllowed` | | One answer line per console request, format 1, with stable codes (`unknown-command`, `bad-argument`, a bad id) | unit `ConsoleCommandsTest`; live "an unknown command still gets one answer", "bad arguments answer bad-argument", "a bad request id is answered in plain words" | | `describe` lists every command and marks only the optional arguments | unit `describeListsEveryCommand`, `describeMarksTheOptionalArgumentsAndOnlyThose`; live | diff --git a/src/main/java/com/extantape/sheriff/ConsoleCommands.java b/src/main/java/com/extantape/sheriff/ConsoleCommands.java index f03cebc..0c5e554 100644 --- a/src/main/java/com/extantape/sheriff/ConsoleCommands.java +++ b/src/main/java/com/extantape/sheriff/ConsoleCommands.java @@ -257,7 +257,7 @@ private static Set profileList(String token) throws Request.Bad, Refusal private void apply(CommandSender sender, Request r, List rest) throws Request.Bad, Refusal { ApplyArgs a = parseApply(rest); - Set confirmed = LatchkeyHook.confirmedCodes(plugin.settings(), r.yes()); + Set confirmed = WelcomeWagonHook.confirmedCodes(plugin.settings(), r.yes()); var request = new ChangeService.Request(r.id(), ChangeService.Actor.console(a.via()), a.uuid(), a.name(), a.add(), a.remove(), confirmed, a.whitelist()); plugin.changes().submit(request).whenComplete((result, e) -> plugin.runMain(() -> { if (e != null || result == null) { fail(sender, r.id(), Errors.INTERNAL, "Something went wrong inside Sheriff. See the server log."); return; } @@ -275,7 +275,7 @@ private void answer(CommandSender sender, Request r, ChangeService.Result result } case "warn" -> { List qs = new ArrayList<>(); - for (Planner.Warning w : result.warnings()) qs.add(new Confirm.Question(LatchkeyHook.toYesId(w.code()), w.text())); + for (Planner.Warning w : result.warnings()) qs.add(new Confirm.Question(WelcomeWagonHook.toYesId(w.code()), w.text())); if (r.json()) send(sender, Answer.needs(r.id(), qs)); else { sender.sendMessage(result.message()); diff --git a/src/main/java/com/extantape/sheriff/Settings.java b/src/main/java/com/extantape/sheriff/Settings.java index 0bb6f30..572e2cf 100644 --- a/src/main/java/com/extantape/sheriff/Settings.java +++ b/src/main/java/com/extantape/sheriff/Settings.java @@ -63,7 +63,7 @@ public static Settings from(ConfigurationSection config) { ConfigurationSection p = section.getConfigurationSection(key); if (p == null) { problems.add("Profile " + key + " isn't a section, so it's left out."); continue; } if (!ID.matcher(id).matches()) { problems.add("Profile " + key + " needs an id of letters, digits, - or _, so it's left out."); continue; } - // "none" stands for an empty list in commands (sheriff apply, latchkey invite create), so it can't be a profile. + // "none" stands for an empty list in commands (sheriff apply, welcomewagon invite create), so it can't be a profile. if (id.equals("none")) { problems.add("Profile none can't be called that: \"none\" means no profiles in commands. Rename it; it's left out."); continue; } String group = p.getString("group", "").trim().toLowerCase(Locale.ROOT); if (!GROUP.matcher(group).matches()) { problems.add("Profile " + id + " has no usable group, so it's left out."); continue; } diff --git a/src/main/java/com/extantape/sheriff/SheriffPlugin.java b/src/main/java/com/extantape/sheriff/SheriffPlugin.java index 6461a94..ed95362 100644 --- a/src/main/java/com/extantape/sheriff/SheriffPlugin.java +++ b/src/main/java/com/extantape/sheriff/SheriffPlugin.java @@ -57,7 +57,7 @@ public void onEnable() { getServer().getPluginManager().registerEvents(attempts, this); getServer().getPluginManager().registerEvents(access, this); getServer().getPluginManager().registerEvents(menus, this); - LatchkeyHook.register(this); + WelcomeWagonHook.register(this); PlayerCommands players = new PlayerCommands(this); ConsoleCommands console = new ConsoleCommands(this); diff --git a/src/main/java/com/extantape/sheriff/LatchkeyHook.java b/src/main/java/com/extantape/sheriff/WelcomeWagonHook.java similarity index 83% rename from src/main/java/com/extantape/sheriff/LatchkeyHook.java rename to src/main/java/com/extantape/sheriff/WelcomeWagonHook.java index d9c294f..a7c1d46 100644 --- a/src/main/java/com/extantape/sheriff/LatchkeyHook.java +++ b/src/main/java/com/extantape/sheriff/WelcomeWagonHook.java @@ -18,25 +18,25 @@ import java.util.logging.Level; /** - * Latchkey invites (owner, 2026-11-09; crossing latchkey-sheriff). When an admin makes an invite naming profiles, Sheriff + * WelcomeWagon invites (owner, 2026-11-09; crossing welcomewagon-sheriff). When an admin makes an invite naming profiles, Sheriff * runs its usual checks then, so a bad combination is refused before anyone arrives. When the invite is used, Sheriff * applies the profiles through the same change path as any approval, with the yeses given when the invite was made. * - *

Latchkey is optional and Sheriff doesn't build against it: its events are found by name at startup and read through - * their public methods, which are the agreed contract. If Latchkey isn't there, or its events don't match, this does + *

WelcomeWagon is optional and Sheriff doesn't build against it: its events are found by name at startup and read through + * their public methods, which are the agreed contract. If WelcomeWagon isn't there, or its events don't match, this does * nothing and says so once. */ -final class LatchkeyHook implements Listener { - static final String CHECK = "com.extantape.latchkey.api.InviteCheckEvent"; - static final String REDEEMED = "com.extantape.latchkey.api.InviteRedeemedEvent"; - static final String ACTOR = "Latchkey invite"; +final class WelcomeWagonHook implements Listener { + static final String CHECK = "com.extantape.welcomewagon.api.InviteCheckEvent"; + static final String REDEEMED = "com.extantape.welcomewagon.api.InviteRedeemedEvent"; + static final String ACTOR = "WelcomeWagon invite"; /** What Sheriff says about an invite's profiles: a refusal, or questions keyed by their command-line id. */ record Check(String refusalCode, String refusalText, Map questions) { boolean refused() { return refusalCode != null; } } - private LatchkeyHook() {} + private WelcomeWagonHook() {} /** * Sheriff's checks for a new player. Invites are for people who haven't arrived, so the plan starts from no groups; @@ -74,12 +74,12 @@ static Set confirmedCodes(Settings settings, Set yesIds) { return out; } - /** Hooks into Latchkey's events if Latchkey is installed. */ + /** Hooks into WelcomeWagon's events if WelcomeWagon is installed. */ static void register(SheriffPlugin sheriff) { - Plugin latchkey = Bukkit.getPluginManager().getPlugin("Latchkey"); - if (latchkey == null) return; + Plugin welcomewagon = Bukkit.getPluginManager().getPlugin("WelcomeWagon"); + if (welcomewagon == null) return; try { - ClassLoader cl = latchkey.getClass().getClassLoader(); + ClassLoader cl = welcomewagon.getClass().getClassLoader(); Class checkClass = cl.loadClass(CHECK).asSubclass(Event.class); Class redeemedClass = cl.loadClass(REDEEMED).asSubclass(Event.class); Method getProfiles = checkClass.getMethod("getProfiles"), getConfirmed = checkClass.getMethod("getConfirmed"); @@ -87,7 +87,7 @@ static void register(SheriffPlugin sheriff) { Method checkedBy = checkClass.getMethod("checkedBy", String.class); Method rUuid = redeemedClass.getMethod("getUuid"), rName = redeemedClass.getMethod("getName"); Method rProfiles = redeemedClass.getMethod("getProfiles"), rConfirmed = redeemedClass.getMethod("getConfirmed"); - Listener owner = new LatchkeyHook(); + Listener owner = new WelcomeWagonHook(); Bukkit.getPluginManager().registerEvent(checkClass, owner, EventPriority.NORMAL, (l, event) -> { if (!checkClass.isInstance(event)) return; @@ -99,7 +99,7 @@ static void register(SheriffPlugin sheriff) { if (c.refused()) refuse.invoke(event, c.refusalCode(), c.refusalText()); else for (var q : c.questions().entrySet()) need.invoke(event, q.getKey(), q.getValue()); } catch (ReflectiveOperationException | RuntimeException e) { - sheriff.getLogger().log(Level.WARNING, "Couldn't check a Latchkey invite's profiles", e); + sheriff.getLogger().log(Level.WARNING, "Couldn't check a WelcomeWagon invite's profiles", e); try { refuse.invoke(event, "internal", "Sheriff couldn't check these profiles. See the server log."); } catch (ReflectiveOperationException ignored) {} } }, sheriff); @@ -113,12 +113,12 @@ static void register(SheriffPlugin sheriff) { @SuppressWarnings("unchecked") Set yes = (Set) rConfirmed.invoke(event); apply(sheriff, uuid, name, profiles, yes); } catch (ReflectiveOperationException | RuntimeException e) { - sheriff.getLogger().log(Level.WARNING, "Couldn't read a used Latchkey invite, so its profiles weren't applied", e); + sheriff.getLogger().log(Level.WARNING, "Couldn't read a used WelcomeWagon invite, so its profiles weren't applied", e); } }, sheriff); - sheriff.getLogger().info("Checking and applying the profiles on Latchkey invites."); + sheriff.getLogger().info("Checking and applying the profiles on WelcomeWagon invites."); } catch (ReflectiveOperationException | ClassCastException e) { - sheriff.getLogger().warning("Latchkey is installed but its invite events don't match what Sheriff expects, so invite profiles " + sheriff.getLogger().warning("WelcomeWagon is installed but its invite events don't match what Sheriff expects, so invite profiles " + "won't be applied. Update both plugins. (" + e.getClass().getSimpleName() + ")"); } } @@ -130,7 +130,7 @@ static void apply(SheriffPlugin sheriff, UUID uuid, String name, List pr return; } Set confirmed = confirmedCodes(sheriff.settings(), yesIds); - var request = new ChangeService.Request(null, new ChangeService.Actor(ACTOR, null, "latchkey"), uuid, name, + var request = new ChangeService.Request(null, new ChangeService.Actor(ACTOR, null, "welcomewagon"), uuid, name, new LinkedHashSet<>(profiles), Set.of(), confirmed, false); sheriff.changes().submit(request).thenAccept(result -> { if (result.ok()) return; diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 9fddb24..2dfbc80 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -11,9 +11,9 @@ dependencies: load: BEFORE required: false join-classpath: true - # Optional: with Latchkey, Sheriff checks an invite's profiles when it's made and applies them when it's used. - # Sheriff reads Latchkey's events by name, so it builds and runs without it. - Latchkey: + # Optional: with WelcomeWagon, Sheriff checks an invite's profiles when it's made and applies them when it's used. + # Sheriff reads WelcomeWagon's events by name, so it builds and runs without it. + WelcomeWagon: load: BEFORE required: false permissions: diff --git a/src/test/java/com/extantape/sheriff/LatchkeyHookTest.java b/src/test/java/com/extantape/sheriff/WelcomeWagonHookTest.java similarity index 62% rename from src/test/java/com/extantape/sheriff/LatchkeyHookTest.java rename to src/test/java/com/extantape/sheriff/WelcomeWagonHookTest.java index acb4b8f..d80347f 100644 --- a/src/test/java/com/extantape/sheriff/LatchkeyHookTest.java +++ b/src/test/java/com/extantape/sheriff/WelcomeWagonHookTest.java @@ -8,8 +8,8 @@ import static org.junit.jupiter.api.Assertions.*; -/** Sheriff's checks on Latchkey invites, without a server. The reflection wiring is covered by the bot tests. */ -class LatchkeyHookTest { +/** Sheriff's checks on WelcomeWagon invites, without a server. The reflection wiring is covered by the bot tests. */ +class WelcomeWagonHookTest { Settings settings; FakeStore store; @@ -20,45 +20,45 @@ class LatchkeyHookTest { } @Test void aPlainProfileIsFine() { - var c = LatchkeyHook.check(settings, store, List.of("builders"), Set.of()); + var c = WelcomeWagonHook.check(settings, store, List.of("builders"), Set.of()); assertFalse(c.refused()); assertTrue(c.questions().isEmpty()); } @Test void anUnknownProfileIsRefusedWithACommandFriendlyCode() { - var c = LatchkeyHook.check(settings, store, List.of("wizards"), Set.of()); + var c = WelcomeWagonHook.check(settings, store, List.of("wizards"), Set.of()); assertEquals("unknown-profile", c.refusalCode()); assertTrue(c.refusalText().contains("wizards")); } @Test void aProfileThatGrantsAdminIsRefused() { store.perm("builders", "*"); - var c = LatchkeyHook.check(settings, store, List.of("builders"), Set.of()); + var c = WelcomeWagonHook.check(settings, store, List.of("builders"), Set.of()); assertEquals("restricted", c.refusalCode()); } @Test void twoCirclesAtOnceIsAQuestionNotARefusal() { - var c = LatchkeyHook.check(settings, store, List.of("young", "grown"), Set.of()); + var c = WelcomeWagonHook.check(settings, store, List.of("young", "grown"), Set.of()); assertFalse(c.refused()); assertTrue(c.questions().containsKey("bridge"), String.valueOf(c.questions())); } @Test void everyQuestionIdFitsTheCommandFormat() { - var c = LatchkeyHook.check(settings, store, List.of("young", "grown", "young_sky", "grown_sky", "builders"), Set.of()); + var c = WelcomeWagonHook.check(settings, store, List.of("young", "grown", "young_sky", "grown_sky", "builders"), Set.of()); for (String id : c.questions().keySet()) assertTrue(id.matches("[a-z0-9-]{1,64}"), id); } @Test void yesIdsMapBackToSheriffsCodes() { - assertEquals("confirm-grown-sky", LatchkeyHook.toYesId("confirm:grown_sky")); - assertEquals(Set.of("confirm:grown_sky", "bridge"), LatchkeyHook.confirmedCodes(settings, Set.of("confirm-grown-sky", "bridge"))); - assertEquals(Set.of(), LatchkeyHook.confirmedCodes(settings, Set.of("confirm-nothing", "made-up"))); + assertEquals("confirm-grown-sky", WelcomeWagonHook.toYesId("confirm:grown_sky")); + assertEquals(Set.of("confirm:grown_sky", "bridge"), WelcomeWagonHook.confirmedCodes(settings, Set.of("confirm-grown-sky", "bridge"))); + assertEquals(Set.of(), WelcomeWagonHook.confirmedCodes(settings, Set.of("confirm-nothing", "made-up"))); } @Test void aQuestionIsAnsweredOnlyWithItsOwnYes() { - var asked = LatchkeyHook.check(settings, store, List.of("grown_sky"), Set.of()); + var asked = WelcomeWagonHook.check(settings, store, List.of("grown_sky"), Set.of()); assertTrue(asked.questions().containsKey("confirm-grown-sky"), String.valueOf(asked.questions())); - var answered = LatchkeyHook.check(settings, store, List.of("grown_sky"), Set.of("confirm-grown-sky")); - assertEquals(asked.questions().keySet(), answered.questions().keySet(), "every question is still listed; Latchkey drops the answered ones"); + var answered = WelcomeWagonHook.check(settings, store, List.of("grown_sky"), Set.of("confirm-grown-sky")); + assertEquals(asked.questions().keySet(), answered.questions().keySet(), "every question is still listed; WelcomeWagon drops the answered ones"); } @Test void withoutLuckPermsNothingIsAllowed() { @@ -69,6 +69,6 @@ class LatchkeyHookTest { public Set lineage(String g) { return store.lineage(g); } public boolean grants(String g, String p) { return store.grants(g, p); } }; - assertEquals("luckperms-missing", LatchkeyHook.check(settings, down, List.of("builders"), Set.of()).refusalCode()); + assertEquals("luckperms-missing", WelcomeWagonHook.check(settings, down, List.of("builders"), Set.of()).refusalCode()); } }