♻️ Refactor the follow activitypub process

This commit is contained in:
2025-12-31 22:52:31 +08:00
parent add9fa49e5
commit c11b30d0bb
2 changed files with 86 additions and 101 deletions

View File

@@ -26,10 +26,10 @@ public class ActivityPubActivityHandler(
{ {
var uri = new Uri(objectUri); var uri = new Uri(objectUri);
var domain = uri.Host; var domain = uri.Host;
// Remote post // Remote post
if (domain != Domain) return await db.Posts.FirstOrDefaultAsync(c => c.FediverseUri == objectUri); if (domain != Domain) return await db.Posts.FirstOrDefaultAsync(c => c.FediverseUri == objectUri);
// Local post, extract ID from path like /posts/{guid} // Local post, extract ID from path like /posts/{guid}
var path = uri.AbsolutePath.Trim('/'); var path = uri.AbsolutePath.Trim('/');
var segments = path.Split('/'); var segments = path.Split('/');
@@ -39,36 +39,37 @@ public class ActivityPubActivityHandler(
{ {
return await db.Posts.FirstOrDefaultAsync(p => p.Id == id); return await db.Posts.FirstOrDefaultAsync(p => p.Id == id);
} }
return null; return null;
} }
public async Task<bool> HandleIncomingActivityAsync( public async Task<bool> HandleIncomingActivityAsync(
HttpContext context, HttpContext context,
string username, string username,
Dictionary<string, object> activity Dictionary<string, object> activity
) )
{ {
logger.LogInformation("Incoming activity request. Username: {Username}, Path: {Path}", logger.LogInformation("Incoming activity request. Username: {Username}, Path: {Path}",
username, context.Request.Path); username, context.Request.Path);
var activityType = activity.GetValueOrDefault("type")?.ToString(); var activityType = activity.GetValueOrDefault("type")?.ToString();
var activityId = activity.GetValueOrDefault("id")?.ToString(); var activityId = activity.GetValueOrDefault("id")?.ToString();
var actor = activity.GetValueOrDefault("actor")?.ToString(); var actor = activity.GetValueOrDefault("actor")?.ToString();
logger.LogInformation("Activity details - Type: {Type}, ID: {Id}, Actor: {Actor}", logger.LogInformation("Activity details - Type: {Type}, ID: {Id}, Actor: {Actor}",
activityType, activityId, actor); activityType, activityId, actor);
if (!signatureService.VerifyIncomingRequest(context, out var actorUri)) if (!signatureService.VerifyIncomingRequest(context, out var actorUri))
{ {
logger.LogWarning("Failed to verify signature for incoming activity. Type: {Type}, From: {Actor}", logger.LogWarning("Failed to verify signature for incoming activity. Type: {Type}, From: {Actor}",
activityType, actor); activityType, actor);
return false; return false;
} }
if (string.IsNullOrEmpty(actorUri)) if (string.IsNullOrEmpty(actorUri))
return false; return false;
logger.LogInformation("Signature verified successfully. Handling {Type} from {ActorUri}", logger.LogInformation("Signature verified successfully. Handling {Type} from {ActorUri}",
activityType, actorUri); activityType, actorUri);
try try
@@ -111,60 +112,44 @@ public class ActivityPubActivityHandler(
{ {
var objectUri = activity.GetValueOrDefault("object")?.ToString(); var objectUri = activity.GetValueOrDefault("object")?.ToString();
var activityId = activity.GetValueOrDefault("id")?.ToString(); var activityId = activity.GetValueOrDefault("id")?.ToString();
logger.LogInformation("Handling Follow. Actor: {ActorUri}, Target: {ObjectUri}, ActivityId: {Id}", logger.LogInformation("Handling Follow. Actor: {ActorUri}, Target: {ObjectUri}, ActivityId: {Id}",
actorUri, objectUri, activityId); actorUri, objectUri, activityId);
if (string.IsNullOrEmpty(objectUri)) if (string.IsNullOrEmpty(objectUri))
{ {
logger.LogWarning("Follow activity missing object field"); logger.LogWarning("Follow activity missing object field");
return false; return false;
} }
var actor = await GetOrCreateActorAsync(actorUri);
var targetUsername = ExtractUsernameFromUri(objectUri);
var targetPublisher = await db.Publishers
.FirstOrDefaultAsync(p => p.Name == targetUsername);
if (targetPublisher == null)
{
logger.LogWarning("Target publisher not found: {Uri}, ExtractedUsername: {Username}",
objectUri, targetUsername);
return false;
}
var localActor = await deliveryService.GetLocalActorAsync(targetPublisher.Id); var actor = await GetOrCreateActorAsync(actorUri);
if (localActor == null) // This might be fail, but we assume it works great.
{ var targetActor = await GetOrCreateActorAsync(objectUri);
logger.LogWarning("Target publisher has no enabled fediverse actor");
return false;
}
logger.LogInformation("Target publisher found: {PublisherName} (ID: {Id})",
targetPublisher.Name, targetPublisher.Id);
var existingRelationship = await db.FediverseRelationships var existingRelationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ActorId == actor.Id && r.ActorId == actor.Id &&
r.TargetActorId == localActor.Id); r.TargetActorId == targetActor.Id);
switch (existingRelationship) switch (existingRelationship)
{ {
case { State: RelationshipState.Accepted }: case { State: RelationshipState.Accepted }:
logger.LogInformation("Follow relationship already exists and is accepted. ActorId: {ActorId}, PublisherId: {PublisherId}", logger.LogInformation(
actor.Id, targetPublisher.Id); "Follow relationship already exists and is accepted. ActorId: {ActorId}, TargetId: {TargetId}",
actor.Id, targetActor.Id);
return true; return true;
case null: case null:
existingRelationship = new SnFediverseRelationship existingRelationship = new SnFediverseRelationship
{ {
ActorId = actor.Id, ActorId = actor.Id,
TargetActorId = localActor.Id, TargetActorId = targetActor.Id,
State = RelationshipState.Accepted, State = RelationshipState.Accepted,
FollowedBackAt = SystemClock.Instance.GetCurrentInstant() FollowedBackAt = SystemClock.Instance.GetCurrentInstant()
}; };
db.FediverseRelationships.Add(existingRelationship); db.FediverseRelationships.Add(existingRelationship);
logger.LogInformation("Created new follow relationship. ActorId: {ActorId}, TargetActorId: {TargetActorId}", logger.LogInformation(
actor.Id, localActor.Id); "Created new follow relationship. ActorId: {ActorId}, TargetActorId: {TargetId}",
actor.Id, targetActor.Id);
break; break;
default: default:
existingRelationship.State = RelationshipState.Accepted; existingRelationship.State = RelationshipState.Accepted;
@@ -177,7 +162,7 @@ public class ActivityPubActivityHandler(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await deliveryService.SendAcceptActivityAsync( await deliveryService.SendAcceptActivityAsync(
targetPublisher.Id, targetActor,
actorUri, actorUri,
activityId ?? "" activityId ?? ""
); );
@@ -192,9 +177,9 @@ public class ActivityPubActivityHandler(
var objectUri = activity.GetValueOrDefault("object")?.ToString(); var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri)) if (string.IsNullOrEmpty(objectUri))
return false; return false;
var actor = await GetOrCreateActorAsync(actorUri); var actor = await GetOrCreateActorAsync(actorUri);
var relationship = await db.FediverseRelationships var relationship = await db.FediverseRelationships
.Include(r => r.Actor) .Include(r => r.Actor)
.Include(r => r.TargetActor) .Include(r => r.TargetActor)
@@ -210,6 +195,7 @@ public class ActivityPubActivityHandler(
logger.LogWarning("Local actor not found for accept object: {ObjectUri}", objectUri); logger.LogWarning("Local actor not found for accept object: {ObjectUri}", objectUri);
return false; return false;
} }
relationship = new SnFediverseRelationship relationship = new SnFediverseRelationship
{ {
ActorId = localActor.Id, ActorId = localActor.Id,
@@ -226,7 +212,7 @@ public class ActivityPubActivityHandler(
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
logger.LogInformation("Handled accept from {Actor}", actorUri); logger.LogInformation("Handled accept from {Actor}", actorUri);
return true; return true;
} }
@@ -236,24 +222,24 @@ public class ActivityPubActivityHandler(
var objectUri = activity.GetValueOrDefault("object")?.ToString(); var objectUri = activity.GetValueOrDefault("object")?.ToString();
if (string.IsNullOrEmpty(objectUri)) if (string.IsNullOrEmpty(objectUri))
return false; return false;
var actor = await GetOrCreateActorAsync(actorUri); var actor = await GetOrCreateActorAsync(actorUri);
var relationship = await db.FediverseRelationships var relationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.TargetActorId == actor.Id); r.TargetActorId == actor.Id);
if (relationship == null) if (relationship == null)
{ {
logger.LogWarning("No relationship found for reject"); logger.LogWarning("No relationship found for reject");
return false; return false;
} }
relationship.State = RelationshipState.Rejected; relationship.State = RelationshipState.Rejected;
relationship.RejectReason = "Remote rejected follow"; relationship.RejectReason = "Remote rejected follow";
await db.SaveChangesAsync(); await db.SaveChangesAsync();
logger.LogInformation("Handled reject from {Actor}", actorUri); logger.LogInformation("Handled reject from {Actor}", actorUri);
return true; return true;
} }
@@ -278,37 +264,41 @@ public class ActivityPubActivityHandler(
var objectValue = activity.GetValueOrDefault("object"); var objectValue = activity.GetValueOrDefault("object");
if (objectValue is not Dictionary<string, object> objectDict) if (objectValue is not Dictionary<string, object> objectDict)
return false; return false;
var objectType = objectDict.GetValueOrDefault("type")?.ToString(); var objectType = objectDict.GetValueOrDefault("type")?.ToString();
if (objectType != "Note" && objectType != "Article") if (objectType != "Note" && objectType != "Article")
{ {
logger.LogInformation("Skipping non-note content type: {Type}", objectType); logger.LogInformation("Skipping non-note content type: {Type}", objectType);
return true; return true;
} }
var actor = await GetOrCreateActorAsync(actorUri); var actor = await GetOrCreateActorAsync(actorUri);
var contentUri = objectDict.GetValueOrDefault("id")?.ToString(); var contentUri = objectDict.GetValueOrDefault("id")?.ToString();
if (string.IsNullOrEmpty(contentUri)) if (string.IsNullOrEmpty(contentUri))
return false; return false;
var existingContent = await db.Posts var existingContent = await db.Posts
.FirstOrDefaultAsync(c => c.FediverseUri == contentUri); .FirstOrDefaultAsync(c => c.FediverseUri == contentUri);
if (existingContent != null) if (existingContent != null)
{ {
logger.LogInformation("Content already exists: {Uri}", contentUri); logger.LogInformation("Content already exists: {Uri}", contentUri);
return true; return true;
} }
var content = new SnPost var content = new SnPost
{ {
FediverseUri = contentUri, FediverseUri = contentUri,
FediverseType = objectType == "Article" ? FediverseContentType.FediverseArticle : FediverseContentType.FediverseNote, FediverseType = objectType == "Article"
? FediverseContentType.FediverseArticle
: FediverseContentType.FediverseNote,
Title = objectDict.GetValueOrDefault("name")?.ToString(), Title = objectDict.GetValueOrDefault("name")?.ToString(),
Description = objectDict.GetValueOrDefault("summary")?.ToString(), Description = objectDict.GetValueOrDefault("summary")?.ToString(),
Content = objectDict.GetValueOrDefault("content")?.ToString(), Content = objectDict.GetValueOrDefault("content")?.ToString(),
ContentType = objectDict.GetValueOrDefault("contentMap") != null ? PostContentType.Html : PostContentType.Markdown, ContentType = objectDict.GetValueOrDefault("contentMap") != null
? PostContentType.Html
: PostContentType.Markdown,
PublishedAt = ParseInstant(objectDict.GetValueOrDefault("published")), PublishedAt = ParseInstant(objectDict.GetValueOrDefault("published")),
EditedAt = ParseInstant(objectDict.GetValueOrDefault("updated")), EditedAt = ParseInstant(objectDict.GetValueOrDefault("updated")),
ActorId = actor.Id, ActorId = actor.Id,
@@ -319,10 +309,10 @@ public class ActivityPubActivityHandler(
Visibility = PostVisibility.Public, Visibility = PostVisibility.Public,
Metadata = BuildMetadataFromActivity(objectDict) Metadata = BuildMetadataFromActivity(objectDict)
}; };
db.Posts.Add(content); db.Posts.Add(content);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
logger.LogInformation("Created federated content: {Uri}", contentUri); logger.LogInformation("Created federated content: {Uri}", contentUri);
return true; return true;
} }
@@ -341,19 +331,19 @@ public class ActivityPubActivityHandler(
logger.LogWarning("Content not found for like: {Uri}", objectUri); logger.LogWarning("Content not found for like: {Uri}", objectUri);
return false; return false;
} }
var existingReaction = await db.PostReactions var existingReaction = await db.PostReactions
.FirstOrDefaultAsync(r => .FirstOrDefaultAsync(r =>
r.ActorId == actor.Id && r.ActorId == actor.Id &&
r.PostId == content.Id && r.PostId == content.Id &&
r.Symbol == "thumb_up"); r.Symbol == "thumb_up");
if (existingReaction != null) if (existingReaction != null)
{ {
logger.LogInformation("Like already exists"); logger.LogInformation("Like already exists");
return true; return true;
} }
var reaction = new SnPostReaction var reaction = new SnPostReaction
{ {
FediverseUri = activity.GetValueOrDefault("id")?.ToString() ?? Guid.NewGuid().ToString(), FediverseUri = activity.GetValueOrDefault("id")?.ToString() ?? Guid.NewGuid().ToString(),
@@ -366,12 +356,12 @@ public class ActivityPubActivityHandler(
CreatedAt = SystemClock.Instance.GetCurrentInstant(), CreatedAt = SystemClock.Instance.GetCurrentInstant(),
UpdatedAt = SystemClock.Instance.GetCurrentInstant() UpdatedAt = SystemClock.Instance.GetCurrentInstant()
}; };
db.PostReactions.Add(reaction); db.PostReactions.Add(reaction);
content.Upvotes++; content.Upvotes++;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
logger.LogInformation("Handled like from {Actor}", actorUri); logger.LogInformation("Handled like from {Actor}", actorUri);
return true; return true;
} }
@@ -437,10 +427,10 @@ public class ActivityPubActivityHandler(
logger.LogInformation("Undid follow relationship failed, no target actor uri provided."); logger.LogInformation("Undid follow relationship failed, no target actor uri provided.");
return false; return false;
} }
var actor = await GetOrCreateActorAsync(actorUri); var actor = await GetOrCreateActorAsync(actorUri);
var targetActor = await GetOrCreateActorAsync(targetActorUri); var targetActor = await GetOrCreateActorAsync(targetActorUri);
var relationship = await db.FediverseRelationships var relationship = await db.FediverseRelationships
.FirstOrDefaultAsync(r => r.ActorId == actor.Id && r.TargetActorId == targetActor.Id); .FirstOrDefaultAsync(r => r.ActorId == actor.Id && r.TargetActorId == targetActor.Id);
@@ -499,23 +489,22 @@ public class ActivityPubActivityHandler(
{ {
var actor = await db.FediverseActors var actor = await db.FediverseActors
.FirstOrDefaultAsync(a => a.Uri == actorUri); .FirstOrDefaultAsync(a => a.Uri == actorUri);
if (actor == null) if (actor != null) return actor;
var instance = await GetOrCreateInstanceAsync(actorUri);
actor = new SnFediverseActor
{ {
var instance = await GetOrCreateInstanceAsync(actorUri); Uri = actorUri,
actor = new SnFediverseActor Username = ExtractUsernameFromUri(actorUri),
{ DisplayName = ExtractUsernameFromUri(actorUri),
Uri = actorUri, InstanceId = instance.Id
Username = ExtractUsernameFromUri(actorUri), };
DisplayName = ExtractUsernameFromUri(actorUri), db.FediverseActors.Add(actor);
InstanceId = instance.Id await db.SaveChangesAsync();
};
db.FediverseActors.Add(actor); await discoveryService.FetchActorDataAsync(actor);
await db.SaveChangesAsync();
await discoveryService.FetchActorDataAsync(actor);
}
return actor; return actor;
} }
@@ -524,7 +513,7 @@ public class ActivityPubActivityHandler(
var domain = ExtractDomainFromUri(actorUri); var domain = ExtractDomainFromUri(actorUri);
var instance = await db.FediverseInstances var instance = await db.FediverseInstances
.FirstOrDefaultAsync(i => i.Domain == domain); .FirstOrDefaultAsync(i => i.Domain == domain);
if (instance == null) if (instance == null)
{ {
instance = new SnFediverseInstance instance = new SnFediverseInstance
@@ -536,7 +525,7 @@ public class ActivityPubActivityHandler(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await discoveryService.FetchInstanceMetadataAsync(instance); await discoveryService.FetchInstanceMetadataAsync(instance);
} }
return instance; return instance;
} }
@@ -555,13 +544,13 @@ public class ActivityPubActivityHandler(
{ {
if (value == null) if (value == null)
return null; return null;
if (value is Instant instant) if (value is Instant instant)
return instant; return instant;
if (DateTimeOffset.TryParse(value.ToString(), out var dateTimeOffset)) if (DateTimeOffset.TryParse(value.ToString(), out var dateTimeOffset))
return Instant.FromDateTimeOffset(dateTimeOffset); return Instant.FromDateTimeOffset(dateTimeOffset);
return null; return null;
} }
@@ -646,4 +635,4 @@ public class ActivityPubActivityHandler(
return metadata; return metadata;
} }
} }

View File

@@ -29,16 +29,12 @@ public class ActivityPubDeliveryService(
} }
public async Task<bool> SendAcceptActivityAsync( public async Task<bool> SendAcceptActivityAsync(
Guid publisherId, SnFediverseActor actor,
string followerActorUri, string followerActorUri,
string followActivityId string followActivityId
) )
{ {
var publisher = await db.Publishers.FindAsync(publisherId); var actorUrl = actor.Uri;
if (publisher == null)
return false;
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var followerActor = await db.FediverseActors var followerActor = await db.FediverseActors
.FirstOrDefaultAsync(a => a.Uri == followerActorUri); .FirstOrDefaultAsync(a => a.Uri == followerActorUri);