✨ Enrich instance metadata fetching (for mastodon only now)
This commit is contained in:
@@ -16,6 +16,11 @@ public class SnFediverseInstance : ModelBase
|
|||||||
[MaxLength(4096)] public string? Description { get; set; }
|
[MaxLength(4096)] public string? Description { get; set; }
|
||||||
[MaxLength(2048)] public string? Software { get; set; }
|
[MaxLength(2048)] public string? Software { get; set; }
|
||||||
[MaxLength(2048)] public string? Version { get; set; }
|
[MaxLength(2048)] public string? Version { get; set; }
|
||||||
|
[MaxLength(2048)] public string? IconUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? ThumbnailUrl { get; set; }
|
||||||
|
[MaxLength(512)] public string? ContactEmail { get; set; }
|
||||||
|
[MaxLength(256)] public string? ContactAccountUsername { get; set; }
|
||||||
|
public int? ActiveUsers { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Metadata { get; set; }
|
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
|
||||||
public bool IsBlocked { get; set; } = false;
|
public bool IsBlocked { get; set; } = false;
|
||||||
@@ -27,4 +32,5 @@ public class SnFediverseInstance : ModelBase
|
|||||||
|
|
||||||
public Instant? LastFetchedAt { get; set; }
|
public Instant? LastFetchedAt { get; set; }
|
||||||
public Instant? LastActivityAt { get; set; }
|
public Instant? LastActivityAt { get; set; }
|
||||||
|
public Instant? MetadataFetchedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class ActivityPubActivityProcessor(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ActivityPubSignatureService signatureService,
|
ActivityPubSignatureService signatureService,
|
||||||
ActivityPubDeliveryService deliveryService,
|
ActivityPubDeliveryService deliveryService,
|
||||||
|
ActivityPubDiscoveryService discoveryService,
|
||||||
ILogger<ActivityPubActivityProcessor> logger
|
ILogger<ActivityPubActivityProcessor> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -451,6 +452,7 @@ public class ActivityPubActivityProcessor(
|
|||||||
};
|
};
|
||||||
db.FediverseInstances.Add(instance);
|
db.FediverseInstances.Add(instance);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
await discoveryService.FetchInstanceMetadataAsync(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace DysonNetwork.Sphere.ActivityPub;
|
|||||||
public class ActivityPubDeliveryService(
|
public class ActivityPubDeliveryService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ActivityPubSignatureService signatureService,
|
ActivityPubSignatureService signatureService,
|
||||||
|
ActivityPubDiscoveryService discoveryService,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<ActivityPubDeliveryService> logger
|
ILogger<ActivityPubDeliveryService> logger
|
||||||
@@ -310,6 +311,7 @@ public class ActivityPubDeliveryService(
|
|||||||
};
|
};
|
||||||
db.FediverseInstances.Add(instance);
|
db.FediverseInstances.Add(instance);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
await discoveryService.FetchInstanceMetadataAsync(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
actor = new SnFediverseActor
|
actor = new SnFediverseActor
|
||||||
|
|||||||
@@ -6,6 +6,54 @@ using System.Xml.Linq;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.ActivityPub;
|
namespace DysonNetwork.Sphere.ActivityPub;
|
||||||
|
|
||||||
|
public class MastodonInstanceV2Response
|
||||||
|
{
|
||||||
|
public string Domain { get; set; } = null!;
|
||||||
|
public string Title { get; set; } = null!;
|
||||||
|
public string Version { get; set; } = null!;
|
||||||
|
public string? SourceUrl { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public MastodonUsage? Usage { get; set; }
|
||||||
|
public MastodonThumbnail? Thumbnail { get; set; }
|
||||||
|
public List<MastodonIcon>? Icon { get; set; }
|
||||||
|
public List<string>? Languages { get; set; }
|
||||||
|
public MastodonContact? Contact { get; set; }
|
||||||
|
public Dictionary<string, object>? Registrations { get; set; }
|
||||||
|
public Dictionary<string, object>? Configuration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonUsage
|
||||||
|
{
|
||||||
|
public MastodonUserUsage? Users { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonUserUsage
|
||||||
|
{
|
||||||
|
public int ActiveMonth { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonThumbnail
|
||||||
|
{
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonIcon
|
||||||
|
{
|
||||||
|
public string? Src { get; set; }
|
||||||
|
public string? Size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonContact
|
||||||
|
{
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public MastodonContactAccount? Account { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MastodonContactAccount
|
||||||
|
{
|
||||||
|
public string? Username { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public partial class ActivityPubDiscoveryService(
|
public partial class ActivityPubDiscoveryService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
@@ -210,6 +258,105 @@ public partial class ActivityPubDiscoveryService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task FetchInstanceMetadataAsync(SnFediverseInstance instance)
|
||||||
|
{
|
||||||
|
if (instance.MetadataFetchedAt != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Fetching instance metadata from Mastodon API: {Domain}", instance.Domain);
|
||||||
|
|
||||||
|
var apiUrl = $"https://{instance.Domain}/api/v2/instance";
|
||||||
|
var response = await HttpClient.GetAsync(apiUrl);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Mastodon API not available for {Domain} (Status: {StatusCode}), skipping metadata fetch",
|
||||||
|
instance.Domain, response.StatusCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var apiResponse = JsonSerializer.Deserialize<MastodonInstanceV2Response>(content);
|
||||||
|
|
||||||
|
if (apiResponse == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parse Mastodon API response for {Domain}", instance.Domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Name = apiResponse.Title;
|
||||||
|
instance.Description = apiResponse.Description;
|
||||||
|
instance.Software = "Mastodon";
|
||||||
|
instance.Version = apiResponse.Version;
|
||||||
|
instance.ThumbnailUrl = apiResponse.Thumbnail?.Url;
|
||||||
|
|
||||||
|
if (apiResponse.Icon != null && apiResponse.Icon.Count > 0)
|
||||||
|
{
|
||||||
|
var largestIcon = apiResponse.Icon
|
||||||
|
.Where(i => i.Src != null)
|
||||||
|
.OrderByDescending(i => GetIconSizePixels(i.Size))
|
||||||
|
.FirstOrDefault();
|
||||||
|
instance.IconUrl = largestIcon?.Src;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.ContactEmail = apiResponse.Contact?.Email;
|
||||||
|
instance.ContactAccountUsername = apiResponse.Contact?.Account?.Username;
|
||||||
|
instance.ActiveUsers = apiResponse.Usage?.Users?.ActiveMonth;
|
||||||
|
|
||||||
|
var metadata = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
if (apiResponse.Languages != null && apiResponse.Languages.Count > 0)
|
||||||
|
metadata["languages"] = apiResponse.Languages;
|
||||||
|
|
||||||
|
if (apiResponse.SourceUrl != null)
|
||||||
|
metadata["source_url"] = apiResponse.SourceUrl;
|
||||||
|
|
||||||
|
if (apiResponse.Registrations != null)
|
||||||
|
metadata["registrations"] = apiResponse.Registrations;
|
||||||
|
|
||||||
|
if (apiResponse.Configuration != null)
|
||||||
|
{
|
||||||
|
var filteredConfig = new Dictionary<string, object>();
|
||||||
|
if (apiResponse.Configuration.TryGetValue("media_attachments", out var mediaConfig))
|
||||||
|
filteredConfig["media_attachments"] = mediaConfig;
|
||||||
|
if (apiResponse.Configuration.TryGetValue("polls", out var pollConfig))
|
||||||
|
filteredConfig["polls"] = pollConfig;
|
||||||
|
if (apiResponse.Configuration.TryGetValue("translation", out var translationConfig))
|
||||||
|
filteredConfig["translation"] = translationConfig;
|
||||||
|
metadata["configuration"] = filteredConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.Count > 0)
|
||||||
|
instance.Metadata = metadata;
|
||||||
|
|
||||||
|
instance.MetadataFetchedAt = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Successfully fetched instance metadata for {Domain}", instance.Domain);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to fetch instance metadata for {Domain}", instance.Domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetIconSizePixels(string? size)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(size))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var parts = size.Split('x');
|
||||||
|
if (parts.Length != 2)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (int.TryParse(parts[0], out var width) && int.TryParse(parts[1], out var height))
|
||||||
|
return width * height;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SnFediverseActor?> StoreActorAsync(
|
private async Task<SnFediverseActor?> StoreActorAsync(
|
||||||
string actorUri,
|
string actorUri,
|
||||||
string username,
|
string username,
|
||||||
@@ -255,8 +402,9 @@ public partial class ActivityPubDiscoveryService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
logger.LogInformation("Successfully stored actor from Webfinger: {Username}@{Domain}", username, domain);
|
logger.LogInformation("Successfully stored actor from Webfinger: {Username}@{Domain}", username, domain);
|
||||||
|
|
||||||
await FetchActorDataAsync(actor);
|
await FetchActorDataAsync(actor);
|
||||||
|
await FetchInstanceMetadataAsync(instance);
|
||||||
|
|
||||||
actor.Instance = instance;
|
actor.Instance = instance;
|
||||||
return actor;
|
return actor;
|
||||||
@@ -311,7 +459,6 @@ public partial class ActivityPubDiscoveryService(
|
|||||||
actor.IsLocked = actorData.GetValueOrDefault("manuallyApprovesFollowers")?.ToString() == "true";
|
actor.IsLocked = actorData.GetValueOrDefault("manuallyApprovesFollowers")?.ToString() == "true";
|
||||||
actor.IsDiscoverable = actorData.GetValueOrDefault("discoverable")?.ToString() != "false";
|
actor.IsDiscoverable = actorData.GetValueOrDefault("discoverable")?.ToString() != "false";
|
||||||
|
|
||||||
// Store additional fields in Metadata
|
|
||||||
var excludedKeys = new HashSet<string>
|
var excludedKeys = new HashSet<string>
|
||||||
{
|
{
|
||||||
"id", "name", "summary", "preferredUsername", "inbox", "outbox", "followers", "following", "featured",
|
"id", "name", "summary", "preferredUsername", "inbox", "outbox", "followers", "following", "featured",
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ public class ActivityPubFollowController(
|
|||||||
};
|
};
|
||||||
db.FediverseInstances.Add(instance);
|
db.FediverseInstances.Add(instance);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
await discSrv.FetchInstanceMetadataAsync(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
var actor = new SnFediverseActor
|
var actor = new SnFediverseActor
|
||||||
|
|||||||
@@ -671,11 +671,25 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int?>("ActiveUsers")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("active_users");
|
||||||
|
|
||||||
b.Property<string>("BlockReason")
|
b.Property<string>("BlockReason")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)")
|
.HasColumnType("character varying(2048)")
|
||||||
.HasColumnName("block_reason");
|
.HasColumnName("block_reason");
|
||||||
|
|
||||||
|
b.Property<string>("ContactAccountUsername")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("contact_account_username");
|
||||||
|
|
||||||
|
b.Property<string>("ContactEmail")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasColumnName("contact_email");
|
||||||
|
|
||||||
b.Property<Instant>("CreatedAt")
|
b.Property<Instant>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
@@ -695,6 +709,11 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("domain");
|
.HasColumnName("domain");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)")
|
||||||
|
.HasColumnName("icon_url");
|
||||||
|
|
||||||
b.Property<bool>("IsBlocked")
|
b.Property<bool>("IsBlocked")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("is_blocked");
|
.HasColumnName("is_blocked");
|
||||||
@@ -715,6 +734,10 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("metadata");
|
.HasColumnName("metadata");
|
||||||
|
|
||||||
|
b.Property<Instant?>("MetadataFetchedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("metadata_fetched_at");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
@@ -725,6 +748,11 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("character varying(2048)")
|
.HasColumnType("character varying(2048)")
|
||||||
.HasColumnName("software");
|
.HasColumnName("software");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)")
|
||||||
|
.HasColumnName("thumbnail_url");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
|||||||
Reference in New Issue
Block a user