Frostyh Plugins

Plugins / SmartWhitelist

SmartWhitelist Coming soon

Whitelist applications in game, on the web or on Discord, with automatic checks, a score, staff review and join waves.

Get SmartWhitelist

Coming soon Not on sale yet. This page gets a buy button the day it is.

Overview

SmartWhitelist

Run a whitelisted server without drowning in applications: players apply, the plugin checks them, staff decide with one click.

A whitelisted SMP lives on its applications, and most servers still collect them in a Discord channel, copy names by hand and type /whitelist add one by one. SmartWhitelist turns that into one flow. A player who is not on the whitelist is told exactly how to apply (or applies right there in a dialog), every application gets automatic checks and a score from 0 to 100, and staff accept or deny in game or with Discord buttons. Accepting adds the player to the whitelist, at once or with the next wave.

The vanilla whitelist stays on and stays your real list, so the server is still closed if you ever remove the plugin.

Latest version
1.0.0
Server
Paper or Purpur, Java 21
BuiltByBit
coming soon

Features

Why server owners pick it

What makes it different

What you get

FAQ

Which servers does it run on?

Paper or Purpur 1.21.11 or a newer 1.21.x, with Java 21.

What if I remove the plugin?

Your server stays closed. Accepted players are on the vanilla whitelist, which never gets switched off.

Does it work behind Velocity or BungeeCord?

Yes, on the backend server. For the same-connection check, turn on IP forwarding; without it every player shares the proxy's address, so that check switches itself off.

Do I need a Discord bot?

No. Discord is optional. Webhook mode needs only a webhook link; bot mode needs your own bot token.

Is there DRM or a licence key?

No. There is nothing to activate and no licence server to reach, and the code is not obfuscated.

Can I try it before I buy?

Yes. This page has a test server with the plugin installed.

Where do I get help?

In the active support Discord or in the discussion tab on this page.

Updates

The first release is v1.0.0. Every update is free, new Minecraft versions included.

Also by Frostyh

If this plugin helps your server, a short review on this page would mean a lot. Thank you!

Installation

Requirements

Server software
Paper, or a Paper fork such as Purpur. Spigot and CraftBukkit are not supported: the plugin uses Paper-only APIs.
Minecraft version
plugin.yml api-version 1.21; built against Paper API 1.21.11
Java
21 or newer
Required plugins
none
Optional plugins
none
Libraries
org.xerial:sqlite-jdbc:3.53.2.1, net.dv8tion:JDA:6.7.0, downloaded by the server at startup (plugin.yml libraries)

Steps

  1. Download SmartWhitelist.jar from BuiltByBit. There is no licence key to enter.
  2. Stop the server and put the jar into the plugins folder.
  3. Start the server. The plugin creates plugins/SmartWhitelist/config.yml with the defaults shown under Configuration.
  4. Edit config.yml to suit your server.
  5. Apply the changes with /swl reload (Read config.yml again), or restart the server.

Setup guide

  1. Drop SmartWhitelist.jar into /plugins and restart. The server downloads the SQLite and Discord libraries on the first start.
  2. Run /whitelist on.
  3. Pick kick or limbo mode in config.yml, write your questions, and turn on the web form or Discord if you want them.
  4. Run /swl reload and open /swl.

Commands

From plugin.yml. "Not set" in the permission column means plugin.yml attaches no permission to the command itself; the nodes the plugin checks are listed under Permissions.

CommandAliasesUsageDescriptionPermission
/swl/smartwhitelist, /applications/swl [pending|review|accept|deny|note|queue|wave|log|banlist|reload|help]The application desk for staff.not set
/applynone/applyFill in your application (while you wait to be let in).not set

Subcommands in detail

/swl                              The staff desk (a dialog).
/swl pending                      Applications waiting for a decision.
/swl review <name|#id>            One application with its checks and answers.
/swl accept <name|#id>            Accept (whitelist now, or queue when waves are on).
/swl deny <name|#id> [reason]     Deny with a reason the player sees.
/swl note <name|#id> <text>       A note only staff see.
/swl queue                        Accepted players waiting for the next wave.
/swl wave                         Let the next wave in now.
/swl log                          The audit log.
/swl banlist                      Read the ban list file and URLs again.
/swl reload                       Read config.yml again.
/apply                            For players in the waiting room: open the form.

