using Microsoft.AspNetCore.Mvc; using MongoDB.Entities; using PrivaPub.Models.Federation; using PrivaPub.Services.Federation; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Controllers.ServerToServer { [ApiController, Route("peasants")] public class PeasantsController : ControllerBase { const string ActivityContentType = "application/activity+json; charset=utf-8"; const int OutboxSize = 20; readonly ILocalActorService _localActors; readonly IInboxService _inbox; readonly DbEntities _dbEntities; readonly ILogger _logger; public PeasantsController(ILocalActorService localActors, IInboxService inbox, DbEntities dbEntities, ILogger logger) { _localActors = localActors; _inbox = inbox; _dbEntities = dbEntities; _logger = logger; } [HttpGet, Route("{actor}")] public async Task GetActor(string actor, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); return local == default ? NotFound() : Activity(ActivityPubRenderer.Actor(local)); } [HttpGet, Route("{actor}/anus")] public async Task Outbox(string actor, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); if (local == default) return NotFound(); var total = local.Kind == LocalActorKind.Group ? await DB.Default.CountAsync(p => p.GroupId == local.Id, token) : await DB.Default.CountAsync(p => p.GroupUserId == local.Id && !p.IsFederatedCopy, token); var latest = await (local.Kind == LocalActorKind.Group ? _dbEntities.Posts.Match(p => p.GroupId == local.Id) : _dbEntities.Posts.Match(p => p.GroupUserId == local.Id && !p.IsFederatedCopy)) .Sort(p => p.CreationDate, Order.Descending) .Limit(OutboxSize) .ExecuteAsync(token); var items = new List(); foreach (var post in latest) { if (local.Kind == LocalActorKind.Group) { items.Add(ActivityPubRenderer.Announce(local, post.ObjectURI, $"announce-{post.ID}")); continue; } var group = string.IsNullOrEmpty(post.GroupId) ? default : await _localActors.FindById(LocalActorKind.Group, post.GroupId, token); var note = ActivityPubRenderer.Note(post, local, group, default); items.Add(ActivityPubRenderer.Create(local, note, $"create-{post.ID}")); } return Activity(ActivityPubRenderer.OrderedCollection(local.Outbox, (int)total, items)); } [HttpGet, Route("{actor}/followers")] public async Task Followers(string actor, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); if (local == default) return NotFound(); var count = await DB.Default.CountAsync( f => f.LocalActorId == local.Id && f.LocalActorKind == local.Kind && f.IsAccepted, token); return Activity(ActivityPubRenderer.OrderedCollection(local.Followers, (int)count, default)); } [HttpGet, Route("{actor}/following")] public async Task Following(string actor, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); return local == default ? NotFound() : Activity(ActivityPubRenderer.OrderedCollection(local.Following, 0, Enumerable.Empty())); } [HttpGet, Route("{actor}/posts/{postId}")] public async Task Post(string actor, string postId, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); if (local == default || local.Kind != LocalActorKind.Person) return NotFound(); var post = await _dbEntities.Posts .Match(p => p.ID == postId && p.GroupUserId == local.Id && !p.IsFederatedCopy) .ExecuteFirstAsync(token); if (post == default) return NotFound(); var group = string.IsNullOrEmpty(post.GroupId) ? default : await _localActors.FindById(LocalActorKind.Group, post.GroupId, token); var replyTo = await ReplyTarget(post.AnsweringToPostId, token); var note = ActivityPubRenderer.Note(post, local, group, replyTo); note["@context"] = ActivityPubRenderer.ActivityStreams; return Activity(note); } [HttpPost, Route("{actor}/mouth")] public async Task Inbox(string actor, CancellationToken token) { var local = await _localActors.FindByUserName(actor, token); if (local == default) return NotFound(); return Answer(await _inbox.Receive(Request, local, token)); } [HttpPost, Route("{actor}/human-centipede")] public async Task ActorSharedInbox(string actor, CancellationToken token) => Answer(await _inbox.Receive(Request, default, token)); [HttpPost, Route("/human-centipede")] public async Task SharedInbox(CancellationToken token) => Answer(await _inbox.Receive(Request, default, token)); async Task ReplyTarget(string answeringToPostId, CancellationToken token) { if (string.IsNullOrEmpty(answeringToPostId)) return default; if (answeringToPostId.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) return answeringToPostId; return (await _dbEntities.Posts.MatchID(answeringToPostId).ExecuteFirstAsync(token))?.ObjectURI; } IActionResult Answer(InboxResult result) { if (result.Error != default) _logger.LogInformation("Inbox refused with {Status}: {Error}", result.StatusCode, result.Error); return result.Error == default ? StatusCode(result.StatusCode) : StatusCode(result.StatusCode, result.Error); } ContentResult Activity(JsonObject document) => new() { Content = document.ToJsonString(), ContentType = ActivityContentType, StatusCode = StatusCodes.Status200OK }; } }