⚗️ Activity pub

This commit is contained in:
2025-12-28 18:08:35 +08:00
parent f06d93a348
commit 2471fa2e75
27 changed files with 6506 additions and 9 deletions

View File

@@ -0,0 +1,543 @@
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Text.Json;
namespace DysonNetwork.Sphere.ActivityPub;
public class ActivityPubActivityProcessor(
AppDatabase db,
ActivityPubSignatureService signatureService,
ActivityPubDeliveryService deliveryService,
ILogger<ActivityPubActivityProcessor> logger
)
{
public async Task<bool> ProcessIncomingActivityAsync(
HttpContext context,
string username,
Dictionary<string, object> activity
)
{
if (!signatureService.VerifyIncomingRequest(context, out var actorUri))
{
logger.LogWarning("Failed to verify signature for incoming activity");
return false;
}
if (string.IsNullOrEmpty(actorUri))
return false;
var activityType = activity.GetValueOrDefault("type")?.ToString();
logger.LogInformation("Processing activity type: {Type} from actor: {Actor}", activityType, actorUri);
switch (activityType)
{
case "Follow":
return await ProcessFollowAsync(actorUri, activity);
case "Accept":
return await ProcessAcceptAsync(actorUri, activity);
case "Reject":
return await ProcessRejectAsync(actorUri, activity);
case "Undo":
return await ProcessUndoAsync(actorUri, activity);
case "Create":
return await ProcessCreateAsync(actorUri, activity);
case "Like":
return await ProcessLikeAsync(actorUri, activity);
case "Announce":
return await ProcessAnnounceAsync(actorUri, activity);
case "Delete":
return await ProcessDeleteAsync(actorUri, activity);
case "Update":
return await ProcessUpdateAsync(actorUri, activity);
default:
logger.LogWarning("Unsupported activity type: {Type}", activityType);
return false;
}
}
private async Task<bool> ProcessFollowAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var actor = await GetOrCreateActorAsync(actorUri);
var targetPublisher = await db.Publishers
.FirstOrDefaultAsync(p => p.Name == ExtractUsernameFromUri(objectUri));
if (targetPublisher == null)
{
logger.LogWarning("Target publisher not found: {Uri}", objectUri);
return false;
}
var existingRelationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r =>
r.ActorId == actor.Id &&
r.TargetActorId == actor.Id &&
r.IsLocalActor);
if (existingRelationship != null && existingRelationship.State == RelationshipState.Accepted)
{
logger.LogInformation("Follow relationship already exists");
return true;
}
if (existingRelationship == null)
{
existingRelationship = new SnFediverseRelationship
{
ActorId = actor.Id,
TargetActorId = actor.Id,
IsLocalActor = true,
LocalPublisherId = targetPublisher.Id,
State = RelationshipState.Pending,
IsFollowing = false,
IsFollowedBy = true
};
db.FediverseRelationships.Add(existingRelationship);
}
await db.SaveChangesAsync();
await deliveryService.SendAcceptActivityAsync(
targetPublisher.Id,
actorUri,
activity.GetValueOrDefault("id")?.ToString() ?? ""
);
logger.LogInformation("Processed follow from {Actor} to {Target}", actorUri, objectUri);
return true;
}
private async Task<bool> ProcessAcceptAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var actor = await GetOrCreateActorAsync(actorUri);
var relationship = await db.FediverseRelationships
.Include(r => r.Actor)
.Include(r => r.TargetActor)
.FirstOrDefaultAsync(r =>
r.IsLocalActor &&
r.TargetActorId == actor.Id &&
r.State == RelationshipState.Pending);
if (relationship == null)
{
logger.LogWarning("No pending relationship found for accept");
return false;
}
relationship.State = RelationshipState.Accepted;
relationship.IsFollowing = true;
relationship.FollowedAt = SystemClock.Instance.GetCurrentInstant();
await db.SaveChangesAsync();
logger.LogInformation("Processed accept from {Actor}", actorUri);
return true;
}
private async Task<bool> ProcessRejectAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var actor = await GetOrCreateActorAsync(actorUri);
var relationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r =>
r.IsLocalActor &&
r.TargetActorId == actor.Id);
if (relationship == null)
{
logger.LogWarning("No relationship found for reject");
return false;
}
relationship.State = RelationshipState.Rejected;
relationship.IsFollowing = false;
relationship.RejectReason = "Remote rejected follow";
await db.SaveChangesAsync();
logger.LogInformation("Processed reject from {Actor}", actorUri);
return true;
}
private async Task<bool> ProcessUndoAsync(string actorUri, Dictionary<string, object> activity)
{
var objectValue = activity.GetValueOrDefault("object");
if (objectValue == null)
return false;
var objectDict = objectValue as Dictionary<string, object>;
if (objectDict != null)
{
var objectType = objectDict.GetValueOrDefault("type")?.ToString();
switch (objectType)
{
case "Follow":
return await UndoFollowAsync(actorUri, objectDict.GetValueOrDefault("id")?.ToString());
case "Like":
return await UndoLikeAsync(actorUri, objectDict.GetValueOrDefault("id")?.ToString());
case "Announce":
return await UndoAnnounceAsync(actorUri, objectDict.GetValueOrDefault("id")?.ToString());
default:
return false;
}
}
return false;
}
private async Task<bool> ProcessCreateAsync(string actorUri, Dictionary<string, object> activity)
{
var objectValue = activity.GetValueOrDefault("object");
if (objectValue == null || !(objectValue is Dictionary<string, object> objectDict))
return false;
var objectType = objectDict.GetValueOrDefault("type")?.ToString();
if (objectType != "Note" && objectType != "Article")
{
logger.LogInformation("Skipping non-note content type: {Type}", objectType);
return true;
}
var actor = await GetOrCreateActorAsync(actorUri);
var instance = await GetOrCreateInstanceAsync(actorUri);
var contentUri = objectDict.GetValueOrDefault("id")?.ToString();
if (string.IsNullOrEmpty(contentUri))
return false;
var existingContent = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == contentUri);
if (existingContent != null)
{
logger.LogInformation("Content already exists: {Uri}", contentUri);
return true;
}
var content = new SnFediverseContent
{
Uri = contentUri,
Type = objectType == "Article" ? FediverseContentType.Article : FediverseContentType.Note,
Title = objectDict.GetValueOrDefault("name")?.ToString(),
Summary = objectDict.GetValueOrDefault("summary")?.ToString(),
Content = objectDict.GetValueOrDefault("content")?.ToString(),
ContentHtml = objectDict.GetValueOrDefault("contentMap")?.ToString(),
PublishedAt = ParseInstant(objectDict.GetValueOrDefault("published")),
EditedAt = ParseInstant(objectDict.GetValueOrDefault("updated")),
IsSensitive = bool.TryParse(objectDict.GetValueOrDefault("sensitive")?.ToString(), out var sensitive) && sensitive,
ActorId = actor.Id,
InstanceId = instance.Id,
Attachments = ParseAttachments(objectDict.GetValueOrDefault("attachment")),
Mentions = ParseMentions(objectDict.GetValueOrDefault("tag")),
Tags = ParseTags(objectDict.GetValueOrDefault("tag")),
InReplyTo = objectDict.GetValueOrDefault("inReplyTo")?.ToString()
};
db.FediverseContents.Add(content);
await db.SaveChangesAsync();
logger.LogInformation("Created federated content: {Uri}", contentUri);
return true;
}
private async Task<bool> ProcessLikeAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var actor = await GetOrCreateActorAsync(actorUri);
var content = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == objectUri);
if (content == null)
{
logger.LogWarning("Content not found for like: {Uri}", objectUri);
return false;
}
var existingReaction = await db.FediverseReactions
.FirstOrDefaultAsync(r =>
r.ActorId == actor.Id &&
r.ContentId == content.Id &&
r.Type == FediverseReactionType.Like);
if (existingReaction != null)
{
logger.LogInformation("Like already exists");
return true;
}
var reaction = new SnFediverseReaction
{
Uri = activity.GetValueOrDefault("id")?.ToString() ?? Guid.NewGuid().ToString(),
Type = FediverseReactionType.Like,
IsLocal = false,
ContentId = content.Id,
ActorId = actor.Id
};
db.FediverseReactions.Add(reaction);
content.LikeCount++;
await db.SaveChangesAsync();
logger.LogInformation("Processed like from {Actor}", actorUri);
return true;
}
private async Task<bool> ProcessAnnounceAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var actor = await GetOrCreateActorAsync(actorUri);
var content = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == objectUri);
if (content != null)
{
content.BoostCount++;
await db.SaveChangesAsync();
}
logger.LogInformation("Processed announce from {Actor}", actorUri);
return true;
}
private async Task<bool> ProcessDeleteAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var content = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == objectUri);
if (content != null)
{
content.DeletedAt = SystemClock.Instance.GetCurrentInstant();
await db.SaveChangesAsync();
logger.LogInformation("Deleted federated content: {Uri}", objectUri);
}
return true;
}
private async Task<bool> ProcessUpdateAsync(string actorUri, Dictionary<string, object> activity)
{
var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri))
return false;
var content = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == objectUri);
if (content != null)
{
content.EditedAt = SystemClock.Instance.GetCurrentInstant();
content.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
await db.SaveChangesAsync();
logger.LogInformation("Updated federated content: {Uri}", objectUri);
}
return true;
}
private async Task<bool> UndoFollowAsync(string actorUri, string? activityId)
{
var actor = await GetOrCreateActorAsync(actorUri);
var relationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r =>
r.ActorId == actor.Id ||
r.TargetActorId == actor.Id);
if (relationship != null)
{
relationship.IsFollowing = false;
relationship.IsFollowedBy = false;
await db.SaveChangesAsync();
logger.LogInformation("Undid follow relationship");
}
return true;
}
private async Task<bool> UndoLikeAsync(string actorUri, string? activityId)
{
var actor = await GetOrCreateActorAsync(actorUri);
var reactions = await db.FediverseReactions
.Where(r => r.ActorId == actor.Id && r.Type == FediverseReactionType.Like)
.ToListAsync();
foreach (var reaction in reactions)
{
var content = await db.FediverseContents.FindAsync(reaction.ContentId);
if (content != null)
{
content.LikeCount--;
}
db.FediverseReactions.Remove(reaction);
}
await db.SaveChangesAsync();
return true;
}
private async Task<bool> UndoAnnounceAsync(string actorUri, string? activityId)
{
var content = await db.FediverseContents
.FirstOrDefaultAsync(c => c.Uri == activityId);
if (content != null)
{
content.BoostCount = Math.Max(0, content.BoostCount - 1);
await db.SaveChangesAsync();
}
return true;
}
private async Task<SnFediverseActor> GetOrCreateActorAsync(string actorUri)
{
var actor = await db.FediverseActors
.FirstOrDefaultAsync(a => a.Uri == actorUri);
if (actor == null)
{
var instance = await GetOrCreateInstanceAsync(actorUri);
actor = new SnFediverseActor
{
Uri = actorUri,
Username = ExtractUsernameFromUri(actorUri),
DisplayName = ExtractUsernameFromUri(actorUri),
InstanceId = instance.Id
};
db.FediverseActors.Add(actor);
await db.SaveChangesAsync();
}
return actor;
}
private async Task<SnFediverseInstance> GetOrCreateInstanceAsync(string actorUri)
{
var domain = ExtractDomainFromUri(actorUri);
var instance = await db.FediverseInstances
.FirstOrDefaultAsync(i => i.Domain == domain);
if (instance == null)
{
instance = new SnFediverseInstance
{
Domain = domain,
Name = domain
};
db.FediverseInstances.Add(instance);
await db.SaveChangesAsync();
}
return instance;
}
private string ExtractUsernameFromUri(string uri)
{
return uri.Split('/').Last();
}
private string ExtractDomainFromUri(string uri)
{
var uriObj = new Uri(uri);
return uriObj.Host;
}
private Instant? ParseInstant(object? value)
{
if (value == null)
return null;
if (value is Instant instant)
return instant;
if (DateTimeOffset.TryParse(value.ToString(), out var dateTimeOffset))
return Instant.FromDateTimeOffset(dateTimeOffset);
return null;
}
private List<ContentAttachment>? ParseAttachments(object? value)
{
if (value == null)
return null;
if (value is JsonElement element && element.ValueKind == JsonValueKind.Array)
{
return element.EnumerateArray()
.Select(attachment => new ContentAttachment
{
Url = attachment.GetProperty("url").GetString(),
MediaType = attachment.GetProperty("mediaType").GetString(),
Name = attachment.GetProperty("name").GetString()
})
.ToList();
}
return null;
}
private List<ContentMention>? ParseMentions(object? value)
{
if (value == null)
return null;
if (value is JsonElement element && element.ValueKind == JsonValueKind.Array)
{
return element.EnumerateArray()
.Where(e => e.GetProperty("type").GetString() == "Mention")
.Select(mention => new ContentMention
{
Username = mention.GetProperty("name").GetString(),
ActorUri = mention.GetProperty("href").GetString()
})
.ToList();
}
return null;
}
private List<ContentTag>? ParseTags(object? value)
{
if (value == null)
return null;
if (value is JsonElement element && element.ValueKind == JsonValueKind.Array)
{
return element.EnumerateArray()
.Where(e => e.GetProperty("type").GetString() == "Hashtag")
.Select(tag => new ContentTag
{
Name = tag.GetProperty("name").GetString(),
Url = tag.GetProperty("href").GetString()
})
.ToList();
}
return null;
}
}

