diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs index dcc3c65..47c04e7 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs @@ -355,6 +355,122 @@ public class ActivityPubDeliveryService( return successCount > 0; } + public async Task SendLikeActivityToLocalPostAsync( + Guid publisherId, + Guid postId, + Guid actorId + ) + { + var publisher = await db.Publishers + .Include(p => p.Members) + .FirstOrDefaultAsync(p => p.Id == publisherId); + + if (publisher == null) + return false; + + var publisherActor = await GetLocalActorAsync(publisherId); + if (publisherActor == null) + return false; + + var actor = await db.FediverseActors + .FirstOrDefaultAsync(a => a.Id == actorId); + + if (actor == null) + return false; + + var actorUrl = publisherActor.Uri; + var postUrl = $"https://{Domain}/posts/{postId}"; + + var activity = new Dictionary + { + ["@context"] = "https://www.w3.org/ns/activitystreams", + ["id"] = $"{actorUrl}/likes/{Guid.NewGuid()}", + ["type"] = "Like", + ["actor"] = actor.Uri, + ["object"] = postUrl, + ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, + ["cc"] = new[] { $"{actorUrl}/followers" } + }; + + var followers = await db.FediverseRelationships + .Include(r => r.TargetActor) + .Where(r => r.ActorId == publisherActor.Id && r.IsFollowedBy) + .Select(r => r.TargetActor) + .ToListAsync(); + + var successCount = 0; + + foreach (var follower in followers) + { + if (follower.InboxUri == null) continue; + var success = await SendActivityToInboxAsync(activity, follower.InboxUri, actorUrl); + if (success) + successCount++; + } + + logger.LogInformation("Sent Like activity for post {PostId} to {Count}/{Total} followers", + postId, successCount, followers.Count); + + return successCount > 0; + } + + public async Task SendUndoLikeActivityAsync( + Guid publisherId, + Guid postId, + string likeActivityId + ) + { + var publisher = await db.Publishers + .Include(p => p.Members) + .FirstOrDefaultAsync(p => p.Id == publisherId); + + if (publisher == null) + return false; + + var publisherActor = await GetLocalActorAsync(publisherId); + if (publisherActor == null) + return false; + + var actorUrl = publisherActor.Uri; + var postUrl = $"https://{Domain}/posts/{postId}"; + + var activity = new Dictionary + { + ["@context"] = "https://www.w3.org/ns/activitystreams", + ["id"] = $"{actorUrl}/undo/{Guid.NewGuid()}", + ["type"] = "Undo", + ["actor"] = actorUrl, + ["object"] = new Dictionary + { + ["type"] = "Like", + ["object"] = postUrl + }, + ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, + ["cc"] = new[] { $"{actorUrl}/followers" } + }; + + var followers = await db.FediverseRelationships + .Include(r => r.TargetActor) + .Where(r => r.ActorId == publisherActor.Id && r.IsFollowedBy) + .Select(r => r.TargetActor) + .ToListAsync(); + + var successCount = 0; + + foreach (var follower in followers) + { + if (follower.InboxUri == null) continue; + var success = await SendActivityToInboxAsync(activity, follower.InboxUri, actorUrl); + if (success) + successCount++; + } + + logger.LogInformation("Sent Undo Like activity for post {PostId} to {Count}/{Total} followers", + postId, successCount, followers.Count); + + return successCount > 0; + } + public async Task SendLikeActivityAsync( Guid postId, Guid accountId, diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 829a0ec..ffef682 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -26,7 +26,8 @@ public partial class PostService( FileReferenceService.FileReferenceServiceClient fileRefs, Publisher.PublisherService ps, WebReaderService reader, - AccountService.AccountServiceClient accounts + AccountService.AccountServiceClient accounts, + ActivityPub.ActivityPubDeliveryService activityPubDeliveryService ) { private const string PostFileUsageIdentifier = "post"; @@ -582,6 +583,66 @@ public partial class PostService( await db.SaveChangesAsync(); + // Send ActivityPub Like/Undo activities if post's publisher has actor + if (post.PublisherId.HasValue && reaction.AccountId.HasValue) + { + var publisherActor = await activityPubDeliveryService.GetLocalActorAsync(post.PublisherId.Value); + + if (publisherActor != null && reaction.Attitude == Shared.Models.PostReactionAttitude.Positive) + { + var likerActor = await db.FediverseActors + .FirstOrDefaultAsync(a => a.PublisherId.HasValue && a.PublisherId.Value == reaction.AccountId.Value); + + if (likerActor != null) + { + if (!isRemoving) + { + // Sending Like - deliver to publisher's remote followers + _ = Task.Run(async () => + { + try + { + using var scope = factory.CreateScope(); + var deliveryService = scope.ServiceProvider + .GetRequiredService(); + await deliveryService.SendLikeActivityToLocalPostAsync( + post.PublisherId.Value, + post.Id, + likerActor.Id + ); + } + catch (Exception ex) + { + logger.LogError($"Error sending ActivityPub Like: {ex.Message}"); + } + }); + } + else + { + // Sending Undo Like - deliver to publisher's remote followers + _ = Task.Run(async () => + { + try + { + using var scope = factory.CreateScope(); + var deliveryService = scope.ServiceProvider + .GetRequiredService(); + await deliveryService.SendUndoLikeActivityAsync( + post.PublisherId.Value, + post.Id, + string.Empty + ); + } + catch (Exception ex) + { + logger.LogError($"Error sending ActivityPub Undo Like: {ex.Message}"); + } + }); + } + } + } + } + if (!isSelfReact) _ = Task.Run(async () => {