using System.Text.Json; using Backend.Domain; namespace Backend.Services; public sealed record NominationLinkBlacklistEntry(string Url); public static class NominationLinkBlacklistSettings { public static readonly NominationLinkBlacklistEntry[] Defaults = [ new("https://www.twitch.tv/"), new("https://kick.com/"), new("https://www.youtube.com/"), ]; public static NominationLinkBlacklistEntry[] Read(SiteSettings? settings) { var entries = Parse(settings?.NominationLinkBlacklistJson); return entries.Length > 0 ? entries : Defaults; } public static string Serialize(IEnumerable entries) { var normalizedEntries = entries .Select(entry => NormalizeEntry(entry.Url)) .Where(entry => entry is not null) .Select(entry => new NominationLinkBlacklistEntry(entry!)) .DistinctBy(entry => BuildComparisonKey(entry.Url), StringComparer.OrdinalIgnoreCase) .OrderBy(entry => entry.Url, StringComparer.OrdinalIgnoreCase) .ToArray(); return JsonSerializer.Serialize(normalizedEntries); } public static bool TryNormalizeUrl(string? rawUrl, out string normalizedUrl) { normalizedUrl = NormalizeEntry(rawUrl) ?? string.Empty; return !string.IsNullOrWhiteSpace(normalizedUrl); } public static bool IsBlocked(string rawUrl, IEnumerable entries) { var submittedKey = BuildComparisonKey(rawUrl); return !string.IsNullOrWhiteSpace(submittedKey) && entries.Any(entry => string.Equals(BuildComparisonKey(entry.Url), submittedKey, StringComparison.OrdinalIgnoreCase)); } private static NominationLinkBlacklistEntry[] Parse(string? json) { if (string.IsNullOrWhiteSpace(json)) { return []; } try { var entries = JsonSerializer.Deserialize(json); return entries?.Where(entry => !string.IsNullOrWhiteSpace(entry.Url)).ToArray() ?? []; } catch (JsonException) { return []; } } private static string? NormalizeEntry(string? rawUrl) { if (string.IsNullOrWhiteSpace(rawUrl)) { return null; } var candidate = rawUrl.Trim(); if (!candidate.Contains("://", StringComparison.Ordinal)) { candidate = $"https://{candidate}"; } if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri)) { return null; } if (uri.Scheme is not ("http" or "https") || string.IsNullOrWhiteSpace(uri.Host)) { return null; } var builder = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps, Host = uri.Host.ToLowerInvariant(), Port = -1, Query = string.Empty, Fragment = string.Empty, }; var normalized = builder.Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped); return normalized.EndsWith('/') ? normalized : $"{normalized}/"; } private static string BuildComparisonKey(string? rawUrl) { if (NormalizeEntry(rawUrl) is not { } normalized) { return string.Empty; } if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri)) { return string.Empty; } var host = uri.Host.StartsWith("www.", StringComparison.OrdinalIgnoreCase) ? uri.Host[4..] : uri.Host; var path = uri.AbsolutePath.TrimEnd('/'); return $"{host.ToLowerInvariant()}{path.ToLowerInvariant()}"; } }