Everything except the dialogs works from the console.

Permissions

From plugin.yml.

NodeDefaultDescriptionChildren
smartwhitelist.reviewoperatorsReview, accept, deny and add notes to applications.none
smartwhitelist.adminoperatorsEverything, plus waves, the ban list and reload.smartwhitelist.review

Placeholders

This plugin registers no PlaceholderAPI placeholders.

Configuration reference

The default files as they ship in the jar, with their comments. The plugin writes them to plugins/SmartWhitelist/ on first start.

config.yml

Top-level keys:

KeyDefaultComment above it
gatesection, 3 keysnone
applysection, 6 keysnone
checkssection, 8 keysnone
banlistsection, 3 keys███ ██ █ █ █ ███ ███ ███ █ █ █ █ ██ █ █ █ █ █ ███ ████ █ ██ █ █ ██ █ █ █ █ █ █ █ █ █ █ █ ███ █ █ █ █ ████ ███ ███ █ BAN LIST: players already banned elsewhere. One per line: a UUID or name, optionally " | reason".
wavessection, 4 keys█ █ ██ █ █ ████ ███ █ █ █ █ █ █ █ █ █ █ █ ████ █ █ ███ ██ ██ ██ █ █ ██ █ █ █ █ █ █ █ ████ ███ WAVES: accepted players join in groups instead of all at once (good for a season launch).
websection, 7 keys█ █ ████ ███ ████ ██ ███ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ ███ ███ ███ █ █ ███ █ █ █ ██ ██ █ █ █ █ █ █ █ █ █ █ █ █ ████ ███ █ ██ █ █ █ █ WEB FORM: a small application page served by the plugin (plain HTTP). For HTTPS put it behind the reverse proxy of...
discordsection, 8 keys███ ███ ███ ███ ██ ███ ███ █ █ █ █ █ █ █ █ █ █ █ █ █ █ ██ █ █ █ ███ █ █ █ █ █ █ █ █ █ █ █ █ █ ███ ███ ███ ███ ██ █ █ ███ DISCORD: optional. off, webhook (posts only) or bot (posts with Accept/Deny buttons, /apply).
themesection, 12 keys███ █ █ ████ █ █ ████
menusection, 2 keysnone
messagessection, 196 keys█ █ ████ ███ ███ ██ ███ ████ ███ ██ ██ █ █ █ █ █ █ █ █ █ █ █ ███ ██ ██ ████ █ ██ ███ ██ █ █ █ █ █ █ █ █ █ █ █ █ █ ████ ███ ███ █ █ ███ ████ ███ MESSAGES: MiniMessage with the theme tags <accent> <text> <muted> <value> <good> <warn> <bad>. An empty message...
plugins/SmartWhitelist/config.yml
# ------------------------------------------------------------------------------------------------
#  SmartWhitelist by Frostyh
#  Applications for whitelisted servers. Players who are not on the whitelist apply in game, on a
#  small web page or through Discord; every application gets automatic checks and a score; staff
#  accept or deny with /swl or Discord buttons, with notes and a full audit log.
#
#  The vanilla whitelist stays ON and stays the real list: accepting adds the player to it, so the
#  server is still closed if this plugin is ever removed. Run /whitelist on before you start.
# ------------------------------------------------------------------------------------------------

# ============================================================================
#   ███  ██  ███ ████
#  █    █  █  █  █
#  █ ██ ████  █  ███
#  █  █ █  █  █  █
#   ███ █  █  █  ████
#  GATE: what happens to someone who is not on the whitelist.
gate:
  # kick:  turned away with a message that says how to apply (web link and code, or the Discord
  #        command), or where their application stands.
  # limbo: let in to a frozen waiting room where they fill in the form in a dialog. They cannot
  #        move, chat, build, take damage, see anyone or use other commands until accepted.
  # off:   SmartWhitelist does not touch joins (applications can still come in from Discord).
  # Keep the quotes on "off": unquoted, YAML reads it as false.
  mode: "kick"
  # true: only act while the vanilla whitelist is on. With it off the server is open on purpose.
  require-vanilla-whitelist: true
  limbo:
    # world,x,y,z[,yaw,pitch]. Empty: the main world's spawn.
    location: ""
    hide-players: true
    # Where an accepted player goes: spawn (the main world's spawn) or stay (where they waited).
    release-to: spawn
    # The only commands a waiting player can use.
    allowed-commands: [apply]

