diff --git a/DysonNetwork.Sphere/ActivityPub/ActivityPubDiscoveryService.cs b/DysonNetwork.Sphere/ActivityPub/ActivityPubDiscoveryService.cs index 0bfb855..8d18403 100644 --- a/DysonNetwork.Sphere/ActivityPub/ActivityPubDiscoveryService.cs +++ b/DysonNetwork.Sphere/ActivityPub/ActivityPubDiscoveryService.cs @@ -24,11 +24,11 @@ public partial class ActivityPubDiscoveryService( if (username == null || domain == null) return null; - var actorUri = await GetActorUriFromWebfingerAsync(username, domain); + var (actorUri, avatarUrl) = await GetActorUriFromWebfingerAsync(username, domain); if (actorUri == null) return null; - return await FetchAndStoreActorAsync(actorUri); + return await StoreActorAsync(actorUri, username, domain, avatarUrl); } public async Task> SearchActorsAsync( @@ -51,9 +51,9 @@ public partial class ActivityPubDiscoveryService( var (username, domain) = ParseHandle(query); if (username == null || domain == null) return localResults; { - var actorUri = await GetActorUriFromWebfingerAsync(username, domain); + var (actorUri, avatarUrl) = await GetActorUriFromWebfingerAsync(username, domain); if (actorUri == null) return localResults; - var remoteActor = await FetchAndStoreActorAsync(actorUri); + var remoteActor = await StoreActorAsync(actorUri, username, domain, avatarUrl); if (remoteActor == null || localResults.Any(a => a.Uri == actorUri)) return localResults; var combined = new List(localResults) { remoteActor }; return combined.Take(limit).ToList(); @@ -66,10 +66,10 @@ public partial class ActivityPubDiscoveryService( return !match.Success ? (null, null) : (match.Groups[1].Value, match.Groups[2].Value); } - private async Task GetActorUriFromWebfingerAsync(string username, string domain) + private async Task<(string? actorUri, string? avatarUrl)> GetActorUriFromWebfingerAsync(string username, string domain) { if (domain == Domain) - return null; + return (null, null); try { @@ -80,7 +80,7 @@ public partial class ActivityPubDiscoveryService( if (!response.IsSuccessStatusCode) { logger.LogWarning("Webfinger request failed: {Url} - {StatusCode}", webfingerUrl, response.StatusCode); - return null; + return (null, null); } var contentType = response.Content.Headers.ContentType?.MediaType; @@ -89,39 +89,39 @@ public partial class ActivityPubDiscoveryService( var content = await response.Content.ReadAsStringAsync(); logger.LogDebug("Webfinger response from {Url}: {Content}", webfingerUrl, content); - string? actorUri; + (string? actorUri, string? avatarUrl) result; if (contentType?.Contains("json") == true) { - actorUri = ParseJsonWebfingerResponse(content, webfingerUrl); + result = ParseJsonWebfingerResponse(content, webfingerUrl); } else if (contentType?.Contains("xml") == true || content.TrimStart().StartsWith(" + l.Rel == "http://webfinger.net/rel/avatar"); + + return (selfLink.Href, avatarLink?.Href); } catch (Exception ex) { logger.LogError(ex, "Error parsing JSON Webfinger response from {Url}", sourceUrl); - return null; + return (null, null); } } - private string? ParseXmlWebfingerResponse(string xml, string sourceUrl) + private (string? actorUri, string? avatarUrl) ParseXmlWebfingerResponse(string xml, string sourceUrl) { try { @@ -167,6 +170,9 @@ public partial class ActivityPubDiscoveryService( var links = xDoc.Descendants(xNamespace + "Link").ToList(); logger.LogDebug("Found {LinkCount} links in XML Webfinger response", links.Count); + string? actorUri = null; + string? avatarUrl = null; + foreach (var link in links) { var rel = link.Attribute("rel")?.Value; @@ -176,21 +182,31 @@ public partial class ActivityPubDiscoveryService( if (rel == "self" && type == "application/activity+json" && href != null) { - return href; + actorUri = href; + } + + if (rel == "http://webfinger.net/rel/avatar" && href != null) + { + avatarUrl = href; } } - logger.LogWarning("No self link found in XML Webfinger response from {Url}", sourceUrl); - return null; + if (actorUri == null) + { + logger.LogWarning("No self link found in XML Webfinger response from {Url}", sourceUrl); + return (null, null); + } + + return (actorUri, avatarUrl); } catch (Exception ex) { logger.LogError(ex, "Error parsing XML Webfinger response from {Url}", sourceUrl); - return null; + return (null, null); } } - private async Task FetchAndStoreActorAsync(string actorUri) + private async Task StoreActorAsync(string actorUri, string username, string domain, string? webfingerAvatarUrl) { var existingActor = await db.FediverseActors .FirstOrDefaultAsync(a => a.Uri == actorUri); @@ -200,25 +216,8 @@ public partial class ActivityPubDiscoveryService( try { - logger.LogInformation("Fetching actor: {ActorUri}", actorUri); + logger.LogInformation("Storing actor from Webfinger: {ActorUri}", actorUri); - var response = await HttpClient.GetAsync(actorUri); - if (!response.IsSuccessStatusCode) - { - logger.LogWarning("Failed to fetch actor: {Url} - {StatusCode}", actorUri, response.StatusCode); - return null; - } - - var json = await response.Content.ReadAsStringAsync(); - var actorData = JsonSerializer.Deserialize>(json); - - if (actorData == null) - { - logger.LogWarning("Invalid actor response from {Url}", actorUri); - return null; - } - - var domain = new Uri(actorUri).Host; var instance = await db.FediverseInstances .FirstOrDefaultAsync(i => i.Domain == domain); @@ -236,21 +235,8 @@ public partial class ActivityPubDiscoveryService( var actor = new SnFediverseActor { Uri = actorUri, - Username = actorData.GetValueOrDefault("preferredUsername")?.ToString() ?? 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(), - FeaturedUri = actorData.GetValueOrDefault("featured")?.ToString(), - AvatarUrl = ExtractAvatarUrl(actorData.GetValueOrDefault("icon")), - HeaderUrl = ExtractImageUrl(actorData.GetValueOrDefault("image")), - PublicKeyId = ExtractPublicKeyId(actorData.GetValueOrDefault("publicKey")), - PublicKey = ExtractPublicKeyPem(actorData.GetValueOrDefault("publicKey")), - IsBot = actorData.GetValueOrDefault("type")?.ToString() == "Service", - IsLocked = actorData.GetValueOrDefault("manuallyApprovesFollowers")?.ToString() == "true", - IsDiscoverable = actorData.GetValueOrDefault("discoverable")?.ToString() != "false", + Username = username, + AvatarUrl = webfingerAvatarUrl, InstanceId = instance.Id, LastFetchedAt = NodaTime.SystemClock.Instance.GetCurrentInstant() }; @@ -258,19 +244,67 @@ public partial class ActivityPubDiscoveryService( db.FediverseActors.Add(actor); await db.SaveChangesAsync(); - logger.LogInformation("Successfully fetched and stored actor: {Username}@{Domain}", actor.Username, domain); + logger.LogInformation("Successfully stored actor from Webfinger: {Username}@{Domain}", username, domain); + + await FetchAdditionalActorDataAsync(actor); + return actor; } catch (Exception ex) { - logger.LogError(ex, "Error fetching actor: {Uri}", actorUri); + logger.LogError(ex, "Error storing actor: {Uri}", actorUri); return null; } } - private string ExtractUsername(string actorUri) + private async Task FetchAdditionalActorDataAsync(SnFediverseActor actor) { - return actorUri.Split('/').Last(); + try + { + logger.LogInformation("Attempting to fetch additional actor data from: {ActorUri}", actor.Uri); + + var request = new HttpRequestMessage(HttpMethod.Get, actor.Uri); + request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/activity+json")); + + var response = await HttpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + logger.LogWarning("Failed to fetch actor data: {Url} - {StatusCode}, using Webfinger data only", actor.Uri, response.StatusCode); + return; + } + + var json = await response.Content.ReadAsStringAsync(); + var actorData = JsonSerializer.Deserialize>(json); + + if (actorData == null) + { + logger.LogWarning("Invalid actor response from {Url}, using Webfinger data only", actor.Uri); + return; + } + + actor.DisplayName = actorData.GetValueOrDefault("name")?.ToString(); + actor.Bio = actorData.GetValueOrDefault("summary")?.ToString(); + actor.InboxUri = actorData.GetValueOrDefault("inbox")?.ToString(); + actor.OutboxUri = actorData.GetValueOrDefault("outbox")?.ToString(); + actor.FollowersUri = actorData.GetValueOrDefault("followers")?.ToString(); + actor.FollowingUri = actorData.GetValueOrDefault("following")?.ToString(); + actor.FeaturedUri = actorData.GetValueOrDefault("featured")?.ToString(); + actor.AvatarUrl = ExtractAvatarUrl(actorData.GetValueOrDefault("icon")) ?? actor.AvatarUrl; + actor.HeaderUrl = ExtractImageUrl(actorData.GetValueOrDefault("image")); + actor.PublicKeyId = ExtractPublicKeyId(actorData.GetValueOrDefault("publicKey")); + actor.PublicKey = ExtractPublicKeyPem(actorData.GetValueOrDefault("publicKey")); + actor.IsBot = actorData.GetValueOrDefault("type")?.ToString() == "Service"; + actor.IsLocked = actorData.GetValueOrDefault("manuallyApprovesFollowers")?.ToString() == "true"; + actor.IsDiscoverable = actorData.GetValueOrDefault("discoverable")?.ToString() != "false"; + + await db.SaveChangesAsync(); + + logger.LogInformation("Successfully fetched additional actor data for: {Username}", actor.Username); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to fetch additional actor data for {Uri}, using Webfinger data only", actor.Uri); + } } private static string? ExtractAvatarUrl(object? iconData)