12 releases, newest first.
Cross-server sync delivered nothing when two backends shared a server-name.
The punishment feed told a peer's row from its own by comparing the row's server
column against server-name. That value is an owner-typed display string that ships
as "Server" in every jar, and the ordinary way to deploy a network - copy the plugin
folder to each backend - leaves it identical on all of them. Every peer's row then read
as locally issued, and the feed skipped all of it:
/unmute that did not free anyone either.Nothing was logged, on either side.
A row is now recognised by its id, recorded inside the insert's own transaction so
the feed cannot read a row this install wrote before its id is known. This is exact on a
network whose backends all still call themselves Server, and needs no configuration
change to take effect.
server-name still has to be unique - network presence and the /helpop and /report
feeds are keyed by it and have no id to fall back on - so a duplicate is now reported
instead of being silent:
server-name is still the shipped default on a shared database.Also in this release:
/unmute working while the name cannot be trusted.
The API notification stays behind the name check, being the half that is not idempotent.No configuration change is required, and no config key was added or renamed. The
server-name comment in config.yml was rewritten and SnLib merges it on boot.
The punishment tables are still being prepared on the database worker... on every start, whether or not anything had happened. It described the pre-schema login window instead of detecting it, so a healthy server whose tables already existed warned about nothing./helpop <message> and /report <player> <message> - the first two commands of SnBans a player runs. Both reach the console and every snbans.requests.receive holder on every server of the network, carrying the server the request was filed on:[HelpOp] Snopeyy (Dev): my chest got griefed
[Report] Snopeyy reported Alex on Dev: flying in spawn{server} names where the player is, not where the plugin is. On a proxy install it is the backend the sender is standing on rather than the proxy, which is the whole point of the token.staff-requests in config.yml: a toggle per kind plus cooldown-seconds (default 60, range 0-3600), keyed per player and per kind. Unlike the attempt notices, a throttled player is told how long is left.snbans.requests.receive is its own permission, separate from snbans.notify. snbans.helpop and snbans.report are the only two nodes of this plugin that default to true.snbans_requests, created on boot with no migration. It is a delivery envelope rather than a history: each peer reads a row once and a sweep drops it minutes later.command.aliases: [kick] on Velocity used to register /snbans over SnBans' own /kick with no warning - the reserved-root list was missing /kick and /ipkick.default: true lives in plugin.yml and a proxy never reads one. The proxy now reproduces the descriptor's defaults as well as its children map.No API surface change (API_VERSION stays 1.0.0).
Fixes found by an adversarial review of the 1.7.0 attempt notices. Two are real defects, both in the new code only - nothing that existed before 1.7.0 is affected.
AttemptNotifier.report swallowed
every throwable, but its own catch body built a string and called the logging handler
unguarded - and a throwable out of that reached LoginGate's fail-open branch, which exists
for an unreachable database and admits the login. The warning is now guarded too, so the
no-notice-can-change-a-decision promise is literal.attempt-notices.cooldown-seconds is now clamped to 0-3600, with a console warning when a
written value had to be moved. The window is also how long one throttle entry is held in
memory, so a day-long window was a day-long retention for one entry per account that knocked,
and 2147483647 meant the prune never ran again.{server} in an attempt notice now names the server the attempt was made against, not the
one the punishment was issued on. On a shared MySQL those differ constantly, and "which backend
is this account knocking on" is the only reading that tells staff something the line does not
already say.No config or lang key was renamed or removed, so an existing 1.7.0 install needs no edit.
Staff are now told when a punishment refuses somebody, not only when one is handed out.
A ban, IP ban or blacklist that denies a login, and a mute that cancels a chat message or one
of mute.blocked-commands, are reported to the console and to snbans.notify holders.
attempt-notices section in config.yml: a toggle per kind (login, chat, command)
plus cooldown-seconds, which reports the same account's same kind of attempt once per window
(default 60, 0 for every attempt). The window is what keeps the feature usable: a banned
client reconnects every few seconds on its own and a muted player types faster the less they
are heard. Only the staff notice is throttled - the refusal the player gets is unchanged.messages.attempt block in the lang file, one line per kind.{player} is the alt that ran into the punishment while {id} is the row that
stopped them - which is what makes a ban-evading alt visible the moment it knocks.alts.notify-states.No new permission node (snbans.notify covers it), no schema change and no API change
(API_VERSION stays 1.0.0).
/alts (alts.hidden)List names or UUIDs under alts.hidden in config.yml and no player ever sees those accounts in
an alt scan. The rule runs both ways, because hiding only one end would leak the same link
from the other:
/alts and from the join warning;/alts <hidden>
reads exactly like an account nobody shares an address with.The console is the only exception - it always sees the whole scan. There is deliberately no permission node for it: a node can be mis-granted, being the console cannot.
Nothing about enforcement changes. A hidden account is still scanned, stored, banned, muted
and kicked exactly like any other, and /snbans match is not filtered either - it takes two
explicit names and reports shared addresses rather than listing accounts.
On a shared MySQL install the alt account list was already network-wide - login history carries no server column - but the Online marker was not: it read this backend's player list, so an alt connected to another server rendered as offline.
Each server now publishes its connected players into a new snbans_presence table and refreshes
them on a 20-second heartbeat, and /alts reports the union. A server that dies without a clean
shutdown stops counting within a minute and its rows are swept on its next boot.
The same applies to the join scan. Its decisions never depended on presence, so what changes there is the colour of a line it already printed.
Automatic on a shared backend and absent on a single-server one, exactly like cross-server
punishment sync - there is no key to configure. snbans_presence is created on boot alongside the
other tables, so an existing install needs no migration.
API_VERSION stays 1.0.0 - no surface was added, removed or renamed. Two observable-behaviour
notes for consumers:
AltAccountView's ONLINE state now means "connected anywhere on the network" on a shared
MySQL. It used to mean "connected to this backend".scanAlts and AltScanCompletedEvent are not filtered by alts.hidden: server-side code
runs with the same trust as the console.New: /snbans wipe <ban|mute|blacklist|all> [confirm] - the bulk amnesty. One command
instead of one /unban per player, for an amnesty, a season reset, or undoing a bad mass-ban.
ban, mute and blacklist cover their own kind; all does the three together.
snbans.admin.wipe node only makes the subcommand grantable and visible in /snbans help -
a node can be mis-granted, and being the console cannot.confirm. Without the token it counts, writes nothing and
prints the exact command that would do it. There is no config key to skip that step./history./snbans rollback follows. Wiped
punishments leave the player's history entirely and count towards no template ladder. Take a
database backup first if that history matters to you.broadcasts.wipe toggle, the messages.wipe.* lang block and
the wipe: block of webhooks.yml. A wipe that erased nothing announces nothing.Paper and Velocity alike. New keys are auto-merged into config.yml, the lang file and
webhooks.yml on boot; no configuration change is required. Public developer API unchanged
(API_VERSION stays 1.0.0).
/snbans import litebans <host:port> <database> <user> <password> [prefix] [confirm] imports
another network's LiteBans database into SnBans: its bans, its mutes and its login history.
Without confirm it is a dry run - it connects, counts what it would write, and writes
nothing. Run it from the console: the password is a command argument, so it reaches the log
either way.
(player, address) row per pair, which is what makes /history, /alts, /snbans match and
name resolution work from the first boot.retention.days are skipped; the next daily
purge would delete them anyway.punish_ip: true
sets that flag on every punishment, so on such a network every imported row covers the
address too - which is what that network is already enforcing today.templates.yml
ids - so imported punishments count towards no escalation ladder.# when it knew none) import as account-only,
and are skipped on the login side: treating that placeholder as an address would make every
account behind it look like an alt of every other.Unknown-<uuid prefix>.A successful run records itself in the new snbans_imports table and any later run refuses -
importing twice would duplicate every punishment with no way to tell the copies apart. A run that
fails partway records nothing and can be retried, but whatever it already wrote is still there.
snbans_imports is created on boot if missing, on both platforms, so an existing install gains it
with no migration.
/kick and /ipkick - the two commands SnBans was missing.
/kick <player> <reason...> disconnects a player./ipkick <player> <reason...> disconnects every account currently connected from that
player's last known address, so it reaches their online alts even when the target
themselves is offline.Both are also subcommands of /snbans, so /snbans help lists them, and both obey the
LuckPerms hierarchy check, the -s / -p visibility flags and snbans.noreason.
A kick stores nothing. It disconnects, announces and posts its webhook, and writes no
row: no id, no expiry, no revert, nothing in /history or /staffhistory, and it never
counts towards a template ladder. A kick that reaches nobody says so instead of announcing a
kick that never happened.
Known limitation. Because there is no row, a kick cannot travel between servers the way a
ban does. On a multi-backend Paper install /kick only reaches players on the server it
was run on; a target on another backend is answered messages.kick.not-online. On a
Velocity install it is network-wide, because the proxy holds every player.
Added on boot by the automatic merge; existing values and comments are preserved.
config.yml: punishments.kick.silent-by-default, broadcasts.kick, broadcasts.ipkicklang/messages_en.yml: messages.kick.* and messages.ipkick.* (announce, notify, the
disconnect screen, and kick.not-online), plus format.type-kick / format.type-ipkickwebhooks.yml: the kick and ipkick event blockssnbans.kick, snbans.ipkick (both granted by snbans.admin)No API change; API_VERSION stays 1.0.0.
Templates now carry the reason they store. templates.yml gains a reason: key: the
template id stays the short word staff type (hacks), while the reason is the sentence stored
on the row and shown by every broadcast, history line, Discord embed and disconnect screen. A
template without the key keeps its id as its reason, so existing ladders behave exactly as before.
Template ids tab-complete as reasons. /ban Notch <TAB> now lists the ban ladders this
server has, /mute Notch <TAB> the mute ones - filtered by type, because a mute ladder is
ignored by /ban.
/snbans help lists every command. Each of the eleven punishment and lookup commands is
now also a subcommand of /snbans (/snbans ban Notch hacks works), so the generated help
lists the whole surface instead of only match and rollback. Both forms share one handler, so
neither can drift from the other. The flat /ban, /mute, ... are unchanged.
Self-targeting no longer answers "their rank is equal or higher than yours." You can never
fail to outrank yourself, so /alts <you>, /snbans match <you> <other> and lifting your own
punishment (/unban, /unmute) now work. Issuing a punishment on your own account and rolling
back your own window stay refused - and now do so whether or not LuckPerms is installed.
/hist aliases /history, on Paper and on the proxy.
Rollback erases what it undoes. A confirmed /snbans rollback now DELETEs the punishments
it reverts instead of stamping them as lifted, so they leave the sanctioned player's history
entirely. On a shared MySQL the server that ran the sweep lifts its own mutes at once; other
backends drop theirs when the player next connects there.
New snbans.noreason permission. Holders may run /ban <player> or /mute <player> with no
reason, and the row records the messages.format.no-reason text ("No reason"). Without the node
the usage line still answers, which stays the default. The console always may.
templates.yml: reason: per template (optional; seeded file only, existing files are never
auto-updated - add the key by hand to use it)lang/messages_en.yml: messages.format.no-reasonplugin.yml: snbans.noreason, and aliases: [hist] on /historytemplates.yml is seeded once and never auto-updated, so an existing file will not grow
the new reason: key. Add the line by hand where you want it; templates without it keep
storing their id as the reason, exactly as before.PunishmentRevokedEvent
dispatches, because the sweep now deletes its rows and the cross-server revert feed only
carries rows whose removal was recorded. PunishmentRollbackEvent and
PunishmentRevokedEvent fire on the sweeping server only. Ordinary /unban and /unmute
reverts still propagate row by row, unchanged.Adds a public developer API. API_VERSION 1.0.0. No behavior changes to the plugin itself: every punishment, command and message works exactly as in v1.0.0.
Veto a punishment before it is written (three cancellable events, fired before the row exists, so cancelling leaves no partial state):
| Event | Fires before |
|---|---|
PunishmentIssueEvent | a ban, mute or blacklist is written |
PunishmentRevokeEvent | an unban, unmute or unblacklist is applied |
PunishmentRollbackEvent | a staff member's window is swept, carrying how many rows it matched |
Observe what happened (three notifications, immutable, never cancellable):
| Event | Fires after |
|---|---|
PunishmentIssuedEvent | a punishment is committed, with origin LOCAL or REMOTE |
PunishmentRevokedEvent | a punishment is lifted, same origin marker |
AltScanCompletedEvent | a join scan that actually found other accounts on the address |
REMOTE means a peer backend issued it and this server picked it up through cross-server sync, so
a listener on one backend can observe the whole network's activity. The cancellable twin never
fires for a remote punishment: it is already committed elsewhere, and vetoing it would desync the
network.
Query the state through SnBansAPI, resolved from the Bukkit ServicesManager via
SnBansProvider: isMuted and activeMute answer synchronously from the session cache, while
activePunishments, hasActive, history, staffHistory and scanAlts are database-backed and
return a CompletableFuture that completes off the server thread. Four immutable view records
carry the payloads, and a player's IP address is deliberately not among them.
api-events.enabled (new, config.yml, default true) is the master switch. Set it false and no
API event is dispatched at all, at zero cost, with cancellable hooks reporting "not cancelled"
so punishments proceed normally. The query facade stays available either way.messages.api-denied line rather than being met with silence. The cancelling plugin should
still send its own reason.AltScanView.truncated() cannot report a capped list as complete.command.aliases comment in config.yml: aliases are re-sourced on reload on both
platforms, which the old comment denied for Velocity.First stable release. One jar, two platforms: Paper 1.20.5+ (1.21.x included) and Velocity 3.4.0+.
/ipban, /ipmute, and a per-type
ip-by-default so the base command can punish the last known address too).templates.yml climbs that
template's duration ladder based on the player's prior punishments of the same kind./alts), plus /snbans match <a> <b> for a
historical shared-address check inside the retention window./history and /staffhistory with clickable next/prev arrows./snbans rollback <staff> <time>: a dry run by default that prints the exact command to
confirm, then reverts every punishment that staff member issued inside the window.-s / -p) on every command, with per-type defaults.sync.interval-seconds.Five full audit cycles, 28 independent auditors per cycle across 8 dimensions, finishing at 8.8/10 with 16 CRITICAL findings closed. Highlights of what those cycles fixed:
snbans.admin now grants its children on the proxy, as it already did on backends.@everyone can no longer ping a Discord guild from either platform.