A remote account's Block of a persona is enforced
Inbound Block was dropped as an unknown type (the pasture's last
Mastodon expected failure, G-0002). Now, as Mastodon does it: the
follows between the blocker and the persona end here too (nothing is
sent back; the blocker already ended its side), the blocker's posts and
notifications are hidden from the persona and kept out of its home, the
persona's posts are no longer addressed to the blocker by mention or
reply, and the relationship says blocked_by. Undo{Block} lifts it. The
block is kept in BlockedBy, unique per persona and blocker.
The Mastodon scenario checks blocked_by instead of expecting a failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
08acdc0503
commit
d405269526
13 files changed
+189
-7
No files matched your search
@@ -385,7 +385,9 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
var followedBy = uri == default ? default : await _dbEntities.Followers.Match(f => f.LocalActorId == MyId && f.ActorURI == uri).ExecuteFirstAsync(token);
|
||||
var blocking = uri != default && await DB.Default.Find<Block>().Match(b => b.AvatarId == MyId && b.TargetActorURI == uri).ExecuteAnyAsync(token);
|
||||
var mute = uri == default ? default : await DB.Default.Find<Mute>().Match(m => m.AvatarId == MyId && m.TargetActorURI == uri).ExecuteFirstAsync(token);
|
||||
var blockedBy = local != default && await DB.Default.Find<Block>().Match(b => b.AvatarId == local.Id && b.TargetActorURI == Me.Uri).ExecuteAnyAsync(token);
|
||||
var blockedBy = local != default
|
||||
? await DB.Default.Find<Block>().Match(b => b.AvatarId == local.Id && b.TargetActorURI == Me.Uri).ExecuteAnyAsync(token)
|
||||
: remote != default && await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == MyId && b.ActorURI == remote.ActorURI).ExecuteAnyAsync(token);
|
||||
var domainBlocked = remote != default && await DB.Default.Find<AccountDomainBlock>().Match(b => b.AvatarId == MyId && b.Domain == remote.Domain).ExecuteAnyAsync(token);
|
||||
return new Relationship
|
||||
{
|
||||
|
||||
@@ -179,6 +179,9 @@ namespace PrivaPub.Domain.Relationships
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var block in await DB.Default.Find<Block>().Match(b => b.AvatarId == avatarId && uris.Contains(b.TargetActorURI)).ExecuteAsync(token))
|
||||
hidden.Add(block.TargetActorURI);
|
||||
//an account that blocked the persona is hidden from it too, as Mastodon hides it from the blocked account's home
|
||||
foreach (var blocker in await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == avatarId && uris.Contains(b.ActorURI)).ExecuteAsync(token))
|
||||
hidden.Add(blocker.ActorURI);
|
||||
foreach (var mute in await DB.Default.Find<Mute>().Match(m => m.AvatarId == avatarId && uris.Contains(m.TargetActorURI)).ExecuteAsync(token))
|
||||
if ((mute.ExpiresAt == null || mute.ExpiresAt > now) && (!forNotifications || mute.HideNotifications))
|
||||
hidden.Add(mute.TargetActorURI);
|
||||
@@ -198,6 +201,8 @@ namespace PrivaPub.Domain.Relationships
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var block in await DB.Default.Find<Block>().Match(b => ids.Contains(b.AvatarId) && b.TargetActorURI == actorUri).ExecuteAsync(token))
|
||||
hiding.Add(block.AvatarId);
|
||||
foreach (var blocked in await DB.Default.Find<BlockedBy>().Match(b => ids.Contains(b.AvatarId) && b.ActorURI == actorUri).ExecuteAsync(token))
|
||||
hiding.Add(blocked.AvatarId);
|
||||
foreach (var mute in await DB.Default.Find<Mute>().Match(m => ids.Contains(m.AvatarId) && m.TargetActorURI == actorUri).ExecuteAsync(token))
|
||||
if (mute.ExpiresAt == null || mute.ExpiresAt > now)
|
||||
hiding.Add(mute.AvatarId);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using MongoDB.Driver;
|
||||
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
|
||||
{
|
||||
// A remote account blocks one of our personas, as Mastodon does it: the follows between them end here too (the blocker
|
||||
// already ended its side, so nothing is sent back), the persona stops seeing the blocker's posts and notifications, the
|
||||
// relationship says blocked_by, and nothing of the persona's is addressed to the blocker any more. Undo{Block} lifts it.
|
||||
public class BlockHandler : IActivityHandler
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
|
||||
public BlockHandler(DbEntities dbEntities, ILocalActorService localActors)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
}
|
||||
|
||||
public string Type => "Block";
|
||||
|
||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var target = Id(activity["object"]) is { } uri ? await _localActors.FindByUri(uri, token) : default;
|
||||
if (target is not { Kind: LocalActorKind.Person })
|
||||
{
|
||||
Arrival.Drop("unknown-object");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new BlockedBy { AvatarId = target.Id, ActorURI = actor.ActorURI, AccountId = actor.ID, ActivityURI = Id(activity) }, token);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
Arrival.Drop("duplicate");
|
||||
return;
|
||||
}
|
||||
Arrival.Accept("stored");
|
||||
|
||||
await DB.Default.DeleteAsync<Following>(f => f.AvatarId == target.Id && f.TargetActorURI == actor.ActorURI);
|
||||
await DB.Default.DeleteAsync<Follower>(f => f.LocalActorId == target.Id && f.ActorURI == actor.ActorURI);
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.AvatarId == target.Id && e.AuthorAccountId == actor.ID);
|
||||
await DB.Default.DeleteAsync<Notification>(n => n.AvatarId == target.Id && n.FromAccountId == actor.ID);
|
||||
}
|
||||
|
||||
// Undo{Block}: the block it names by id, or else the blocker's block of the persona the inner Block names
|
||||
public static async Task<bool> Undo(JsonNode inner, string innerId, ForeignAvatar actor, ILocalActorService localActors, CancellationToken token)
|
||||
{
|
||||
var block = innerId == default
|
||||
? default
|
||||
: await DB.Default.Find<BlockedBy>().Match(b => b.ActivityURI == innerId && b.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
|
||||
if (block == default && inner is JsonObject && Id(inner["object"]) is { } objectUri
|
||||
&& await localActors.FindByUri(objectUri, token) is { Kind: LocalActorKind.Person } persona)
|
||||
block = await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == persona.Id && b.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
|
||||
if (block == default)
|
||||
return false;
|
||||
await DB.Default.DeleteAsync<BlockedBy>(block.ID);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
undone |= await UndoAnnounce(innerId, actor, token);
|
||||
if (innerType is null or "Dislike")
|
||||
undone |= await UndoDislike(inner, innerId, actor, token);
|
||||
if (innerType is null or "Block")
|
||||
undone |= await BlockHandler.Undo(inner, innerId, actor, _localActors, token);
|
||||
if (innerType is null or "Like" or "EmojiReact")
|
||||
undone |= await _reactions.Withdraw(actor, innerId, inner is JsonObject ? Id(inner["object"]) : default,
|
||||
inner is JsonObject ? Value(inner, "_misskey_reaction") ?? Value(inner, "content") : default, token);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
@@ -43,9 +46,12 @@ namespace PrivaPub.Federation.Outbox
|
||||
if (post.Visibility is PostVisibility.Public or PostVisibility.Unlisted or PostVisibility.FollowersOnly)
|
||||
inboxes.AddRange(await _delivery.FollowerInboxes(author, token));
|
||||
|
||||
//nothing is addressed to an account that blocked the author (it would refuse it, and Mastodon counts on that)
|
||||
var blockers = (await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == author.Id).ExecuteAsync(token))
|
||||
.Select(b => b.ActorURI).ToHashSet(StringComparer.Ordinal);
|
||||
var addressed = post.Mentions.Where(m => !m.IsLocal).Select(m => m.ActorURI)
|
||||
.Concat(post.Visibility == PostVisibility.Direct ? post.To.Concat(post.Cc) : Enumerable.Empty<string>())
|
||||
.Where(uri => !uri.StartsWith(author.BaseAddress + "/", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(uri => !uri.StartsWith(author.BaseAddress + "/", StringComparison.OrdinalIgnoreCase) && !blockers.Contains(uri))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
foreach (var uri in addressed)
|
||||
@@ -58,7 +64,7 @@ namespace PrivaPub.Federation.Outbox
|
||||
if (post.Visibility != PostVisibility.Direct && !string.IsNullOrEmpty(post.InReplyToAccountId))
|
||||
{
|
||||
var parentAuthor = await _dbEntities.ForeignAvatars.MatchID(post.InReplyToAccountId).ExecuteFirstAsync(token);
|
||||
if (parentAuthor != default && !string.IsNullOrEmpty(parentAuthor.InboxURL))
|
||||
if (parentAuthor != default && !string.IsNullOrEmpty(parentAuthor.InboxURL) && !blockers.Contains(parentAuthor.ActorURI))
|
||||
inboxes.Add(parentAuthor.InboxURL);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ namespace PrivaPub.Infrastructure.Data
|
||||
foreach (var pair in new (Func<Task>, string)[]
|
||||
{
|
||||
(() => DB.Default.Index<Block>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "block"),
|
||||
(() => DB.Default.Index<BlockedBy>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.ActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "blocked by"),
|
||||
(() => DB.Default.Index<Mute>().Key(m => m.AvatarId, KeyType.Ascending).Key(m => m.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "mute"),
|
||||
(() => DB.Default.Index<AccountDomainBlock>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.Domain, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "domain block"),
|
||||
(() => DB.Default.Index<Bookmark>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.PostId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "bookmark"),
|
||||
@@ -102,6 +103,7 @@ namespace PrivaPub.Infrastructure.Data
|
||||
})
|
||||
await pair.Item1();
|
||||
await Plain<Block>(token, b => b.TargetActorURI);
|
||||
await Plain<BlockedBy>(token, b => b.ActorURI);
|
||||
await Plain<Mute>(token, m => m.TargetActorURI);
|
||||
await Plain<Report>(token, r => r.IsResolved, r => r.ID);
|
||||
await Unique<DomainBlock>(b => b.Domain, Builders<DomainBlock>.Filter.Type(b => b.Domain, BsonType.String), token);
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace PrivaPub.Middleware
|
||||
.AddSingleton<IActivityHandler, JoinHandler>()
|
||||
.AddSingleton<IActivityHandler, AnnounceHandler>()
|
||||
.AddSingleton<IActivityHandler, FlagHandler>()
|
||||
.AddSingleton<IActivityHandler, BlockHandler>()
|
||||
.AddSingleton<IActivityHandler, CreateHandler>()
|
||||
.AddSingleton<IActivityHandler, DeleteHandler>()
|
||||
.AddSingleton<IActivityHandler, UpdateHandler>()
|
||||
|
||||
@@ -10,6 +10,17 @@ namespace PrivaPub.Models.Social
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
//a remote account that blocked one of our personas (an inbound Block): its posts and notifications are hidden from the
|
||||
//persona, the follows between them are gone, and the relationship says blocked_by, until it sends Undo{Block}
|
||||
public class BlockedBy : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string ActorURI { get; set; }
|
||||
public string AccountId { get; set; }//the blocker's ForeignAvatar.ID
|
||||
public string ActivityURI { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Mute : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
|
||||
Reference in new issue
Block a user