using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; namespace PrivaPub.Federation.Objects { public sealed record NodeInfoFacts(string Version, string Software, string SoftwareVersion, string NodeName, string NodeDescription, List Protocols, bool? OpenRegistrations, long? UsersTotal, long? UsersActiveMonth, long? UsersActiveHalfyear, long? LocalPosts, long? LocalComments, List Languages); public sealed record InstanceApiFacts(string Title, List Languages, string RegistrationMode, int? MaxPostChars, int? ApiVersion, string SourceUrl); public static partial class InstanceDocuments { public static readonly string[] Schemas = { "http://nodeinfo.diaspora.software/ns/schema/2.2", "http://nodeinfo.diaspora.software/ns/schema/2.1", "http://nodeinfo.diaspora.software/ns/schema/2.0" }; static readonly HashSet MastodonLike = new(StringComparer.OrdinalIgnoreCase) { "mastodon", "hometown", "glitch-soc", "gotosocial", "pleroma", "akkoma", "sharkey", "iceshrimp", "iceshrimp.net", "firefish", "catodon", "friendica", "pixelfed", "takahe", "snac", "honk", "mitra", "pachli" }; const long MaxCount = 1_000_000_000; public static bool SpeaksMastodonApi(string software) => software != default && MastodonLike.Contains(software); public static (string Href, string Version) NodeInfoLink(JsonElement links) { if (links.ValueKind != JsonValueKind.Object || !links.TryGetProperty("links", out var list) || list.ValueKind != JsonValueKind.Array) return default; foreach (var schema in Schemas) foreach (var link in list.EnumerateArray()) if (link.ValueKind == JsonValueKind.Object && Text(link, "rel") == schema && Text(link, "href") is { } href && href.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) return (href, schema[(schema.LastIndexOf('/') + 1)..]); return default; } public static NodeInfoFacts NodeInfo(JsonElement root, string version) { var software = Section(root, "software"); var usage = Section(root, "usage"); var users = Section(usage, "users"); var metadata = Section(root, "metadata"); return new NodeInfoFacts( version, Text(software, "name", 200), Text(software, "version", 200), Text(metadata, "nodeName", 200), Plain(Text(metadata, "nodeDescription", 2000), 500), Strings(root, "protocols", 16, value => value.Length <= 40), root.ValueKind == JsonValueKind.Object && root.TryGetProperty("openRegistrations", out var open) && open.ValueKind is JsonValueKind.True or JsonValueKind.False ? open.GetBoolean() : (bool?)null, Count(users, "total"), Count(users, "activeMonth"), Count(users, "activeHalfyear"), Count(usage, "localPosts"), Count(usage, "localComments"), Languages(metadata, "languages") ?? Languages(metadata, "langs")); } public static InstanceApiFacts InstanceApi(JsonElement root) { var registrations = root.ValueKind == JsonValueKind.Object && root.TryGetProperty("registrations", out var r) ? r : default; string mode; if (registrations.ValueKind == JsonValueKind.Object) mode = Flag(registrations, "enabled") switch { true => Flag(registrations, "approval_required") == true ? "approval" : "open", false => "closed", _ => default }; else if (registrations.ValueKind is JsonValueKind.True or JsonValueKind.False) mode = registrations.GetBoolean() ? Flag(root, "approval_required") == true ? "approval" : "open" : "closed"; else mode = default; var statuses = Section(Section(root, "configuration"), "statuses"); var maxChars = Count(statuses, "max_characters") ?? Count(root, "max_toot_chars"); var apiVersion = Count(Section(root, "api_versions"), "mastodon"); var source = Text(root, "source_url", 300); return new InstanceApiFacts( Plain(Text(root, "title", 300), 200), Languages(root, "languages"), mode, maxChars is { } chars ? (int)Math.Min(chars, int.MaxValue) : (int?)null, apiVersion is { } api ? (int)Math.Min(api, int.MaxValue) : (int?)null, source != default && source.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? source : default); } static JsonElement Section(JsonElement parent, string name) => parent.ValueKind == JsonValueKind.Object && parent.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Object ? value : default; static string Text(JsonElement parent, string name, int max = 2048) { if (parent.ValueKind != JsonValueKind.Object || !parent.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String) return default; var text = value.GetString(); return text.Length <= max ? text : text[..max]; } static bool? Flag(JsonElement parent, string name) => parent.ValueKind == JsonValueKind.Object && parent.TryGetProperty(name, out var value) && value.ValueKind is JsonValueKind.True or JsonValueKind.False ? value.GetBoolean() : (bool?)null; static long? Count(JsonElement parent, string name) { if (parent.ValueKind != JsonValueKind.Object || !parent.TryGetProperty(name, out var value)) return default; long? number = value.ValueKind switch { JsonValueKind.Number when value.TryGetInt64(out var whole) => whole, JsonValueKind.Number when value.TryGetDouble(out var real) && real is >= 0 and < MaxCount => (long)real, JsonValueKind.String when long.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) => parsed, _ => (long?)null }; return number is { } n ? Math.Clamp(n, 0, MaxCount) : (long?)null; } static List Strings(JsonElement parent, string name, int max, Func keep) => parent.ValueKind == JsonValueKind.Object && parent.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Array ? value.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()).Where(keep).Distinct().Take(max).ToList() : new List(); static List Languages(JsonElement parent, string name) { var languages = Strings(parent, name, 16, language => LanguageCode().IsMatch(language.ToLowerInvariant())) .Select(language => language.ToLowerInvariant()).Distinct().ToList(); return languages.Count == 0 ? default : languages; } static string Plain(string text, int max) { if (string.IsNullOrWhiteSpace(text)) return default; var plain = Tags().Replace(text, " ").Trim(); plain = Spaces().Replace(plain, " "); return plain.Length <= max ? plain : plain[..max]; } [GeneratedRegex("^[a-z]{2,3}(-[a-z0-9]{2,8})?$")] private static partial Regex LanguageCode(); [GeneratedRegex("<[^>]*>")] private static partial Regex Tags(); [GeneratedRegex("\\s+")] private static partial Regex Spaces(); } }