Blocks, mutes, bookmarks, pins and reports
- 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
This commit is contained in:
1 parent
525b5368a1
commit
8f75317050
20 files changed
+813
-21
No files matched your search
@@ -0,0 +1,66 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public class FlagHandler : IActivityHandler
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
|
||||
public FlagHandler(DbEntities dbEntities, ILocalActorService localActors)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
}
|
||||
|
||||
public string Type => "Flag";
|
||||
|
||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var objects = activity["object"] switch
|
||||
{
|
||||
JsonArray array => array.Select(Id).Where(i => i != default).ToList(),
|
||||
JsonNode single when Id(single) is { } id => new List<string> { id },
|
||||
_ => new List<string>()
|
||||
};
|
||||
|
||||
LocalActor target = default;
|
||||
var postUris = new List<string>();
|
||||
foreach (var uri in objects.Take(50))
|
||||
{
|
||||
var local = await _localActors.FindByUri(uri, token);
|
||||
if (local is { Kind: LocalActorKind.Person } && uri.TrimEnd('/') == local.Uri)
|
||||
target ??= local;
|
||||
else
|
||||
postUris.Add(uri);
|
||||
}
|
||||
var posts = postUris.Count == 0
|
||||
? new List<Models.Post.Post>()
|
||||
: await _dbEntities.Posts.Match(p => postUris.Contains(p.ObjectURI) && !p.IsFederatedCopy).ExecuteAsync(token);
|
||||
if (target == default && posts.Count > 0)
|
||||
target = await _localActors.FindById(LocalActorKind.Person, posts[0].GroupUserId, token);
|
||||
if (target == default)
|
||||
return;
|
||||
|
||||
await DB.Default.SaveAsync(new Report
|
||||
{
|
||||
ReporterActorURI = actor.ActorURI,
|
||||
TargetAccountId = target.Id,
|
||||
TargetActorURI = target.Uri,
|
||||
PostIds = posts.Where(p => p.GroupUserId == target.Id).Select(p => p.ID).ToList(),
|
||||
ObjectURIs = posts.Where(p => p.GroupUserId == target.Id).Select(p => p.ObjectURI).ToList(),
|
||||
Comment = Value(activity, "content")
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,20 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user