- Blocks are per persona and never federate (a Block activity would tell
the other server who blocked whom): the blocked account is removed as a
follower, with a Reject{Follow} if it is remote, unfollowed, cleared from
home and notifications, and refused with Reject if it follows again.
- Mutes (optionally timed, optionally sparing notifications) and per-
persona domain blocks keep authors out of home timelines, notifications
and every status list the API returns; the boosts of a hidden author's
posts are hidden too.
- Bookmarks and pins (at most five, public or unlisted, own posts) with
their Mastodon endpoints and flags; pinned posts are the actor's
`featured` collection at /trophies, and `featuredTags` points at
/tattoos.
- Reports: /api/v1/reports stores the report and, when forwarding to a
remote account, sends Flag from the instance actor, so the reporting
persona is never named to the other server. An inbound Flag about a local
persona or its posts becomes a report; moderators list and resolve them
under /clientapi/moderator/reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
94 lines
3.1 KiB
C#
94 lines
3.1 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.FindByUri(Id(follow["object"]), token);
|
|
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
|
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);
|
|
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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|