kimi gone wild
This commit is contained in:
250
lib/odinsea/anticheat/autoban_manager.ex
Normal file
250
lib/odinsea/anticheat/autoban_manager.ex
Normal 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
|
||||
Reference in New Issue
Block a user