- Kotlin 100%
PlayerNegotiationEvent is defined but never fired in NeoForge 1.21.x; use RegisterConfigurationTasksEvent + custom ConfigurationTask instead. terminal outcomes disconnect in config phase (clean disconnect packet through Velocity); NeedsConfirmation stashes for PlayerLoggedInEvent. bump 0.2.0 -> 0.2.2. |
||
|---|---|---|
| .claude | ||
| gradle | ||
| src | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
| TESTING.md | ||
Telegram Auth
NeoForge 1.21.1 server-only mod that gates offline-mode logins through a Telegram bot.
- First join — player is kicked with an 8-character bind code; sending
/start <code>to the bot links the Minecraft account to that Telegram user. - Subsequent join — player is frozen on-spawn; the bot DMs
Login attemptwith Accept / Decline buttons. - Accept trusts the player's IP for a configurable window (default 7 days); reconnects from the same IP within the window pass silently.
- Decline kicks the player and applies a soft-ban (default 30 min) clearable via
/unlockin Telegram. - Telegram unreachable? Trusted-IP joins still pass; untrusted joins are kicked.
Requirements
- NeoForge 1.21.1 (tested against 21.1.230+)
- Kotlin For Forge 5.9.0+ (auto-loaded as required dep)
- JDK 21
- A Telegram bot token from @BotFather
Install
- Drop the jar into your server's
mods/folder. - Make sure
kotlinforforge-neoforgeis also inmods/(or let Forge fetch it). - Start the server once — this generates
config/mc_tg_auth-server.toml. - Edit the config, set
telegram.botTokenandtelegram.serverDisplayName. - Restart.
Configuration (config/mc_tg_auth-server.toml)
[telegram]
botToken = "" # required; from @BotFather
serverDisplayName = "My Server" # shown in Telegram prompts
[session]
durationDays = 7 # how long an accepted IP stays trusted
declineCooldownMinutes = 30 # soft-ban duration after Decline
confirmationTimeoutSeconds = 120
[bind]
codeTtlMinutes = 10
[freeze]
strategy = "inplace" # only 'inplace' implemented in v1
[ratelimit]
bindAttemptsPerTgUserPerMin = 5
joinKickPerIpPerMin = 3
[i18n]
defaultLocale = "en" # en | ru
[whitelist]
enabled = false # optional chat-membership gate
chatId = 0 # telegram chat id; negative for groups/supergroups/channels
cacheTtlSeconds = 300 # cache per-user membership for N seconds (30..3600)
Whitelist mode (optional)
When whitelist.enabled = true and whitelist.chatId is set, every join checks that the bound Telegram account is still a member of that chat (getChatMember). Non-members are kicked; new binds from non-members are refused. The check supersedes the trusted-IP shortcut, so removing someone from the chat kicks them on next join. Telegram API errors fail open. The bot must be a member of the chat (admin recommended) for getChatMember to work.
Bot DM commands
| Command | Action |
|---|---|
/start [code] |
Bind a new MC account (with code) or list bindings |
/accounts |
List bound MC names + active sessions |
/unbind <name> |
Remove a binding |
/revoke [name] |
Drop trusted IPs (one or all) |
/unlock [name] |
Clear soft-ban (one or all) |
/help |
Command list |
Bare bind codes also work — paste the 8-char code without /start.
Admin console commands (op level 3)
| Command | Action |
|---|---|
/tgauth status |
Bot reachability, stats |
/tgauth info <player> |
Show binding |
/tgauth unbind <player> |
Force-unbind |
/tgauth revoke <player> |
Wipe their sessions |
/tgauth unlock <player> |
Clear their soft-ban |
/tgauth force-login <player> |
Unfreeze a stuck player |
/tgauth reload |
Notice — config hot-reloads; restart needed only for token change |
Freeze coverage
While awaiting auth, the player is fully restricted:
- Movement: walking/flying speed forced to 0; position snapped back on drift; Slowness 255 + Blindness + neg-Jump mob effects applied client-side.
- Damage:
isInvulnerable = trueplusLivingDamageEvent.Prezeroes incoming damage; healing blocked too. - Inventory: containers auto-close on open; items can't be dropped (returned to inventory) or picked up; XP orbs ignored.
- Interactions: left/right click on blocks, items, entities — all cancelled. Attack-entity blocked.
- World: block break/place cancelled. Mounting horses/boats blocked.
- Communication: chat and commands cancelled.
i18n
Lang files at assets/mc_tg_auth/lang/en_us.json and ru_ru.json. Every user-facing string — kick screens, bot replies, in-game chat — is keyed and editable. Format is MiniMessage; in Telegram messages the <b>/<i>/<code> tags pass through as Telegram HTML and color tags are stripped.
Build
./gradlew build
Produces build/libs/mc_tg_auth-<version>.jar.
Set MOD_VERSION_SUFFIX to append a tag to the jar version (e.g. for testing builds):
MOD_VERSION_SUFFIX=dirty+$(git rev-parse --short=8 HEAD) ./gradlew build
Tests
./gradlew test
Covers: Clock, BindCodeGenerator, RateLimiter, PendingState, AuthService decision table, AuthRepository against real SQLite, inbound command handlers, callback handler, Messages/i18n, plus an end-to-end integration test (bind → confirm → trust IP) against real SQLite.
Dev server
./gradlew runServer
Starts a dedicated NeoForge dev server in run/ with the mod loaded. Edit run/config/mc_tg_auth-server.toml to add a bot token, then connect with a 1.21.1 client to localhost:25565.
Architecture
PlayerGate (NeoForge events)
↓
AuthService (pure domain, sealed AuthOutcome)
↓
AuthRepository (raw JDBC + SQLite) TelegramAdapter (JDK HttpClient + Bot API)
- PlayerGate owns the freeze state machine and applies kicks/unfreezes on the MC main thread.
- AuthService is pure Kotlin; takes UUID + name + IP, returns one of
Trusted/NeedsBinding/NeedsConfirmation/SoftBanned/BotUnreachable/TelegramBlocked. Heavily tested. - AuthRepository uses raw
java.sqlagainstsqlite-jdbc. No ORM — keeps the runtime classpath narrow and avoids JPMS conflicts with KFF. - TelegramAdapter speaks the Bot API directly via
java.net.http.HttpClient+kotlinx-serialization-json(provided by KFF). Long-polling at 25s.
License
MIT