using PrivaPub.Federation.Actors; using PrivaPub.Federation.Rendering; using PrivaPub.Models.Federation; using PrivaPub.Models.Post; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Outbox { public interface IOutboxPublisher { Task> Audience(LocalActor author, PostEntity post, CancellationToken token); Task Publish(LocalActor author, PostEntity post, JsonObject activity, CancellationToken token); Task PublishProfile(LocalActor actor, CancellationToken token); } public class OutboxPublisher : IOutboxPublisher { readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IDeliveryService _delivery; public OutboxPublisher(DbEntities dbEntities, ILocalActorService localActors, IDeliveryService delivery) { _dbEntities = dbEntities; _localActors = localActors; _delivery = delivery; } public async Task> Audience(LocalActor author, PostEntity post, CancellationToken token) { if (post.Visibility is PostVisibility.Circle or PostVisibility.LocalGeo || post.IsLocalOnly) return Array.Empty(); var inboxes = new List(); if (post.Visibility is PostVisibility.Public or PostVisibility.Unlisted or PostVisibility.FollowersOnly) inboxes.AddRange(await _delivery.FollowerInboxes(author, token)); var addressed = post.Mentions.Where(m => !m.IsLocal).Select(m => m.ActorURI) .Concat(post.Visibility == PostVisibility.Direct ? post.To.Concat(post.Cc) : Enumerable.Empty()) .Where(uri => !uri.StartsWith(author.BaseAddress + "/", StringComparison.OrdinalIgnoreCase)) .Distinct(StringComparer.Ordinal) .ToList(); foreach (var uri in addressed) { var actor = await _dbEntities.ForeignAvatars.Match(a => a.ActorURI == uri).ExecuteFirstAsync(token); if (actor != default && !string.IsNullOrEmpty(actor.InboxURL)) inboxes.Add(actor.InboxURL); } if (post.Visibility != PostVisibility.Direct && !string.IsNullOrEmpty(post.InReplyToAccountId)) { var parentAuthor = await _dbEntities.ForeignAvatars.MatchID(post.InReplyToAccountId).ExecuteFirstAsync(token); if (parentAuthor != default && !string.IsNullOrEmpty(parentAuthor.InboxURL)) inboxes.Add(parentAuthor.InboxURL); } if (!string.IsNullOrEmpty(post.GroupId) && post.Visibility is PostVisibility.Public or PostVisibility.Unlisted) { var group = await _localActors.FindById(LocalActorKind.Group, post.GroupId, token); if (group is { IsFederated: true }) inboxes.AddRange(await _delivery.FollowerInboxes(group, token)); } return inboxes.Where(i => !string.IsNullOrEmpty(i)).Distinct(StringComparer.Ordinal).ToList(); } public async Task Publish(LocalActor author, PostEntity post, JsonObject activity, CancellationToken token) { var inboxes = await Audience(author, post, token); if (inboxes.Count > 0) await _delivery.Enqueue(author, inboxes, activity, token); } public async Task PublishProfile(LocalActor actor, CancellationToken token) { var document = ActivityPubRenderer.Actor(actor); document.Remove("@context"); var update = new JsonObject { ["@context"] = ActivityPubRenderer.Context(), ["id"] = actor.ActivityUri($"update-profile-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"), ["type"] = "Update", ["actor"] = actor.Uri, ["to"] = new JsonArray(ActivityPubRenderer.Public), ["cc"] = new JsonArray(actor.Followers), ["object"] = document }; await _delivery.EnqueueToFollowers(actor, update, token); } } }