using System.Net.Http.Headers; using System.Text.Json; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Vanguard.Services; /// Resolves an Authentik username to the Discord user id stored on their OAuth source connection. public sealed class AuthentikClient { private readonly IHttpClientFactory _http; private readonly ILogger _log; public AuthentikClient(IHttpClientFactory http, ILogger log) { _http = http; _log = log; } public async Task GetDiscordIdForUsernameAsync(string baseUrl, string token, string username, CancellationToken ct) { if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(token)) return null; var c = _http.CreateClient(NamedClient.Default); c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var b = baseUrl.TrimEnd('/'); try { using var u = await c.GetAsync($"{b}/api/v3/core/users/?username={Uri.EscapeDataString(username)}", ct).ConfigureAwait(false); if (!u.IsSuccessStatusCode) { _log.LogWarning("Authentik users lookup {User}: {Status}", username, u.StatusCode); return null; } using var ud = JsonDocument.Parse(await u.Content.ReadAsStringAsync(ct).ConfigureAwait(false)); var results = ud.RootElement.GetProperty("results"); if (results.GetArrayLength() == 0) return null; var pk = results[0].GetProperty("pk").GetInt32(); using var cres = await c.GetAsync($"{b}/api/v3/sources/user_connections/oauth/?user={pk}", ct).ConfigureAwait(false); if (!cres.IsSuccessStatusCode) { _log.LogWarning("Authentik connections lookup {User}: {Status}", username, cres.StatusCode); return null; } using var cd = JsonDocument.Parse(await cres.Content.ReadAsStringAsync(ct).ConfigureAwait(false)); foreach (var conn in cd.RootElement.GetProperty("results").EnumerateArray()) { var slug = conn.TryGetProperty("source", out var src) && src.ValueKind == JsonValueKind.Object && src.TryGetProperty("slug", out var sl) ? sl.GetString() : null; var ident = conn.TryGetProperty("identifier", out var idp) ? idp.GetString() : null; if (!string.IsNullOrEmpty(ident) && (slug is null || slug.Contains("discord", StringComparison.OrdinalIgnoreCase))) { return ident; } } } catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException) { _log.LogWarning(ex, "Authentik lookup failed for {User}", username); } return null; } }