diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs index 818d829..d26dc81 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs @@ -8,7 +8,7 @@ using Swashbuckle.AspNetCore.Annotations; namespace DysonNetwork.Sphere.ActivityPub; [ApiController] -[Route("activitypub")] +[Route("activitypub/actors/{username}")] public class ActivityPubController( AppDatabase db, IConfiguration configuration, @@ -20,7 +20,7 @@ public class ActivityPubController( { private string Domain => configuration["ActivityPub:Domain"] ?? "localhost"; - [HttpGet("actors/{username}")] + [HttpGet("")] [Produces("application/activity+json")] [ProducesResponseType(typeof(ActivityPubActor), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -88,7 +88,7 @@ public class ActivityPubController( return Ok(actor); } - [HttpPost("actors/{username}/inbox")] + [HttpPost("inbox")] [Consumes("application/activity+json")] [ProducesResponseType(StatusCodes.Status202Accepted)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -120,7 +120,7 @@ public class ActivityPubController( return Accepted(); } - [HttpGet("actors/{username}/outbox")] + [HttpGet("outbox")] [Produces("application/activity+json")] [ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ActivityPubCollectionPage), StatusCodes.Status200OK)] @@ -157,32 +157,21 @@ public class ActivityPubController( .Take(pageSize) .ToListAsync(); - var items = posts.Select(post => new + var items = posts.Select(post => { - id = $"https://{Domain}/activitypub/objects/{post.Id}/activity", - type = "Create", - actor = actorUrl, - published = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - to = new[] { "https://www.w3.org/ns/activitystreams#Public" }, - cc = new[] { $"{actorUrl}/followers" }, - @object = new + var postObject = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl); + postObject["url"] = $"https://{Domain}/posts/{post.Id}"; + return new Dictionary { - id = $"https://{Domain}/activitypub/objects/{post.Id}", - type = post.Type == PostType.Article ? "Article" : "Note", - published = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - attributedTo = actorUrl, - content = post.Content ?? "", - url = $"https://{Domain}/posts/{post.Id}", - to = new[] { "https://www.w3.org/ns/activitystreams#Public" }, - cc = new[] { $"{actorUrl}/followers" }, - attachment = post.Attachments.Select(a => new - { - type = "Document", - mediaType = a.MimeType, - url = $"{configuration["ActivityPub:FileBaseUrl"] ?? $"https://{Domain}/files"}/{a.Id}" - }) - } - }).ToList(); + ["id"] = $"https://{Domain}/activitypub/objects/{post.Id}/activity", + ["type"] = "Create", + ["actor"] = actorUrl, + ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), + ["to"] = ActivityPubObjectFactory.PublicTo, + ["cc"] = new[] { $"{actorUrl}/followers" }, + ["@object"] = postObject + }; + }).Cast().ToList(); var collectionPage = new ActivityPubCollectionPage { @@ -213,7 +202,7 @@ public class ActivityPubController( } } - [HttpGet("actors/{username}/followers")] + [HttpGet("followers")] [Produces("application/activity+json")] [ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ActivityPubCollectionPage), StatusCodes.Status200OK)] @@ -279,7 +268,7 @@ public class ActivityPubController( } } - [HttpGet("actors/{username}/following")] + [HttpGet("following")] [Produces("application/activity+json")] [ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ActivityPubCollectionPage), StatusCodes.Status200OK)] @@ -354,19 +343,19 @@ public class ActivityPubController( logger.LogInformation("Using existing public key for publisher: {PublisherId}", publisher.Id); return publicKeyPem; } - - logger.LogInformation("Generating new key pair for publisher: {PublisherId} ({Name})", + + logger.LogInformation("Generating new key pair for publisher: {PublisherId} ({Name})", publisher.Id, publisher.Name); - + var (newPrivate, newPublic) = keyService.GenerateKeyPair(); SavePublisherKey(publisher, "private_key", newPrivate); SavePublisherKey(publisher, "public_key", newPublic); - + db.Update(publisher); await db.SaveChangesAsync(); - + logger.LogInformation("Saved new key pair to database for publisher: {PublisherId}", publisher.Id); - + return newPublic; } diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs index 6322b8e..10f9885 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs @@ -166,24 +166,9 @@ public class ActivityPubDeliveryService( ["type"] = "Create", ["actor"] = actorUrl, ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, + ["to"] = ActivityPubObjectFactory.PublicTo, ["cc"] = new[] { $"{actorUrl}/followers" }, - ["object"] = new Dictionary - { - ["id"] = postUrl, - ["type"] = post.Type == PostType.Article ? "Article" : "Note", - ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["attributedTo"] = actorUrl, - ["content"] = post.Content ?? "", - ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, - ["cc"] = new[] { $"{actorUrl}/followers" }, - ["attachment"] = post.Attachments.Select(a => new Dictionary - { - ["type"] = "Document", - ["mediaType"] = "image/jpeg", - ["url"] = $"{AssetsBaseUrl}/{a.Id}" - }).ToList() - } + ["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) }; var followers = await GetRemoteFollowersAsync(); @@ -217,25 +202,9 @@ public class ActivityPubDeliveryService( ["type"] = "Update", ["actor"] = actorUrl, ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, + ["to"] = ActivityPubObjectFactory.PublicTo, ["cc"] = new[] { $"{actorUrl}/followers" }, - ["object"] = new Dictionary - { - ["id"] = postUrl, - ["type"] = post.Type == PostType.Article ? "Article" : "Note", - ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), - ["updated"] = post.EditedAt?.ToDateTimeOffset() ?? new DateTimeOffset(), - ["attributedTo"] = actorUrl, - ["content"] = post.Content ?? "", - ["to"] = new[] { "https://www.w3.org/ns/activitystreams#Public" }, - ["cc"] = new[] { $"{actorUrl}/followers" }, - ["attachment"] = post.Attachments.Select(a => new Dictionary - { - ["type"] = "Document", - ["mediaType"] = "image/jpeg", - ["url"] = $"{AssetsBaseUrl}/{a.Id}" - }).ToList() - } + ["object"] = ActivityPubObjectFactory.CreatePostObject(configuration, post, actorUrl) }; var followers = await GetRemoteFollowersAsync(); diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs new file mode 100644 index 0000000..5ae52fd --- /dev/null +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubObjectFactory.cs @@ -0,0 +1,69 @@ +using System.Text; +using DysonNetwork.Shared.Models; +using Markdig; + +namespace DysonNetwork.Sphere.ActivityPub; + +public static class ActivityPubObjectFactory +{ + public static readonly string[] PublicTo = ["https://www.w3.org/ns/activitystreams#Public"]; + + public static Dictionary CreatePostObject( + IConfiguration configuration, + SnPost post, + string actorUrl + ) + { + var baseDomain = configuration["ActivityPub:Domain"] ?? "localhost"; + var assetsBaseUrl = configuration["ActivityPub:FileBaseUrl"] ?? $"https://{baseDomain}/files"; + var postUrl = $"https://{baseDomain}/posts/{post.Id}"; + + // Build content by combining title, description, and main content + var contentBuilder = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(post.Title)) + contentBuilder.Append($"# {post.Title}\n\n"); + + if (!string.IsNullOrWhiteSpace(post.Description)) + contentBuilder.Append($"{post.Description}\n\n"); + + if (!string.IsNullOrWhiteSpace(post.Content)) + contentBuilder.Append(post.Content); + + // Ensure content is not empty for ActivityPub compatibility + if (contentBuilder.Length == 0) + contentBuilder.Append("Shared media"); + + if (post.Tags.Count > 0) + { + contentBuilder.Append("\n\n"); + contentBuilder.Append( + string.Join(' ', post.Tags.Select(x => $"#{x.Slug}")) + ); + } + + var finalContent = contentBuilder.ToString(); + + var postObject = new Dictionary + { + ["id"] = postUrl, + ["type"] = post.Type == PostType.Article ? "Article" : "Note", + ["published"] = (post.PublishedAt ?? post.CreatedAt).ToDateTimeOffset(), + ["attributedTo"] = actorUrl, + ["content"] = Markdown.ToHtml(finalContent), + ["to"] = PublicTo, + ["cc"] = new[] { $"{actorUrl}/followers" }, + ["attachment"] = post.Attachments.Select(a => new Dictionary + { + ["type"] = "Document", + ["mediaType"] = a.MimeType ?? "media/jpeg", + ["url"] = $"{assetsBaseUrl}/{a.Id}" + }).ToList() + }; + + if (post.EditedAt.HasValue) + postObject["updated"] = post.EditedAt.Value.ToDateTimeOffset(); + + return postObject; + } +} \ No newline at end of file