namespace Aeshnidae.AdminAudit;
///
/// Admin-only, all of them: the audit trail names who did what, and its own status
/// tells you whether anyone would notice if you did something.
///
public static class Commands
{
[CommandHandler("adminaudit", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Audit trail status, tail and search.",
"/adminaudit [ hooks | tail [n] | find [n] | rebind ]")]
public static void HandleAudit(Session session, params string[] parameters)
{
try
{
var verb = parameters.Length > 0 ? parameters[0].ToLowerInvariant() : "status";
switch (verb)
{
case "hooks": Hooks(session); return;
case "rebind": Rebind(session); return;
case "tail": Tail(session, Count(parameters, 1, 20), null); return;
case "find" when parameters.Length > 1:
Tail(session, Count(parameters, 2, 20), parameters[1]);
return;
default: Status(session); return;
}
}
catch (Exception ex)
{
Reply(session, $"/adminaudit failed: {ex.Message}");
ModManager.Log($"[{Mod.Name}] /adminaudit failed: {ex}", ModManager.LogLevel.Error);
}
}
private static void Status(Session? session)
{
var container = Mod.Container;
var audit = Mod.Auditor;
var text = new StringBuilder()
.AppendLine($"{Mod.Name} v{container?.Meta.Version ?? "?"} - {container?.Status.ToString() ?? "not loaded"}");
if (audit is null)
{
text.AppendLine("Not recording. Check the server log for why.");
Reply(session, text.ToString());
return;
}
var uptime = DateTime.UtcNow - Mod.StartedUtc;
text.AppendLine($"Recording since {Mod.StartedUtc.ToLocalTime():yyyy-MM-dd HH:mm:ss} ({uptime.TotalHours:0.#}h), threshold {Mod.Settings.Threshold}")
.AppendLine($"Directory: {audit.Log.Directory}")
.AppendLine($"Recorded {audit.Recorded:N0}, written {audit.Log.Written:N0}, pending {audit.Log.Pending}, dropped {audit.Log.Dropped:N0}")
.AppendLine($"Below threshold, not recorded: {audit.Filtered:N0}");
if (!string.IsNullOrEmpty(audit.Log.LastError))
text.AppendLine($"Last file error: {audit.Log.LastError}");
if (audit.Sinks.Count > 0)
{
foreach (var sink in audit.Sinks)
{
text.AppendLine($"Discord [{sink.Label}]: {(sink.Parked ? "PARKED" : "on")}, posted {sink.Posted:N0}, pending {sink.Pending}, dropped {sink.Dropped:N0}");
if (!string.IsNullOrEmpty(sink.LastError))
text.AppendLine($" last error: {sink.LastError}");
}
}
else
{
text.AppendLine("Discord: off");
}
var bound = SiblingPatches.Status.Count(h => h.Bound);
text.AppendLine($"Sibling hooks: {bound}/{SiblingPatches.Status.Count} bound (/adminaudit hooks)");
Reply(session, text.ToString());
}
private static void Hooks(Session? session)
{
var text = new StringBuilder().AppendLine("Sibling-mod hooks:");
foreach (var hook in SiblingPatches.Status)
text.AppendLine($" {(hook.Bound ? "[ok] " : "[--] ")}{hook.Target} - {hook.Note}");
if (SiblingPatches.Status.Count == 0)
text.AppendLine(" none attempted (AuditSiblingMods is off)");
text.AppendLine("Unbound hooks are normal if that mod is disabled. After /mod find, run /adminaudit rebind.");
Reply(session, text.ToString());
}
private static void Rebind(Session? session)
{
var bound = Mod.Rebind();
Reply(session, $"Rebound sibling hooks: {bound}/{SiblingPatches.Status.Count} bound.");
}
private static void Tail(Session? session, int count, string? filter)
{
var audit = Mod.Auditor;
if (audit is null)
{
Reply(session, "Not recording.");
return;
}
var lines = audit.Log.Tail(count, filter);
if (lines.Count == 0)
{
Reply(session, filter is null ? "Nothing recorded yet." : $"No audit records matching \"{filter}\".");
return;
}
var text = new StringBuilder()
.AppendLine($"Last {lines.Count} audit record(s){(filter is null ? "" : $" matching \"{filter}\"")}:");
foreach (var line in lines)
text.AppendLine(" " + Summarize(line));
Reply(session, text.ToString());
}
/// Turns a stored JSONL line back into the one-line form, falling back to the raw line.
private static string Summarize(string jsonLine)
{
try
{
return JsonSerializer.Deserialize(jsonLine)?.ToLine() ?? jsonLine;
}
catch
{
return jsonLine;
}
}
private static int Count(string[] parameters, int index, int fallback) =>
parameters.Length > index && int.TryParse(parameters[index], out var n) && n is > 0 and <= 200 ? n : fallback;
[CommandHandler("adminaudit-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Restart the audit mod, re-reading Settings.json.",
"/adminaudit-reload")]
public static void HandleReload(Session session, params string[] parameters)
{
var container = Mod.Container;
if (container is null)
{
Reply(session, $"{Mod.Name} is not loaded.");
return;
}
container.Restart();
Reply(session, $"{Mod.Name} restarted.");
}
///
/// The client renders an embedded newline as a music note, so a multi-line
/// message has to go out as one SendMessage per line.
///
private static void Reply(Session? session, string message)
{
if (session?.Player is null)
{
ModManager.Log(message);
return;
}
foreach (var line in (message ?? "").Split('\n'))
{
var text = line.TrimEnd('\r');
if (!string.IsNullOrWhiteSpace(text))
session.Player.SendMessage(text);
}
}
}