using MongoDB.Entities; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Rendering; using PrivaPub.Models.Federation; 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 { // so the announce's id resolves (PeasantsController.Grunt); a repeat of the same announce keeps the first static async Task Keep(LocalActor group, JsonObject announce, string objectUri, CancellationToken token) { var body = (JsonObject)announce.DeepClone(); body.Remove("@context"); try { await DB.Default.SaveAsync(new GroupAnnouncement { GroupId = group.Id, ActivityURI = body["id"]!.GetValue(), ObjectURI = objectUri, Body = body.ToJsonString() }, token); } catch (MongoDB.Driver.MongoWriteException ex) when (ex.WriteError?.Category == MongoDB.Driver.ServerErrorCategory.DuplicateKey) { } } 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); await Keep(group, announce, objectUri, token); if (isNewPost && !string.IsNullOrEmpty(objectUri)) { var boost = ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}"); await _delivery.EnqueueToFollowers(group, boost, token); await Keep(group, boost, objectUri, token); } } } }