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}"; } }