using MongoDB.Entities; using PrivaPub.Infrastructure.Http; using PrivaPub.Infrastructure.Jobs; using PrivaPub.Models.Jobs; using System.Text.Json; namespace PrivaPub.Federation.Objects { public class InstanceDescriber : IJobHandler { const int MaxNodeInfoChars = 64 * 1024; static readonly string[] Schemas = { "http://nodeinfo.diaspora.software/ns/schema/2.1", "http://nodeinfo.diaspora.software/ns/schema/2.0" }; readonly IFederationHttp _http; public InstanceDescriber(IFederationHttp http) => _http = http; public JobKind Kind => JobKind.DescribeInstance; public int Concurrency => 1; public int MaxAttempts => 2; public int PerHostLimit => 1; public async Task Handle(Job job, CancellationToken token) { var host = job.Payload; using var scope = HttpScope.For("nodeinfo"); using var links = await _http.GetJson($"https://{host}/.well-known/nodeinfo", "application/json", sign: default, token); var href = links == default ? default : Schemas.Select(schema => LinkFor(links.Root, schema)).FirstOrDefault(link => link != default); if (href == default) return await Described(host, default, "no NodeInfo", token); using var nodeInfo = await _http.GetJson(href, "application/json", sign: default, token); return nodeInfo == default ? await Described(host, default, "NodeInfo unreadable", token) : await Described(host, nodeInfo.Root, default, token); } static async Task Described(string host, JsonElement? nodeInfo, string error, CancellationToken token) { var update = DB.Default.Update() .Match(i => i.Host == host) .Modify(i => i.DescribedAt, DateTime.UtcNow) .Modify(i => i.DescriptionError, error) .Option(o => o.IsUpsert = true); if (nodeInfo is { } root) { var raw = root.GetRawText(); update = update .Modify(i => i.Software, Text(root, "software", "name")) .Modify(i => i.SoftwareVersion, Text(root, "software", "version")) .Modify(i => i.NodeName, Text(root, "metadata", "nodeName")) .Modify(i => i.Protocols, root.TryGetProperty("protocols", out var protocols) && protocols.ValueKind == JsonValueKind.Array ? protocols.EnumerateArray().Where(p => p.ValueKind == JsonValueKind.String).Select(p => p.GetString()).Take(16).ToList() : new List()) .Modify(i => i.OpenRegistrations, root.TryGetProperty("openRegistrations", out var open) && open.ValueKind is JsonValueKind.True or JsonValueKind.False ? open.GetBoolean() : default(bool?)) .Modify(i => i.NodeInfo, raw.Length <= MaxNodeInfoChars ? raw : default); } await update.ExecuteAsync(token); return JobOutcome.Done; } static string LinkFor(JsonElement root, string schema) => root.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array ? links.EnumerateArray() .Where(l => l.ValueKind == JsonValueKind.Object && l.TryGetProperty("rel", out var rel) && rel.ValueKind == JsonValueKind.String && rel.GetString() == schema) .Select(l => l.TryGetProperty("href", out var href) && href.ValueKind == JsonValueKind.String ? href.GetString() : default) .FirstOrDefault(h => h != default && h.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) : default; static string Text(JsonElement root, string parent, string name) { if (!root.TryGetProperty(parent, out var section) || section.ValueKind != JsonValueKind.Object || !section.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String) return default; var text = value.GetString(); return text.Length <= 200 ? text : text[..200]; } } }