mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Fixed .crypto sparklines
This commit is contained in:
@@ -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
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -187,7 +190,7 @@ public partial class Searches
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user