Files
SocialPub/PrivaPub/Federation/Objects/InstanceDescriber.cs
T

85 lines
3.6 KiB
C#
Raw Normal View History

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<JobOutcome> 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<JobOutcome> Described(string host, JsonElement? nodeInfo, string error, CancellationToken token)
{
var update = DB.Default.Update<RemoteInstance>()
.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<string>())
.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];
}
}
}