using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using MongoDB.Entities; using PrivaPub.Models.Federation; using PrivaPub.Models.Post; using PrivaPub.Models.User; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using PostEntity = PrivaPub.Models.Post.Post; using PrivaPub.Domain.Privacy; using PrivaPub.Federation.Actors; using PrivaPub.Infrastructure; namespace PrivaPub.Federation.Controllers { [ApiController] public class WellKnownController : ControllerBase { readonly ILocalActorService _localActors; readonly DbEntities _dbEntities; readonly IOptionsMonitor _registrations; public WellKnownController(ILocalActorService localActors, DbEntities dbEntities, IOptionsMonitor registrations) { _registrations = registrations; _localActors = localActors; _dbEntities = dbEntities; } [HttpGet, Route("/.well-known/webfinger")] public async Task WebFinger([FromQuery] string resource, CancellationToken token) { if (string.IsNullOrEmpty(resource)) return BadRequest(); LocalActor actor; // Mastodon, GoToSocial and Pleroma also take a bare user@domain or @user@domain, so we do too. var acct = resource.StartsWith("acct:", StringComparison.OrdinalIgnoreCase) ? resource[5..] : !resource.Contains("://", StringComparison.Ordinal) && resource.Contains('@') ? resource : default; if (acct != default) { var parts = acct.TrimStart('@').Split('@'); var domain = new Uri(_localActors.BaseAddress).Authority; if (parts.Length != 2 || !parts[1].Equals(domain, StringComparison.OrdinalIgnoreCase)) return NotFound(); actor = await _localActors.FindByUserName(parts[0], token); if (actor == default && await _localActors.Gone(parts[0], token) != default) return StatusCode(StatusCodes.Status410Gone); } else actor = await _localActors.FindByUri(resource, token); if (actor is not { IsFederated: true }) return NotFound(); var document = new JsonObject { ["subject"] = $"acct:{actor.Handle}", ["aliases"] = actor.Kind == LocalActorKind.Application ? new JsonArray(actor.Uri) : new JsonArray(actor.HtmlUrl, actor.Uri), ["links"] = actor.Kind == LocalActorKind.Application ? new JsonArray(new JsonObject { ["rel"] = "self", ["type"] = "application/activity+json", ["href"] = actor.Uri }) : new JsonArray( new JsonObject { ["rel"] = "http://webfinger.net/rel/profile-page", ["type"] = "text/html", ["href"] = actor.HtmlUrl }, new JsonObject { ["rel"] = "self", ["type"] = "application/activity+json", ["href"] = actor.Uri }) }; return Content(document.ToJsonString(), "application/jrd+json; charset=utf-8"); } [HttpGet, Route("/.well-known/nodeinfo")] public IActionResult NodeInfoLinks() { var document = new JsonObject { ["links"] = new JsonArray( new JsonObject { ["rel"] = "http://nodeinfo.diaspora.software/ns/schema/2.1", ["href"] = $"{_localActors.BaseAddress}/nodeinfo/2.1" }, new JsonObject { ["rel"] = "http://nodeinfo.diaspora.software/ns/schema/2.0", ["href"] = $"{_localActors.BaseAddress}/nodeinfo/2.0" }) }; return Content(document.ToJsonString(), "application/json; charset=utf-8"); } [HttpGet, Route("/nodeinfo/{version:regex(^2\\.[[01]]$)}")] public async Task NodeInfo(string version, CancellationToken token) { var users = await Counted.Users(token); var posts = await Counted.LocalPosts(token); var software = new JsonObject { ["name"] = "privapub", ["version"] = BuildInfo.Ref }; if (version == "2.1") { software["repository"] = "https://git.thepra.dev/thepra/SocialPub"; software["homepage"] = "https://git.thepra.dev/thepra/SocialPub"; } var document = new JsonObject { ["version"] = version, ["software"] = software, ["protocols"] = new JsonArray("activitypub"), ["services"] = new JsonObject { ["inbound"] = new JsonArray(), ["outbound"] = new JsonArray() }, ["openRegistrations"] = _registrations.CurrentValue.IsOpen, ["usage"] = new JsonObject { ["users"] = new JsonObject { ["total"] = users, ["activeMonth"] = await Counted.ActiveUsers(30, token), ["activeHalfyear"] = await Counted.ActiveUsers(180, token) }, ["localPosts"] = posts }, ["metadata"] = new JsonObject { ["nodeName"] = "PrivaPub", ["nodeDescription"] = "A small ActivityPub server where one private login keeps several unlinkable public personas.", ["federation"] = new JsonObject { ["document"] = "https://git.thepra.dev/thepra/SocialPub/src/branch/master/FEDERATION.md" } } }; return Content(document.ToJsonString(), $"application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/{version}#\"; charset=utf-8"); } } }