# ============================================================================
#   ██  ███  ███  █    █ █
#  █  █ █  █ █  █ █    █ █
#  ████ ███  ███  █     █
#  █  █ █    █    █     █
#  █  █ █    █    ████  █
#  APPLY: the form, the cooldown after a denial and the apply codes.
apply:
  # Hours a denied player waits before they may apply again. 0: straight away.
  cooldown-after-deny-hours: 72
  # How long a code from the kick message stays valid, in minutes.
  code-minutes: 60
  # The reason filled in on the deny screen; staff can change it.
  default-deny-reason: "Thanks for applying. We are not taking you on right now."
  # Lines shown above the form in game.
  intro:
    - "<title>Welcome, {player}.</title>"
    - "<text>This server is whitelisted. Answer a few questions and staff will look at your application.</text>"
  # Shown as a short list on the form. Leave empty to hide.
  rules:
    - "Be respectful to everyone."
    - "No cheats, x-ray or duplication."
    - "No griefing or stealing."
  # Up to 10 questions. type: text (one line), paragraph (several lines), choice (pick one of
  # options) or toggle (a yes/no box). min and max are characters. The same list drives the in-game
  # form, the web page and the Discord form (Discord fits 5 questions; with more, players answer
  # in game or on the web).
  questions:
    - id: age
      label: "How old are you?"
      type: text
      required: true
      max: 3
    - id: why
      label: "Why do you want to join?"
      type: paragraph
      required: true
      min: 30
      max: 600
    - id: found
      label: "How did you find us?"
      type: choice
      options: ["A friend", "YouTube or TikTok", "A server list", "Somewhere else"]
    - id: rules
      label: "I have read and accept the rules"
      type: toggle
      required: true

# ============================================================================
#   ███ █  █ ████  ███ █  █  ███
#  █    █  █ █    █    █ █  █
#  █    ████ ███  █    ██    ██
#  █    █  █ █    █    █ █     █
#   ███ █  █ ████  ███ █  █ ███
#  CHECKS: the automatic checks and the score (0 to 100).
#
#  What is checked: shared ban lists and your own bans, other accounts on the same address (stored
#  hashed with a salt that never leaves the server), whether the name is a paid account (on
#  offline-mode servers), whether the account has its own skin, how much the player wrote, and for
#  a linked Discord account its age and membership.
#  What is NOT checked: Minecraft account age and old names. Mojang does not publish either.
checks:
  base: 50
  points:
    on-ban-list: -100
    banned-here: -100
    address-banned: -50
    address-denied: -15
    address-shared: -5
    paid-account: 10
    not-paid: -20
    own-skin: 5
    default-skin: -5
    good-answers: 10
    poor-answers: -10
    discord-linked: 10
    discord-old: 10
    discord-new: -15
    discord-member: 5
    discord-not-member: -5
    discord-missing: -20
  answers:
    # Characters written across the text questions.
    good-length: 120
    poor-length: 40
  discord:
    # A Discord account at least this many days old counts as established...
    min-days: 30
    # ...and one younger than this counts as new.
    new-days: 7
  address:
    # Needs the players' real addresses: behind Velocity or BungeeCord, turn on IP forwarding.
    # Loopback and LAN addresses (a proxy without forwarding) are never compared.
    enabled: true
  # Deny anyone on a ban list straight away (they can still be accepted by hand later).
  auto-deny-ban-list: true
  # Accept automatically at or above this score. 0: never, staff decide.
  auto-accept-at: 0
  # Deny automatically below this score. 0: never.
  auto-deny-below: 0

# ============================================================================
#  ███   ██  █  █    █    ███  ███ ███
#  █  █ █  █ ██ █    █     █  █     █
#  ███  ████ █ ██    █     █   ██   █
#  █  █ █  █ █  █    █     █     █  █
#  ███  █  █ █  █    ████ ███ ███   █
#  BAN LIST: players already banned elsewhere. One per line: a UUID or name, optionally " | reason".
banlist:
  # In plugins/SmartWhitelist/.
  file: banlist.txt
  # Plain-text lists in the same format, fetched in the background (for example a list your
  # network shares between servers).
  urls: []
  refresh-minutes: 60

