TASK 4 ship step 5.5: social bar bug fixes + bar on the live output — direction-snap drag (SocialBarSnap.Decide + ClearValue, local Canvas.Top was overriding the binding), fediverse nodeinfo subdomain probing + load-time heal (settable FediverseSoftware), SocialBarRenderer strip blitted above the flash via a re-read FramePump socialBar seam — 155 tests passing, 0 warnings

This commit is contained in:
2026-08-13 09:29:58 -07:00
parent e72ba71165
commit ac60a26d82
16 changed files with 666 additions and 52 deletions
+55 -10
View File
@@ -23,6 +23,11 @@ public sealed class SocialLookupResult
public interface ISocialValidator
{
Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct);
/// <summary>Best-effort nodeinfo software name (mastodon/peertube/...) for a
/// fediverse domain, or null when it can't be resolved. The load-time heal
/// uses this to fix a bar entry whose stored software is missing.</summary>
Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct);
}
/// <summary>
@@ -35,14 +40,19 @@ public interface ISocialValidator
public sealed class HttpSocialValidator : ISocialValidator
{
private readonly HttpClient _client;
private readonly Action<string>? _log;
public HttpSocialValidator(HttpClient? client = null)
public HttpSocialValidator(HttpClient? client = null, Action<string>? log = null)
{
_client = client ?? new HttpClient();
_client.DefaultRequestHeaders.UserAgent.ParseAdd(
"Mozilla/5.0 (compatible; ytLlive social validator)");
_log = log;
}
public Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct)
=> TryFetchFediverseSoftwareAsync(domain, ct);
public async Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct)
{
var input = handleOrUrl.Trim();
@@ -127,32 +137,67 @@ public sealed class HttpSocialValidator : ISocialValidator
/// nodeinfo link, read `software.name`. If the identity domain is itself a
/// redirect (e.g. a YunoHost domain whose default app lives on a subdomain),
/// the bare root 302s to the real instance — follow it and ask that host.
/// Failures return null — validation still succeeds, the icon just falls
/// back to the generic fediverse glyph.
/// If neither answers, probe well-known subdomains (mastodon.example.com)
/// so a landing-page domain still resolves its real instance. The whole
/// resolution is bounded by a ~15s budget. Failures return null — validation
/// still succeeds, the icon just falls back to the generic fediverse glyph.
/// </summary>
private async Task<string?> TryFetchFediverseSoftwareAsync(string domain, CancellationToken ct)
{
try
{
var software = await FetchSoftwareNameAsync(domain, ct);
using var budget = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(ct);
budget.CancelAfter(TimeSpan.FromSeconds(15));
var software = await FetchSoftwareNameAsync(domain, budget.Token);
if (software != null) return software;
var resolved = await ResolveInstanceHostAsync(domain, ct);
if (resolved == null
|| string.Equals(resolved, domain, System.StringComparison.OrdinalIgnoreCase))
return null;
return await FetchSoftwareNameAsync(resolved, ct);
var resolved = await ResolveInstanceHostAsync(domain, budget.Token);
if (resolved != null
&& !string.Equals(resolved, domain, System.StringComparison.OrdinalIgnoreCase))
{
software = await FetchSoftwareNameAsync(resolved, budget.Token);
if (software != null) return software;
}
foreach (var sub in FediverseSubdomainCandidates)
{
var host = $"{sub}.{domain}";
software = await FetchSoftwareNameAsync(host, budget.Token);
if (software != null)
{
_log?.Invoke($"SocialValidator: fediverse software '{software}' for '{domain}' resolved via {host}");
return software;
}
}
_log?.Invoke($"SocialValidator: no nodeinfo found for '{domain}'");
return null;
}
catch (System.OperationCanceledException) when (ct.IsCancellationRequested)
{
return null; // caller checks ct.IsCancellationRequested and reports Canceled
}
catch
catch (System.OperationCanceledException)
{
_log?.Invoke($"SocialValidator: nodeinfo resolution for '{domain}' timed out");
return null;
}
catch (System.Exception ex)
{
_log?.Invoke($"SocialValidator: nodeinfo resolution for '{domain}' failed: {ex.Message}");
return null;
}
}
/// <summary>Common instance subdomains, probed in order when the identity
/// domain itself doesn't serve nodeinfo (a landing page or SSO gate in front
/// of a self-hosted instance).</summary>
private static readonly string[] FediverseSubdomainCandidates =
{
"mastodon", "social", "fediverse", "tube", "peertube",
"pixelfed", "lemmy", "pleroma", "misskey", "gotosocial", "fedi", "instance",
};
private async Task<string?> FetchSoftwareNameAsync(string domain, CancellationToken ct)
{
var nodeInfoUrl = await FetchNodeInfoUrlAsync(domain, ct);