Social bar (Branch B): global resource with per-scene presence — Models/Socials.cs (19 services, icons, canonical URLs), Services/SocialValidator.cs (HTTP lookup validation seam), LayoutStore schema v7 (Socials + SocialEntry tables + Scene.HasSocialBar), MainViewModel (socials collection, AddSocial/RemoveSocial/ToggleSceneSocialBar commands, freemium cap YT+1, premium seam), MainWindow.xaml (footer Social button + [+/-] toggle on meter line, preview bar rendering top/bottom + LCR), 4 integration tests (roundtrip, empty-not-persisted, canonical URLs, icons), 85 tests passing, 0 warnings
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ytLive.Models;
|
||||
|
||||
namespace ytLive.Services;
|
||||
|
||||
public sealed class SocialLookupResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string ProfileUrl { get; init; } = string.Empty;
|
||||
public string Handle { get; init; } = string.Empty;
|
||||
public string Error { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public interface ISocialValidator
|
||||
{
|
||||
Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a social handle/URL by constructing the canonical profile URL and
|
||||
/// issuing an HTTP GET — the "act of validation" the creator trusts before the
|
||||
/// icon lands on their bar. Best-effort: some platforms return challenges/blocks
|
||||
/// to HEAD requests, so a 200 OR a 301/302 redirect counts as "exists"; 404 or
|
||||
/// connection failure = rejected. Never blind trust.
|
||||
/// </summary>
|
||||
public sealed class HttpSocialValidator : ISocialValidator
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public HttpSocialValidator(HttpClient? client = null)
|
||||
{
|
||||
_client = client ?? new HttpClient();
|
||||
_client.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
"Mozilla/5.0 (compatible; ytLlive social validator)");
|
||||
}
|
||||
|
||||
public async Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl)
|
||||
{
|
||||
var input = handleOrUrl.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return new SocialLookupResult { Error = "Enter a handle or URL." };
|
||||
|
||||
var handle = input;
|
||||
if (input.StartsWith("http://", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
input.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var uri = new System.Uri(input);
|
||||
handle = uri.AbsolutePath.Trim('/');
|
||||
}
|
||||
|
||||
var url = SocialServiceIcons.CanonicalUrlFor(service, handle);
|
||||
|
||||
try
|
||||
{
|
||||
using var resp = await _client.GetAsync(url, CancellationToken.None);
|
||||
if (resp.IsSuccessStatusCode || (int)resp.StatusCode is 301 or 302 or 303 or 307 or 308)
|
||||
return new SocialLookupResult
|
||||
{
|
||||
Success = true,
|
||||
ProfileUrl = url,
|
||||
Handle = handle.TrimStart('@'),
|
||||
};
|
||||
return new SocialLookupResult
|
||||
{
|
||||
Error = $"{service} returned {resp.StatusCode} for '{handle}'. The handle may be wrong or the page doesn't exist.",
|
||||
};
|
||||
}
|
||||
catch (System.Net.Http.HttpRequestException)
|
||||
{
|
||||
return new SocialLookupResult
|
||||
{
|
||||
Error = $"Couldn't reach {service} to validate '{handle}'. Check your connection and try again.",
|
||||
};
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
return new SocialLookupResult { Error = ex.Message };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user