2026-10-04 23:49:15 +02:00
|
|
|
using MongoDB.Driver;
|
|
|
|
|
using MongoDB.Entities;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.Federation.Actors;
|
2026-10-05 06:10:25 +02:00
|
|
|
using PrivaPub.Federation.Objects;
|
2026-10-04 23:49:15 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2026-10-05 06:10:25 +02:00
|
|
|
if (Community(activity, actor) is { } community)
|
|
|
|
|
{
|
|
|
|
|
await Ban(activity, target, community, token);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-10-04 23:49:15 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 06:10:25 +02:00
|
|
|
// 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
|
2026-10-04 23:49:15 +02:00
|
|
|
public static async Task<bool> Undo(JsonNode inner, string innerId, ForeignAvatar actor, ILocalActorService localActors, CancellationToken token)
|
|
|
|
|
{
|
2026-10-05 06:10:25 +02:00
|
|
|
var by = Community(inner, actor) ?? actor.ActorURI;
|
2026-10-04 23:49:15 +02:00
|
|
|
var block = innerId == default
|
|
|
|
|
? default
|
2026-10-05 06:10:25 +02:00
|
|
|
: await DB.Default.Find<BlockedBy>().Match(b => b.ActivityURI == innerId && b.ActorURI == by).ExecuteFirstAsync(token);
|
2026-10-04 23:49:15 +02:00
|
|
|
if (block == default && inner is JsonObject && Id(inner["object"]) is { } objectUri
|
|
|
|
|
&& await localActors.FindByUri(objectUri, token) is { Kind: LocalActorKind.Person } persona)
|
2026-10-05 06:10:25 +02:00
|
|
|
block = await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == persona.Id && b.ActorURI == by).ExecuteFirstAsync(token);
|
2026-10-04 23:49:15 +02:00
|
|
|
if (block == default)
|
|
|
|
|
return false;
|
|
|
|
|
await DB.Default.DeleteAsync<BlockedBy>(block.ID);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|