Activitypub supports reply and repost

This commit is contained in:
2026-01-01 13:37:29 +08:00
parent fb15930611
commit 7c5c92a501
5 changed files with 114 additions and 72 deletions

View File

@@ -15,7 +15,8 @@ public class ActivityPubController(
ILogger<ActivityPubController> logger, ILogger<ActivityPubController> logger,
ActivityPubSignatureService signatureService, ActivityPubSignatureService signatureService,
ActivityPubActivityHandler activityHandler, ActivityPubActivityHandler activityHandler,
ActivityPubKeyService keyService ActivityPubKeyService keyService,
ActivityPubObjectFactory objFactory
) : ControllerBase ) : ControllerBase
{ {
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost"; private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
@@ -159,7 +160,7 @@ public class ActivityPubController(
var items = posts.Select(post => 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}"; postObject["url"] = $"https://{Domain}/posts/{post.Id}";
return new Dictionary<string, object> return new Dictionary<string, object>
{ {
@@ -167,7 +168,7 @@ public class ActivityPubController(
["type"] = "Create", ["type"] = "Create",
["actor"] = actorUrl, ["actor"] = actorUrl,
["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(),
["to"] = ActivityPubObjectFactory.PublicTo, ["to"] = new[] { ActivityPubObjectFactory.PublicTo },
["cc"] = new[] { $"{actorUrl}/followers" }, ["cc"] = new[] { $"{actorUrl}/followers" },
["@object"] = postObject ["@object"] = postObject
}; };

View File

@@ -10,7 +10,8 @@ public class ActivityPubDeliveryService(
ActivityPubQueueService queueService, ActivityPubQueueService queueService,
IConfiguration configuration, IConfiguration configuration,
ILogger<ActivityPubDeliveryService> logger, ILogger<ActivityPubDeliveryService> logger,
IClock clock IClock clock,
ActivityPubObjectFactory objFactory
) )
{ {
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost"; private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
@@ -53,7 +54,7 @@ public class ActivityPubDeliveryService(
string targetActorUri string targetActorUri
) )
{ {
var localActor = await GetLocalActorAsync(publisherId); var localActor = await objFactory.GetLocalActorAsync(publisherId);
if (localActor == null) if (localActor == null)
return false; return false;
@@ -106,7 +107,7 @@ public class ActivityPubDeliveryService(
string targetActorUri string targetActorUri
) )
{ {
var localActor = await GetLocalActorAsync(publisherId); var localActor = await objFactory.GetLocalActorAsync(publisherId);
if (localActor == null) if (localActor == null)
return false; return false;
@@ -151,7 +152,7 @@ public class ActivityPubDeliveryService(
{ {
if (post.PublisherId == null) if (post.PublisherId == null)
return false; return false;
var localActor = await GetLocalActorAsync(post.PublisherId.Value); var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value);
if (localActor == null) if (localActor == null)
return false; return false;
@@ -166,9 +167,9 @@ public class ActivityPubDeliveryService(
["type"] = "Create", ["type"] = "Create",
["actor"] = actorUrl, ["actor"] = actorUrl,
["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(),
["to"] = ActivityPubObjectFactory.PublicTo, ["to"] = new[] { ActivityPubObjectFactory.PublicTo },
["cc"] = new[] { $"{actorUrl}/followers" }, ["cc"] = new[] { $"{actorUrl}/followers" },
["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) ["object"] = objFactory.CreatePostObject(post, actorUrl)
}; };
var followers = await GetRemoteFollowersAsync(); var followers = await GetRemoteFollowersAsync();
@@ -187,7 +188,7 @@ public class ActivityPubDeliveryService(
{ {
if (post.PublisherId == null) if (post.PublisherId == null)
return false; return false;
var localActor = await GetLocalActorAsync(post.PublisherId.Value); var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value);
if (localActor == null) if (localActor == null)
return false; return false;
@@ -202,9 +203,9 @@ public class ActivityPubDeliveryService(
["type"] = "Update", ["type"] = "Update",
["actor"] = actorUrl, ["actor"] = actorUrl,
["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(),
["to"] = ActivityPubObjectFactory.PublicTo, ["to"] = new[] { ActivityPubObjectFactory.PublicTo },
["cc"] = new[] { $"{actorUrl}/followers" }, ["cc"] = new[] { $"{actorUrl}/followers" },
["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) ["object"] = objFactory.CreatePostObject(post, actorUrl)
}; };
var followers = await GetRemoteFollowersAsync(); var followers = await GetRemoteFollowersAsync();
@@ -223,7 +224,7 @@ public class ActivityPubDeliveryService(
{ {
if (post.PublisherId == null) if (post.PublisherId == null)
return false; return false;
var localActor = await GetLocalActorAsync(post.PublisherId.Value); var localActor = await objFactory.GetLocalActorAsync(post.PublisherId.Value);
if (localActor == null) if (localActor == null)
return false; return false;
@@ -499,8 +500,10 @@ public class ActivityPubDeliveryService(
stats.TotalDeliveries = deliveries.Count; stats.TotalDeliveries = deliveries.Count;
stats.SentDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Sent); stats.SentDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Sent);
stats.FailedDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Failed || d.Status == DeliveryStatus.ExhaustedRetries); stats.FailedDeliveries = deliveries.Count(d =>
stats.PendingDeliveries = deliveries.Count(d => d.Status == DeliveryStatus.Pending || d.Status == DeliveryStatus.Processing); d.Status == DeliveryStatus.Failed || d.Status == DeliveryStatus.ExhaustedRetries);
stats.PendingDeliveries =
deliveries.Count(d => d.Status == DeliveryStatus.Pending || d.Status == DeliveryStatus.Processing);
return stats; return stats;
} }
@@ -578,13 +581,6 @@ public class ActivityPubDeliveryService(
.ToListAsync(); .ToListAsync();
} }
public async Task<SnFediverseActor?> GetLocalActorAsync(Guid publisherId)
{
return await db.FediverseActors
.Include(a => a.Instance)
.FirstOrDefaultAsync(a => a.PublisherId == publisherId);
}
public async Task<SnFediverseActor?> GetOrCreateLocalActorAsync(SnPublisher publisher) public async Task<SnFediverseActor?> GetOrCreateLocalActorAsync(SnPublisher publisher)
{ {
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}"; var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";

View File

@@ -1,15 +1,22 @@
using System.Text; using System.Text;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Markdig; using Markdig;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.ActivityPub; 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<string, object> CreatePostObject( public async Task<SnFediverseActor?> GetLocalActorAsync(Guid publisherId)
IConfiguration configuration, {
return await db.FediverseActors
.Include(a => a.Instance)
.FirstOrDefaultAsync(a => a.PublisherId == publisherId);
}
public Dictionary<string, object> CreatePostObject(
SnPost post, SnPost post,
string actorUrl string actorUrl
) )
@@ -44,6 +51,23 @@ public static class ActivityPubObjectFactory
var finalContent = contentBuilder.ToString(); var finalContent = contentBuilder.ToString();
var postReceivers = new List<string> { 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<string, object> var postObject = new Dictionary<string, object>
{ {
["id"] = postUrl, ["id"] = postUrl,
@@ -51,7 +75,7 @@ public static class ActivityPubObjectFactory
["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(),
["attributedTo"] = actorUrl, ["attributedTo"] = actorUrl,
["content"] = Markdown.ToHtml(finalContent), ["content"] = Markdown.ToHtml(finalContent),
["to"] = PublicTo, ["to"] = postReceivers,
["cc"] = new[] { $"{actorUrl}/followers" }, ["cc"] = new[] { $"{actorUrl}/followers" },
["attachment"] = post.Attachments.Select(a => new Dictionary<string, object> ["attachment"] = post.Attachments.Select(a => new Dictionary<string, object>
{ {
@@ -61,6 +85,26 @@ public static class ActivityPubObjectFactory
}).ToList<object>() }).ToList<object>()
}; };
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) if (post.EditedAt.HasValue)
postObject["updated"] = post.EditedAt.Value.ToDateTimeOffset(); postObject["updated"] = post.EditedAt.Value.ToDateTimeOffset();

View File

@@ -27,7 +27,7 @@ public partial class PostService(
Publisher.PublisherService ps, Publisher.PublisherService ps,
WebReaderService reader, WebReaderService reader,
AccountService.AccountServiceClient accounts, AccountService.AccountServiceClient accounts,
ActivityPubDeliveryService apDelivery ActivityPubObjectFactory objFactory
) )
{ {
private const string PostFileUsageIdentifier = "post"; private const string PostFileUsageIdentifier = "post";
@@ -586,15 +586,18 @@ public partial class PostService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
if (isSelfReact) return isRemoving;
// Send ActivityPub Like/Undo activities if post's publisher has actor // 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 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(); .FirstOrDefaultAsync();
var accountActor = accountPublisher is null var accountActor = accountPublisher is null
? null ? null
: await apDelivery.GetLocalActorAsync(accountPublisher.Id); : await objFactory.GetLocalActorAsync(accountPublisher.Id);
if (accountActor != null && reaction.Attitude == Shared.Models.PostReactionAttitude.Positive) 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<Publisher.PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<RingService.RingServiceClient>();
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
try
{ {
using var scope = factory.CreateScope(); if (post.PublisherId == null) return;
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>(); var members = await pub.GetPublisherMembers(post.PublisherId.Value);
var nty = scope.ServiceProvider.GetRequiredService<RingService.RingServiceClient>(); var queryRequest = new GetAccountBatchRequest();
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>(); queryRequest.Id.AddRange(members.Select(m => m.AccountId.ToString()));
try var queryResponse = await accounts.GetAccountBatchAsync(queryRequest);
foreach (var member in queryResponse.Accounts)
{ {
if (post.PublisherId == null) return; if (member is null) continue;
var members = await pub.GetPublisherMembers(post.PublisherId.Value); CultureService.SetCultureInfo(member);
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);
await nty.SendPushNotificationToUserAsync( await nty.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest new SendPushNotificationToUserRequest
{
UserId = member.Id,
Notification = new PushNotification
{ {
UserId = member.Id, Topic = "posts.reactions.new",
Notification = new PushNotification Title = localizer["PostReactTitle", sender.Nick],
{ Body = string.IsNullOrWhiteSpace(post.Title)
Topic = "posts.reactions.new", ? localizer["PostReactBody", sender.Nick, reaction.Symbol]
Title = localizer["PostReactTitle", sender.Nick], : localizer["PostReactContentBody", sender.Nick, reaction.Symbol,
Body = string.IsNullOrWhiteSpace(post.Title) post.Title],
? localizer["PostReactBody", sender.Nick, reaction.Symbol] IsSavable = true,
: localizer["PostReactContentBody", sender.Nick, reaction.Symbol, ActionUri = $"/posts/{post.Id}"
post.Title],
IsSavable = true,
ActionUri = $"/posts/{post.Id}"
}
} }
); }
} );
} }
catch (Exception ex) }
{ catch (Exception ex)
logger.LogError($"Error when sending post reactions notification: {ex.Message} {ex.StackTrace}"); {
} logger.LogError($"Error when sending post reactions notification: {ex.Message} {ex.StackTrace}");
}); }
});
return isRemoving; return isRemoving;
} }

View File

@@ -110,9 +110,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<ActivityPubActivityHandler>(); services.AddScoped<ActivityPubActivityHandler>();
services.AddScoped<ActivityPubDeliveryService>(); services.AddScoped<ActivityPubDeliveryService>();
services.AddScoped<ActivityPubDiscoveryService>(); services.AddScoped<ActivityPubDiscoveryService>();
services.AddScoped<ActivityPubObjectFactory>();
services.AddSingleton<ActivityPubQueueService>(); services.AddSingleton<ActivityPubQueueService>();
services.AddScoped<ActivityPubFollowController>();
services.AddScoped<ActivityPubController>();
var translationProvider = configuration["Translation:Provider"]?.ToLower(); var translationProvider = configuration["Translation:Provider"]?.ToLower();
switch (translationProvider) switch (translationProvider)