using System.Text.Json;
namespace ClawdDotNet.Tools.RocketChat;
///
/// Die Tool-Konfiguration eines Agenten, aus dem freien Wörterbuch gelesen.
///
/// Die Werte kommen je nach Weg als (frisch deserialisiert) oder
/// als einfache .NET-Werte (nach einer Bearbeitung in der Oberfläche) an — deshalb liest
/// jeder Zugriff über die Helfer unten, nie über eine direkte Umwandlung.
///
internal sealed record RocketChatConfig(
string BaseUrl,
string UserId,
string AuthToken,
string? Username,
IReadOnlyList AllowedRooms,
string? DefaultRoom,
bool MentionOnly,
IReadOnlyList AgentUsernames,
int MaxWakesPerRoomPerHour,
int MaxMessagesPerRoom,
TimeSpan InitialLookback,
long MaxUploadBytes,
long MaxDownloadBytes,
bool AllowDangerousDownloads)
{
///
/// Liest die Konfiguration. Gibt eine sprechende Fehlermeldung zurück, statt mit einer
/// Ausnahme zu scheitern — das Modell soll aus dem Ergebnis erkennen, was fehlt.
///
public static (RocketChatConfig? Config, string? Error) Read(
IReadOnlyDictionary raw)
{
var baseUrl = GetString(raw, "baseUrl")?.TrimEnd('/');
if (string.IsNullOrWhiteSpace(baseUrl))
return (null, "Configuration error: 'baseUrl' is missing (e.g. \"https://chat.example.org\").");
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp))
return (null, $"Configuration error: 'baseUrl' is not a valid http(s) address: {baseUrl}");
var userId = GetString(raw, "userId");
if (string.IsNullOrWhiteSpace(userId))
return (null, "Configuration error: 'userId' is missing (the Rocket.Chat user ID of this agent).");
var authToken = GetString(raw, "authToken");
if (string.IsNullOrWhiteSpace(authToken))
return (null, "Configuration error: 'authToken' is missing (access token of the agent user).");
return (new RocketChatConfig(
BaseUrl: baseUrl,
UserId: userId,
AuthToken: authToken,
Username: GetString(raw, "username"),
AllowedRooms: GetStringList(raw, "allowedRooms"),
DefaultRoom: GetString(raw, "defaultRoom"),
MentionOnly: GetBool(raw, "mentionOnly", defaultValue: true),
AgentUsernames: GetStringList(raw, "agentUsernames"),
MaxWakesPerRoomPerHour: GetInt(raw, "maxWakesPerRoomPerHour", defaultValue: 12),
MaxMessagesPerRoom: Math.Clamp(GetInt(raw, "maxMessagesPerRoom", defaultValue: 20), 1, 100),
InitialLookback: TimeSpan.FromMinutes(
Math.Clamp(GetInt(raw, "initialLookbackMinutes", defaultValue: 5), 1, 1440)),
MaxUploadBytes: Math.Clamp(
GetInt(raw, "maxUploadMb", defaultValue: 25), 1, 100) * 1024L * 1024L,
MaxDownloadBytes: Math.Clamp(
GetInt(raw, "maxDownloadMb", defaultValue: 25), 1, 100) * 1024L * 1024L,
AllowDangerousDownloads: GetBool(raw, "allowDangerousDownloads", defaultValue: false)),
null);
}
///
/// Prüft, ob ein Raum benutzt werden darf. Eine leere Liste erlaubt jeden Raum — wer
/// keine Allowlist pflegt, hat sich dagegen entschieden; das Tool erfindet keine.
/// Verglichen wird gegen Raum-Id und Raumname, weil das Modell beides kennt.
///
public bool IsRoomAllowed(string roomIdOrName)
{
if (AllowedRooms.Count == 0)
return true;
return AllowedRooms.Any(r =>
string.Equals(r, roomIdOrName, StringComparison.OrdinalIgnoreCase));
}
public bool IsAgentUser(string? username)
=> username is not null
&& AgentUsernames.Any(a => string.Equals(a, username, StringComparison.OrdinalIgnoreCase));
// ─── Lesehelfer für das freie Wörterbuch ───
private static string? GetString(IReadOnlyDictionary raw, string key)
=> raw.TryGetValue(key, out var v) ? AsString(v) : null;
private static string? AsString(object? value) => value switch
{
null => null,
string s => string.IsNullOrWhiteSpace(s) ? null : s,
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
JsonElement je => je.ToString(),
_ => value.ToString()
};
private static bool GetBool(IReadOnlyDictionary raw, string key, bool defaultValue)
{
if (!raw.TryGetValue(key, out var v) || v is null)
return defaultValue;
return v switch
{
bool b => b,
JsonElement { ValueKind: JsonValueKind.True } => true,
JsonElement { ValueKind: JsonValueKind.False } => false,
_ => bool.TryParse(AsString(v), out var parsed) ? parsed : defaultValue
};
}
private static int GetInt(IReadOnlyDictionary raw, string key, int defaultValue)
{
if (!raw.TryGetValue(key, out var v) || v is null)
return defaultValue;
return v switch
{
int i => i,
long l => (int)l,
JsonElement { ValueKind: JsonValueKind.Number } je when je.TryGetInt32(out var n) => n,
_ => int.TryParse(AsString(v), out var parsed) ? parsed : defaultValue
};
}
private static IReadOnlyList GetStringList(
IReadOnlyDictionary raw, string key)
{
if (!raw.TryGetValue(key, out var v) || v is null)
return [];
return v switch
{
JsonElement { ValueKind: JsonValueKind.Array } je =>
je.EnumerateArray()
.Select(e => e.ValueKind == JsonValueKind.String ? e.GetString() : e.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s!)
.ToList(),
IEnumerable list => list.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(),
_ => AsString(v) is { } single ? [single] : []
};
}
}