Build / Build (push) Successful in 1m3s
A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits. /clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published, Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids. Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones. Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
157 lines
5.7 KiB
C#
157 lines
5.7 KiB
C#
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<RegistrationOptions> _registrations;
|
|
readonly Domain.Instance.IInstanceSettings _settings;
|
|
|
|
public WellKnownController(ILocalActorService localActors, DbEntities dbEntities, IOptionsMonitor<RegistrationOptions> registrations,
|
|
Domain.Instance.IInstanceSettings settings)
|
|
{
|
|
_registrations = registrations;
|
|
_settings = settings;
|
|
_localActors = localActors;
|
|
_dbEntities = dbEntities;
|
|
}
|
|
|
|
[HttpGet, Route("/.well-known/webfinger")]
|
|
public async Task<IActionResult> WebFinger([FromQuery] string resource, CancellationToken token)
|
|
{
|
|
if (string.IsNullOrEmpty(resource))
|
|
return BadRequest();
|
|
// the server itself (FEP-d556): its instance actor
|
|
if (resource.TrimEnd('/') == _localActors.BaseAddress)
|
|
{
|
|
var instance = await _localActors.GetInstanceActor(token);
|
|
return Content(new JsonObject
|
|
{
|
|
["subject"] = resource,
|
|
["links"] = new JsonArray(new JsonObject
|
|
{
|
|
["rel"] = "https://www.w3.org/ns/activitystreams#Service", ["type"] = "application/activity+json", ["href"] = instance.Uri
|
|
})
|
|
}.ToJsonString(), "application/jrd+json; charset=utf-8");
|
|
}
|
|
|
|
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.IsServerActor ? new JsonArray(actor.Uri) : new JsonArray(actor.HtmlUrl, actor.Uri),
|
|
["links"] = actor.IsServerActor
|
|
? 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 async Task<IActionResult> NodeInfoLinks(CancellationToken token)
|
|
{
|
|
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"
|
|
},
|
|
// the application actor (FEP-2677)
|
|
new JsonObject
|
|
{
|
|
["rel"] = "https://www.w3.org/ns/activitystreams#Application",
|
|
["href"] = (await _localActors.GetInstanceActor(token)).Uri
|
|
})
|
|
};
|
|
return Content(document.ToJsonString(), "application/json; charset=utf-8");
|
|
}
|
|
|
|
[HttpGet, Route("/nodeinfo/{version:regex(^2\\.[[01]]$)}")]
|
|
public async Task<IActionResult> 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
|
|
{
|
|
// the server's name and short description, as its administrator set them (IInstanceSettings)
|
|
["nodeName"] = _settings.Current.Title,
|
|
["nodeDescription"] = _settings.Current.ShortDescription ?? string.Empty,
|
|
["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");
|
|
}
|
|
}
|
|
}
|