Lemmy takes a report only from a person or a service, about one post or comment, addressed to its community, and it answered PrivaPub's Flag (the instance actor's, an Application, with no `to` and the account and posts as its object) 400. A report of a post or comment in a community on a server whose NodeInfo names Lemmy now leaves from `privapub_reports`, a Service with its own key that names nobody: one Flag per post, `to` the community (its own audience, else its thread's), with the persona's words, or the category, in `summary` and `content`, sent to the community's inbox. This is the second exception to "a server's software is for display" (owner decision 2026-10-06, `ReportService.ServiceReportTakers`). Every other server keeps the instance actor's report. An account alone is not reported to Lemmy, which takes no such report, and `forwarded` now says whether anything left. The reporter is read unsigned in SecureMode and answers WebFinger like the instance actor. Nobody follows or mentions it, the Mastodon API has no account for it, and a migration reserves its name. Checked live: Lemmy 1.0 and 0.19 keep the reports of a thread and of a comment, with alice's words, from "Reports from privapub.test", and none names her (69 checks). A sweep of every scenario with this and the next commit: 876 checks pass; Ghost's Network feed listed alice's post too late once, and Ghost passes alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
99 lines
3.3 KiB
C#
99 lines
3.3 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Federation.Outbox;
|
|
using PrivaPub.Federation.Rendering;
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.Group;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
using static PrivaPub.Federation.Inbox.ForeignMembers;
|
|
using static PrivaPub.Federation.Objects.ActivityJson;
|
|
|
|
using PostEntity = PrivaPub.Models.Post.Post;
|
|
|
|
namespace PrivaPub.Federation.Inbox.Handlers
|
|
{
|
|
public class FollowHandler : IActivityHandler
|
|
{
|
|
readonly DbEntities _dbEntities;
|
|
readonly ILocalActorService _localActors;
|
|
readonly IRemoteActorService _remoteActors;
|
|
readonly IDeliveryService _delivery;
|
|
|
|
public FollowHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery)
|
|
{
|
|
_dbEntities = dbEntities;
|
|
_localActors = localActors;
|
|
_remoteActors = remoteActors;
|
|
_delivery = delivery;
|
|
}
|
|
|
|
public string Type => "Follow";
|
|
|
|
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
|
{
|
|
var follow = activity;
|
|
var follower = actor;
|
|
|
|
var target = await _localActors.FindByAddress(Id(follow["object"]), token);
|
|
if (target is not { IsFederated: true } || target.IsServerActor)
|
|
{
|
|
Arrival.Drop("unknown-recipient");
|
|
return;
|
|
}
|
|
if (target.Kind == LocalActorKind.Person
|
|
&& await DB.Default.Find<Models.Social.Block>().Match(b => b.AvatarId == target.Id && b.TargetActorURI == follower.ActorURI).ExecuteAnyAsync(token))
|
|
{
|
|
var reject = new JsonObject
|
|
{
|
|
["@context"] = ActivityPubRenderer.ActivityStreams,
|
|
["id"] = target.ActivityUri($"reject-{Guid.NewGuid():N}"),
|
|
["type"] = "Reject",
|
|
["actor"] = target.Uri,
|
|
["object"] = follow.DeepClone()
|
|
};
|
|
await _delivery.Enqueue(target, new[] { follower.InboxURL }, reject, token);
|
|
Arrival.Reject("blocked");
|
|
return;
|
|
}
|
|
|
|
var existing = await _dbEntities.Followers
|
|
.Match(f => f.LocalActorId == target.Id && f.LocalActorKind == target.Kind && f.ActorURI == follower.ActorURI)
|
|
.ExecuteFirstAsync(token);
|
|
var record = existing ?? new Follower
|
|
{
|
|
LocalActorId = target.Id,
|
|
LocalActorKind = target.Kind,
|
|
ActorURI = follower.ActorURI
|
|
};
|
|
record.InboxURL = follower.InboxURL;
|
|
record.SharedInboxURL = follower.SharedInboxURL;
|
|
record.FollowActivityURI = Id(follow);
|
|
record.IsAccepted = existing?.IsAccepted == true || !target.ManuallyApprovesFollowers;
|
|
await DB.Default.SaveAsync(record, token);
|
|
Arrival.Accept(record.IsAccepted ? "auto-accepted" : "pending");
|
|
|
|
if (target.Kind == LocalActorKind.Person)
|
|
await Notifications.Add(target.Id, record.IsAccepted ? NotificationType.Follow : NotificationType.FollowRequest,
|
|
follower.ID, follower.ActorURI, default, token);
|
|
if (!record.IsAccepted)
|
|
return;
|
|
|
|
if (target.Kind == LocalActorKind.Group)
|
|
await AddForeignMember(target.Id, follower.ActorURI, token);
|
|
|
|
var accept = ActivityPubRenderer.Accept(target, follow, $"accept-{record.ID}-{DateTime.UtcNow.Ticks}");
|
|
await _delivery.Enqueue(target, new[] { follower.InboxURL }, accept, token);
|
|
return;
|
|
}
|
|
}
|
|
}
|