kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View File

@@ -0,0 +1,250 @@
defmodule Odinsea.AntiCheat.AutobanManager do
@moduledoc """
Autoban manager for handling automatic bans based on accumulated points.
Ported from: server.AutobanManager.java
This module:
- Accumulates anti-cheat points per account
- Triggers autoban when threshold is reached (5000 points)
- Handles point expiration over time
- Broadcasts ban notifications
- Tracks ban reasons
## Architecture
The AutobanManager is a singleton GenServer that:
- Stores points per account in its state
- Tracks expiration entries for automatic point decay
- Provides async ban operations
"""
use GenServer
require Logger
alias Odinsea.Database.Context
# Autoban threshold - 5000 points triggers automatic ban
@autoban_points 5000
# How often to check for point expiration (ms)
@expiration_check_interval 30_000
# =============================================================================
# Public API
# =============================================================================
@doc """
Starts the AutobanManager.
"""
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Adds points to an account for a cheating offense.
"""
def add_points(account_id, points, expiration, reason) do
GenServer.call(__MODULE__, {:add_points, account_id, points, expiration, reason})
end
@doc """
Immediately autobans an account.
"""
def autoban(account_id, reason) do
# Add maximum points to trigger ban
add_points(account_id, @autoban_points, 0, reason)
end
@doc """
Gets current points for an account.
"""
def get_points(account_id) do
GenServer.call(__MODULE__, {:get_points, account_id})
end
@doc """
Gets ban reasons for an account.
"""
def get_reasons(account_id) do
GenServer.call(__MODULE__, {:get_reasons, account_id})
end
@doc """
Clears points for an account (e.g., after manual review).
"""
def clear_points(account_id) do
GenServer.call(__MODULE__, {:clear_points, account_id})
end
# =============================================================================
# GenServer Callbacks
# =============================================================================
@impl true
def init(_) do
# Schedule expiration checks
schedule_expiration_check()
{:ok, %{
# Map of account_id => current_points
points: %{},
# Map of account_id => [reasons]
reasons: %{},
# List of expiration entries: %{time: timestamp, account_id: id, points: points}
expirations: []
}}
end
@impl true
def handle_call({:add_points, account_id, points, expiration, reason}, _from, state) do
# Get current points
current_points = Map.get(state.points, account_id, 0)
# Check if already banned
if current_points >= @autoban_points do
{:reply, :already_banned, state}
else
# Add points
new_points = current_points + points
# Add reason
current_reasons = Map.get(state.reasons, account_id, [])
new_reasons = [reason | current_reasons]
# Update state
new_state = %{state |
points: Map.put(state.points, account_id, new_points),
reasons: Map.put(state.reasons, account_id, new_reasons)
}
# Add expiration entry if expiration > 0
new_state = if expiration > 0 do
expiration_time = System.system_time(:millisecond) + expiration
entry = %{time: expiration_time, account_id: account_id, points: points}
%{new_state | expirations: [entry | state.expirations]}
else
new_state
end
# Check if autoban threshold reached
new_state = if new_points >= @autoban_points do
execute_autoban(account_id, new_reasons, reason)
new_state
else
new_state
end
{:reply, :ok, new_state}
end
end
@impl true
def handle_call({:get_points, account_id}, _from, state) do
points = Map.get(state.points, account_id, 0)
{:reply, points, state}
end
@impl true
def handle_call({:get_reasons, account_id}, _from, state) do
reasons = Map.get(state.reasons, account_id, [])
{:reply, reasons, state}
end
@impl true
def handle_call({:clear_points, account_id}, _from, state) do
new_state = %{state |
points: Map.delete(state.points, account_id),
reasons: Map.delete(state.reasons, account_id),
expirations: Enum.reject(state.expirations, &(&1.account_id == account_id))
}
{:reply, :ok, new_state}
end
@impl true
def handle_info(:check_expirations, state) do
now = System.system_time(:millisecond)
# Process expirations that are due
{expired, remaining} = Enum.split_with(state.expirations, &(&1.time <= now))
# Decrement points for expired entries
new_points = Enum.reduce(expired, state.points, fn entry, acc ->
current = Map.get(acc, entry.account_id, 0)
new_amount = max(0, current - entry.points)
Map.put(acc, entry.account_id, new_amount)
end)
# Schedule next check
schedule_expiration_check()
{:noreply, %{state | points: new_points, expirations: remaining}}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
# =============================================================================
# Private Functions
# =============================================================================
defp schedule_expiration_check do
Process.send_after(self(), :check_expirations, @expiration_check_interval)
end
defp execute_autoban(account_id, all_reasons, last_reason) do
Logger.warning("[AutobanManager] Executing autoban for account #{account_id}")
# Build ban reason
reason_string =
all_reasons
|> Enum.reverse()
|> Enum.join(", ")
full_reason = "Autoban: #{reason_string} (Last: #{last_reason})"
# Get character info if available
# Note: This is simplified - in production, you'd look up the active character
# Ban the account
case Context.ban_account(account_id, full_reason, false) do
{:ok, _} ->
Logger.info("[AutobanManager] Account #{account_id} banned successfully")
# Broadcast to all channels
broadcast_ban_notification(account_id, last_reason)
# Disconnect any active sessions
disconnect_sessions(account_id)
{:error, reason} ->
Logger.error("[AutobanManager] Failed to ban account #{account_id}: #{inspect(reason)}")
end
end
defp broadcast_ban_notification(account_id, reason) do
# Build notification message
message = "[Autoban] Account #{account_id} was banned (Reason: #{reason})"
# TODO: Broadcast to all channels
# This would typically go through the World service
Logger.info("[AutobanManager] Broadcast: #{message}")
# TODO: Send to Discord if configured
# DiscordClient.send_message_admin(message)
:ok
end
defp disconnect_sessions(account_id) do
# TODO: Find and disconnect all active sessions for this account
# This would look up sessions in the Client registry
Logger.info("[AutobanManager] Disconnecting sessions for account #{account_id}")
:ok
end
end