mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Fixed .crypto sparklines
This commit is contained in:
@@ -5,6 +5,10 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.crypto` sparklines
|
||||
|
||||
## [4.1.6] - 14.05.2022
|
||||
|
||||
### Fixed
|
||||
|
@@ -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