View File

@@ -0,0 +1,224 @@
using System.Net.Mime;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.ActivityPub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Swashbuckle.AspNetCore.Annotations;
namespace DysonNetwork.Sphere.ActivityPub;
[ApiController]
[Route("activitypub")]
public class ActivityPubController(
AppDatabase db,
IConfiguration configuration,
ILogger<ActivityPubController> logger,
ActivityPubSignatureService signatureService,
ActivityPubActivityProcessor activityProcessor,
ActivityPubKeyService keyService
) : ControllerBase
{
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
[HttpGet("actors/{username}")]
[Produces("application/activity+json")]
[ProducesResponseType(typeof(ActivityPubActor), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation(
Summary = "Get ActivityPub actor",
Description = "Returns the ActivityPub actor (user) profile in JSON-LD format",
OperationId = "GetActivityPubActor"
)]
public async Task<ActionResult<ActivityPubActor>> GetActor(string username)
{
var publisher = await db.Publishers
.Include(p => p.Members)
.FirstOrDefaultAsync(p => p.Name == username);
if (publisher == null)
return NotFound();
var actorUrl = $"https://{Domain}/activitypub/actors/{username}";
var inboxUrl = $"{actorUrl}/inbox";
var outboxUrl = $"{actorUrl}/outbox";
var followersUrl = $"{actorUrl}/followers";
var followingUrl = $"{actorUrl}/following";
var actor = new ActivityPubActor
{
Context = ["https://www.w3.org/ns/activitystreams"],
Id = actorUrl,
Type = "Person",
Name = publisher.Nick,
PreferredUsername = publisher.Name,
Summary = publisher.Bio,
Inbox = inboxUrl,
Outbox = outboxUrl,
Followers = followersUrl,
Following = followingUrl,
Published = publisher.CreatedAt,
Url = $"https://{Domain}/users/{publisher.Name}",
PublicKeys =
[
new ActivityPubPublicKey
{
Id = $"{actorUrl}#main-key",
Owner = actorUrl,
PublicKeyPem = GetPublicKey(publisher, keyService)
}
]
};
return Ok(actor);
}
[HttpGet("actors/{username}/inbox")]
[Consumes("application/activity+json")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation(
Summary = "Receive ActivityPub activities",
Description = "Endpoint for receiving ActivityPub activities (Create, Follow, Like, etc.) from remote servers",
OperationId = "ReceiveActivity"
)]
public async Task<IActionResult> PostInbox(string username, [FromBody] Dictionary<string, object> activity)
{
if (!signatureService.VerifyIncomingRequest(HttpContext, out var actorUri))
{
logger.LogWarning("Failed to verify signature for incoming activity");
return Unauthorized(new { error = "Invalid signature" });
}
var success = await activityProcessor.ProcessIncomingActivityAsync(HttpContext, username, activity);
if (!success)
{
logger.LogWarning("Failed to process activity for actor {Username}", username);
return BadRequest(new { error = "Failed to process activity" });
}
logger.LogInformation("Successfully processed activity for actor {Username}: {Type}", username,
activity.GetValueOrDefault("type")?.ToString());
return Accepted();
}
[HttpGet("actors/{username}/outbox")]
[Produces("application/activity+json")]
[ProducesResponseType(typeof(ActivityPubCollection), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation(
Summary = "Get ActivityPub outbox",
Description = "Returns the actor's outbox collection containing their public activities",
OperationId = "GetActorOutbox"
)]
public async Task<ActionResult<ActivityPubCollection>> GetOutbox(string username)
{
var publisher = await db.Publishers
.FirstOrDefaultAsync(p => p.Name == username);
if (publisher == null)
return NotFound();
var actorUrl = $"https://{Domain}/activitypub/actors/{username}";
var outboxUrl = $"{actorUrl}/outbox";
var posts = await db.Posts
.Where(p => p.PublisherId == publisher.Id && p.Visibility == PostVisibility.Public)
.OrderByDescending(p => p.PublishedAt ?? p.CreatedAt)
.Take(20)
.ToListAsync();
var items = posts.Select(post => new
{
id = $"https://{Domain}/activitypub/objects/{post.Id}",
type = post.Type == PostType.Article ? "Article" : "Note",
published = post.PublishedAt ?? post.CreatedAt,
attributedTo = actorUrl,
content = post.Content,
url = $"https://{Domain}/posts/{post.Id}"
}).ToList<object>();
var collection = new ActivityPubCollection
{
Context = ["https://www.w3.org/ns/activitystreams"],
Id = outboxUrl,
Type = "OrderedCollection",
TotalItems = items.Count,
First = $"{outboxUrl}?page=1"
};
return Ok(collection);
}
private string GetPublicKey(SnPublisher publisher, ActivityPubKeyService keyService)
{
var publicKeyPem = GetPublisherKey(publisher, "public_key");
if (string.IsNullOrEmpty(publicKeyPem))
{
var (newPrivate, newPublic) = keyService.GenerateKeyPair();
SavePublisherKey(publisher, "private_key", newPrivate);
SavePublisherKey(publisher, "public_key", newPublic);
return newPublic;
}
return publicKeyPem;
}
private string? GetPublisherKey(SnPublisher publisher, string keyName)
{
if (publisher.Meta == null)
return null;
var metadata = publisher.Meta as Dictionary<string, object>;
return metadata?.GetValueOrDefault(keyName)?.ToString();
}
private void SavePublisherKey(SnPublisher publisher, string keyName, string keyValue)
{
publisher.Meta ??= new Dictionary<string, object>();
var metadata = publisher.Meta as Dictionary<string, object>;
if (metadata != null)
{
metadata[keyName] = keyValue;
}
}
}
public class ActivityPubActor
{
[JsonPropertyName("@context")] public List<object> Context { get; set; } = [];
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("preferredUsername")] public string? PreferredUsername { get; set; }
[JsonPropertyName("summary")] public string? Summary { get; set; }
[JsonPropertyName("inbox")] public string? Inbox { get; set; }
[JsonPropertyName("outbox")] public string? Outbox { get; set; }
[JsonPropertyName("followers")] public string? Followers { get; set; }
[JsonPropertyName("following")] public string? Following { get; set; }
[JsonPropertyName("published")] public Instant? Published { get; set; }
[JsonPropertyName("url")] public string? Url { get; set; }
[JsonPropertyName("publicKey")] public List<ActivityPubPublicKey>? PublicKeys { get; set; }
}
public class ActivityPubPublicKey
{
[JsonPropertyName("id")] public string? Id { get; set; }
[JsonPropertyName("owner")] public string? Owner { get; set; }
[JsonPropertyName("publicKeyPem")] public string? PublicKeyPem { get; set; }
}
public class ActivityPubCollection
{
[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("first")] public string? First { get; set; }
[JsonPropertyName("items")] public List<object>? Items { get; set; }
}

View File

@@ -0,0 +1,348 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.ActivityPub;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace DysonNetwork.Sphere.ActivityPub;
public class ActivityPubDeliveryService(
AppDatabase db,
ActivityPubSignatureService signatureService,
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<ActivityPubDeliveryService> logger
)
{
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
private HttpClient HttpClient => httpClientFactory.CreateClient();
public async Task<bool> SendAcceptActivityAsync(
Guid publisherId,
string followerActorUri,
string followActivityId
)
{
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var followerActor = await db.FediverseActors
.FirstOrDefaultAsync(a => a.Uri == followerActorUri);
if (followerActor?.InboxUri == null)
{
logger.LogWarning("Follower actor or inbox not found: {Uri}", followerActorUri);
return false;
}
var activity = new Dictionary<string, object>
{
["@context"] = "https://www.w3.org/ns/activitystreams",
["id"] = $"{actorUrl}/accepts/{Guid.NewGuid()}",
["type"] = "Accept",
["actor"] = actorUrl,
["object"] = followActivityId
};
return await SendActivityToInboxAsync(activity, followerActor.InboxUri, actorUrl);
}
public async Task<bool> SendFollowActivityAsync(
Guid publisherId,
string targetActorUri
)
{
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var targetActor = await GetOrFetchActorAsync(targetActorUri);
if (targetActor?.InboxUri == null)
{
logger.LogWarning("Target actor or inbox not found: {Uri}", targetActorUri);
return false;
}
var activity = new Dictionary<string, object>
{
["@context"] = "https://www.w3.org/ns/activitystreams",
["id"] = $"{actorUrl}/follows/{Guid.NewGuid()}",
["type"] = "Follow",
["actor"] = actorUrl,
["object"] = targetActorUri
};
await db.FediverseRelationships.AddAsync(new SnFediverseRelationship
{
IsLocalActor = true,
LocalPublisherId = publisher.Id,
ActorId = Guid.NewGuid(),
TargetActorId = targetActor.Id,
State = RelationshipState.Pending,
IsFollowing = true,
IsFollowedBy = false
});
await db.SaveChangesAsync();
return await SendActivityToInboxAsync(activity, targetActor.InboxUri, actorUrl);
}
public async Task<bool> SendCreateActivityAsync(SnPost post)
{
var publisher = await db.Publishers.FindAsync(post.PublisherId);
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var postUrl = $"https://{Domain}/posts/{post.Id}";
var activity = new Dictionary<string, object>
{
["@context"] = "https://www.w3.org/ns/activitystreams",
["id"] = $"{postUrl}/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 Dictionary<string, object>
{
["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<string, object>
{
["type"] = "Document",
["mediaType"] = "image/jpeg",
["url"] = $"https://{Domain}/api/files/{a.Id}"
}).ToList<object>()
}
};
var followers = await GetRemoteFollowersAsync(publisher.Id);
var successCount = 0;
foreach (var follower in followers)
{
if (follower.InboxUri != null)
{
var success = await SendActivityToInboxAsync(activity, follower.InboxUri, actorUrl);
if (success)
successCount++;
}
}
logger.LogInformation("Sent Create activity to {Count}/{Total} followers",
successCount, followers.Count);
return successCount > 0;
}
public async Task<bool> SendLikeActivityAsync(
Guid postId,
Guid accountId,
string targetActorUri
)
{
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == accountId))
.FirstOrDefaultAsync();
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var postUrl = $"https://{Domain}/posts/{postId}";
var targetActor = await GetOrFetchActorAsync(targetActorUri);
if (targetActor?.InboxUri == null)
return false;
var activity = new Dictionary<string, object>
{
["@context"] = "https://www.w3.org/ns/activitystreams",
["id"] = $"{actorUrl}/likes/{Guid.NewGuid()}",
["type"] = "Like",
["actor"] = actorUrl,
["object"] = postUrl
};
return await SendActivityToInboxAsync(activity, targetActor.InboxUri, actorUrl);
}
public async Task<bool> SendUndoActivityAsync(
string activityType,
string objectUri,
Guid publisherId
)
{
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var followers = await GetRemoteFollowersAsync(publisher.Id);
var activity = new Dictionary<string, object>
{
["@context"] = "https://www.w3.org/ns/activitystreams",
["id"] = $"{actorUrl}/undo/{Guid.NewGuid()}",
["type"] = "Undo",
["actor"] = actorUrl,
["object"] = new Dictionary<string, object>
{
["type"] = activityType,
["object"] = objectUri
}
};
var successCount = 0;
foreach (var follower in followers)
{
if (follower.InboxUri != null)
{
var success = await SendActivityToInboxAsync(activity, follower.InboxUri, actorUrl);
if (success)
successCount++;
}
}
return successCount > 0;
}
private async Task<bool> SendActivityToInboxAsync(
Dictionary<string, object> activity,
string inboxUrl,
string actorUri
)
{
try
{
var json = JsonSerializer.Serialize(activity);
var request = new HttpRequestMessage(HttpMethod.Post, inboxUrl);
request.Content = new StringContent(json, Encoding.UTF8, "application/activity+json");
request.Headers.Date = DateTimeOffset.UtcNow;
var signatureHeaders = await signatureService.SignOutgoingRequest(request, actorUri);
var signature = signatureHeaders;
var signatureString = $"keyId=\"{signature["keyId"]}\"," +
$"algorithm=\"{signature["algorithm"]}\"," +
$"headers=\"{signature["headers"]}\"," +
$"signature=\"{signature["signature"]}\"";
request.Headers.Add("Signature", signatureString);
request.Headers.Add("Host", new Uri(inboxUrl).Host);
request.Headers.Add("Content-Type", "application/activity+json");
var response = await HttpClient.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
logger.LogWarning("Failed to send activity to {Inbox}. Status: {Status}, Response: {Response}",
inboxUrl, response.StatusCode, responseContent);
return false;
}
logger.LogInformation("Successfully sent activity to {Inbox}", inboxUrl);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Error sending activity to {Inbox}", inboxUrl);
return false;
}
}
private async Task<List<SnFediverseActor>> GetRemoteFollowersAsync(Guid publisherId)
{
return await db.FediverseRelationships
.Include(r => r.TargetActor)
.Where(r =>
r.LocalPublisherId == publisherId &&
r.IsFollowedBy &&
r.IsLocalActor)
.Select(r => r.TargetActor)
.ToListAsync();
}
private async Task<SnFediverseActor?> GetOrFetchActorAsync(string actorUri)
{
var actor = await db.FediverseActors
.FirstOrDefaultAsync(a => a.Uri == actorUri);
if (actor != null)
return actor;
try
{
var response = await HttpClient.GetAsync(actorUri);
if (!response.IsSuccessStatusCode)
return null;
var json = await response.Content.ReadAsStringAsync();
var actorData = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
if (actorData == null)
return null;
var domain = new Uri(actorUri).Host;
var instance = await db.FediverseInstances
.FirstOrDefaultAsync(i => i.Domain == domain);
if (instance == null)
{
instance = new SnFediverseInstance
{
Domain = domain,
Name = domain
};
db.FediverseInstances.Add(instance);
await db.SaveChangesAsync();
}
actor = new SnFediverseActor
{
Uri = actorUri,
Username = ExtractUsername(actorUri),
DisplayName = actorData.GetValueOrDefault("name")?.ToString(),
Bio = actorData.GetValueOrDefault("summary")?.ToString(),
InboxUri = actorData.GetValueOrDefault("inbox")?.ToString(),
OutboxUri = actorData.GetValueOrDefault("outbox")?.ToString(),
FollowersUri = actorData.GetValueOrDefault("followers")?.ToString(),
FollowingUri = actorData.GetValueOrDefault("following")?.ToString(),
AvatarUrl = actorData.GetValueOrDefault("icon")?.ToString(),
InstanceId = instance.Id
};
db.FediverseActors.Add(actor);
await db.SaveChangesAsync();
return actor;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to fetch actor: {Uri}", actorUri);
return null;
}
}
private string ExtractUsername(string actorUri)
{
return actorUri.Split('/').Last();
}
}

