using MongoDB.Driver; using MongoDB.Entities; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Objects; 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; } if (Community(activity, actor) is { } community) { await Ban(activity, target, community, token); 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(f => f.AvatarId == target.Id && f.TargetActorURI == actor.ActorURI); await DB.Default.DeleteAsync(f => f.LocalActorId == target.Id && f.ActorURI == actor.ActorURI); await DB.Default.DeleteAsync(e => e.AvatarId == target.Id && e.AuthorAccountId == actor.ID); await DB.Default.DeleteAsync(n => n.AvatarId == target.Id && n.FromAccountId == actor.ID); } // Lemmy also sends a community's ban to the banned account's own server, as its moderator's Block with the community // as `target`: a ban from that community, as the community's Announce brings it, never the moderator's own block. The // moderator must be on the community's server. static string Community(JsonNode block, ForeignAvatar actor) => block is JsonObject && Id(block["target"]) is { } community && community != actor.ActorURI && Origin.Same(community, actor.ActorURI) ? community : default; async Task Ban(JsonNode activity, LocalActor persona, string communityUri, CancellationToken token) { var community = await _dbEntities.ForeignAvatars.Match(f => f.ActorURI == communityUri && f.AvatarType == AvatarType.Group).ExecuteFirstAsync(token); if (community == default) { Arrival.Drop("unknown-community"); return; } try { await DB.Default.SaveAsync(new BlockedBy { AvatarId = persona.Id, ActorURI = community.ActorURI, AccountId = community.ID, ActivityURI = Id(activity) }, token); Arrival.Accept("banned"); } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { Arrival.Drop("duplicate"); } } // Undo{Block}: the block it names by id, or else the blocker's block of the persona the inner Block names; a // community's ban sent by its moderator is the community's public static async Task Undo(JsonNode inner, string innerId, ForeignAvatar actor, ILocalActorService localActors, CancellationToken token) { var by = Community(inner, actor) ?? actor.ActorURI; var block = innerId == default ? default : await DB.Default.Find().Match(b => b.ActivityURI == innerId && b.ActorURI == by).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().Match(b => b.AvatarId == persona.Id && b.ActorURI == by).ExecuteFirstAsync(token); if (block == default) return false; await DB.Default.DeleteAsync(block.ID); return true; } } }