The tree had not compiled since its first commit: Group, DmGroup and
IGroupUsersService were referenced and never written, an IDE rename had
turned the user-settings DTO into the ViewAvatarServer enum, and the settings
were saved as an entity they no longer were.
Built now:
- Group (an ActivityPub Group actor with its own keys, members, invitation
code and optional password) and DmGroup (a conversation), with
/clientapi/group/{list,insert,update,join,leave,approve}.
- Posts and DMs: /clientapi/post/{list,insert,delete}, /clientapi/dm/{list,insert};
DM recipients are local usernames or user@host handles resolved by WebFinger.
- Invitation sign-up and login against the group's invitation code, checking
the password before any account is created.
- Federation: WebFinger, NodeInfo 2.0, actors at /peasants/{name} (Person,
Group, and an Application instance actor) with SPKI keys, draft-cavage
RSA-SHA256 HTTP signatures both ways, an inbox handling Follow (+Accept),
Undo, Create, Delete and Update, an outbox, notes at /posts/{id}, and a
persisted, retried, signed delivery queue. A post to a group is announced
by the group to its followers (FEP-1b12).
- The unused, broken typed ActivityPub models are replaced by a renderer;
NSign's HMAC setup, which could not federate, is gone.
Upgrade: net10.0, MongoDB.Entities 25.1 (instance DB API, Standard GUIDs),
Swashbuckle 10 / OpenApi 2, Serilog.AspNetCore 10, MailKit 4.18,
PasswordGenerator 3. The JWT keys are 64 bytes (IdentityModel 8 refuses
shorter for HS512). Production runs its own mongod on 127.0.0.1:27022, as
Sintopia's apps do, and deploys to privapub.thepra.dev from the build runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
97 lines
3.1 KiB
C#
97 lines
3.1 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.Services.Federation;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
using PostEntity = PrivaPub.Models.Post.Post;
|
|
|
|
namespace PrivaPub.Controllers.ServerToServer
|
|
{
|
|
[ApiController]
|
|
public class WellKnownController : ControllerBase
|
|
{
|
|
readonly ILocalActorService _localActors;
|
|
readonly DbEntities _dbEntities;
|
|
|
|
public WellKnownController(ILocalActorService localActors, DbEntities dbEntities)
|
|
{
|
|
_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();
|
|
|
|
LocalActor actor;
|
|
if (resource.StartsWith("acct:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = resource[5..].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);
|
|
}
|
|
else
|
|
actor = await _localActors.FindByUri(resource, token);
|
|
|
|
if (actor == default)
|
|
return NotFound();
|
|
|
|
var document = new JsonObject
|
|
{
|
|
["subject"] = $"acct:{actor.Handle}",
|
|
["aliases"] = new JsonArray(actor.Uri),
|
|
["links"] = new JsonArray(
|
|
new JsonObject { ["rel"] = "self", ["type"] = "application/activity+json", ["href"] = actor.Uri },
|
|
new JsonObject { ["rel"] = "http://webfinger.net/rel/profile-page", ["type"] = "text/html", ["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.0",
|
|
["href"] = $"{_localActors.BaseAddress}/nodeinfo/2.0"
|
|
})
|
|
};
|
|
return Content(document.ToJsonString(), "application/json; charset=utf-8");
|
|
}
|
|
|
|
[HttpGet, Route("/nodeinfo/2.0")]
|
|
public async Task<IActionResult> NodeInfo(CancellationToken token)
|
|
{
|
|
var users = await DB.Default.CountAsync<Avatar>(a => !a.DeletionAt.HasValue, token);
|
|
var posts = await DB.Default.CountAsync<PostEntity>(p => !p.IsFederatedCopy, token);
|
|
var document = new JsonObject
|
|
{
|
|
["version"] = "2.0",
|
|
["software"] = new JsonObject { ["name"] = "privapub", ["version"] = BuildInfo.Ref },
|
|
["protocols"] = new JsonArray("activitypub"),
|
|
["services"] = new JsonObject { ["inbound"] = new JsonArray(), ["outbound"] = new JsonArray() },
|
|
["openRegistrations"] = true,
|
|
["usage"] = new JsonObject
|
|
{
|
|
["users"] = new JsonObject { ["total"] = users },
|
|
["localPosts"] = posts
|
|
},
|
|
["metadata"] = new JsonObject()
|
|
};
|
|
return Content(document.ToJsonString(), "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.0#\"; charset=utf-8");
|
|
}
|
|
}
|
|
}
|