View File

@@ -0,0 +1,91 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace DysonNetwork.Sphere.ActivityPub;
public class ActivityPubKeyService(ILogger<ActivityPubKeyService> logger)
{
public (string privateKeyPem, string publicKeyPem) GenerateKeyPair()
{
using var rsa = RSA.Create(2048);
var privateKey = rsa.ExportRSAPrivateKey();
var publicKey = rsa.ExportRSAPublicKey();
var privateKeyPem = ConvertToPem(privateKey, "RSA PRIVATE KEY");
var publicKeyPem = ConvertToPem(publicKey, "PUBLIC KEY");
logger.LogInformation("Generated new RSA key pair for ActivityPub");
return (privateKeyPem, publicKeyPem);
}
public string Sign(string privateKeyPem, string dataToSign)
{
using var rsa = CreateRsaFromPrivateKeyPem(privateKeyPem);
var signature = rsa.SignData(
Encoding.UTF8.GetBytes(dataToSign),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
return Convert.ToBase64String(signature);
}
public bool Verify(string publicKeyPem, string data, string signatureBase64)
{
try
{
using var rsa = CreateRsaFromPublicKeyPem(publicKeyPem);
var signature = Convert.FromBase64String(signatureBase64);
return rsa.VerifyData(
Encoding.UTF8.GetBytes(data),
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to verify signature");
return false;
}
}
private static string ConvertToPem(byte[] keyData, string keyType)
{
var sb = new StringBuilder();
sb.AppendLine($"-----BEGIN {keyType}-----");
sb.AppendLine(Convert.ToBase64String(keyData));
sb.AppendLine($"-----END {keyType}-----");
return sb.ToString();
}
private static RSA CreateRsaFromPrivateKeyPem(string privateKeyPem)
{
var rsa = RSA.Create();
var lines = privateKeyPem.Split('\n')
.Where(line => !line.StartsWith("-----") && !string.IsNullOrWhiteSpace(line))
.ToArray();
var keyBytes = Convert.FromBase64String(string.Join("", lines));
rsa.ImportRSAPrivateKey(keyBytes, out _);
return rsa;
}
private static RSA CreateRsaFromPublicKeyPem(string publicKeyPem)
{
var rsa = RSA.Create();
var lines = publicKeyPem.Split('\n')
.Where(line => !line.StartsWith("-----") && !string.IsNullOrWhiteSpace(line))
.ToArray();
var keyBytes = Convert.FromBase64String(string.Join("", lines));
rsa.ImportRSAPublicKey(keyBytes, out _);
return rsa;
}
}

View File

@@ -0,0 +1,230 @@
using System.Security.Cryptography;
using System.Text;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.ActivityPub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.ActivityPub;
public class ActivityPubSignatureService(
AppDatabase db,
ActivityPubKeyService keyService,
ILogger<ActivityPubSignatureService> logger,
IConfiguration configuration
)
{
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
public bool VerifyIncomingRequest(HttpContext context, out string? actorUri)
{
actorUri = null;
if (!context.Request.Headers.ContainsKey("Signature"))
return false;
var signatureHeader = context.Request.Headers["Signature"].ToString();
var signatureParts = ParseSignatureHeader(signatureHeader);
if (signatureParts == null)
{
logger.LogWarning("Invalid signature header format");
return false;
}
actorUri = signatureParts.GetValueOrDefault("keyId");
if (string.IsNullOrEmpty(actorUri))
{
logger.LogWarning("No keyId in signature");
return false;
}
var actor = GetActorByKeyId(actorUri);
if (actor == null)
{
logger.LogWarning("Actor not found for keyId: {KeyId}", actorUri);
return false;
}
if (string.IsNullOrEmpty(actor.PublicKey))
{
logger.LogWarning("Actor has no public key: {ActorId}", actor.Id);
return false;
}
var signingString = BuildSigningString(context, signatureParts);
var signature = signatureParts.GetValueOrDefault("signature");
if (string.IsNullOrEmpty(signingString) || string.IsNullOrEmpty(signature))
{
logger.LogWarning("Failed to build signing string or extract signature");
return false;
}
var isValid = keyService.Verify(actor.PublicKey, signingString, signature);
if (!isValid)
logger.LogWarning("Signature verification failed for actor: {ActorUri}", actorUri);
return isValid;
}
public async Task<Dictionary<string, string>> SignOutgoingRequest(
HttpRequestMessage request,
string actorUri
)
{
var publisher = await GetPublisherByActorUri(actorUri);
if (publisher == null)
throw new InvalidOperationException("Publisher not found");
var keyPair = GetOrGenerateKeyPair(publisher);
var keyId = $"{actorUri}#main-key";
var headersToSign = new[] { "(request-target)", "host", "date" };
var signingString = BuildSigningStringForRequest(request, headersToSign);
var signature = keyService.Sign(keyPair.privateKeyPem, signingString);
return new Dictionary<string, string>
{
["keyId"] = keyId,
["algorithm"] = "rsa-sha256",
["headers"] = string.Join(" ", headersToSign),
["signature"] = signature
};
}
private SnFediverseActor? GetActorByKeyId(string keyId)
{
var actorUri = keyId.Split('#')[0];
return db.FediverseActors.FirstOrDefault(a => a.Uri == actorUri);
}
private async Task<SnPublisher?> GetPublisherByActorUri(string actorUri)
{
var username = actorUri.Split('/')[^1];
return await db.Publishers.FirstOrDefaultAsync(p => p.Name == username);
}
private (string? privateKeyPem, string? publicKeyPem) GetOrGenerateKeyPair(SnPublisher publisher)
{
var privateKeyPem = GetPublisherKey(publisher, "private_key");
var publicKeyPem = GetPublisherKey(publisher, "public_key");
if (string.IsNullOrEmpty(privateKeyPem) || string.IsNullOrEmpty(publicKeyPem))
{
var (newPrivate, newPublic) = keyService.GenerateKeyPair();
SavePublisherKey(publisher, "private_key", newPrivate);
SavePublisherKey(publisher, "public_key", newPublic);
return (newPrivate, newPublic);
}
return (privateKeyPem, publicKeyPem);
}
private string? GetPublisherKey(SnPublisher publisher, string keyName)
{
if (publisher.Meta == null)
return null;
var metadata = publisher.Meta as Dictionary<string, object>;
return metadata?.GetValueOrDefault(keyName)?.ToString();
}
private void SavePublisherKey(SnPublisher publisher, string keyName, string keyValue)
{
publisher.Meta ??= new Dictionary<string, object>();
var metadata = publisher.Meta as Dictionary<string, object>;
if (metadata != null)
{
metadata[keyName] = keyValue;
}
}
private Dictionary<string, string>? ParseSignatureHeader(string signatureHeader)
{
var parts = new Dictionary<string, string>();
foreach (var item in signatureHeader.Split(','))
{
var keyValue = item.Trim().Split('=', 2);
if (keyValue.Length != 2)
continue;
var key = keyValue[0];
var value = keyValue[1].Trim('"');
parts[key] = value;
}
return parts;
}
private string BuildSigningString(HttpContext context, Dictionary<string, string> signatureParts)
{
var headers = signatureParts.GetValueOrDefault("headers")?.Split(' ');
if (headers == null || headers.Length == 0)
return string.Empty;
var sb = new StringBuilder();
foreach (var header in headers)
{
if (sb.Length > 0)
sb.AppendLine();
sb.Append(header.ToLower());
sb.Append(": ");
if (header == "(request-target)")
{
var method = context.Request.Method.ToLower();
var path = context.Request.Path.Value ?? "";
sb.Append($"{method} {path}");
}
else
{
if (context.Request.Headers.TryGetValue(header, out var values))
{
sb.Append(values.ToString());
}
}
}
return sb.ToString();
}
private string BuildSigningStringForRequest(HttpRequestMessage request, string[] headers)
{
var sb = new StringBuilder();
foreach (var header in headers)
{
if (sb.Length > 0)
sb.AppendLine();
sb.Append(header.ToLower());
sb.Append(": ");
if (header == "(request-target)")
{
var method = request.Method.Method.ToLower();
var path = request.RequestUri?.PathAndQuery ?? "/";
sb.Append($"{method} {path}");
}
else if (header == "host")
{
sb.Append(request.RequestUri?.Host);
}
else if (header == "date")
{
if (request.Headers.Contains("Date"))
{
sb.Append(request.Headers.GetValues("Date").First());
}
}
}
return sb.ToString();
}
}

View File

@@ -0,0 +1,85 @@
using System.Net.Mime;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.ActivityPub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.ActivityPub;
[ApiController]
[Route(".well-known")]
public class WebFingerController(
AppDatabase db,
IConfiguration configuration,
ILogger<WebFingerController> logger
) : ControllerBase
{
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
[HttpGet("webfinger")]
[Produces("application/jrd+json")]
public async Task<ActionResult<WebFingerResponse>> GetWebFinger([FromQuery] string resource)
{
if (string.IsNullOrEmpty(resource))
return BadRequest("Missing resource parameter");
if (!resource.StartsWith("acct:"))
return BadRequest("Invalid resource format");
var account = resource[5..];
var parts = account.Split('@');
if (parts.Length != 2)
return BadRequest("Invalid account format");
var username = parts[0];
var domain = parts[1];
if (domain != Domain)
return NotFound();
var publisher = await db.Publishers
.Include(p => p.Members)
.FirstOrDefaultAsync(p => p.Name == username);
if (publisher == null)
return NotFound();
var actorUrl = $"https://{Domain}/activitypub/actors/{username}";
var response = new WebFingerResponse
{
Subject = resource,
Links =
[
new WebFingerLink
{
Rel = "self",
Type = "application/activity+json",
Href = actorUrl
},
new WebFingerLink
{
Rel = "http://webfinger.net/rel/profile-page",
Type = "text/html",
Href = $"https://{Domain}/users/{username}"
}
]
};
return Ok(response);
}
}
public class WebFingerResponse
{
public string Subject { get; set; } = null!;
public List<WebFingerLink> Links { get; set; } = [];
}
public class WebFingerLink
{
public string Rel { get; set; } = null!;
public string Type { get; set; } = null!;
public string Href { get; set; } = null!;
}

View File

@@ -48,6 +48,13 @@ public class AppDatabase(
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
public DbSet<StickerPackOwnership> StickerPackOwnerships { get; set; } = null!;
public DbSet<SnFediverseInstance> FediverseInstances { get; set; } = null!;
public DbSet<SnFediverseActor> FediverseActors { get; set; } = null!;
public DbSet<SnFediverseContent> FediverseContents { get; set; } = null!;
public DbSet<SnFediverseActivity> FediverseActivities { get; set; } = null!;
public DbSet<SnFediverseRelationship> FediverseRelationships { get; set; } = null!;
public DbSet<SnFediverseReaction> FediverseReactions { get; set; } = null!;
public DbSet<WebArticle> WebArticles { get; set; } = null!;
public DbSet<WebFeed> WebFeeds { get; set; } = null!;
public DbSet<WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
@@ -142,6 +149,56 @@ public class AppDatabase(
.HasIndex(a => a.Url)
.IsUnique();
modelBuilder.Entity<SnFediverseActor>()
.HasOne(a => a.Instance)
.WithMany(i => i.Actors)
.HasForeignKey(a => a.InstanceId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseContent>()
.HasOne(c => c.Actor)
.WithMany(a => a.Contents)
.HasForeignKey(c => c.ActorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseContent>()
.HasOne(c => c.Instance)
.WithMany(i => i.Contents)
.HasForeignKey(c => c.InstanceId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseActivity>()
.HasOne(a => a.Actor)
.WithMany(actor => actor.Activities)
.HasForeignKey(a => a.ActorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseActivity>()
.HasOne(a => a.Content)
.WithMany(c => c.Activities)
.HasForeignKey(a => a.ContentId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseRelationship>()
.HasOne(r => r.Actor)
.WithMany(a => a.FollowingRelationships)
.HasForeignKey(r => r.ActorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseRelationship>()
.HasOne(r => r.TargetActor)
.WithMany(a => a.FollowerRelationships)
.HasForeignKey(r => r.TargetActorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseReaction>()
.HasOne(r => r.Content)
.WithMany(c => c.Reactions)
.HasForeignKey(r => r.ContentId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SnFediverseReaction>()
.HasOne(r => r.Actor)
.WithMany()
.HasForeignKey(r => r.ActorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.ApplySoftDeleteFilters();
}

View File

@@ -19,7 +19,7 @@
<PackageReference Include="Markdig" Version="0.44.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -37,6 +37,7 @@
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.ServiceModel.Syndication" Version="10.0.1" />
<PackageReference Include="TencentCloudSDK.Tmt" Version="3.0.1335" />
</ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
using System;
using System.Collections.Generic;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class AddActivityPub : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, object>>(
name: "meta",
table: "publishers",
type: "jsonb",
nullable: true);
migrationBuilder.CreateTable(
name: "fediverse_instances",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
domain = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
software = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
version = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
metadata = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
is_blocked = table.Column<bool>(type: "boolean", nullable: false),
is_silenced = table.Column<bool>(type: "boolean", nullable: false),
block_reason = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
last_fetched_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
last_activity_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_instances", x => x.id);
});
migrationBuilder.CreateTable(
name: "fediverse_actors",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
username = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
display_name = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
bio = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
inbox_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
outbox_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
followers_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
following_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
featured_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
public_key_id = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
public_key = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true),
metadata = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
avatar_url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
header_url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
is_bot = table.Column<bool>(type: "boolean", nullable: false),
is_locked = table.Column<bool>(type: "boolean", nullable: false),
is_discoverable = table.Column<bool>(type: "boolean", nullable: false),
instance_id = table.Column<Guid>(type: "uuid", nullable: false),
last_fetched_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
last_activity_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_actors", x => x.id);
table.ForeignKey(
name: "fk_fediverse_actors_fediverse_instances_instance_id",
column: x => x.instance_id,
principalTable: "fediverse_instances",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "fediverse_contents",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
summary = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
content = table.Column<string>(type: "text", nullable: true),
content_html = table.Column<string>(type: "text", nullable: true),
language = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
in_reply_to = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
announced_content_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
published_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
edited_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_sensitive = table.Column<bool>(type: "boolean", nullable: false),
attachments = table.Column<List<ContentAttachment>>(type: "jsonb", nullable: true),
mentions = table.Column<List<ContentMention>>(type: "jsonb", nullable: true),
tags = table.Column<List<ContentTag>>(type: "jsonb", nullable: true),
emojis = table.Column<List<ContentEmoji>>(type: "jsonb", nullable: true),
metadata = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
actor_id = table.Column<Guid>(type: "uuid", nullable: false),
instance_id = table.Column<Guid>(type: "uuid", nullable: false),
reply_count = table.Column<int>(type: "integer", nullable: false),
boost_count = table.Column<int>(type: "integer", nullable: false),
like_count = table.Column<int>(type: "integer", nullable: false),
local_post_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_contents", x => x.id);
table.ForeignKey(
name: "fk_fediverse_contents_fediverse_actors_actor_id",
column: x => x.actor_id,
principalTable: "fediverse_actors",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_fediverse_contents_fediverse_instances_instance_id",
column: x => x.instance_id,
principalTable: "fediverse_instances",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "fediverse_relationships",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
actor_id = table.Column<Guid>(type: "uuid", nullable: false),
target_actor_id = table.Column<Guid>(type: "uuid", nullable: false),
state = table.Column<int>(type: "integer", nullable: false),
is_following = table.Column<bool>(type: "boolean", nullable: false),
is_followed_by = table.Column<bool>(type: "boolean", nullable: false),
is_muting = table.Column<bool>(type: "boolean", nullable: false),
is_blocking = table.Column<bool>(type: "boolean", nullable: false),
followed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
followed_back_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
reject_reason = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
is_local_actor = table.Column<bool>(type: "boolean", nullable: false),
local_account_id = table.Column<Guid>(type: "uuid", nullable: true),
local_publisher_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_relationships", x => x.id);
table.ForeignKey(
name: "fk_fediverse_relationships_fediverse_actors_actor_id",
column: x => x.actor_id,
principalTable: "fediverse_actors",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_fediverse_relationships_fediverse_actors_target_actor_id",
column: x => x.target_actor_id,
principalTable: "fediverse_actors",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "fediverse_activities",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
object_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
target_uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
published_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_local = table.Column<bool>(type: "boolean", nullable: false),
raw_data = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
actor_id = table.Column<Guid>(type: "uuid", nullable: false),
content_id = table.Column<Guid>(type: "uuid", nullable: true),
target_actor_id = table.Column<Guid>(type: "uuid", nullable: true),
local_post_id = table.Column<Guid>(type: "uuid", nullable: true),
local_account_id = table.Column<Guid>(type: "uuid", nullable: true),
status = table.Column<int>(type: "integer", nullable: false),
error_message = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_activities", x => x.id);
table.ForeignKey(
name: "fk_fediverse_activities_fediverse_actors_actor_id",
column: x => x.actor_id,
principalTable: "fediverse_actors",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_fediverse_activities_fediverse_actors_target_actor_id",
column: x => x.target_actor_id,
principalTable: "fediverse_actors",
principalColumn: "id");
table.ForeignKey(
name: "fk_fediverse_activities_fediverse_contents_content_id",
column: x => x.content_id,
principalTable: "fediverse_contents",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "fediverse_reactions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
uri = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
emoji = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
is_local = table.Column<bool>(type: "boolean", nullable: false),
content_id = table.Column<Guid>(type: "uuid", nullable: false),
actor_id = table.Column<Guid>(type: "uuid", nullable: false),
local_account_id = table.Column<Guid>(type: "uuid", nullable: true),
local_reaction_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_reactions", x => x.id);
table.ForeignKey(
name: "fk_fediverse_reactions_fediverse_actors_actor_id",
column: x => x.actor_id,
principalTable: "fediverse_actors",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_fediverse_reactions_fediverse_contents_content_id",
column: x => x.content_id,
principalTable: "fediverse_contents",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_fediverse_activities_actor_id",
table: "fediverse_activities",
column: "actor_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_activities_content_id",
table: "fediverse_activities",
column: "content_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_activities_target_actor_id",
table: "fediverse_activities",
column: "target_actor_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_actors_instance_id",
table: "fediverse_actors",
column: "instance_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_actors_uri",
table: "fediverse_actors",
column: "uri",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_fediverse_contents_actor_id",
table: "fediverse_contents",
column: "actor_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_contents_instance_id",
table: "fediverse_contents",
column: "instance_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_contents_uri",
table: "fediverse_contents",
column: "uri",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_fediverse_instances_domain",
table: "fediverse_instances",
column: "domain",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_fediverse_reactions_actor_id",
table: "fediverse_reactions",
column: "actor_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_reactions_content_id",
table: "fediverse_reactions",
column: "content_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_relationships_actor_id",
table: "fediverse_relationships",
column: "actor_id");
migrationBuilder.CreateIndex(
name: "ix_fediverse_relationships_target_actor_id",
table: "fediverse_relationships",
column: "target_actor_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "fediverse_activities");
migrationBuilder.DropTable(
name: "fediverse_reactions");
migrationBuilder.DropTable(
name: "fediverse_relationships");
migrationBuilder.DropTable(
name: "fediverse_contents");
migrationBuilder.DropTable(
name: "fediverse_actors");
migrationBuilder.DropTable(
name: "fediverse_instances");
migrationBuilder.DropColumn(
name: "meta",
table: "publishers");
}
}
}

View File

@@ -22,7 +22,7 @@ namespace DysonNetwork.Sphere.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -140,7 +140,7 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("uuid")
.HasColumnName("forwarded_message_id");
b.Property<List<Guid>>("MembersMentioned")
b.PrimitiveCollection<string>("MembersMentioned")
.HasColumnType("jsonb")
.HasColumnName("members_mentioned");
@@ -302,6 +302,592 @@ namespace DysonNetwork.Sphere.Migrations
b.ToTable("chat_rooms", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseActivity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("ActorId")
.HasColumnType("uuid")
.HasColumnName("actor_id");
b.Property<Guid?>("ContentId")
.HasColumnType("uuid")
.HasColumnName("content_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("ErrorMessage")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("error_message");
b.Property<bool>("IsLocal")
.HasColumnType("boolean")
.HasColumnName("is_local");
b.Property<Guid?>("LocalAccountId")
.HasColumnType("uuid")
.HasColumnName("local_account_id");
b.Property<Guid?>("LocalPostId")
.HasColumnType("uuid")
.HasColumnName("local_post_id");
b.Property<string>("ObjectUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("object_uri");
b.Property<Instant?>("PublishedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("published_at");
b.Property<Dictionary<string, object>>("RawData")
.HasColumnType("jsonb")
.HasColumnName("raw_data");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<Guid?>("TargetActorId")
.HasColumnType("uuid")
.HasColumnName("target_actor_id");
b.Property<string>("TargetUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("target_uri");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("Uri")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("uri");
b.HasKey("Id")
.HasName("pk_fediverse_activities");
b.HasIndex("ActorId")
.HasDatabaseName("ix_fediverse_activities_actor_id");
b.HasIndex("ContentId")
.HasDatabaseName("ix_fediverse_activities_content_id");
b.HasIndex("TargetActorId")
.HasDatabaseName("ix_fediverse_activities_target_actor_id");
b.ToTable("fediverse_activities", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseActor", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("AvatarUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("avatar_url");
b.Property<string>("Bio")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("bio");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DisplayName")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("display_name");
b.Property<string>("FeaturedUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("featured_uri");
b.Property<string>("FollowersUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("followers_uri");
b.Property<string>("FollowingUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("following_uri");
b.Property<string>("HeaderUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("header_url");
b.Property<string>("InboxUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("inbox_uri");
b.Property<Guid>("InstanceId")
.HasColumnType("uuid")
.HasColumnName("instance_id");
b.Property<bool>("IsBot")
.HasColumnType("boolean")
.HasColumnName("is_bot");
b.Property<bool>("IsDiscoverable")
.HasColumnType("boolean")
.HasColumnName("is_discoverable");
b.Property<bool>("IsLocked")
.HasColumnType("boolean")
.HasColumnName("is_locked");
b.Property<Instant?>("LastActivityAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_activity_at");
b.Property<Instant?>("LastFetchedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_fetched_at");
b.Property<Dictionary<string, object>>("Metadata")
.HasColumnType("jsonb")
.HasColumnName("metadata");
b.Property<string>("OutboxUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("outbox_uri");
b.Property<string>("PublicKey")
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("public_key");
b.Property<string>("PublicKeyId")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("public_key_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("Uri")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("uri");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("username");
b.HasKey("Id")
.HasName("pk_fediverse_actors");
b.HasIndex("InstanceId")
.HasDatabaseName("ix_fediverse_actors_instance_id");
b.HasIndex("Uri")
.IsUnique()
.HasDatabaseName("ix_fediverse_actors_uri");
b.ToTable("fediverse_actors", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseContent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("ActorId")
.HasColumnType("uuid")
.HasColumnName("actor_id");
b.Property<string>("AnnouncedContentUri")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("announced_content_uri");
b.Property<List<ContentAttachment>>("Attachments")
.HasColumnType("jsonb")
.HasColumnName("attachments");
b.Property<int>("BoostCount")
.HasColumnType("integer")
.HasColumnName("boost_count");
b.Property<string>("Content")
.HasColumnType("text")
.HasColumnName("content");
b.Property<string>("ContentHtml")
.HasColumnType("text")
.HasColumnName("content_html");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("EditedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("edited_at");
b.Property<List<ContentEmoji>>("Emojis")
.HasColumnType("jsonb")
.HasColumnName("emojis");
b.Property<string>("InReplyTo")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("in_reply_to");
b.Property<Guid>("InstanceId")
.HasColumnType("uuid")
.HasColumnName("instance_id");
b.Property<bool>("IsSensitive")
.HasColumnType("boolean")
.HasColumnName("is_sensitive");
b.Property<string>("Language")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("language");
b.Property<int>("LikeCount")
.HasColumnType("integer")
.HasColumnName("like_count");
b.Property<Guid?>("LocalPostId")
.HasColumnType("uuid")
.HasColumnName("local_post_id");
b.Property<List<ContentMention>>("Mentions")
.HasColumnType("jsonb")
.HasColumnName("mentions");
b.Property<Dictionary<string, object>>("Metadata")
.HasColumnType("jsonb")
.HasColumnName("metadata");
b.Property<Instant?>("PublishedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("published_at");
b.Property<int>("ReplyCount")
.HasColumnType("integer")
.HasColumnName("reply_count");
b.Property<string>("Summary")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("summary");
b.Property<List<ContentTag>>("Tags")
.HasColumnType("jsonb")
.HasColumnName("tags");
b.Property<string>("Title")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("title");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("Uri")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("uri");
b.HasKey("Id")
.HasName("pk_fediverse_contents");
b.HasIndex("ActorId")
.HasDatabaseName("ix_fediverse_contents_actor_id");
b.HasIndex("InstanceId")
.HasDatabaseName("ix_fediverse_contents_instance_id");
b.HasIndex("Uri")
.IsUnique()
.HasDatabaseName("ix_fediverse_contents_uri");
b.ToTable("fediverse_contents", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseInstance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("BlockReason")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("block_reason");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<string>("Domain")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("domain");
b.Property<bool>("IsBlocked")
.HasColumnType("boolean")
.HasColumnName("is_blocked");
b.Property<bool>("IsSilenced")
.HasColumnType("boolean")
.HasColumnName("is_silenced");
b.Property<Instant?>("LastActivityAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_activity_at");
b.Property<Instant?>("LastFetchedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_fetched_at");
b.Property<Dictionary<string, object>>("Metadata")
.HasColumnType("jsonb")
.HasColumnName("metadata");
b.Property<string>("Name")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("name");
b.Property<string>("Software")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("software");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("Version")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("version");
b.HasKey("Id")
.HasName("pk_fediverse_instances");
b.HasIndex("Domain")
.IsUnique()
.HasDatabaseName("ix_fediverse_instances_domain");
b.ToTable("fediverse_instances", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseReaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("ActorId")
.HasColumnType("uuid")
.HasColumnName("actor_id");
b.Property<Guid>("ContentId")
.HasColumnType("uuid")
.HasColumnName("content_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Emoji")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("emoji");
b.Property<bool>("IsLocal")
.HasColumnType("boolean")
.HasColumnName("is_local");
b.Property<Guid?>("LocalAccountId")
.HasColumnType("uuid")
.HasColumnName("local_account_id");
b.Property<Guid?>("LocalReactionId")
.HasColumnType("uuid")
.HasColumnName("local_reaction_id");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("Uri")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("uri");
b.HasKey("Id")
.HasName("pk_fediverse_reactions");
b.HasIndex("ActorId")
.HasDatabaseName("ix_fediverse_reactions_actor_id");
b.HasIndex("ContentId")
.HasDatabaseName("ix_fediverse_reactions_content_id");
b.ToTable("fediverse_reactions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseRelationship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("ActorId")
.HasColumnType("uuid")
.HasColumnName("actor_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("FollowedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("followed_at");
b.Property<Instant?>("FollowedBackAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("followed_back_at");
b.Property<bool>("IsBlocking")
.HasColumnType("boolean")
.HasColumnName("is_blocking");
b.Property<bool>("IsFollowedBy")
.HasColumnType("boolean")
.HasColumnName("is_followed_by");
b.Property<bool>("IsFollowing")
.HasColumnType("boolean")
.HasColumnName("is_following");
b.Property<bool>("IsLocalActor")
.HasColumnType("boolean")
.HasColumnName("is_local_actor");
b.Property<bool>("IsMuting")
.HasColumnType("boolean")
.HasColumnName("is_muting");
b.Property<Guid?>("LocalAccountId")
.HasColumnType("uuid")
.HasColumnName("local_account_id");
b.Property<Guid?>("LocalPublisherId")
.HasColumnType("uuid")
.HasColumnName("local_publisher_id");
b.Property<string>("RejectReason")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("reject_reason");
b.Property<int>("State")
.HasColumnType("integer")
.HasColumnName("state");
b.Property<Guid>("TargetActorId")
.HasColumnType("uuid")
.HasColumnName("target_actor_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_fediverse_relationships");
b.HasIndex("ActorId")
.HasDatabaseName("ix_fediverse_relationships_actor_id");
b.HasIndex("TargetActorId")
.HasDatabaseName("ix_fediverse_relationships_target_actor_id");
b.ToTable("fediverse_relationships", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnPoll", b =>
{
b.Property<Guid>("Id")
@@ -533,7 +1119,7 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("uuid")
.HasColumnName("replied_post_id");
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
b.PrimitiveCollection<string>("SensitiveMarks")
.HasColumnType("jsonb")
.HasColumnName("sensitive_marks");
@@ -912,6 +1498,10 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Dictionary<string, object>>("Meta")
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
@@ -1572,6 +2162,108 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Sender");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseActivity", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "Actor")
.WithMany("Activities")
.HasForeignKey("ActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_activities_fediverse_actors_actor_id");
b.HasOne("DysonNetwork.Shared.Models.SnFediverseContent", "Content")
.WithMany("Activities")
.HasForeignKey("ContentId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_fediverse_activities_fediverse_contents_content_id");
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "TargetActor")
.WithMany()
.HasForeignKey("TargetActorId")
.HasConstraintName("fk_fediverse_activities_fediverse_actors_target_actor_id");
b.Navigation("Actor");
b.Navigation("Content");
b.Navigation("TargetActor");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseActor", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFediverseInstance", "Instance")
.WithMany("Actors")
.HasForeignKey("InstanceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_actors_fediverse_instances_instance_id");
b.Navigation("Instance");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseContent", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "Actor")
.WithMany("Contents")
.HasForeignKey("ActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_contents_fediverse_actors_actor_id");
b.HasOne("DysonNetwork.Shared.Models.SnFediverseInstance", "Instance")
.WithMany("Contents")
.HasForeignKey("InstanceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_contents_fediverse_instances_instance_id");
b.Navigation("Actor");
b.Navigation("Instance");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseReaction", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "Actor")
.WithMany()
.HasForeignKey("ActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_reactions_fediverse_actors_actor_id");
b.HasOne("DysonNetwork.Shared.Models.SnFediverseContent", "Content")
.WithMany("Reactions")
.HasForeignKey("ContentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_reactions_fediverse_contents_content_id");
b.Navigation("Actor");
b.Navigation("Content");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseRelationship", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "Actor")
.WithMany("FollowingRelationships")
.HasForeignKey("ActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_relationships_fediverse_actors_actor_id");
b.HasOne("DysonNetwork.Shared.Models.SnFediverseActor", "TargetActor")
.WithMany("FollowerRelationships")
.HasForeignKey("TargetActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_fediverse_relationships_fediverse_actors_target_actor_id");
b.Navigation("Actor");
b.Navigation("TargetActor");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnPoll", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnPublisher", "Publisher")
@@ -1891,6 +2583,31 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Members");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseActor", b =>
{
b.Navigation("Activities");
b.Navigation("Contents");
b.Navigation("FollowerRelationships");
b.Navigation("FollowingRelationships");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseContent", b =>
{
b.Navigation("Activities");
b.Navigation("Reactions");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFediverseInstance", b =>
{
b.Navigation("Actors");
b.Navigation("Contents");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnPoll", b =>
{
b.Navigation("Questions");

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Geometry;
using DysonNetwork.Sphere.ActivityPub;
using DysonNetwork.Sphere.Autocompletion;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Chat.Realtime;
@@ -102,6 +103,10 @@ public static class ServiceCollectionExtensions
services.AddScoped<DiscoveryService>();
services.AddScoped<PollService>();
services.AddScoped<AutocompletionService>();
services.AddScoped<ActivityPubKeyService>();
services.AddScoped<ActivityPubSignatureService>();
services.AddScoped<ActivityPubActivityProcessor>();
services.AddScoped<ActivityPubDeliveryService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower();
switch (translationProvider)