# ============================================================================
#  █   █  ██  █  █ ████  ███
#  █   █ █  █ █  █ █    █
#  █ █ █ ████ █  █ ███   ██
#  ██ ██ █  █  ██  █       █
#  █   █ █  █  █   ████ ███
#  WAVES: accepted players join in groups instead of all at once (good for a season launch).
waves:
  enabled: false
  # How many players each wave lets in.
  size: 10
  # Minutes between waves. /swl wave runs one now.
  every-minutes: 60
  announce: true

# ============================================================================
#  █   █ ████ ███     ████  ██  ███  █   █
#  █   █ █    █  █    █    █  █ █  █ ██ ██
#  █ █ █ ███  ███     ███  █  █ ███  █ █ █
#  ██ ██ █    █  █    █    █  █ █ █  █   █
#  █   █ ████ ███     █     ██  █  █ █   █
#  WEB FORM: a small application page served by the plugin (plain HTTP). For HTTPS put it behind
#  the reverse proxy of your website and set public-url to that address.
web:
  enabled: false
  bind: "0.0.0.0"
  port: 8765
  # The address players open, without a trailing slash. Shown in the kick message.
  public-url: "http://play.example.com:8765"
  server-name: "our server"
  max-forms-per-hour: 6
  # Read the visitor's address from X-Forwarded-For (only behind your own reverse proxy).
  trust-proxy-header: false

# ============================================================================
#  ███  ███  ███  ███  ██  ███  ███
#  █  █  █  █    █    █  █ █  █ █  █
#  █  █  █   ██  █    █  █ ███  █  █
#  █  █  █     █ █    █  █ █ █  █  █
#  ███  ███ ███   ███  ██  █  █ ███
#  DISCORD: optional. off, webhook (posts only) or bot (posts with Accept/Deny buttons, /apply).
discord:
  # Quoted on purpose: an unquoted off is read by YAML as false.
  mode: "off"
  # Shown in the kick message so players know where to apply.
  invite: "https://discord.gg/your-invite"
  webhook-url: ""
  webhook-name: "SmartWhitelist"
  # A picture of the player in the post. {uuid} and {name} are filled in. Empty: no picture.
  avatar-url: "https://mc-heads.net/avatar/{uuid}/64"
  # true: an application without a linked Discord account loses points (bot mode only).
  require-link: false
  default-deny-reason: "Denied on Discord."
  bot:
    # Your own bot's token (discord.com/developers). It needs no privileged intents.
    token: ""
    guild-id: ""
    # The channel applications are posted in.
    channel-id: ""
    # Members with this role (or Manage Server) can press Accept and Deny.
    staff-role-id: ""

# ============================================================================
#  ███ █  █ ████ █   █ ████
#   █  █  █ █    ██ ██ █
#   █  ████ ███  █ █ █ ███
#   █  █  █ █    █   █ █
#   █  █  █ ████ █   █ ████
#  THEME: the colours every message and screen uses.
theme:
  accent: "#4fd1a5"
  accent2: "#a7f0d6"
  title: "#f2fbf7"
  text: "#c4d6cf"
  muted: "#6e8a80"
  value: "#ffffff"
  good: "#57e08a"
  warn: "#ffc857"
  bad: "#ff6b6b"
  sounds: true
  item-icons: true
  small-caps: false

menu:
  width: 320
  body-width: 360

