Files
SocialPub/PrivaPub/Federation/Outbox/GroupDistributor.cs
T
thepraandClaude Opus 5.5 af2c61292b A community announces back to its author's own server
Lemmy keeps its user's post in a community pending until the community announces it back. A community hosted here
announced only to its followers, so a post from a server where nobody follows the community stayed pending there; the
author's own server (its shared inbox) now gets the announces too. INTEROP records, confirmed live, why Lemmy refuses
our Flags (an Application reporter, no `to`, an array object). FEDERATION.md's custom emoji line is put back before the
replies paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-06 16:29:04 +02:00

94 lines
3.6 KiB
C#

using MongoDB.Entities;
using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Objects;
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
// a remote author's server's inbox (its shared one when it has one); none for our own personas
static async Task<string[]> AuthorsInbox(JsonObject activity, CancellationToken token)
{
if (ActivityJson.Id(activity["actor"]) is not { } actorUri)
return default;
var author = await DB.Default.Find<Models.User.ForeignAvatar>().Match(a => a.ActorURI == actorUri).ExecuteFirstAsync(token);
var inbox = string.IsNullOrEmpty(author?.SharedInboxURL) ? author?.InboxURL : author.SharedInboxURL;
return string.IsNullOrEmpty(inbox) ? default : new[] { inbox };
}
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<string>(),
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<string>() ?? 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, and the author's own server whether anyone there follows the community or not: 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").
var authorsServer = await AuthorsInbox(activity, token);
await _delivery.EnqueueToFollowers(group, announce, token, authorsServer);
await Keep(group, announce, objectUri, token);
// a new post or comment is also boosted as itself: microblogs (Mastodon, Akkoma, Misskey) drop an Announce of an
// activity, so without it their members never see what is said in the community; Lemmy answers it 400, harmlessly
if (isNewPost && !string.IsNullOrEmpty(objectUri))
{
var boost = ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}");
await _delivery.EnqueueToFollowers(group, boost, token, authorsServer);
await Keep(group, boost, objectUri, token);
}
}
}
}