Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ name: Build

on:
push:
branches: [main]
paths-ignore: ['**.md', 'docs/**']
pull_request:
paths-ignore: ['**.md', 'docs/**']

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Leak check (emails and build output; private terms are only checked locally)
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/leak-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Leak check

# Every change, docs included: the build skips docs-only changes, and README or docs text is where a stray
# email address most often slips in. A few seconds on ubuntu, no Gradle.
on:
push:
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
leak-check:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: bash scripts/leak-check.sh
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ on:
permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
Expand Down
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing els

- 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.
- For 0.3.x only, the old names still work and log a line saying what to change: the permission `stationmaster.approve` and the console command `stationmaster` (also `smaster`). Both go in the next release. Grant `usher.approve` instead: `lp group <group> permission set usher.approve true`.
- The console result line is now `usher-result`, and the API package is `com.extantape.usher.api` (`UsherProblemEvent` was `StationmasterProblemEvent`). Nothing published used the old ones.
- 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 <group> permission set usher.approve true`.
- The API package is `com.extantape.usher.api` (`UsherProblemEvent` was `StationmasterProblemEvent`).

## Commands

Expand All @@ -99,34 +99,40 @@ Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing els
| `/profile <name>` | approvers, console | Shows someone's profiles, with a button to change them. |
| `/profile <name> edit` | approvers | Opens the menu to change their profiles. |
| `/usher reload` | approvers, console | Re-reads `config.yml`. |
| `usher <command> <request-id> …` | console | The commands below, for Leaf Juice and scripts. |
| `usher <command> …` | console | The commands below, for Leaf Juice, scripts and people. |

Approvers need `usher.approve`. It's `false` by default, operators included.

## Console commands

Each takes a request ID you choose (1 to 64 letters, digits, `.`, `_`, `:` or `-`). Each answers with exactly one line, `usher-result <request-id> <json>`, sometimes a moment later. Every answer has `status` (`ok`, `warn`, `refused` or `error`), `code` and a `message` that's fine to show a person.
They use the shared [Signalbox](https://github.com/ExtantApe/Signalbox) format. A person types them as they are and gets an answer in plain words. A tool adds `--json <id>` at the end, with an id it chooses (1 to 36 of `a-z`, `0-9` and `-`), and gets exactly one line back, `ea-result <id> <json>`, sometimes a moment later:

- done: `{"id":…,"ok":true, …fields}`
- refused or failed: `{"id":…,"ok":false,"error":{"code":…,"message":…}}`, with a `message` that's fine to show a person
- needs a yes: `{"id":…,"ok":false,"needs":[{"id":…,"question":…}]}`, and nothing changed

| Command | Answers with |
| --- | --- |
| `profiles <id>` | The profiles that can be offered, restricted groups, and config problems. |
| `resolve <id> <name>` | The account's `uuid`, correctly capitalized `name`, `whitelisted`, `playedBefore`, and `triedToJoin` (how many times the whitelist turned it away) with `lastTried`. Warn before approving an account that has neither played nor tried. |
| `show <id> <uuid or name>` | Their `profiles`, profiles they get `throughOtherGroups`, their `circles`, and `whitelisted`. |
| `apply <id> <uuid> <name> [+profile] [-profile] [confirm=code,code] [whitelist=no] [via=leaf-juice]` | Adds and removes profiles, then whitelists them unless `whitelist=no`. |
| `attempts <id>` | People turned away by the whitelist: name, uuid, count, first and last try. |
| `log <id> [count]` | The last entries in `changes.log`, newest last. |
| `profiles` | The profiles that can be offered, restricted groups, and config problems. |
| `resolve <name>` | The account's `uuid`, correctly capitalized `name`, `whitelisted`, `playedBefore`, and `triedToJoin` (how many times the whitelist turned it away) with `lastTried`. Warn before approving an account that has neither played nor tried. |
| `show <uuid or name>` | Their `profiles`, profiles they get `throughOtherGroups`, their `circles`, and `whitelisted`. |
| `apply <uuid> <name> <add\|none> <remove\|none> [whitelist] [via]` | Adds and removes profiles (comma-separated lists), then whitelists them unless `whitelist` is `false`. `via` names who asked, such as `leaf-juice`. |
| `attempts [limit] [cursor]` | People turned away by the whitelist: name, uuid, count, first and last try. `next` is the cursor for the next page. |
| `log [limit]` | The last entries in `changes.log`, newest last. |
| `reload` | Re-reads `config.yml`. |
| `describe` | Every command with its typed arguments and its questions, format 1. |

How `apply` works:

- If a change needs a yes, the answer is `status: warn` with `warnings` (each has a `code` and `text`), and nothing changes. To go ahead, send the same command again with `confirm=` and those codes, such as `confirm=bridge`.
- Sending the same request ID again after it went through returns the first answer with `replayed: true`. It isn't applied twice.
- If a change needs a yes, the answer lists the questions and nothing changes. To go ahead, send the same command again with `--yes` and their ids, such as `--yes bridge` or `--yes confirm-grown-sky`.
- Sending the same `--json` id again after it went through returns the first answer with `replayed: true`. It isn't applied twice.
- Changes from the console are logged with `actor: Console` and the `via` you give.

Example:

```
> usher apply r42 069a79f4-44e9-4726-a5be-fca90e38aaf5 Notch +builders via=leaf-juice
usher-result r42 {"status":"ok","code":"applied","message":"Notch is approved and can join now, with Builders.", ...}
> usher 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
Expand Down
36 changes: 25 additions & 11 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
# Usher 0.3.1
# Usher 0.4.0