# ============================================================================
#  █   █ ████  ███  ███  ██   ███ ████  ███
#  ██ ██ █    █    █    █  █ █    █    █
#  █ █ █ ███   ██   ██  ████ █ ██ ███   ██
#  █   █ █       █    █ █  █ █  █ █       █
#  █   █ ████ ███  ███  █  █  ███ ████ ███
#  MESSAGES: MiniMessage with the theme tags <accent> <text> <muted> <value> <good> <warn> <bad>.
#  An empty message is not sent at all.
messages:
  prefix: "<accent>Whitelist</accent> <muted>»</muted> "

  # ---- turned away at the door (kick mode) and the waiting room (limbo mode)
  kick-apply-web: "<title>This server is whitelisted.</title><newline><newline><text>Apply here:</text> <accent>{url}</accent><newline><text>Your code:</text> <value>{code}</value><newline><newline><muted>The code works for one hour.</muted>"
  kick-apply-discord: "<title>This server is whitelisted.</title><newline><newline><text>Join our Discord and type</text> <value>/apply {code}</value><newline><accent>{invite}</accent><newline><newline><muted>The code works for one hour.</muted>"
  kick-apply-other: "<title>This server is whitelisted.</title><newline><newline><text>Ask staff on our Discord to be let in:</text><newline><accent>{invite}</accent>"
  kick-pending: "<title>Your application is being reviewed.</title><newline><newline><text>Application</text> <value>#{id}</value><text>. You will be able to join as soon as staff accept it.</text>"
  kick-queued: "<title>You are accepted.</title><newline><newline><text>You are</text> <value>#{position}</value> <text>in the queue. Next group joins in</text> <value>{next}</value><text>.</text>"
  kick-denied: "<title>Your application was not accepted.</title><newline><newline><text>{reason}</text><newline><newline><muted>You can apply again in {again}.</muted>"
  limbo-welcome: "<text>Fill in the form to apply. Closed it? Type</text> <accent>/apply</accent><text>.</text>"
  limbo-no-chat: "<warn>Chat opens once your application is accepted.</warn>"
  limbo-no-commands: "<warn>Only</warn> <accent>/apply</accent> <warn>works while you wait.</warn>"

  # ---- the form
  form-title: "<title>Apply to {server}</title>"
  form-rules-head: "<accent>Rules</accent>"
  form-rules-row: "<muted>•</muted> <text>{rule}</text>"
  form-error: "<bad>{error}</bad>"
  btn-send: "<white><icon:apply></white><accent>Send application</accent>"
  btn-send-tip: "<text>Staff will see your answers.</text>"
  btn-later: "<text>Later</text>"
  invalid-required: "Please answer: {question}"
  invalid-required-toggle: "Please tick: {question}"
  invalid-short: "That answer is too short: {question}"
  invalid-long: "That answer is too long: {question}"
  invalid-choice: "Pick one of the options: {question}"
  form-sent: "<good>Application #{id} sent.</good> <text>You will hear back here.</text>"
  already-in: "<text>You are already on the whitelist.</text>"
  cooldown: "<warn>You can apply again in {again}.</warn>"
  status-title: "<title>Your application</title>"
  status-pending: "<text>Application</text> <value>#{id}</value> <text>is waiting for staff (sent {when} ago).</text>"
  status-queued: "<good>You are accepted.</good> <text>You are</text> <value>#{position}</value> <text>in the queue; the next group joins in</text> <value>{next}</value><text>.</text>"
  status-hint: "<muted>You can close this. Your place is kept.</muted>"
  you-accepted: "<good>You are accepted. Welcome!</good>"
  you-queued: "<good>You are accepted.</good> <text>You will be let in with the next group.</text>"
  waves-manual: "when staff open it"

  # ---- staff: notices
  staff-new: "<text>New application from</text> <value>{name}</value> <muted>(#{id}, score {score})</muted><text>.</text> <accent><click:run_command:'/swl review #{id}'>[review]</click></accent>"
  staff-accepted: "<value>{name}</value> <text>was accepted by</text> <value>{by}</value><text>.</text>"
  staff-queued: "<value>{name}</value> <text>was accepted by</text> <value>{by}</value> <text>and waits for the next wave.</text>"
  staff-denied: "<value>{name}</value> <text>was denied by</text> <value>{by}</value><text>.</text>"
  wave-broadcast: "<good>Welcome to the server:</good> <value>{names}</value> <muted>({count})</muted>"

  # ---- staff: the desk
  home-title: "<title>Applications</title>"
  home-counts: "<value>{pending}</value> <text>waiting</text> <muted>·</muted> <value>{queued}</value> <text>queued</text> <muted>·</muted> <good>{accepted}</good> <text>accepted and</text> <bad>{denied}</bad> <text>denied today</text>"
  home-gate: "<muted>Gate</muted> <value>{mode}</value> <muted>· web form</muted> <value>{web}</value> <muted>· Discord</muted> <value>{discord}</value> <muted>· ban list</muted> <value>{bans}</value> <muted>· waiting room</muted> <value>{waiting}</value>"
  home-waves: "<muted>Waves of</muted> <value>{size}</value><muted>, next in</muted> <value>{next}</value>"
  word-on: "on"
  word-off: "off"
  word-yes: "yes"
  word-no: "no"
  btn-pending: "<white><icon:pending></white><accent>Waiting</accent> <value>{count}</value>"
  btn-pending-tip: "<text>Applications staff have not decided yet.</text>"
  btn-queue: "<white><icon:queue></white><accent>Queue</accent> <value>{count}</value>"
  btn-queue-tip: "<text>Accepted players waiting for the next wave.</text>"
  btn-log: "<white><icon:log></white><accent>Audit log</accent>"
  btn-log-tip: "<text>Every application, decision and note, newest first.</text>"
  btn-banlist: "<white><icon:ban></white><accent>Reload ban list</accent>"
  btn-banlist-tip: "<text>Reads banlist.txt and every URL again.</text>"
  btn-help: "<white><icon:help></white><accent>Help</accent>"
  btn-help-tip: "<text>Commands and how the gate works.</text>"
  btn-prev: "<white><icon:back></white><text>Previous</text>"
  btn-prev-tip: "<text>The page before.</text>"
  btn-next: "<white><icon:next></white><text>Next</text>"
  btn-next-tip: "<text>The page after.</text>"
  btn-back: "<white><icon:back></white><text>Back</text>"
  btn-back-tip: "<text>One screen back.</text>"
  btn-close: "<text>Close</text>"
  banlist-reloaded: "<text>Ban list reloaded:</text> <value>{count}</value> <text>entries.</text>"
  pending-title: "<title>Waiting for a decision</title>"
  pending-empty: "<text>Nothing is waiting. New applications show up here and in chat.</text>"
  pending-head: "<value>{count}</value> <text>waiting, oldest first</text> <muted>(page {page} of {pages})</muted>"
  pending-row: "<value>{name}</value> {bar} <muted>{score}</muted>{flag}"
  pending-flag-ban: " <bad>ban</bad>"
  pending-row-tip: "<text>Sent {when} ago via {source}.</text>"
  review-title: "<title>{name}</title>"
  review-head: "<value>{name}</value> <muted>#{id} · via {source} · {when} ago ·</muted> {status}"
  status-word-pending: "<warn>waiting</warn>"
  status-word-accepted: "<good>accepted</good>"
  status-word-queued: "<good>queued</good>"
  status-word-denied: "<bad>denied</bad>"
  review-score: "<text>Score</text> <value>{score}</value> {bar}"
  review-checks-head: "<accent>Checks</accent>"
  review-check-row: "<{tag}>{delta}</{tag}> <text>{label}</text>"
  review-answer: "<accent>{question}</accent><newline><text>{answer}</text>"
  review-notes-head: "<accent>Staff notes</accent>"
  review-note-row: "<value>{author}</value> <muted>{when} ago:</muted> <text>{text}</text>"
  review-decided: "<muted>Decided by</muted> <value>{by}</value><muted>.</muted> <text>{reason}</text>"
  btn-accept: "<white><icon:accept></white><good>Accept</good>"
  btn-accept-tip: "<text>Adds them to the whitelist now.</text>"
  btn-accept-tip-waves: "<text>Puts them in the queue for the next wave.</text>"
  btn-deny: "<white><icon:deny></white><bad>Deny</bad>"
  btn-deny-tip: "<text>Asks for a reason first.</text>"
  btn-note: "<white><icon:note></white><text>Add a note</text>"
  btn-note-tip: "<text>Only staff see notes.</text>"
  already-decided: "<warn>{name} was already decided by someone else.</warn>"
  you-accepted-staff: "<good>Accepted</good> <value>{name}</value><text>.</text>"
  you-queued-staff: "<good>Accepted</good> <value>{name}</value><text>; they join with the next wave.</text>"
  you-denied-staff: "<bad>Denied</bad> <value>{name}</value><text>.</text>"
  deny-title: "<title>Deny {name}</title>"
  deny-body: "<text>The reason is shown to the player. They can apply again after</text> <value>{cooldown}</value> <text>hours.</text>"
  deny-reason-label: "<text>Reason</text>"
  btn-deny-confirm: "<white><icon:deny></white><bad>Deny</bad>"
  btn-deny-confirm-tip: "<text>Sends the reason and kicks them from the waiting room.</text>"
  note-title: "<title>Note on {name}</title>"
  note-label: "<text>Note (only staff see it)</text>"
  btn-note-save: "<white><icon:note></white><accent>Save note</accent>"
  btn-note-save-tip: "<text>Added to the application and the audit log.</text>"
  note-saved: "<text>Note saved on</text> <value>{name}</value><text>.</text>"
  queue-title: "<title>Queue</title>"
  queue-waves-off: "<muted>Waves are off, so accepted players get in straight away. Turn waves.enabled on to queue them.</muted>"
  queue-empty: "<text>Nobody is queued.</text>"
  queue-head: "<value>{count}</value> <text>queued; next wave in</text> <value>{next}</value>"
  queue-row: "<muted>{pos}.</muted> <value>{name}</value> <muted>accepted {when} ago</muted>"
  queue-more: "<muted>and {count} more</muted>"
  btn-wave-now: "<white><icon:wave></white><accent>Let in the next {size}</accent>"
  btn-wave-now-tip: "<text>Runs the next wave right now.</text>"
  wave-done: "<text>Wave done:</text> <value>{count}</value> <text>players whitelisted.</text>"
  log-title: "<title>Audit log</title>"
  log-head: "<accent>Newest first</accent>"
  log-empty: "<text>Nothing yet.</text>"
  log-row: "<muted>{when} ago</muted> <value>{actor}</value> <accent>{action}</accent> <value>{target}</value> <muted>{detail}</muted>"
  help-title: "<title>SmartWhitelist</title>"
  help-lines:
    - "<accent>/swl</accent> <text>opens this desk.</text>"
    - "<accent>/swl review</accent> <value>[name|#id]</value><text>, </text><accent>accept</accent> <value>[name]</value><text>, </text><accent>deny</accent> <value>[name] [reason]</value><text>, </text><accent>note</accent> <value>[name] [text]</value>"
    - "<accent>/swl queue</accent><text>,</text> <accent>wave</accent><text>,</text> <accent>log</accent><text>,</text> <accent>banlist</accent><text>,</text> <accent>reload</accent>"
    - "<text>Players apply with</text> <accent>/apply</accent> <text>in the waiting room (limbo mode), on the web page from their kick message, or with /apply on Discord.</text>"
    - "<text>Accepting adds the player to the vanilla whitelist, so keep</text> <value>/whitelist on</value><text>.</text>"

  # ---- staff: errors and the console
  unknown-application: "<warn>No application found.</warn>"
  no-permission: "<bad>You do not have permission for that.</bad>"
  players-only: "<warn>Only players can do that.</warn>"
  console-usage: "<text>Use /swl pending, review, accept, deny, note, queue, wave, log, banlist or reload.</text>"
  console-pending: "<value>{count}</value> <text>waiting:</text>"
  console-row: "<muted>#{id}</muted> <value>{name}</value> <text>score</text> <value>{score}</value> <muted>{flags}</muted>"
  console-queue: "<value>{count}</value> <text>queued, next wave in</text> <value>{next}</value>"
  usage-review: "<warn>/swl review [name|#id]</warn>"
  usage-accept: "<warn>/swl accept [name|#id]</warn>"
  usage-deny: "<warn>/swl deny [name|#id] [reason]</warn>"
  usage-note: "<warn>/swl note [name|#id] [text]</warn>"
  reloaded: "<good>Reloaded.</good>"
  unknown-sub: "<warn>Unknown. Try: {subs}</warn>"

  # ---- the checks (shown to staff)
  check-banlist: "On a shared ban list ({where})"
  check-local-ban: "Banned on this server"
  check-address-banned: "Same connection as {name}, who is banned"
  check-address-denied: "Same connection as {name}, who was denied"
  check-address-shared: "Shares a connection with {count} other account(s)"
  check-paid-online: "Paid Minecraft account (online mode)"
  check-paid-unknown: "Could not reach Mojang to check the account"
  check-paid-yes: "Paid Minecraft account"
  check-paid-no: "Not a paid Minecraft account"
  check-skin-yes: "Has their own skin"
  check-skin-no: "Default skin"
  check-answers-good: "Wrote a proper application ({chars} characters)"
  check-answers-poor: "Very short answers ({chars} characters)"
  check-discord-linked: "Discord account linked"
  check-discord-old: "Discord account is {days} days old"
  check-discord-new: "Discord account is only {days} days old"
  check-discord-age: "Discord account is {days} days old"
  check-discord-member: "In your Discord server"
  check-discord-not-member: "Not in your Discord server"
  check-discord-missing: "No Discord account linked"
  reason-ban-list: "You are on a ban list this server uses."
  reason-low-score: "Your application did not meet our requirements."

  # ---- Discord (plain text: Discord shows no colours)
  discord-new-title: "New application: {name} (#{id})"
  discord-new-score: "Score {score} of 100, sent via {source}"
  discord-checks: "Checks"
  discord-review-in-game: "Review in game: /swl review {name}"
  discord-review-buttons: "Accept or deny below, or in game with /swl"
  discord-accepted: "{name} (#{id}) was accepted by {by}."
  discord-queued: "{name} (#{id}) was accepted by {by} and joins with the next wave."
  discord-denied: "{name} (#{id}) was denied by {by}. {reason}"
  discord-cmd-description: "Apply to the Minecraft server with the code from your kick message"
  discord-cmd-code: "The code from your kick message"
  discord-btn-accept: "Accept"
  discord-btn-deny: "Deny"
  discord-btn-accepted: "Accepted by {by}"
  discord-btn-denied: "Denied by {by}"
  discord-code-unknown: "That code is unknown or has run out. Join the Minecraft server once more for a new one."
  discord-linked-pending: "Your Discord account is now linked to {name}. Your application is already with staff."
  discord-error: "Something went wrong on our side. Please try again in a minute."
  discord-linked-web: "Linked to {name}. Fill in the application here: {url}"
  discord-linked-game: "Linked to {name}. Join the Minecraft server and type /apply to fill in the form."
  discord-form-title: "Application"
  discord-toggle-hint: "Type yes"
  discord-sent: "Application #{id} for {name} is sent. You will hear back here and in game."
  discord-already-in: "{name} is already on the whitelist."
  discord-cooldown: "You can apply again in {again}."
  discord-invalid: "{detail}"
  discord-not-staff: "Only staff can decide applications."
  discord-already-decided: "Someone already decided this one."

  # ---- the web page (plain text)
  web-title: "Apply to {server}"
  web-intro: "Enter the code from your kick message and answer a few questions. Staff will look at your application."
  web-code-label: "Your code"
  web-send: "Send application"
  web-code-unknown: "That code is unknown or has run out. Join the server once more for a new one."
  web-sent-title: "Application sent"
  web-sent-body: "Thanks, {name}. Application #{id} is with staff. Try joining again later."
  web-already-title: "Already sent"
  web-already-body: "{name}, your application is already with staff."
  web-in-title: "You are in"
  web-in-body: "{name} is already on the whitelist. Just join."
  web-cooldown-title: "Not yet"
  web-cooldown-body: "You can apply again in {again}."
  web-not-found: "Nothing here."
  web-too-many: "Too many forms from your connection. Try again in an hour."
  web-too-big: "That was too much text."

FAQ

Which servers and versions does it run on?

It needs Paper or a Paper fork such as Purpur, because it uses Paper-only APIs (the dialog API for its menus, Adventure text components, Paper player profiles). Spigot and CraftBukkit are not supported. plugin.yml declares api-version 1.21, the jar is built against Paper API 1.21.11, and it needs Java 21 or newer.

Does it need other plugins?

No other plugin is required (plugin.yml has no hard dependencies). The server downloads org.xerial:sqlite-jdbc:3.53.2.1, net.dv8tion:JDA:6.7.0 at startup, so it needs internet access on first start.

How do I apply config changes?

Edit plugins/SmartWhitelist/config.yml and run /swl reload (read config.yml again). A restart works too.

Does it run on Folia, or for Bedrock players?

Not tested on Folia, and plugin.yml does not declare folia-supported. Bedrock players (through Geyser) have not been tested either.

What licence do I get?

The licence terms are the BuiltByBit EULA shown on the listing. There is no licence key to enter, and the plugin never contacts a licence server.