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:
+55
-10
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user