✨ Activitypub supports reply and repost
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +646,6 @@ public partial class PostService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSelfReact)
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
using var scope = factory.CreateScope();
|
using var scope = factory.CreateScope();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user