Usher now works with Latchkey 0.2.0's invites. Nothing else changes, and Usher still runs the same without Latchkey.
Usher's console commands move to the shared Signalbox format, so Leaf Juice and scripts talk to Usher the same way as to Latchkey. The old Stationmaster names are gone, as 0.3.x said they would be.

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.

## New: profiles on Latchkey invites
## Changed

- **When an admin makes an invite that names profiles,** Usher checks them straight away with its usual rules. Unknown or restricted profiles are refused, so a bad invite is never made. Anything that needs a yes, such as joining two chat circles or a profile with a `confirm` question, comes back as a question. The admin answers it with `--yes` on the same command.
- **When the invite is used,** Usher applies the profiles through the same path as any approval, with the yeses given when the invite was made. The change log records it as "Latchkey invite". If something has changed since, nothing is applied and the server log says why.
- **An invite with no profiles** gives lobby-only access until an admin adds more.
- Usher doesn't need Latchkey to build or run. It finds Latchkey's events by name when Latchkey is installed.
- **Console commands use [Signalbox](https://github.com/ExtantApe/Signalbox) format 1.**
- A person types `usher <command> …` and gets plain words.
- A tool adds `--json <id>` and gets exactly one line back, `ea-result <id> <json>`.
- Questions that need a yes come back as `needs` and change nothing; answer them with `--yes`, such as `--yes bridge`.
- `describe` lists every command with its typed arguments, marking the optional ones.
- The README's "Console commands" section has the full list.
- **`apply` takes plain positional arguments:** `apply <uuid> <name> <add|none> <remove|none> [whitelist] [via]`, with profile lists comma-separated. The old `+profile`, `-profile`, `confirm=`, `whitelist=` and `via=` forms are gone.
- **The request id is the `--json` id** and still makes a change safe to resend: it's applied once and answered again with `replayed: true`.
- **Error codes use dashes:** `unknown-profile`, `luckperms-missing` and so on.
- **A profile can't be called `none`,** since commands use `none` for an empty list. A config that has one leaves that profile out and says why.

## Upgrading from 0.3.0
## Removed

Swap the jar with the server stopped. Nothing else changes. The old names from Stationmaster (`stationmaster.approve`, the `stationmaster` and `smaster` commands) still work in 0.3.x, as promised, and go in the next release.
- `usher-result` lines. Nothing published read them.
- The `stationmaster` and `smaster` commands, and the `stationmaster.approve` permission. Grant `usher.approve` instead: `lp group <group> permission set usher.approve true`.

## Upgrading from 0.3.x

- Swap the jar with the server stopped.
- If anything still grants `stationmaster.approve`, grant `usher.approve` too, or that admin can no longer approve.
- Scripts that call `usher` need the new form.
- The old `plugins/Stationmaster/` folder is still copied across on first start if `plugins/Usher/` doesn't exist.

## Tested

- 43 unit tests pass, including 9 new ones for the invite checks.
- Latchkey's bot test runs this code with Latchkey 0.2.0 on throwaway Paper 26.1.2, 26.2 and 26.3 servers, and passes 38/38 on each. It covers a refused profile, a question answered with `--yes`, and both profiles applied when the invite is used.
- 50 unit tests pass, including the argument rules for `apply`, the reserved `none` and the optional markers in `describe`.
- The bot suite drives every console command in the new format and passes 120/120 on throwaway Paper 26.1.2, 26.2 and 26.3 servers. It covers the raw `ea-result` line, `unknown-command` and `bad-argument` answers, `describe`, questions answered with `--yes`, resending an id, a kill -9 mid-change, and the old Stationmaster names being gone.
- Not tested yet: a real server or client.
13 changes: 11 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
plugins {
java
id("com.gradleup.shadow") version "9.6.1"
}

group = "com.extantape"
version = "0.3.1"
version = "0.4.0"

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
Expand All @@ -12,6 +13,7 @@ java {
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://jitpack.io")
}

val paperApi = "io.papermc.paper:paper-api:26.1.2.build.74-stable"
Expand All @@ -20,6 +22,7 @@ val luckPermsApi = "net.luckperms:api:5.4"
dependencies {
compileOnly(paperApi)
compileOnly(luckPermsApi)
implementation("com.github.ExtantApe:Signalbox:v0.2.0")

testImplementation(paperApi)
testImplementation(luckPermsApi)
Expand All @@ -44,10 +47,16 @@ tasks.test {
useJUnitPlatform()
}

tasks.jar {
// 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.
tasks.shadowJar {
archiveBaseName.set("Usher")
archiveClassifier.set("")
from("LICENSE")
relocate("com.extantape.signalbox", "com.extantape.usher.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.
val spy: SourceSet = sourceSets.create("spy") {
Expand Down
1 change: 1 addition & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ Decisions the owner made for Stationmaster, with dates. Follow them without aski
- **Name (2026-11-08, owner):** Stationmaster is renamed Usher, one word in every place: repo, plugin name, jar, package and prose. Released as 0.3.0. Existing servers get their `plugins/Stationmaster/` folder copied on first start (never overwritten), and the old permission and console command keep working for one release with a log warning. The `stationmaster-result` console line became `usher-result` outright, since Leaf Juice doesn't parse it yet.
- **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 <id>`, `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).
13 changes: 2 additions & 11 deletions src/main/java/com/extantape/usher/ChangeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,10 @@
public final class ChangeService {

public static final String PERMISSION = "usher.approve";
/** The node from before the rename (2026-11-09). Honoured for one release (0.3.x), then removed. */
public static final String OLD_PERMISSION = "stationmaster.approve";
private static final java.util.Set<String> warnedOld = java.util.concurrent.ConcurrentHashMap.newKeySet();
static volatile java.util.logging.Logger log;

/** True for approvers. Accepts the old node too, for one release, and logs once per name that it should move. */
/** True for approvers. The old Stationmaster node stopped working in 0.4.0, as 0.3.x said it would. */
public static boolean mayApprove(org.bukkit.permissions.Permissible who) {
if (who.hasPermission(PERMISSION)) return true;
if (!who.hasPermission(OLD_PERMISSION)) return false;
String name = who instanceof org.bukkit.command.CommandSender cs ? cs.getName() : String.valueOf(who);
if (warnedOld.add(name) && log != null)
log.warning(name + " approves through the old permission " + OLD_PERMISSION + ". It still works in 0.3.x but goes in the next release: grant " + PERMISSION + " instead.");
return true;
return who.hasPermission(PERMISSION);
}

/** Who asked. {@code uuid} is null for the console. {@code via} is menu, console or leaf-juice. */
Expand Down
Loading
Loading