Likes and boosts arrive, count, notify, and can be taken back
- Like: a Favourite per account and post (unique), the post's counter, a Favourite notification for a local author; only on posts the liker could see (public and unlisted, or followers-only and direct when they were addressed). - Announce: the original is always refetched from its own origin (or is ours), and must be public or unlisted. A boost of a local post counts and notifies; a boost by an account a persona follows is stored as a reblog row and fanned out to its followers' home timelines (as a reblog, where reblogs are wanted). A boost by a stranger of a stranger's post is ignored. FEP-1b12 group announces of activities wait for P4. - Undo of a Like or an Announce reverses each of them; an Undo naming only an id is matched against follows, likes and boosts in turn. 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
e99b76dbd7
commit
b34c71775c
7 files changed
+349
-20
No files matched your search
@@ -53,7 +53,7 @@ namespace PrivaPub.Tests.Federation
|
|||||||
_processor = new InboxProcessor(remote, new IActivityHandler[]
|
_processor = new InboxProcessor(remote, new IActivityHandler[]
|
||||||
{
|
{
|
||||||
new FollowHandler(db, _local, remote, delivery),
|
new FollowHandler(db, _local, remote, delivery),
|
||||||
new UndoHandler(db, _local, remote, delivery),
|
new UndoHandler(db, _local),
|
||||||
new CreateHandler(db, _local, remote, delivery, _blocks, new Fanout(db), new RemotePosts(db, _local, remote, _blocks, queue)),
|
new CreateHandler(db, _local, remote, delivery, _blocks, new Fanout(db), new RemotePosts(db, _local, remote, _blocks, queue)),
|
||||||
new DeleteHandler(db, _local, remote, delivery),
|
new DeleteHandler(db, _local, remote, delivery),
|
||||||
new UpdateHandler(db, _local, remote)
|
new UpdateHandler(db, _local, remote)
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using MongoDB.Entities;
|
||||||
|
|
||||||
|
using PrivaPub.ClientModels.Post;
|
||||||
|
using PrivaPub.ClientModels.Social;
|
||||||
|
using PrivaPub.Federation.Objects;
|
||||||
|
using PrivaPub.Models.Post;
|
||||||
|
using PrivaPub.Models.Social;
|
||||||
|
using PrivaPub.Tests.Support;
|
||||||
|
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace PrivaPub.Tests.Federation
|
||||||
|
{
|
||||||
|
[Trait("Category", "Integration")]
|
||||||
|
public sealed class InteractionTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
Harness _harness;
|
||||||
|
|
||||||
|
public async ValueTask InitializeAsync()
|
||||||
|
{
|
||||||
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
||||||
|
_harness = await Harness.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_harness != default)
|
||||||
|
await _harness.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<(string Root, Post Post)> LocalPost(string visibility = default)
|
||||||
|
{
|
||||||
|
var (root, alice) = await _harness.Persona("alice");
|
||||||
|
var view = (ViewPost)(await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = "like me", Visibility = visibility },
|
||||||
|
TestContext.Current.CancellationToken)).Data;
|
||||||
|
return (root, await DB.Default.Find<Post>().OneAsync(view.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_like_counts_once_notifies_and_can_be_undone()
|
||||||
|
{
|
||||||
|
var token = TestContext.Current.CancellationToken;
|
||||||
|
var (_, post) = await LocalPost();
|
||||||
|
var carol = new RemoteActor(_harness.Peer, "carol");
|
||||||
|
var like = new JsonObject { ["id"] = $"{Origin(carol)}/likes/{Guid.NewGuid():N}", ["type"] = "Like", ["actor"] = carol.Id, ["object"] = post.ObjectURI };
|
||||||
|
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", like);
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", new JsonObject { ["id"] = $"{Origin(carol)}/likes/{Guid.NewGuid():N}", ["type"] = "Like", ["actor"] = carol.Id, ["object"] = post.ObjectURI });
|
||||||
|
|
||||||
|
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
||||||
|
Assert.Equal(NotificationType.Favourite, (await DB.Default.Find<Notification>().Match(n => n.AvatarId == post.GroupUserId).ExecuteSingleAsync(token)).Type);
|
||||||
|
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(carol)}/undos/{Guid.NewGuid():N}", ["type"] = "Undo", ["actor"] = carol.Id, ["object"] = like
|
||||||
|
});
|
||||||
|
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_stranger_cannot_like_a_followers_only_post()
|
||||||
|
{
|
||||||
|
var token = TestContext.Current.CancellationToken;
|
||||||
|
var (_, post) = await LocalPost("followersonly");
|
||||||
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
||||||
|
|
||||||
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(mallory)}/likes/{Guid.NewGuid():N}", ["type"] = "Like", ["actor"] = mallory.Id, ["object"] = post.ObjectURI
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_boost_of_a_local_post_counts_notifies_and_can_be_undone()
|
||||||
|
{
|
||||||
|
var token = TestContext.Current.CancellationToken;
|
||||||
|
var (_, post) = await LocalPost();
|
||||||
|
var carol = new RemoteActor(_harness.Peer, "carol");
|
||||||
|
var announce = new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(carol)}/announces/{Guid.NewGuid():N}", ["type"] = "Announce", ["actor"] = carol.Id, ["object"] = post.ObjectURI,
|
||||||
|
["to"] = new JsonArray(Addressing.Public), ["cc"] = new JsonArray(carol.Id + "/followers")
|
||||||
|
};
|
||||||
|
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", announce);
|
||||||
|
|
||||||
|
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).ReblogsCount);
|
||||||
|
Assert.Equal(NotificationType.Reblog, (await DB.Default.Find<Notification>().Match(n => n.AvatarId == post.GroupUserId).ExecuteSingleAsync(token)).Type);
|
||||||
|
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(carol)}/undos/{Guid.NewGuid():N}", ["type"] = "Undo", ["actor"] = carol.Id, ["object"] = announce["id"]!.GetValue<string>()
|
||||||
|
});
|
||||||
|
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).ReblogsCount);
|
||||||
|
Assert.False(await DB.Default.Find<Post>().Match(p => p.ObjectURI == announce["id"]!.GetValue<string>()).ExecuteAnyAsync(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_followed_account_boosting_a_remote_post_puts_it_in_home()
|
||||||
|
{
|
||||||
|
var token = TestContext.Current.CancellationToken;
|
||||||
|
var (bobRoot, bob) = await _harness.Persona("bob");
|
||||||
|
var carol = new RemoteActor(_harness.Peer, "carol");
|
||||||
|
var dave = new RemoteActor(_harness.Peer, "dave");
|
||||||
|
var stranger = new RemoteActor(_harness.Peer, "stranger");
|
||||||
|
await _harness.Follows.Follow(bobRoot, new FollowForm { AvatarId = bob.Id, Target = carol.Id }, token);
|
||||||
|
await DB.Default.Update<Following>().Match(f => f.AvatarId == bob.Id).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
||||||
|
var noteId = $"{Origin(dave)}/notes/{Guid.NewGuid():N}";
|
||||||
|
_harness.Peer.Serve(new Uri(noteId).AbsolutePath, new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = noteId, ["type"] = "Note", ["attributedTo"] = dave.Id, ["content"] = "<p>worth sharing</p>", ["to"] = new JsonArray(Addressing.Public)
|
||||||
|
}.ToJsonString());
|
||||||
|
|
||||||
|
await _harness.Deliver(stranger, "/human-centipede", new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(stranger)}/announces/{Guid.NewGuid():N}", ["type"] = "Announce", ["actor"] = stranger.Id, ["object"] = noteId,
|
||||||
|
["to"] = new JsonArray(Addressing.Public)
|
||||||
|
});
|
||||||
|
Assert.False(await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteAnyAsync(token));
|
||||||
|
|
||||||
|
await _harness.Deliver(carol, "/human-centipede", new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = $"{Origin(carol)}/announces/{Guid.NewGuid():N}", ["type"] = "Announce", ["actor"] = carol.Id, ["object"] = noteId,
|
||||||
|
["to"] = new JsonArray(Addressing.Public)
|
||||||
|
});
|
||||||
|
|
||||||
|
var home = (List<ViewPost>)(await _harness.Timelines.Home(bobRoot, bob.Id, default, 20, token)).Data;
|
||||||
|
var boosted = Assert.Single(home);
|
||||||
|
Assert.Equal("<p>worth sharing</p>", boosted.ReblogOf.ContentHtml);
|
||||||
|
Assert.Equal(carol.Id, boosted.AuthorActorURI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,9 @@ namespace PrivaPub.Tests.Support
|
|||||||
new FollowHandler(Db, Local, Remote, Delivery),
|
new FollowHandler(Db, Local, Remote, Delivery),
|
||||||
new AcceptHandler(Db, Local),
|
new AcceptHandler(Db, Local),
|
||||||
new RejectHandler(Db, Local),
|
new RejectHandler(Db, Local),
|
||||||
new UndoHandler(Db, Local, Remote, Delivery),
|
new UndoHandler(Db, Local),
|
||||||
|
new LikeHandler(Db),
|
||||||
|
new AnnounceHandler(Db, Local, RemotePosts, Fanout),
|
||||||
new CreateHandler(Db, Local, Remote, Delivery, new NoBlocks(), Fanout, RemotePosts),
|
new CreateHandler(Db, Local, Remote, Delivery, new NoBlocks(), Fanout, RemotePosts),
|
||||||
new DeleteHandler(Db, Local, Remote, Delivery),
|
new DeleteHandler(Db, Local, Remote, Delivery),
|
||||||
new UpdateHandler(Db, Local, Remote)
|
new UpdateHandler(Db, Local, Remote)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using MongoDB.Entities;
|
||||||
|
|
||||||
|
using PrivaPub.Domain.Social;
|
||||||
|
using PrivaPub.Domain.Timelines;
|
||||||
|
using PrivaPub.Federation.Actors;
|
||||||
|
using PrivaPub.Federation.Objects;
|
||||||
|
using PrivaPub.Infrastructure.Ids;
|
||||||
|
using PrivaPub.Models.Post;
|
||||||
|
using PrivaPub.Models.Social;
|
||||||
|
using PrivaPub.Models.User;
|
||||||
|
using PrivaPub.StaticServices;
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||||
|
|
||||||
|
using PostEntity = PrivaPub.Models.Post.Post;
|
||||||
|
|
||||||
|
namespace PrivaPub.Federation.Inbox.Handlers
|
||||||
|
{
|
||||||
|
public class AnnounceHandler : IActivityHandler
|
||||||
|
{
|
||||||
|
readonly DbEntities _dbEntities;
|
||||||
|
readonly ILocalActorService _localActors;
|
||||||
|
readonly IRemotePosts _remotePosts;
|
||||||
|
readonly IFanout _fanout;
|
||||||
|
|
||||||
|
public AnnounceHandler(DbEntities dbEntities, ILocalActorService localActors, IRemotePosts remotePosts, IFanout fanout)
|
||||||
|
{
|
||||||
|
_dbEntities = dbEntities;
|
||||||
|
_localActors = localActors;
|
||||||
|
_remotePosts = remotePosts;
|
||||||
|
_fanout = fanout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Type => "Announce";
|
||||||
|
|
||||||
|
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = activity["object"];
|
||||||
|
if (inner is JsonObject && Value(inner, "type") is "Create" or "Update" or "Delete" or "Like" or "Dislike" or "Undo" or "Add" or "Remove" or "Block")
|
||||||
|
return;
|
||||||
|
var objectUri = Id(inner);
|
||||||
|
var announceId = Id(activity);
|
||||||
|
if (objectUri == default || announceId == default)
|
||||||
|
return;
|
||||||
|
if (await _dbEntities.Posts.Match(p => p.ObjectURI == announceId).ExecuteAnyAsync(token))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isLocal = objectUri.StartsWith(_localActors.BaseAddress + "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var original = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && !p.DeletedAt.HasValue && p.ReblogOfPostId == null).ExecuteFirstAsync(token);
|
||||||
|
var followed = await _dbEntities.Followings.Match(f => f.TargetActorURI == actor.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
|
||||||
|
if (original is not { IsFederatedCopy: false } && !followed)
|
||||||
|
return;
|
||||||
|
if (original == default && !isLocal)
|
||||||
|
original = await _remotePosts.StoreContext(objectUri, 0, token);
|
||||||
|
if (original == default || original.Visibility is not (PostVisibility.Public or PostVisibility.Unlisted))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var to = Strings(activity["to"]);
|
||||||
|
var cc = Strings(activity["cc"]);
|
||||||
|
var published = DateTimeOffset.TryParse(Value(activity, "published"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var at)
|
||||||
|
? at.UtcDateTime
|
||||||
|
: DateTime.UtcNow;
|
||||||
|
var reblog = new PostEntity
|
||||||
|
{
|
||||||
|
ID = PrivacyIds.At(published),
|
||||||
|
ObjectURI = announceId,
|
||||||
|
ActivityURI = announceId,
|
||||||
|
ActorURI = actor.ActorURI,
|
||||||
|
AuthorAccountId = actor.ID,
|
||||||
|
ReblogOfPostId = original.ID,
|
||||||
|
Visibility = Addressing.Classify(to, cc, actor.FollowersURL),
|
||||||
|
To = to,
|
||||||
|
Cc = cc,
|
||||||
|
IsFederatedCopy = true,
|
||||||
|
CreationDate = published,
|
||||||
|
UpdateDate = published
|
||||||
|
};
|
||||||
|
if (reblog.Visibility == PostVisibility.Direct)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DB.Default.SaveAsync(reblog, token);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DB.Default.Update<PostEntity>().MatchID(original.ID).Modify(b => b.Inc(p => p.ReblogsCount, 1)).ExecuteAsync(token);
|
||||||
|
if (!original.IsFederatedCopy)
|
||||||
|
await Notifications.Add(original.GroupUserId, NotificationType.Reblog, actor.ID, actor.ActorURI, original.ID, token);
|
||||||
|
await _fanout.Distribute(reblog, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<string> Strings(JsonNode node) => node switch
|
||||||
|
{
|
||||||
|
JsonArray array => array.Select(Id).Where(id => id != default).ToList(),
|
||||||
|
JsonNode single when Id(single) is { } id => new List<string> { id },
|
||||||
|
_ => new List<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using MongoDB.Entities;
|
||||||
|
|
||||||
|
using PrivaPub.Domain.Social;
|
||||||
|
using PrivaPub.Models.Post;
|
||||||
|
using PrivaPub.Models.Social;
|
||||||
|
using PrivaPub.Models.User;
|
||||||
|
using PrivaPub.StaticServices;
|
||||||
|
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||||
|
|
||||||
|
using PostEntity = PrivaPub.Models.Post.Post;
|
||||||
|
|
||||||
|
namespace PrivaPub.Federation.Inbox.Handlers
|
||||||
|
{
|
||||||
|
public class LikeHandler : IActivityHandler
|
||||||
|
{
|
||||||
|
readonly DbEntities _dbEntities;
|
||||||
|
|
||||||
|
public LikeHandler(DbEntities dbEntities)
|
||||||
|
{
|
||||||
|
_dbEntities = dbEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Type => "Like";
|
||||||
|
|
||||||
|
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||||
|
{
|
||||||
|
var objectUri = Id(activity["object"]);
|
||||||
|
var post = objectUri == default
|
||||||
|
? default
|
||||||
|
: await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && !p.DeletedAt.HasValue && p.ReblogOfPostId == null).ExecuteFirstAsync(token);
|
||||||
|
if (post == default || !MaySee(post, actor))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DB.Default.SaveAsync(new Favourite { AccountId = actor.ID, ActorURI = actor.ActorURI, PostId = post.ID, ActivityURI = Id(activity) }, token);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(b => b.Inc(p => p.FavouritesCount, 1)).ExecuteAsync(token);
|
||||||
|
if (!post.IsFederatedCopy)
|
||||||
|
await Notifications.Add(post.GroupUserId, NotificationType.Favourite, actor.ID, actor.ActorURI, post.ID, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MaySee(PostEntity post, ForeignAvatar actor) =>
|
||||||
|
post.Visibility is PostVisibility.Public or PostVisibility.Unlisted
|
||||||
|
|| post.Visibility is PostVisibility.FollowersOnly or PostVisibility.Direct
|
||||||
|
&& (post.To.Contains(actor.ActorURI) || post.Cc.Contains(actor.ActorURI) || post.Mentions.Any(m => m.ActorURI == actor.ActorURI));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
using MongoDB.Entities;
|
using MongoDB.Entities;
|
||||||
|
|
||||||
using PrivaPub.Federation.Actors;
|
using PrivaPub.Federation.Actors;
|
||||||
using PrivaPub.Federation.Objects;
|
|
||||||
using PrivaPub.Federation.Outbox;
|
|
||||||
using PrivaPub.Federation.Rendering;
|
|
||||||
using PrivaPub.Models.Federation;
|
using PrivaPub.Models.Federation;
|
||||||
using PrivaPub.Models.Group;
|
using PrivaPub.Models.Social;
|
||||||
using PrivaPub.Models.Post;
|
|
||||||
using PrivaPub.Models.User;
|
using PrivaPub.Models.User;
|
||||||
using PrivaPub.StaticServices;
|
using PrivaPub.StaticServices;
|
||||||
|
|
||||||
@@ -23,38 +19,37 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
|||||||
{
|
{
|
||||||
readonly DbEntities _dbEntities;
|
readonly DbEntities _dbEntities;
|
||||||
readonly ILocalActorService _localActors;
|
readonly ILocalActorService _localActors;
|
||||||
readonly IRemoteActorService _remoteActors;
|
|
||||||
readonly IDeliveryService _delivery;
|
|
||||||
|
|
||||||
public UndoHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery)
|
public UndoHandler(DbEntities dbEntities, ILocalActorService localActors)
|
||||||
{
|
{
|
||||||
_dbEntities = dbEntities;
|
_dbEntities = dbEntities;
|
||||||
_localActors = localActors;
|
_localActors = localActors;
|
||||||
_remoteActors = remoteActors;
|
|
||||||
_delivery = delivery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Type => "Undo";
|
public string Type => "Undo";
|
||||||
|
|
||||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||||
{
|
{
|
||||||
var undo = activity;
|
var inner = activity["object"];
|
||||||
|
|
||||||
var inner = undo["object"];
|
|
||||||
var innerType = inner is JsonObject ? Value(inner, "type") : default;
|
var innerType = inner is JsonObject ? Value(inner, "type") : default;
|
||||||
var innerId = Id(inner);
|
var innerId = Id(inner);
|
||||||
|
|
||||||
if (inner is JsonObject && Id(inner["actor"]) != actor.ActorURI)
|
if (innerType is null or "Follow")
|
||||||
return;
|
await UndoFollow(inner, innerId, actor, token);
|
||||||
if (innerType is not (null or "Follow"))
|
if (innerType is null or "Like")
|
||||||
return;
|
await UndoLike(inner, innerId, actor, token);
|
||||||
|
if (innerType is null or "Announce")
|
||||||
|
await UndoAnnounce(innerId, actor, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UndoFollow(JsonNode inner, string innerId, ForeignAvatar actor, CancellationToken token)
|
||||||
|
{
|
||||||
var followers = await _dbEntities.Followers.Match(f => f.ActorURI == actor.ActorURI).ExecuteAsync(token);
|
var followers = await _dbEntities.Followers.Match(f => f.ActorURI == actor.ActorURI).ExecuteAsync(token);
|
||||||
var targetUri = inner is JsonObject ? Id(inner["object"]) : default;
|
var targetUri = inner is JsonObject ? Id(inner["object"]) : default;
|
||||||
|
var target = targetUri == default ? default : await _localActors.FindByUri(targetUri, token);
|
||||||
foreach (var follower in followers)
|
foreach (var follower in followers)
|
||||||
{
|
{
|
||||||
var matchesActivity = innerId != default && follower.FollowActivityURI == innerId;
|
var matchesActivity = innerId != default && follower.FollowActivityURI == innerId;
|
||||||
var target = targetUri == default ? default : await _localActors.FindByUri(targetUri, token);
|
|
||||||
var matchesTarget = target != default && target.Id == follower.LocalActorId && target.Kind == follower.LocalActorKind;
|
var matchesTarget = target != default && target.Id == follower.LocalActorId && target.Kind == follower.LocalActorKind;
|
||||||
if (!matchesActivity && !matchesTarget)
|
if (!matchesActivity && !matchesTarget)
|
||||||
continue;
|
continue;
|
||||||
@@ -63,7 +58,38 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
|||||||
if (follower.LocalActorKind == LocalActorKind.Group)
|
if (follower.LocalActorKind == LocalActorKind.Group)
|
||||||
await RemoveForeignMember(follower.LocalActorId, actor.ActorURI, token);
|
await RemoveForeignMember(follower.LocalActorId, actor.ActorURI, token);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UndoLike(JsonNode inner, string innerId, ForeignAvatar actor, CancellationToken token)
|
||||||
|
{
|
||||||
|
var favourite = innerId == default
|
||||||
|
? default
|
||||||
|
: await _dbEntities.Favourites.Match(f => f.ActivityURI == innerId && f.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
|
||||||
|
if (favourite == default && inner is JsonObject && Id(inner["object"]) is { } objectUri)
|
||||||
|
{
|
||||||
|
var post = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri).ExecuteFirstAsync(token);
|
||||||
|
favourite = post == default
|
||||||
|
? default
|
||||||
|
: await _dbEntities.Favourites.Match(f => f.PostId == post.ID && f.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
|
||||||
|
}
|
||||||
|
if (favourite == default)
|
||||||
return;
|
return;
|
||||||
|
await DB.Default.DeleteAsync<Favourite>(favourite.ID);
|
||||||
|
await DB.Default.Update<PostEntity>().MatchID(favourite.PostId).Modify(b => b.Inc(p => p.FavouritesCount, -1)).ExecuteAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UndoAnnounce(string innerId, ForeignAvatar actor, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (innerId == default)
|
||||||
|
return;
|
||||||
|
var reblog = await _dbEntities.Posts
|
||||||
|
.Match(p => p.ObjectURI == innerId && p.ActorURI == actor.ActorURI && p.ReblogOfPostId != null)
|
||||||
|
.ExecuteFirstAsync(token);
|
||||||
|
if (reblog == default)
|
||||||
|
return;
|
||||||
|
await DB.Default.DeleteAsync<PostEntity>(reblog.ID);
|
||||||
|
await DB.Default.DeleteAsync<TimelineEntry>(e => e.PostId == reblog.ID);
|
||||||
|
await DB.Default.Update<PostEntity>().MatchID(reblog.ReblogOfPostId).Modify(b => b.Inc(p => p.ReblogsCount, -1)).ExecuteAsync(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,8 @@ namespace PrivaPub.Middleware
|
|||||||
.AddSingleton<IActivityHandler, AcceptHandler>()
|
.AddSingleton<IActivityHandler, AcceptHandler>()
|
||||||
.AddSingleton<IActivityHandler, RejectHandler>()
|
.AddSingleton<IActivityHandler, RejectHandler>()
|
||||||
.AddSingleton<IActivityHandler, UndoHandler>()
|
.AddSingleton<IActivityHandler, UndoHandler>()
|
||||||
|
.AddSingleton<IActivityHandler, LikeHandler>()
|
||||||
|
.AddSingleton<IActivityHandler, AnnounceHandler>()
|
||||||
.AddSingleton<IActivityHandler, CreateHandler>()
|
.AddSingleton<IActivityHandler, CreateHandler>()
|
||||||
.AddSingleton<IActivityHandler, DeleteHandler>()
|
.AddSingleton<IActivityHandler, DeleteHandler>()
|
||||||
.AddSingleton<IActivityHandler, UpdateHandler>()
|
.AddSingleton<IActivityHandler, UpdateHandler>()
|
||||||
|
|||||||
Reference in new issue
Block a user