namespace Aeshnidae.AdminAudit;
/// What kind of privileged action a record describes.
public enum AuditKind
{
/// A privileged command was accepted for execution, or refused.
Command,
/// ACE narrated an admin action on its own audit channel (delete, smite, teleport, server property...).
Narrated,
/// An object was conjured into someone's inventory while a privileged command was running.
ItemCreated,
/// A privileged character dropped an object on the ground.
ItemDropped,
/// A privileged character handed an object to someone.
ItemGiven,
/// XP or luminance granted by command (XpType.Admin).
Grant,
/// Aeshnidae.Bank deposit or withdrawal.
Bank,
/// Aeshnidae.XpCurrency transfer between players.
XpTransfer,
/// Aeshnidae.InstancesNoDat created a landblock copy.
Instance,
}
///
/// One line of the audit trail.
///
/// Serialised as a single JSON object per line (JSONL): appendable without rewriting,
/// greppable with plain tools, and still machine-readable. Property names are short
/// and lowercase because a busy server writes a lot of these and they are read far
/// more often with grep than with a parser.
///
public sealed class AuditEvent
{
[JsonPropertyName("ts")]
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("O");
[JsonPropertyName("kind")]
public string Kind { get; set; } = "";
/// Character name, or "CONSOLE" for a server-console command.
[JsonPropertyName("actor")]
public string Actor { get; set; } = "";
/// Account the actor was logged in under. The character can be renamed; the account is the real identity.
[JsonPropertyName("account")]
public string? Account { get; set; }
[JsonPropertyName("access")]
public string? Access { get; set; }
/// "ingame" or "console".
[JsonPropertyName("source")]
public string Source { get; set; } = "ingame";
/// Command name, or a short verb for non-command events.
[JsonPropertyName("action")]
public string Action { get; set; } = "";
/// Human-readable summary - this is the line you actually read when scanning.
[JsonPropertyName("detail")]
public string Detail { get; set; } = "";
/// Who or what the action was aimed at, when there is one.
[JsonPropertyName("target")]
public string? Target { get; set; }
/// "ok", "denied", or a CommandHandlerResponse name.
[JsonPropertyName("outcome")]
public string? Outcome { get; set; }
/// Where the actor was standing, as a LOC string.
[JsonPropertyName("loc")]
public string? Location { get; set; }
///
/// True for commands that only look at things. Kept rather than dropped, so the
/// record of an admin probing around before acting survives, but the Discord feed
/// can skip them.
///
[JsonPropertyName("readonly")]
public bool ReadOnly { get; set; }
/// The command this event happened underneath, when it was caused by one.
[JsonPropertyName("via")]
public string? Via { get; set; }
/// Kind-specific extras: amounts, guids, weenie ids.
[JsonPropertyName("data")]
public Dictionary? Data { get; set; }
public AuditEvent With(string key, object? value)
{
if (value is null)
return this;
(Data ??= new())[key] = value.ToString() ?? "";
return this;
}
/// The one-line form used for the console, the Discord feed and /adminaudit tail.
public string ToLine()
{
var time = DateTime.TryParse(Timestamp, null, DateTimeStyles.RoundtripKind, out var parsed)
? parsed.ToLocalTime().ToString("HH:mm:ss")
: Timestamp;
var who = string.IsNullOrEmpty(Account) ? Actor : $"{Actor} ({Account})";
var outcome = Outcome is null or "ok" ? "" : $" [{Outcome}]";
return $"{time} {who}: {Detail}{outcome}";
}
}