using PrivaPub.Federation.Actors; using PrivaPub.Federation.Rendering; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; namespace PrivaPub.Federation.Outbox { public interface IGroupDistributor { Task Announce(LocalActor group, JsonObject activity, string objectUri, bool isNewPost, CancellationToken token); } public class GroupDistributor : IGroupDistributor { readonly IDeliveryService _delivery; public GroupDistributor(IDeliveryService delivery) { _delivery = delivery; } public async Task Announce(LocalActor group, JsonObject activity, string objectUri, bool isNewPost, CancellationToken token) { if (group is not { IsCircle: false, Kind: Models.Federation.LocalActorKind.Group }) return; var embedded = (JsonObject)activity.DeepClone(); embedded.Remove("@context"); embedded["audience"] ??= group.Uri; var key = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(activity["id"]?.GetValue() ?? embedded.ToJsonString())))[..24]; var announce = new JsonObject { ["@context"] = ActivityPubRenderer.Context(), ["id"] = group.ActivityUri($"announce-{key}"), ["type"] = "Announce", ["actor"] = group.Uri, ["to"] = new JsonArray(ActivityPubRenderer.Public), ["cc"] = new JsonArray(group.Followers), ["audience"] = group.Uri, ["object"] = embedded }; // Every follower, the author's own server included: Lemmy 1.0 keeps its user's post pending until the community // announces it back, and clears that before it answers the echo 400 ("Object is not remote"). await _delivery.EnqueueToFollowers(group, announce, token); if (isNewPost && !string.IsNullOrEmpty(objectUri)) await _delivery.EnqueueToFollowers(group, ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}"), token); } } }