From 7c5c92a501629ed1c7c19621729ac48ff65ba2ed Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 1 Jan 2026 13:37:29 +0800 Subject: [PATCH] :sparkles: Activitypub supports reply and repost --- .../ActivityPub/ActivityPubController.cs | 7 +- .../ActivityPub/ActivityPubDeliveryService.cs | 38 ++++----- .../ActivityPub/ActivityPubObjectFactory.cs | 54 ++++++++++-- DysonNetwork.Sphere/Post/PostService.cs | 84 ++++++++++--------- .../Startup/ServiceCollectionExtensions.cs | 3 +- 5 files changed, 114 insertions(+), 72 deletions(-) diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs index d26dc81..6410e45 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs @@ -15,7 +15,8 @@ public class ActivityPubController( ILogger logger, ActivityPubSignatureService signatureService, ActivityPubActivityHandler activityHandler, - ActivityPubKeyService keyService + ActivityPubKeyService keyService, + ActivityPubObjectFactory objFactory ) : ControllerBase { private string Domain => configuration["ActivityPub:Domain"] ?? "localhost"; @@ -159,7 +160,7 @@ public class ActivityPubController( var items = posts.Select(post => { - var postObject = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl); + var postObject = objFactory.CreatePostObject(post, actorUrl); postObject["url"] = $"https://{Domain}/posts/{post.Id}"; return new Dictionary { @@ -167,7 +168,7 @@ public class ActivityPubController( ["type"] = "Create", ["actor"] = actorUrl, ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["to"] = ActivityPubObjectFactory.PublicTo, + ["to"] = new[] { ActivityPubObjectFactory.PublicTo }, ["cc"] = new[] { $"{actorUrl}/followers" }, ["@object"] = postObject }; diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs index 10f9885..e4a1559 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs @@ -10,7 +10,8 @@ public class ActivityPubDeliveryService( ActivityPubQueueService queueService, IConfiguration configuration, ILogger logger, - IClock clock + IClock clock, + ActivityPubObjectFactory objFactory ) { private string Domain => configuration["ActivityPub:Domain"] ?? "localhost"; @@ -53,7 +54,7 @@ public class ActivityPubDeliveryService( string targetActorUri ) { - var localActor = await GetLocalActorAsync(publisherId); + var localActor = await objFactory.GetLocalActorAsync(publisherId); if (localActor == null) return false; @@ -106,7 +107,7 @@ public class ActivityPubDeliveryService( string targetActorUri ) { - var localActor = await GetLocalActorAsync(publisherId); + var localActor = await objFactory.GetLocalActorAsync(publisherId); if (localActor == null) return false; @@ -140,7 +141,7 @@ public class ActivityPubDeliveryService( if (relationship == null) return false; var success = await EnqueueActivityDeliveryAsync("Undo", activity, actorUrl, targetActor.InboxUri, activityId); - + db.Remove(relationship); await db.SaveChangesAsync(); @@ -151,7 +152,7 @@ public class ActivityPubDeliveryService( { if (post.PublisherId == null) return false; - var localActor = await GetLocalActorAsync(post.PublisherId.Value); + var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value); if (localActor == null) return false; @@ -166,9 +167,9 @@ public class ActivityPubDeliveryService( ["type"] = "Create", ["actor"] = actorUrl, ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["to"] = ActivityPubObjectFactory.PublicTo, + ["to"] = new[] { ActivityPubObjectFactory.PublicTo }, ["cc"] = new[] { $"{actorUrl}/followers" }, - ["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) + ["object"] = objFactory.CreatePostObject(post, actorUrl) }; var followers = await GetRemoteFollowersAsync(); @@ -187,7 +188,7 @@ public class ActivityPubDeliveryService( { if (post.PublisherId == null) return false; - var localActor = await GetLocalActorAsync(post.PublisherId.Value); + var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value); if (localActor == null) return false; @@ -202,9 +203,9 @@ public class ActivityPubDeliveryService( ["type"] = "Update", ["actor"] = actorUrl, ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["to"] = ActivityPubObjectFactory.PublicTo, + ["to"] = new[] { ActivityPubObjectFactory.PublicTo }, ["cc"] = new[] { $"{actorUrl}/followers" }, - ["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) + ["object"] = objFactory.CreatePostObject(post, actorUrl) }; var followers = await GetRemoteFollowersAsync(); @@ -223,7 +224,7 @@ public class ActivityPubDeliveryService( { if (post.PublisherId == null) return false; - var localActor = await GetLocalActorAsync(post.PublisherId.Value); + var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value); if (localActor == null) return false; @@ -499,8 +500,10 @@ public class ActivityPubDeliveryService( stats.TotalDeliveries = deliveries.Count; stats.SentDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Sent); - stats.FailedDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Failed || d.Status == DeliveryStatus.ExhaustedRetries); - stats.PendingDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Pending || d.Status == DeliveryStatus.Processing); + stats.FailedDeliveries = deliveries.Count(d => + d.Status == DeliveryStatus.Failed || d.Status == DeliveryStatus.ExhaustedRetries); + stats.PendingDeliveries = + deliveries.Count(d => d.Status == DeliveryStatus.Pending || d.Status == DeliveryStatus.Processing); return stats; } @@ -578,13 +581,6 @@ public class ActivityPubDeliveryService( .ToListAsync(); } - public async Task GetLocalActorAsync(Guid publisherId) - { - return await db.FediverseActors - .Include(a => a.Instance) - .FirstOrDefaultAsync(a => a.PublisherId == publisherId); - } - public async Task GetOrCreateLocalActorAsync(SnPublisher publisher) { var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}"; @@ -697,4 +693,4 @@ public class DeliveryStats public int SentDeliveries { get; set; } public int FailedDeliveries { get; set; } public int PendingDeliveries { get; set; } -} +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs index 5ae52fd..f9121d7 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs @@ -1,15 +1,22 @@ using System.Text; using DysonNetwork.Shared.Models; using Markdig; +using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.ActivityPub; -public static class ActivityPubObjectFactory +public class ActivityPubObjectFactory(IConfiguration configuration, AppDatabase db) { - public static readonly string[] PublicTo = ["https://www.w3.org/ns/activitystreams#Public"]; + public static readonly string PublicTo = "https://www.w3.org/ns/activitystreams#Public"; - public static Dictionary CreatePostObject( - IConfiguration configuration, + public async Task GetLocalActorAsync(Guid publisherId) + { + return await db.FediverseActors + .Include(a => a.Instance) + .FirstOrDefaultAsync(a => a.PublisherId == publisherId); + } + + public Dictionary CreatePostObject( SnPost post, string actorUrl ) @@ -44,6 +51,23 @@ public static class ActivityPubObjectFactory var finalContent = contentBuilder.ToString(); + var postReceivers = new List { PublicTo }; + + if (post.RepliedPost != null) + { + // Local post + if (post.RepliedPost.Publisher != null) + { + var actor = GetLocalActorAsync(post.RepliedPost.PublisherId!.Value).GetAwaiter().GetResult(); + if (actor?.FollowersUri != null) + postReceivers.Add(actor.FollowersUri); + } + + // Fediverse post + if (post.Actor?.FollowersUri != null) + postReceivers.Add(post.Actor.FollowersUri); + } + var postObject = new Dictionary { ["id"] = postUrl, @@ -51,7 +75,7 @@ public static class ActivityPubObjectFactory ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), ["attributedTo"] = actorUrl, ["content"] = Markdown.ToHtml(finalContent), - ["to"] = PublicTo, + ["to"] = postReceivers, ["cc"] = new[] { $"{actorUrl}/followers" }, ["attachment"] = post.Attachments.Select(a => new Dictionary { @@ -61,6 +85,26 @@ public static class ActivityPubObjectFactory }).ToList() }; + if (post.RepliedPost != null) + { + // Local post + if (post.RepliedPost.Publisher != null) + postObject["inReplyTo"] = $"https://{baseDomain}/posts/{post.RepliedPostId}"; + // Fediverse post + if (post.RepliedPost.FediverseUri != null) + postObject["inReplyTo"] = post.RepliedPost.FediverseUri; + } + + if (post.ForwardedPost != null) + { + // Local post + if (post.ForwardedPost.Publisher != null) + postObject["quoteUri"] = $"https://{baseDomain}/posts/{post.RepliedPostId}"; + // Fediverse post + if (post.ForwardedPost.FediverseUri != null) + postObject["quoteUri"] = post.ForwardedPost.FediverseUri; + } + if (post.EditedAt.HasValue) postObject["updated"] = post.EditedAt.Value.ToDateTimeOffset(); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 19c353b..bdc61cb 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -27,7 +27,7 @@ public partial class PostService( Publisher.PublisherService ps, WebReaderService reader, AccountService.AccountServiceClient accounts, - ActivityPubDeliveryService apDelivery + ActivityPubObjectFactory objFactory ) { private const string PostFileUsageIdentifier = "post"; @@ -586,15 +586,18 @@ public partial class PostService( await db.SaveChangesAsync(); + if (isSelfReact) return isRemoving; + // Send ActivityPub Like/Undo activities if post's publisher has actor - if (post.PublisherId.HasValue && reaction.AccountId.HasValue) + if (post.PublisherId.HasValue) { + var accountId = Guid.Parse(sender.Id); var accountPublisher = await db.Publishers - .Where(p => p.Members.Any(m => m.AccountId == reaction.AccountId.Value)) + .Where(p => p.Members.Any(m => m.AccountId == accountId)) .FirstOrDefaultAsync(); var accountActor = accountPublisher is null ? null - : await apDelivery.GetLocalActorAsync(accountPublisher.Id); + : await objFactory.GetLocalActorAsync(accountPublisher.Id); if (accountActor != null && reaction.Attitude == Shared.Models.PostReactionAttitude.Positive) { @@ -643,49 +646,48 @@ public partial class PostService( } } - if (!isSelfReact) - _ = Task.Run(async () => + _ = Task.Run(async () => + { + using var scope = factory.CreateScope(); + var pub = scope.ServiceProvider.GetRequiredService(); + var nty = scope.ServiceProvider.GetRequiredService(); + var accounts = scope.ServiceProvider.GetRequiredService(); + try { - using var scope = factory.CreateScope(); - var pub = scope.ServiceProvider.GetRequiredService(); - var nty = scope.ServiceProvider.GetRequiredService(); - var accounts = scope.ServiceProvider.GetRequiredService(); - try + if (post.PublisherId == null) return; + var members = await pub.GetPublisherMembers(post.PublisherId.Value); + var queryRequest = new GetAccountBatchRequest(); + queryRequest.Id.AddRange(members.Select(m => m.AccountId.ToString())); + var queryResponse = await accounts.GetAccountBatchAsync(queryRequest); + foreach (var member in queryResponse.Accounts) { - if (post.PublisherId == null) return; - var members = await pub.GetPublisherMembers(post.PublisherId.Value); - var queryRequest = new GetAccountBatchRequest(); - queryRequest.Id.AddRange(members.Select(m => m.AccountId.ToString())); - var queryResponse = await accounts.GetAccountBatchAsync(queryRequest); - foreach (var member in queryResponse.Accounts) - { - if (member is null) continue; - CultureService.SetCultureInfo(member); + if (member is null) continue; + CultureService.SetCultureInfo(member); - await nty.SendPushNotificationToUserAsync( - new SendPushNotificationToUserRequest + await nty.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = member.Id, + Notification = new PushNotification { - UserId = member.Id, - Notification = new PushNotification - { - Topic = "posts.reactions.new", - Title = localizer["PostReactTitle", sender.Nick], - Body = string.IsNullOrWhiteSpace(post.Title) - ? localizer["PostReactBody", sender.Nick, reaction.Symbol] - : localizer["PostReactContentBody", sender.Nick, reaction.Symbol, - post.Title], - IsSavable = true, - ActionUri = $"/posts/{post.Id}" - } + Topic = "posts.reactions.new", + Title = localizer["PostReactTitle", sender.Nick], + Body = string.IsNullOrWhiteSpace(post.Title) + ? localizer["PostReactBody", sender.Nick, reaction.Symbol] + : localizer["PostReactContentBody", sender.Nick, reaction.Symbol, + post.Title], + IsSavable = true, + ActionUri = $"/posts/{post.Id}" } - ); - } + } + ); } - catch (Exception ex) - { - logger.LogError($"Error when sending post reactions notification: {ex.Message} {ex.StackTrace}"); - } - }); + } + catch (Exception ex) + { + logger.LogError($"Error when sending post reactions notification: {ex.Message} {ex.StackTrace}"); + } + }); return isRemoving; } diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 35b3971..4f6151e 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -110,9 +110,8 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); var translationProvider = configuration["Translation:Provider"]?.ToLower(); switch (translationProvider)