mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Fixed .crypto sparklines
This commit is contained in:
		@@ -157,18 +157,21 @@ public partial class Searches
 | 
			
		||||
            var marketCap = usd.MarketCap.ToString("C0", localCulture);
 | 
			
		||||
            var dominance = (usd.MarketCapDominance / 100).ToString("P2", localCulture);
 | 
			
		||||
 | 
			
		||||
            await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
 | 
			
		||||
            var fileName = $"{crypto.Slug}_7d.png";
 | 
			
		||||
            
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithAuthor($"#{crypto.CmcRank}")
 | 
			
		||||
                            .WithTitle($"{crypto.Name} ({crypto.Symbol})")
 | 
			
		||||
                            .WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
 | 
			
		||||
                            .WithThumbnailUrl( $"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
 | 
			
		||||
                            .WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
 | 
			
		||||
                            .AddField(GetText(strs.market_cap), marketCap, true)
 | 
			
		||||
                            .AddField(GetText(strs.price), price, true)
 | 
			
		||||
                            .AddField(GetText(strs.volume_24h), volume, true)
 | 
			
		||||
                            .AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
 | 
			
		||||
                            .AddField(GetText(strs.market_cap_dominance), dominance, true)
 | 
			
		||||
                            .WithImageUrl($"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{crypto.Id}.png");
 | 
			
		||||
                            .WithImageUrl($"attachment://{fileName}");
 | 
			
		||||
 | 
			
		||||
            if (crypto.CirculatingSupply is double cs)
 | 
			
		||||
            {
 | 
			
		||||
@@ -185,9 +188,9 @@ public partial class Searches
 | 
			
		||||
                    toSend.AddField(GetText(strs.circulating_supply), csStr, true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#nullable enable
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Xml;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
			
		||||
using StringExtensions = NadekoBot.Extensions.StringExtensions;
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +28,75 @@ public class CryptoService : INService
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<(CmcResponseData Data, CmcResponseData Nearest)> GetCryptoData(string name)
 | 
			
		||||
    private PointF[] GetSparklinePointsFromSvgText(string svgText)
 | 
			
		||||
    {
 | 
			
		||||
        var xml = new XmlDocument();
 | 
			
		||||
        xml.LoadXml(svgText);
 | 
			
		||||
 | 
			
		||||
        var gElement = xml["svg"]?["g"];
 | 
			
		||||
        if (gElement is null)
 | 
			
		||||
            return Array.Empty<PointF>();
 | 
			
		||||
 | 
			
		||||
        Span<PointF> points = new PointF[gElement.ChildNodes.Count];
 | 
			
		||||
        var cnt = 0;
 | 
			
		||||
 | 
			
		||||
        bool GetValuesFromAttributes(XmlAttributeCollection attrs,
 | 
			
		||||
            out float x1,
 | 
			
		||||
            out float y1,
 | 
			
		||||
            out float x2,
 | 
			
		||||
            out float y2)
 | 
			
		||||
        {
 | 
			
		||||
            (x1, y1, x2, y2) = (0, 0, 0, 0);
 | 
			
		||||
            return attrs["x1"]?.Value is string x1Str
 | 
			
		||||
                   && float.TryParse(x1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x1)
 | 
			
		||||
                   && attrs["y1"]?.Value is string y1Str
 | 
			
		||||
                   && float.TryParse(y1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y1)
 | 
			
		||||
                   && attrs["x2"]?.Value is string x2Str
 | 
			
		||||
                   && float.TryParse(x2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x2)
 | 
			
		||||
                   && attrs["y2"]?.Value is string y2Str
 | 
			
		||||
                   && float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        foreach (XmlElement x in gElement.ChildNodes)
 | 
			
		||||
        {
 | 
			
		||||
            if (x.Name != "line")
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            if (GetValuesFromAttributes(x.Attributes, out var x1, out var y1, out var x2, out var y2))
 | 
			
		||||
            {
 | 
			
		||||
                points[cnt++] = new(x1, y1);
 | 
			
		||||
                // this point will be set twice to the same value
 | 
			
		||||
                // on all points except the last one
 | 
			
		||||
                if(cnt + 1 < points.Length)
 | 
			
		||||
                    points[cnt + 1] = new(x2, y2);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cnt == 0)
 | 
			
		||||
            return Array.Empty<PointF>();
 | 
			
		||||
        
 | 
			
		||||
        return points.Slice(0, cnt).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private SixLabors.ImageSharp.Image<Rgba32> GenerateSparklineChart(PointF[] points, bool up)
 | 
			
		||||
    {
 | 
			
		||||
        const int width = 164;
 | 
			
		||||
        const int height = 48;
 | 
			
		||||
        
 | 
			
		||||
        var img = new Image<Rgba32>(width, height, Color.Transparent);
 | 
			
		||||
        var color = up
 | 
			
		||||
            ? Color.Green
 | 
			
		||||
            : Color.FromRgb(220, 0, 0);
 | 
			
		||||
 | 
			
		||||
        img.Mutate(x =>
 | 
			
		||||
        {
 | 
			
		||||
            x.DrawLines(color, 2, points);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        return img;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
            return (null, null);
 | 
			
		||||
@@ -29,10 +104,10 @@ public class CryptoService : INService
 | 
			
		||||
        name = name.ToUpperInvariant();
 | 
			
		||||
        var cryptos = await GetCryptoDataInternal();
 | 
			
		||||
 | 
			
		||||
        if (cryptos is null)
 | 
			
		||||
        if (cryptos is null or { Count: 0 })
 | 
			
		||||
            return (null, null);
 | 
			
		||||
 | 
			
		||||
        var crypto = cryptos?.FirstOrDefault(x
 | 
			
		||||
        var crypto = cryptos.FirstOrDefault(x
 | 
			
		||||
            => x.Slug.ToUpperInvariant() == name
 | 
			
		||||
               || x.Name.ToUpperInvariant() == name
 | 
			
		||||
               || x.Symbol.ToUpperInvariant() == name);
 | 
			
		||||
@@ -50,7 +125,7 @@ public class CryptoService : INService
 | 
			
		||||
        return (null, nearest.Elem);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<CmcResponseData>> GetCryptoDataInternal()
 | 
			
		||||
    public async Task<List<CmcResponseData>?> GetCryptoDataInternal()
 | 
			
		||||
    {
 | 
			
		||||
        await _getCryptoLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
@@ -79,6 +154,9 @@ public class CryptoService : INService
 | 
			
		||||
                "",
 | 
			
		||||
                TimeSpan.FromHours(2));
 | 
			
		||||
 | 
			
		||||
            if (fullStrData is null)
 | 
			
		||||
                return default;
 | 
			
		||||
            
 | 
			
		||||
            return JsonSerializer.Deserialize<CryptoResponse>(fullStrData)?.Data ?? new();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -91,4 +169,50 @@ public class CryptoService : INService
 | 
			
		||||
            _getCryptoLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<Stream?> GetSparklineAsync(int id, bool up)
 | 
			
		||||
    {
 | 
			
		||||
        var key = $"crypto:sparkline:{id}";
 | 
			
		||||
        
 | 
			
		||||
        // attempt to get from cache
 | 
			
		||||
        var db = _cache.Redis.GetDatabase();
 | 
			
		||||
        byte[] bytes = await db.StringGetAsync(key);
 | 
			
		||||
        // if it succeeds, return it
 | 
			
		||||
        if (bytes is { Length: > 0 })
 | 
			
		||||
        {
 | 
			
		||||
            return bytes.ToStream();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // if it fails, generate a new one
 | 
			
		||||
        var points = await DownloadSparklinePointsAsync(id);
 | 
			
		||||
        if (points is null)
 | 
			
		||||
            return default;
 | 
			
		||||
        
 | 
			
		||||
        var sparkline = GenerateSparklineChart(points, up);
 | 
			
		||||
 | 
			
		||||
        // add to cache for 1h and return it
 | 
			
		||||
        
 | 
			
		||||
        var stream = sparkline.ToStream();
 | 
			
		||||
        await db.StringSetAsync(key, stream.ToArray(), expiry: TimeSpan.FromHours(1));
 | 
			
		||||
        return stream;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<PointF[]?> DownloadSparklinePointsAsync(int id)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            var str = await http.GetStringAsync(
 | 
			
		||||
                $"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg");
 | 
			
		||||
            var points = GetSparklinePointsFromSvgText(str);
 | 
			
		||||
            return points;
 | 
			
		||||
        }
 | 
			
		||||
        catch(Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex,
 | 
			
		||||
                "Exception occurred while downloading sparkline points: {ErrorMessage}",
 | 
			
		||||
                ex.Message);
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user