✨ Implmentations of activitypub missing features
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
using System.Net.Mime;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.ActivityPub;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -45,6 +43,7 @@ public class ActivityPubController(
|
|||||||
var outboxUrl = $"{actorUrl}/outbox";
|
var outboxUrl = $"{actorUrl}/outbox";
|
||||||
var followersUrl = $"{actorUrl}/followers";
|
var followersUrl = $"{actorUrl}/followers";
|
||||||
var followingUrl = $"{actorUrl}/following";
|
var followingUrl = $"{actorUrl}/following";
|
||||||
|
var assetsBaseUrl = configuration["ActivityPub:FileBaseUrl"] ?? $"https://{Domain}/files";
|
||||||
|
|
||||||
var actor = new ActivityPubActor
|
var actor = new ActivityPubActor
|
||||||
{
|
{
|
||||||
@@ -60,13 +59,29 @@ public class ActivityPubController(
|
|||||||
Following = followingUrl,
|
Following = followingUrl,
|
||||||
Published = publisher.CreatedAt,
|
Published = publisher.CreatedAt,
|
||||||
Url = $"https://{Domain}/users/{publisher.Name}",
|
Url = $"https://{Domain}/users/{publisher.Name}",
|
||||||
|
Icon = publisher.Picture != null
|
||||||
|
? new ActivityPubImage
|
||||||
|
{
|
||||||
|
Type = "Image",
|
||||||
|
MediaType = publisher.Picture.MimeType,
|
||||||
|
Url = $"{assetsBaseUrl}/{publisher.Picture.Id}"
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
Image = publisher.Background != null
|
||||||
|
? new ActivityPubImage
|
||||||
|
{
|
||||||
|
Type = "Image",
|
||||||
|
MediaType = publisher.Background.MimeType,
|
||||||
|
Url = $"{assetsBaseUrl}/{publisher.Background.Id}"
|
||||||
|
}
|
||||||
|
: null,
|
||||||
PublicKeys =
|
PublicKeys =
|
||||||
[
|
[
|
||||||
new ActivityPubPublicKey
|
new ActivityPubPublicKey
|
||||||
{
|
{
|
||||||
Id = $"{actorUrl}#main-key",
|
Id = $"{actorUrl}#main-key",
|
||||||
Owner = actorUrl,
|
Owner = actorUrl,
|
||||||
PublicKeyPem = GetPublicKey(publisher, keyService)
|
PublicKeyPem = GetPublicKey(publisher)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -154,38 +169,160 @@ public class ActivityPubController(
|
|||||||
return Ok(collection);
|
return Ok(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPublicKey(SnPublisher publisher, ActivityPubKeyService keyService)
|
[HttpGet("actors/{username}/followers")]
|
||||||
|
[Produces("application/activity+json")]
|
||||||
|
[ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ActivityPubCollectionPage), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "Get ActivityPub followers",
|
||||||
|
Description = "Returns the actor's followers collection with pagination support",
|
||||||
|
OperationId = "GetActorFollowers"
|
||||||
|
)]
|
||||||
|
public async Task<IActionResult> GetFollowers(string username, [FromQuery] int? page)
|
||||||
|
{
|
||||||
|
var publisher = await db.Publishers
|
||||||
|
.FirstOrDefaultAsync(p => p.Name == username);
|
||||||
|
|
||||||
|
if (publisher == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var actorUrl = $"https://{Domain}/activitypub/actors/{username}";
|
||||||
|
var followersUrl = $"{actorUrl}/followers";
|
||||||
|
|
||||||
|
var relationshipsQuery = db.FediverseRelationships
|
||||||
|
.Include(r => r.Actor)
|
||||||
|
.Where(r => r.LocalPublisherId == publisher.Id && r.IsFollowedBy);
|
||||||
|
|
||||||
|
var totalItems = await relationshipsQuery.CountAsync();
|
||||||
|
|
||||||
|
if (page.HasValue)
|
||||||
|
{
|
||||||
|
const int pageSize = 40;
|
||||||
|
var skip = (page.Value - 1) * pageSize;
|
||||||
|
|
||||||
|
var actorUris = await relationshipsQuery
|
||||||
|
.OrderByDescending(r => r.FollowedAt)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(r => r.Actor.Uri)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var collectionPage = new ActivityPubCollectionPage
|
||||||
|
{
|
||||||
|
Context = ["https://www.w3.org/ns/activitystreams"],
|
||||||
|
Id = $"{followersUrl}?page={page.Value}",
|
||||||
|
Type = "OrderedCollectionPage",
|
||||||
|
TotalItems = totalItems,
|
||||||
|
PartOf = followersUrl,
|
||||||
|
OrderedItems = actorUris
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(collectionPage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var collection = new ActivityPubCollection
|
||||||
|
{
|
||||||
|
Context = ["https://www.w3.org/ns/activitystreams"],
|
||||||
|
Id = followersUrl,
|
||||||
|
Type = "OrderedCollection",
|
||||||
|
TotalItems = totalItems,
|
||||||
|
First = $"{followersUrl}?page=1"
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("actors/{username}/following")]
|
||||||
|
[Produces("application/activity+json")]
|
||||||
|
[ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ActivityPubCollectionPage), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "Get ActivityPub following",
|
||||||
|
Description = "Returns the actors that this actor follows with pagination support",
|
||||||
|
OperationId = "GetActorFollowing"
|
||||||
|
)]
|
||||||
|
public async Task<IActionResult> GetFollowing(string username, [FromQuery] int? page)
|
||||||
|
{
|
||||||
|
var publisher = await db.Publishers
|
||||||
|
.FirstOrDefaultAsync(p => p.Name == username);
|
||||||
|
|
||||||
|
if (publisher == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var actorUrl = $"https://{Domain}/activitypub/actors/{username}";
|
||||||
|
var followingUrl = $"{actorUrl}/following";
|
||||||
|
|
||||||
|
var relationshipsQuery = db.FediverseRelationships
|
||||||
|
.Include(r => r.TargetActor)
|
||||||
|
.Where(r => r.LocalPublisherId == publisher.Id && r.IsFollowing);
|
||||||
|
|
||||||
|
var totalItems = await relationshipsQuery.CountAsync();
|
||||||
|
|
||||||
|
if (page.HasValue)
|
||||||
|
{
|
||||||
|
const int pageSize = 40;
|
||||||
|
var skip = (page.Value - 1) * pageSize;
|
||||||
|
|
||||||
|
var actorUris = await relationshipsQuery
|
||||||
|
.OrderByDescending(r => r.FollowedAt)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(r => r.TargetActor.Uri)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var collectionPage = new ActivityPubCollectionPage
|
||||||
|
{
|
||||||
|
Context = ["https://www.w3.org/ns/activitystreams"],
|
||||||
|
Id = $"{followingUrl}?page={page.Value}",
|
||||||
|
Type = "OrderedCollectionPage",
|
||||||
|
TotalItems = totalItems,
|
||||||
|
PartOf = followingUrl,
|
||||||
|
OrderedItems = actorUris
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(collectionPage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var collection = new ActivityPubCollection
|
||||||
|
{
|
||||||
|
Context = ["https://www.w3.org/ns/activitystreams"],
|
||||||
|
Id = followingUrl,
|
||||||
|
Type = "OrderedCollection",
|
||||||
|
TotalItems = totalItems,
|
||||||
|
First = $"{followingUrl}?page=1"
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPublicKey(SnPublisher publisher)
|
||||||
{
|
{
|
||||||
var publicKeyPem = GetPublisherKey(publisher, "public_key");
|
var publicKeyPem = GetPublisherKey(publisher, "public_key");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(publicKeyPem))
|
if (!string.IsNullOrEmpty(publicKeyPem)) return publicKeyPem;
|
||||||
{
|
var (newPrivate, newPublic) = keyService.GenerateKeyPair();
|
||||||
var (newPrivate, newPublic) = keyService.GenerateKeyPair();
|
SavePublisherKey(publisher, "private_key", newPrivate);
|
||||||
SavePublisherKey(publisher, "private_key", newPrivate);
|
SavePublisherKey(publisher, "public_key", newPublic);
|
||||||
SavePublisherKey(publisher, "public_key", newPublic);
|
return newPublic;
|
||||||
return newPublic;
|
|
||||||
}
|
|
||||||
|
|
||||||
return publicKeyPem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetPublisherKey(SnPublisher publisher, string keyName)
|
private static string? GetPublisherKey(SnPublisher publisher, string keyName)
|
||||||
{
|
{
|
||||||
if (publisher.Meta == null)
|
var metadata = publisher.Meta;
|
||||||
return null;
|
|
||||||
|
|
||||||
var metadata = publisher.Meta as Dictionary<string, object>;
|
|
||||||
return metadata?.GetValueOrDefault(keyName)?.ToString();
|
return metadata?.GetValueOrDefault(keyName)?.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SavePublisherKey(SnPublisher publisher, string keyName, string keyValue)
|
private static void SavePublisherKey(SnPublisher publisher, string keyName, string keyValue)
|
||||||
{
|
{
|
||||||
publisher.Meta ??= new Dictionary<string, object>();
|
publisher.Meta ??= new Dictionary<string, object>();
|
||||||
var metadata = publisher.Meta as Dictionary<string, object>;
|
publisher.Meta[keyName] = keyValue;
|
||||||
if (metadata != null)
|
|
||||||
{
|
|
||||||
metadata[keyName] = keyValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +332,10 @@ public class ActivityPubActor
|
|||||||
[JsonPropertyName("id")] public string? Id { get; set; }
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
[JsonPropertyName("type")] public string? Type { get; set; }
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
[JsonPropertyName("name")] public string? Name { get; set; }
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||||
[JsonPropertyName("preferredUsername")] public string? PreferredUsername { get; set; }
|
|
||||||
|
[JsonPropertyName("preferredUsername")]
|
||||||
|
public string? PreferredUsername { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("summary")] public string? Summary { get; set; }
|
[JsonPropertyName("summary")] public string? Summary { get; set; }
|
||||||
[JsonPropertyName("inbox")] public string? Inbox { get; set; }
|
[JsonPropertyName("inbox")] public string? Inbox { get; set; }
|
||||||
[JsonPropertyName("outbox")] public string? Outbox { get; set; }
|
[JsonPropertyName("outbox")] public string? Outbox { get; set; }
|
||||||
@@ -203,6 +343,8 @@ public class ActivityPubActor
|
|||||||
[JsonPropertyName("following")] public string? Following { get; set; }
|
[JsonPropertyName("following")] public string? Following { get; set; }
|
||||||
[JsonPropertyName("published")] public Instant? Published { get; set; }
|
[JsonPropertyName("published")] public Instant? Published { get; set; }
|
||||||
[JsonPropertyName("url")] public string? Url { get; set; }
|
[JsonPropertyName("url")] public string? Url { get; set; }
|
||||||
|
[JsonPropertyName("icon")] public ActivityPubImage? Icon { get; set; }
|
||||||
|
[JsonPropertyName("image")] public ActivityPubImage? Image { get; set; }
|
||||||
[JsonPropertyName("publicKey")] public List<ActivityPubPublicKey>? PublicKeys { get; set; }
|
[JsonPropertyName("publicKey")] public List<ActivityPubPublicKey>? PublicKeys { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +355,13 @@ public class ActivityPubPublicKey
|
|||||||
[JsonPropertyName("publicKeyPem")] public string? PublicKeyPem { get; set; }
|
[JsonPropertyName("publicKeyPem")] public string? PublicKeyPem { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ActivityPubImage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
[JsonPropertyName("mediaType")] public string? MediaType { get; set; }
|
||||||
|
[JsonPropertyName("url")] public string? Url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class ActivityPubCollection
|
public class ActivityPubCollection
|
||||||
{
|
{
|
||||||
[JsonPropertyName("@context")] public List<object> Context { get; set; } = [];
|
[JsonPropertyName("@context")] public List<object> Context { get; set; } = [];
|
||||||
@@ -222,3 +371,13 @@ public class ActivityPubCollection
|
|||||||
[JsonPropertyName("first")] public string? First { get; set; }
|
[JsonPropertyName("first")] public string? First { get; set; }
|
||||||
[JsonPropertyName("items")] public List<object>? Items { get; set; }
|
[JsonPropertyName("items")] public List<object>? Items { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ActivityPubCollectionPage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("@context")] public List<object> Context { get; set; } = [];
|
||||||
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
[JsonPropertyName("totalItems")] public int TotalItems { get; set; }
|
||||||
|
[JsonPropertyName("partOf")] public string? PartOf { get; set; }
|
||||||
|
[JsonPropertyName("orderedItems")] public List<string>? OrderedItems { get; set; }
|
||||||
|
}
|
||||||
@@ -309,17 +309,20 @@ public class ActivityPubDeliveryService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var assetsBaseUrl = configuration["ActivityPub:FileBaseUrl"] ?? $"https://{Domain}/files";
|
||||||
|
|
||||||
localActor = new SnFediverseActor
|
localActor = new SnFediverseActor
|
||||||
{
|
{
|
||||||
Uri = actorUrl,
|
Uri = actorUrl,
|
||||||
Username = publisher.Name,
|
Username = publisher.Name,
|
||||||
DisplayName = publisher.Name,
|
DisplayName = publisher.Name,
|
||||||
Bio = null,
|
Bio = publisher.Bio,
|
||||||
InboxUri = $"{actorUrl}/inbox",
|
InboxUri = $"{actorUrl}/inbox",
|
||||||
OutboxUri = $"{actorUrl}/outbox",
|
OutboxUri = $"{actorUrl}/outbox",
|
||||||
FollowersUri = $"{actorUrl}/followers",
|
FollowersUri = $"{actorUrl}/followers",
|
||||||
FollowingUri = $"{actorUrl}/following",
|
FollowingUri = $"{actorUrl}/following",
|
||||||
AvatarUrl = null,
|
AvatarUrl = publisher.Picture != null ? $"{assetsBaseUrl}/{publisher.Picture.Id}" : null,
|
||||||
|
HeaderUrl = publisher.Background != null ? $"{assetsBaseUrl}/{publisher.Background.Id}" : null,
|
||||||
InstanceId = instance.Id
|
InstanceId = instance.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user