Fixed .crypto sparklines

This commit is contained in:
Kwoth
2022-05-17 17:05:10 +02:00
parent 43047c0ab0
commit ab1272b491
3 changed files with 141 additions and 10 deletions

View File

@@ -5,6 +5,10 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
## Unreleased ## Unreleased
### Fixed
- Fixed `.crypto` sparklines
## [4.1.6] - 14.05.2022 ## [4.1.6] - 14.05.2022
### Fixed ### Fixed

View File

@@ -157,18 +157,21 @@ public partial class Searches
var marketCap = usd.MarketCap.ToString("C0", localCulture); var marketCap = usd.MarketCap.ToString("C0", localCulture);
var dominance = (usd.MarketCapDominance / 100).ToString("P2", 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() var toSend = _eb.Create()
.WithOkColor() .WithOkColor()
.WithAuthor($"#{crypto.CmcRank}") .WithAuthor($"#{crypto.CmcRank}")
.WithTitle($"{crypto.Name} ({crypto.Symbol})") .WithTitle($"{crypto.Name} ({crypto.Symbol})")
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/") .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.market_cap), marketCap, true)
.AddField(GetText(strs.price), price, true) .AddField(GetText(strs.price), price, true)
.AddField(GetText(strs.volume_24h), volume, true) .AddField(GetText(strs.volume_24h), volume, true)
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true) .AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
.AddField(GetText(strs.market_cap_dominance), dominance, 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) if (crypto.CirculatingSupply is double cs)
{ {
@@ -185,9 +188,9 @@ public partial class Searches
toSend.AddField(GetText(strs.circulating_supply), csStr, true); toSend.AddField(GetText(strs.circulating_supply), csStr, true);
} }
} }
await ctx.Channel.EmbedAsync(toSend); await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
} }
} }
} }

View File

@@ -1,6 +1,13 @@
#nullable disable #nullable enable
using NadekoBot.Modules.Searches.Common; 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.Net.Http.Json;
using System.Xml;
using Color = SixLabors.ImageSharp.Color;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
using StringExtensions = NadekoBot.Extensions.StringExtensions; using StringExtensions = NadekoBot.Extensions.StringExtensions;
@@ -21,7 +28,75 @@ public class CryptoService : INService
_creds = creds; _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)) if (string.IsNullOrWhiteSpace(name))
return (null, null); return (null, null);
@@ -29,10 +104,10 @@ public class CryptoService : INService
name = name.ToUpperInvariant(); name = name.ToUpperInvariant();
var cryptos = await GetCryptoDataInternal(); var cryptos = await GetCryptoDataInternal();
if (cryptos is null) if (cryptos is null or { Count: 0 })
return (null, null); return (null, null);
var crypto = cryptos?.FirstOrDefault(x var crypto = cryptos.FirstOrDefault(x
=> x.Slug.ToUpperInvariant() == name => x.Slug.ToUpperInvariant() == name
|| x.Name.ToUpperInvariant() == name || x.Name.ToUpperInvariant() == name
|| x.Symbol.ToUpperInvariant() == name); || x.Symbol.ToUpperInvariant() == name);
@@ -50,7 +125,7 @@ public class CryptoService : INService
return (null, nearest.Elem); return (null, nearest.Elem);
} }
public async Task<List<CmcResponseData>> GetCryptoDataInternal() public async Task<List<CmcResponseData>?> GetCryptoDataInternal()
{ {
await _getCryptoLock.WaitAsync(); await _getCryptoLock.WaitAsync();
try try
@@ -79,6 +154,9 @@ public class CryptoService : INService
"", "",
TimeSpan.FromHours(2)); TimeSpan.FromHours(2));
if (fullStrData is null)
return default;
return JsonSerializer.Deserialize<CryptoResponse>(fullStrData)?.Data ?? new(); return JsonSerializer.Deserialize<CryptoResponse>(fullStrData)?.Data ?? new();
} }
catch (Exception ex) catch (Exception ex)
@@ -91,4 +169,50 @@ public class CryptoService : INService
_getCryptoLock.Release(); _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;
}
}
} }