Editor/AccountAvatar.cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Editor;
using Sandbox;

namespace HumanoidMocapX.EditorTools;

/// <summary>A round account picture, the name's initial, or an icon when neither is known. Pictures
/// load only over HTTPS from the image CDNs providers use, without redirects, up to 1 MiB.</summary>
sealed class AccountAvatar : Widget
{
    const long MaximumBytes = 1024 * 1024;
    // Avatars are tiny public images; one client for the editor session avoids socket churn.
    static readonly HttpClient Client = new(new HttpClientHandler { AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(10) };
    // Only the image CDNs providers use; the website drops other avatar URLs too.
    static readonly System.Text.RegularExpressions.Regex Hosts = new(@"^(cdn\.discordapp\.com|[a-z0-9-]+\.googleusercontent\.com)$",
        System.Text.RegularExpressions.RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(100));
    readonly float diameter;
    Pixmap image;
    string initial, icon, shown;

    internal AccountAvatar(Widget parent, float diameter) : base(parent) { this.diameter = diameter; FixedSize = diameter; }

    /// <summary>Shows an icon instead of an account.</summary>
    internal void ShowIcon(string fallbackIcon) { shown = null; image = null; initial = null; icon = fallbackIcon; Update(); }

    /// <summary>Shows the name's initial now and the picture at <paramref name="url"/> once it loads.</summary>
    internal void ShowAccount(string url, string name)
    {
        initial = name is { Length: > 0 } ? name[..1].ToUpperInvariant() : null; icon = "person";
        if (url == shown && (image is not null || url is null)) { Update(); return; }
        shown = url; image = null; Update();
        _ = LoadAsync(url);
    }

    async Task LoadAsync(string url)
    {
        Pixmap picture = null;
        if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps && uri.IsDefaultPort && Hosts.IsMatch(uri.Host))
        {
            try
            {
                using var response = await Client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
                if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength is null or <= MaximumBytes)
                {
                    await response.Content.LoadIntoBufferAsync(MaximumBytes);
                    var bytes = await response.Content.ReadAsByteArrayAsync();
                    await EditorPipeline.SwitchToMainThread();
                    picture = Bitmap.CreateFromBytes(bytes) is { } bitmap ? Pixmap.FromBitmap(bitmap) : null;
                }
            }
            // A missing picture only keeps the initial; the profile itself is unaffected.
            catch (Exception failure) when (failure is HttpRequestException or TaskCanceledException or InvalidOperationException or ArgumentException)
            { Log.Info($"Humanoid Mocap X: profile picture unavailable ({failure.GetType().Name})."); }
        }
        await EditorPipeline.SwitchToMainThread();
        if (this.IsValid() && shown == url) { image = picture; Update(); }
    }

    protected override void OnPaint()
    {
        Paint.Antialiasing = true;
        var bounds = new Rect(0, 0, diameter, diameter);
        if (image is not null) { Paint.Draw(bounds, image, 1, diameter / 2); return; }
        Paint.ClearPen(); Paint.SetBrush(Ui.Accent.WithAlpha(.16f)); Paint.DrawRect(bounds, diameter / 2);
        Paint.SetPen(Ui.Accent);
        if (initial is null) Paint.DrawIcon(bounds, icon ?? "person", diameter * .58f);
        else { Paint.SetDefaultFont(diameter * .32f, 700); Paint.DrawText(bounds, initial, TextFlag.Center); }
    }
}