Where a server is, on the public instance API
/api/privapub/v1/instances/:host gains `geo`, the public projection of a server's place already decided for public server locations (PublicGeo.Project): city, coordinates and network for servers reporting at least 10 users and not behind a CDN, the country otherwise, the CDN's name for a CDN-fronted one, with DB-IP's attribution. `?host[]=` answers up to 40 servers at once, and this server describes itself: its host's address located once a day (SelfLocation), or Statistics:Geo:Self when the owner sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
2cd75176a4
commit
d9fb5c582f
8 files changed
+429
-15
No files matched your search
@@ -1,11 +1,16 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Domain.Privacy;
|
||||
using PrivaPub.Domain.Statistics;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Infrastructure;
|
||||
using PrivaPub.Infrastructure.Statistics;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Jobs;
|
||||
using PrivaPub.Models.Post;
|
||||
@@ -19,9 +24,23 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
{
|
||||
public class ProvenanceController : MastodonController
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
const int MaxHosts = 40;
|
||||
|
||||
public ProvenanceController(DbEntities dbEntities) => _dbEntities = dbEntities;
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ISelfLocation _self;
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IOptionsMonitor<StatisticsOptions> _statistics;
|
||||
readonly IOptionsMonitor<RegistrationOptions> _registrations;
|
||||
|
||||
public ProvenanceController(DbEntities dbEntities, ISelfLocation self, ILocalActorService localActors,
|
||||
IOptionsMonitor<StatisticsOptions> statistics, IOptionsMonitor<RegistrationOptions> registrations)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_self = self;
|
||||
_localActors = localActors;
|
||||
_statistics = statistics;
|
||||
_registrations = registrations;
|
||||
}
|
||||
|
||||
[HttpGet("/api/privapub/v1/statuses/{id}/provenance"), Scope("read:statuses", requiresUser: false), AllowAnonymous]
|
||||
public async Task<IActionResult> Status(string id, CancellationToken token)
|
||||
@@ -44,12 +63,65 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
[HttpGet("/api/privapub/v1/instances/{host}"), Scope("read", requiresUser: false), AllowAnonymous]
|
||||
public async Task<IActionResult> Instance(string host, CancellationToken token)
|
||||
{
|
||||
host = host?.Trim().ToLowerInvariant();
|
||||
var instance = await DB.Default.Find<RemoteInstance>().Match(i => i.Host == host).ExecuteFirstAsync(token);
|
||||
return instance == default ? NotFoundError() : Json(Describe(instance));
|
||||
var described = await Describe(new[] { host }, token);
|
||||
return described.Count == 0 ? NotFoundError() : Json(described[0]);
|
||||
}
|
||||
|
||||
static Provenance Describe(PostEntity post, ObjectRecord record, RemoteInstance instance)
|
||||
//up to MaxHosts at once (host[]=...), in the order asked; hosts this server does not know are left out
|
||||
[HttpGet("/api/privapub/v1/instances"), Scope("read", requiresUser: false), AllowAnonymous]
|
||||
public async Task<IActionResult> Instances(CancellationToken token) => Json(await Describe(Params.List("host"), token));
|
||||
|
||||
async Task<List<InstanceDescription>> Describe(IEnumerable<string> hosts, CancellationToken token)
|
||||
{
|
||||
var wanted = hosts.Select(h => h?.Trim().ToLowerInvariant()).Where(h => !string.IsNullOrEmpty(h) && h.Length <= 253)
|
||||
.Distinct().Take(MaxHosts).ToList();
|
||||
var selfHost = Uri.TryCreate(_localActors.BaseAddress, UriKind.Absolute, out var self) ? self.Host.ToLowerInvariant() : default;
|
||||
var remote = await DB.Default.Find<RemoteInstance>().Match(i => wanted.Contains(i.Host)).ExecuteAsync(token);
|
||||
var described = new List<InstanceDescription>();
|
||||
foreach (var host in wanted)
|
||||
{
|
||||
if (host == selfHost)
|
||||
described.Add(await DescribeSelf(host, token));
|
||||
else if (remote.FirstOrDefault(i => i.Host == host) is { } instance)
|
||||
described.Add(Describe(instance, _statistics.CurrentValue.PublicCityMinUsers));
|
||||
}
|
||||
return described;
|
||||
}
|
||||
|
||||
async Task<InstanceDescription> DescribeSelf(string host, CancellationToken token)
|
||||
{
|
||||
var (geo, configured) = await _self.Get(token);
|
||||
var users = await Counted.Users(token);
|
||||
return new InstanceDescription
|
||||
{
|
||||
Host = host,
|
||||
Software = "privapub",
|
||||
Version = BuildInfo.Ref,
|
||||
NodeName = "PrivaPub",
|
||||
Protocols = new() { "activitypub" },
|
||||
OpenRegistrations = _registrations.CurrentValue.IsOpen,
|
||||
DescribedAt = MastodonJson.Time(DateTime.UtcNow),
|
||||
Geo = configured
|
||||
? Located(new PublicLocation(geo.Country, geo.City, geo.Latitude, geo.Longitude, default, default), geo)
|
||||
: Located(PublicGeo.Project(new RemoteInstance { Geo = geo, UsersTotal = users }, _statistics.CurrentValue.PublicCityMinUsers), geo)
|
||||
};
|
||||
}
|
||||
|
||||
//the public projection of a server's place (owner decision on server locations, ROADMAP 2026-10-03)
|
||||
static InstanceLocation Located(PublicLocation location, InstanceGeo geo) => location == default ? default : new InstanceLocation
|
||||
{
|
||||
Precision = location.Cdn != default ? "cdn" : location.Latitude != default ? "city" : "country",
|
||||
Country = location.Country,
|
||||
City = location.City,
|
||||
Latitude = location.Latitude,
|
||||
Longitude = location.Longitude,
|
||||
Network = location.Network,
|
||||
Cdn = location.Cdn,
|
||||
Source = geo?.Source,
|
||||
LocatedAt = geo?.LocatedAt is { } at ? MastodonJson.Time(at) : default
|
||||
};
|
||||
|
||||
Provenance Describe(PostEntity post, ObjectRecord record, RemoteInstance instance)
|
||||
{
|
||||
var raw = record?.Raw == default ? default : JsonNode.Parse(record.Raw) as JsonObject;
|
||||
return new Provenance
|
||||
@@ -86,11 +158,11 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
RawHash = r.RawHash,
|
||||
Raw = r.Raw == default ? default : JsonNode.Parse(r.Raw)
|
||||
}).ToList() ?? new(),
|
||||
Instance = instance == default ? default : Describe(instance)
|
||||
Instance = instance == default ? default : Describe(instance, _statistics.CurrentValue.PublicCityMinUsers)
|
||||
};
|
||||
}
|
||||
|
||||
static InstanceDescription Describe(RemoteInstance instance) => new()
|
||||
static InstanceDescription Describe(RemoteInstance instance, int minUsers) => new()
|
||||
{
|
||||
Host = instance.Host,
|
||||
Software = instance.Software,
|
||||
@@ -107,7 +179,8 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
UnavailableUntil = instance.UnavailableUntil is { } until ? MastodonJson.Time(until) : default,
|
||||
LastSuccessAt = instance.LastSuccessAt is { } success ? MastodonJson.Time(success) : default,
|
||||
LastFailureAt = instance.LastFailureAt is { } failure ? MastodonJson.Time(failure) : default
|
||||
}
|
||||
},
|
||||
Geo = Located(PublicGeo.Project(instance, minUsers), instance.Geo)
|
||||
};
|
||||
|
||||
public class Provenance
|
||||
@@ -170,6 +243,21 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
public string DescriptionError { get; set; }
|
||||
public JsonNode NodeInfo { get; set; }
|
||||
public InstanceDelivery Delivery { get; set; }
|
||||
public InstanceLocation Geo { get; set; }
|
||||
}
|
||||
|
||||
//precision: "city" (coordinates to 0.1°, city and network), "country" (the country only) or "cdn" (the CDN's name only)
|
||||
public class InstanceLocation
|
||||
{
|
||||
public string Precision { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string City { get; set; }
|
||||
public double? Latitude { get; set; }
|
||||
public double? Longitude { get; set; }
|
||||
public string Network { get; set; }
|
||||
public string Cdn { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string LocatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class InstanceDelivery
|
||||
|
||||
Reference in new issue
Block a user