Moderation
Moderation
Voxy's moderation API covers three related domains, each with its own manager off VoxyAPI:
| Domain | Manager | Getter |
|---|---|---|
| Punishments | PunishmentManager |
getPunishmentManager() |
| Ladders | LadderManager |
getLadderManager() |
| Reports | ReportsManager |
getReportsManager() |
Most of the actual work happens on PlayerData (see Player Data), which owns a player's punishments and ladder progressions. The managers handle enforcement, ladder definitions, and report storage.
These APIs are synchronous. Loading a player through DataManager can hit the database, and save(), handlePunishment(...), and Report#save() perform I/O and broadcasts. Call them off the main server thread when the player is not already in memory.
Punishments
Getting the manager
VoxyAPI api = VoxyAPI.getAPI();
PunishmentManager punishments = api.getPunishmentManager();
Punishment types
A punishment carries a PunishmentType:
public enum PunishmentType {
BAN,
BLACKLIST,
MUTE,
KICK,
WARN
}
Building a punishment
A Punishment is created from a PunishmentType and a Duration, then given its issuer and reason:
// 7-day ban
Punishment ban = new Punishment(PunishmentType.BAN, Duration.of("7d"));
ban.setBy("Notch");
ban.setReason("Cheating");
// Permanent mute
Punishment mute = new Punishment(PunishmentType.MUTE, Duration.permanent());
mute.setBy("Notch");
mute.setReason("Chat abuse");
Duration accepts a duration string such as "7d", "1h", or "30m" via Duration.of(String), a raw millisecond value via Duration.of(long), a long plus TimeUnit via Duration.of(long, TimeUnit), or Duration.permanent() for a punishment that never expires.
Issuing a punishment
Applying a punishment is a two-step process. PlayerData#punish(Punishment) records it on the player (revoking any existing active punishment of the same type), and PunishmentManager#handlePunishment(PlayerData, Punishment, boolean) enforces it - notifications, broadcasts, and player actions such as kicking or blocking chat. The boolean is silent: pass true for a staff-only broadcast.
PlayerData target = api.getDataManager().getOrLoad(playerId).orElse(null);
if (target == null) {
return;
}
Punishment ban = new Punishment(PunishmentType.BAN, Duration.of("7d"));
ban.setBy("Notch");
ban.setReason("Cheating");
target.punish(ban); // record on the player
punishments.handlePunishment(target, ban, false); // enforce and announce
target.save(); // persist
A KICK has no lasting state, so it usually just needs enforcement:
Punishment kick = new Punishment(PunishmentType.KICK, Duration.permanent());
kick.setBy("Notch");
kick.setReason("Go cool off");
punishments.handlePunishment(target, kick, false);
Reading active punishments
PlayerData exposes the currently active punishment of each stateful type, plus quick boolean checks:
if (target.isBanned()) {
Punishment current = target.getCurrentBan();
String reason = current.getReason();
String left = current.getDuration().timeLeft(true); // e.g. "6 days"
}
if (target.isMuted()) {
Punishment currentMute = target.getCurrentMute();
}
boolean blacklisted = target.isBlacklisted();
Punishment currentBlacklist = target.getCurrentBlacklist();
To look up the active punishment of any type generically, use findCurrentPunishment:
Punishment activeWarn = target.findCurrentPunishment(PunishmentType.WARN);
if (activeWarn != null) {
// ...
}
If you have mutated a player's punishment list directly, call calculatePunishments() to refresh the cached current ban / mute / blacklist.
Reading punishment history
getPunishments() returns every punishment ever applied to the player, active or not:
Collection<Punishment> history = target.getPunishments();
for (Punishment p : history) {
boolean active = p.isValid(); // not revoked and not expired
boolean revoked = p.isRevoked();
PunishmentType type = p.getType();
String by = p.getBy();
long when = p.getDuration().getCreationTime();
}
Each Punishment also offers convenience type checks: isBan(), isMute(), isBlacklist(), isKick(), and isWarn().
Revoking (unban / unmute)
There is no separate "unban" call. Revoke the punishment object, recalculate the player's active punishments, and save:
Punishment ban = target.getCurrentBan();
if (ban != null) {
ban.revoke("Notch"); // marks revoked, stamps revokedBy and revokedAt
target.calculatePunishments();
target.save();
}
revoke(String) uses the current time; revoke(String, long) lets you supply the revocation timestamp. unrevoke() clears the revoked flag. A punishment is considered live only while isValid() returns true (!isRevoked() and the Duration has not expired).
Ladders
A ladder is a named, ordered set of Steps that escalates automatically as a player re-offends. Each step declares a StepAction - either PUNISH (apply the step's punishment template) or IGNORE (advance without punishing). A player's position on a ladder is tracked by a LadderProgression, which also carries its own Duration so progress expires after a while.
public enum StepAction {
PUNISH,
IGNORE
}
Getting the manager and looking up a ladder
LadderManager ladders = api.getLadderManager();
Optional<Ladder> byName = ladders.getByName("chat"); // case-insensitive
Optional<Ladder> byId = ladders.getById(someLadderId);
Collection<Ladder> all = ladders.getAll();
addOrUpdate(Ladder) and remove(UUID) let you manage the loaded set, and reloadLadders() re-reads them from storage.
Inspecting a ladder's steps
Ladder ladder = byName.orElseThrow();
UUID id = ladder.id();
String name = ladder.name();
for (Step step : ladder.steps()) {
if (step.action() == StepAction.PUNISH) {
Punishment template = step.punishment(); // may be null
}
}
Optional<Step> third = ladder.stepById(3);
Step#punishment() is nullable and only meaningful when the action is PUNISH.
Punishing a player through a ladder
PlayerData#punishByLadder(Ladder, String reason, String by) advances the player one step on the ladder and applies that step's punishment. It returns the Punishment that was applied, or null if the reached step's action was IGNORE:
Punishment applied = target.punishByLadder(ladder, "Spamming", "Notch");
if (applied != null) {
punishments.handlePunishment(target, applied, false);
}
target.save();
Reading a player's progression
A player holds a progression per ladder, keyed by ladder id:
LadderProgression prog = target.getLadderProgression(ladder.id());
if (prog != null && !prog.hasExpired()) {
int step = prog.getCurrentStep();
long updated = prog.getLastUpdated();
}
// Or just the step number (returns 0 if absent or expired)
int step = target.getLadderStep(ladder.id());
// The full map of every ladder the player is progressing on
Map<UUID, LadderProgression> progressions = target.getLadderProgressions();
cleanupExpiredLadderProgressions() drops progressions whose Duration has elapsed, and setLadderProgression(LadderProgression) writes one directly. LadderProgression.create(UUID, Ladder) builds a fresh progression at step 1 using the ladder's duration.
Reports
Getting the manager
ReportsManager reports = api.getReportsManager();
Report categories
Reports are filed under a ReportCategory, a record loaded from configuration:
public record ReportCategory(
String id,
String displayName,
List<String> aliases,
UUID ladderId, // optional ladder this category escalates on, may be null
Set<ReportProof> proofs
) { }
Look categories up by id, or fuzzily by id / display name / alias with match:
Collection<ReportCategory> categories = reports.getCategories();
Optional<ReportCategory> byId = reports.getById("cheating");
Optional<ReportCategory> matched = reports.match("hacks"); // matches id, name, or alias
saveCategory(ReportCategory) persists a category and deleteCategory(String) removes one; reloadCategories() re-reads them from storage.
Creating a report
issueReport(PlayerData reporter, PlayerData reported, ReportCategory category) builds a Report but does not store it - call save() on the returned report to persist it:
ReportCategory category = reports.match("cheating").orElse(null);
if (category == null) {
return;
}
Report report = reports.issueReport(reporter, reported, category);
report.setReporterServer("lobby-1");
report.save();
Querying reports
Counts and pages operate over unsolved reports. Paging is 1-based:
long open = reports.countReports(); // all unsolved
long againstPlayer = reports.countReports(reportedId);
long inCategory = reports.countReports(category);
List<Report> firstPage = reports.getReports(1, 10); // page 1, 10 per page
List<Report> playerPage = reports.getReports(reportedId, 1, 10);
List<Report> categoryPage = reports.getReports(category, 1, 10);
List<Report> everything = reports.getReports(); // all unsolved
Reading and resolving a report
UUID reportId = report.getReportID();
UUID reporter = report.getReporterID();
UUID reported = report.getReportedID();
String categoryId = report.getCategory();
long filedAt = report.getAddedAt();
// Resolve it
report.setSolved(true);
report.setSolvedBy("Notch");
report.save();
The proof-type system
A category can require one or more ReportProof types. Currently the API defines one:
public enum ReportProof {
CHAT_SNAPSHOT // snapshot of the reported player's recent chat
}
When a category requires CHAT_SNAPSHOT, the report captures recent chat at the time of filing. Read it back from the report:
if (category.proofs().contains(ReportProof.CHAT_SNAPSHOT)) {
List<Document> chat = report.getChatSnapshot(); // may be empty, never null
}
Key methods
Punishments
| Member | Signature | Purpose |
|---|---|---|
Punishment |
new Punishment(PunishmentType, Duration) |
Build a punishment |
Punishment#setBy / #setReason |
void setBy(String) / void setReason(String) |
Set issuer and reason |
PlayerData#punish |
void punish(Punishment) |
Record it, revoking any active one of the same type |
PunishmentManager#handlePunishment |
void handlePunishment(PlayerData, Punishment, boolean silent) |
Enforce and announce |
PlayerData#isBanned / #isMuted / #isBlacklisted |
boolean |
Quick active-state checks |
PlayerData#getCurrentBan / #getCurrentMute / #getCurrentBlacklist |
Punishment |
Active punishment of that type |
PlayerData#findCurrentPunishment |
Punishment findCurrentPunishment(PunishmentType) |
Active punishment of any type, or null |
PlayerData#getPunishments |
Collection<Punishment> getPunishments() |
Full punishment history |
PlayerData#calculatePunishments |
void calculatePunishments() |
Recompute cached active punishments |
Punishment#isValid |
boolean isValid() |
Not revoked and not expired |
Punishment#revoke |
void revoke(String) / void revoke(String, long) |
Revoke (unban / unmute) |
Punishment#unrevoke |
void unrevoke() |
Clear the revoked flag |
Ladders
| Member | Signature | Purpose |
|---|---|---|
LadderManager#getByName |
Optional<Ladder> getByName(String) |
Look up a ladder by name (case-insensitive) |
LadderManager#getById |
Optional<Ladder> getById(UUID) |
Look up a ladder by id |
LadderManager#getAll |
Collection<Ladder> getAll() |
All loaded ladders |
LadderManager#addOrUpdate / #remove |
void addOrUpdate(Ladder) / boolean remove(UUID) |
Manage loaded ladders |
Ladder#steps |
List<Step> steps() |
Ordered steps |
Ladder#stepById |
Optional<Step> stepById(int) |
A specific step |
Step#action / #punishment |
StepAction action() / Punishment punishment() |
Step behaviour and template |
PlayerData#punishByLadder |
Punishment punishByLadder(Ladder, String reason, String by) |
Advance and punish, or null on IGNORE |
PlayerData#getLadderProgression |
LadderProgression getLadderProgression(UUID) |
Progression, or null if absent/expired |
PlayerData#getLadderStep |
int getLadderStep(UUID) |
Current step number, 0 if absent/expired |
PlayerData#getLadderProgressions |
Map<UUID, LadderProgression> getLadderProgressions() |
All progressions |
Reports
| Member | Signature | Purpose |
|---|---|---|
ReportsManager#issueReport |
Report issueReport(PlayerData reporter, PlayerData reported, ReportCategory) |
Build a report (not persisted) |
Report#save |
void save() |
Persist a report |
ReportsManager#getCategories |
Collection<ReportCategory> getCategories() |
Loaded categories |
ReportsManager#getById / #match |
Optional<ReportCategory> getById(String) / match(String) |
Look up a category |
ReportsManager#countReports |
long countReports() / countReports(UUID) / countReports(ReportCategory) |
Count unsolved reports |
ReportsManager#getReports |
List<Report> getReports(int page, int pageSize) (plus UUID / ReportCategory overloads, and no-arg) |
Page unsolved reports |
Report#setSolved / #setSolvedBy |
void setSolved(boolean) / void setSolvedBy(String) |
Resolve a report |
Report#getChatSnapshot |
List<Document> getChatSnapshot() |
Captured chat proof |
ReportCategory#proofs |
Set<ReportProof> proofs() |
Required proof types |