From 25fa8a38524302c5b56c59b7d9a811fdb1b0cf52 Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 7 Jul 2024 05:23:59 +0000 Subject: [PATCH] add: Added .coins command which lists top 10 cryptos ordered by marketcap, paginated with 10 per page --- .../Modules/Searches/Crypto/CryptoCommands.cs | 136 ++++++++++++------ .../Modules/Searches/Crypto/CryptoService.cs | 43 ++++++ .../Extensions/StringExtensions.cs | 1 + .../_common/_Extensions/NumberExtensions.cs | 23 +++ src/NadekoBot/data/aliases.yml | 6 +- .../data/strings/commands/commands.en-US.yml | 13 +- 6 files changed, 174 insertions(+), 48 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Crypto/CryptoCommands.cs b/src/NadekoBot/Modules/Searches/Crypto/CryptoCommands.cs index 81c7e156b..64d17fab1 100644 --- a/src/NadekoBot/Modules/Searches/Crypto/CryptoCommands.cs +++ b/src/NadekoBot/Modules/Searches/Crypto/CryptoCommands.cs @@ -16,12 +16,12 @@ public partial class Searches _stocksService = stocksService; _stockDrawingService = stockDrawingService; } - + [Cmd] - public async Task Stock([Leftover]string query) + public async Task Stock([Leftover] string query) { using var typing = ctx.Channel.EnterTypingState(); - + var stock = await _stocksService.GetStockDataAsync(query); if (stock is null) @@ -36,9 +36,9 @@ public partial class Searches var symbol = symbols.First(); var promptEmbed = _sender.CreateEmbed() - .WithDescription(symbol.Description) - .WithTitle(GetText(strs.did_you_mean(symbol.Symbol))); - + .WithDescription(symbol.Description) + .WithTitle(GetText(strs.did_you_mean(symbol.Symbol))); + if (!await PromptUserConfirmAsync(promptEmbed)) return; @@ -54,7 +54,7 @@ public partial class Searches var candles = await _stocksService.GetCandleDataAsync(query); var stockImageTask = _stockDrawingService.GenerateCombinedChartAsync(candles); - + var localCulture = (CultureInfo)Culture.Clone(); localCulture.NumberFormat.CurrencySymbol = "$"; @@ -64,34 +64,34 @@ public partial class Searches var change = (stock.Price - stock.Close).ToString("N2", Culture); var changePercent = (1 - (stock.Close / stock.Price)).ToString("P1", Culture); - + var sign50 = stock.Change50d >= 0 ? "\\🔼" : "\\🔻"; var change50 = (stock.Change50d).ToString("P1", Culture); - + var sign200 = stock.Change200d >= 0 ? "\\🔼" : "\\🔻"; - + var change200 = (stock.Change200d).ToString("P1", Culture); - + var price = stock.Price.ToString("C2", localCulture); var eb = _sender.CreateEmbed() - .WithOkColor() - .WithAuthor(stock.Symbol) - .WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}") - .WithTitle(stock.Name) - .AddField(GetText(strs.price), $"{sign} **{price}**", true) - .AddField(GetText(strs.market_cap), stock.MarketCap, true) - .AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true) - .AddField("Change", $"{change} ({changePercent})", true) - // .AddField("Change 50d", $"{sign50}{change50}", true) - // .AddField("Change 200d", $"{sign200}{change200}", true) - .WithFooter(stock.Exchange); - + .WithOkColor() + .WithAuthor(stock.Symbol) + .WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}") + .WithTitle(stock.Name) + .AddField(GetText(strs.price), $"{sign} **{price}**", true) + .AddField(GetText(strs.market_cap), stock.MarketCap, true) + .AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true) + .AddField("Change", $"{change} ({changePercent})", true) + // .AddField("Change 50d", $"{sign50}{change50}", true) + // .AddField("Change 200d", $"{sign200}{change200}", true) + .WithFooter(stock.Exchange); + var message = await Response().Embed(eb).SendAsync(); await using var imageData = await stockImageTask; if (imageData is null) @@ -105,15 +105,12 @@ public partial class Searches await message.ModifyAsync(mp => { mp.Attachments = - new(new[] - { - attachment - }); + new(new[] { attachment }); mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build(); }); } - + [Cmd] public async Task Crypto(string name) @@ -128,9 +125,9 @@ public partial class Searches if (nearest is not null) { var embed = _sender.CreateEmbed() - .WithTitle(GetText(strs.crypto_not_found)) - .WithDescription( - GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})")))); + .WithTitle(GetText(strs.crypto_not_found)) + .WithDescription( + GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})")))); if (await PromptUserConfirmAsync(embed)) crypto = nearest; @@ -146,7 +143,7 @@ public partial class Searches var localCulture = (CultureInfo)Culture.Clone(); localCulture.NumberFormat.CurrencySymbol = "$"; - + var sevenDay = (usd.PercentChange7d / 100).ToString("P2", localCulture); var lastDay = (usd.PercentChange24h / 100).ToString("P2", localCulture); var price = usd.Price < 0.01 @@ -159,28 +156,29 @@ public partial class Searches await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0); var fileName = $"{crypto.Slug}_7d.png"; - + var toSend = _sender.CreateEmbed() - .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") - .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($"attachment://{fileName}"); + .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") + .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($"attachment://{fileName}"); if (crypto.CirculatingSupply is double cs) { var csStr = cs.ToString("N0", localCulture); - + if (crypto.MaxSupply is double ms) { var perc = (cs / ms).ToString("P1", localCulture); - + toSend.AddField(GetText(strs.circulating_supply), $"{csStr} ({perc})", true); } else @@ -192,5 +190,51 @@ public partial class Searches await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build()); } + + [Cmd] + public async Task Coins(int page = 1) + { + if (--page < 0) + return; + + await Response() + .Paginated() + .PageItems(async (page) => + { + var coins = await _service.GetTopCoins(page + 1); + + return coins; + }) + .PageSize(1) + .Page((items, _) => + { + var embed = _sender.CreateEmbed() + .WithOkColor(); + + if (items.Count > 0) + { + foreach (var coin in items) + { + embed.AddField($"#{coin.MarketCapRank} {coin.Symbol} - {coin.Name}", + $""" + `Price:` {GetArrowEmoji(coin.PercentChange24h)} {coin.CurrentPrice.ToShortString()}$ ({GetSign(coin.PercentChange24h)}{Math.Round(coin.PercentChange24h, 2)}%) + `MarketCap:` {coin.MarketCap.ToShortString()}$ + `Supply:` {(coin.CirculatingSupply?.ToShortString() ?? "?")} / {(coin.TotalSupply?.ToShortString() ?? "?")} + """, + inline: false); + } + } + + return embed; + }) + .AddFooter(false) + .SendAsync(); + } + + private static string GetArrowEmoji(decimal value) + => value > 0 ? "▲" : "▼"; + + private static string GetSign(decimal value) + => value >= 0 ? "+" : "-"; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Crypto/CryptoService.cs b/src/NadekoBot/Modules/Searches/Crypto/CryptoService.cs index dd64e42a6..41ad233d0 100644 --- a/src/NadekoBot/Modules/Searches/Crypto/CryptoService.cs +++ b/src/NadekoBot/Modules/Searches/Crypto/CryptoService.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Globalization; using System.Net.Http.Json; +using System.Text.Json.Serialization; using System.Xml; using Color = SixLabors.ImageSharp.Color; using StringExtensions = NadekoBot.Extensions.StringExtensions; @@ -212,4 +213,46 @@ public class CryptoService : INService var points = GetSparklinePointsFromSvgText(str); return points; } + + public async Task?> GetTopCoins(int page) + { + using var http = _httpFactory.CreateClient(); + + http.AddFakeHeaders(); + + var result = await http.GetFromJsonAsync>( + $"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&page={page}&per_page=10"); + + return result; + } +} + +public sealed class GeckoCoinsResult +{ + [JsonPropertyName("id")] + public required string Id { get; init; } + + [JsonPropertyName("name")] + public required string Name { get; init; } + + [JsonPropertyName("symbol")] + public required string Symbol { get; init; } + + [JsonPropertyName("current_price")] + public required decimal CurrentPrice { get; init; } + + [JsonPropertyName("price_change_percentage_24h")] + public required decimal PercentChange24h { get; init; } + + [JsonPropertyName("market_cap")] + public required decimal MarketCap { get; init; } + + [JsonPropertyName("circulating_supply")] + public required decimal? CirculatingSupply { get; init; } + + [JsonPropertyName("total_supply")] + public required decimal? TotalSupply { get; init; } + + [JsonPropertyName("market_cap_rank")] + public required int MarketCapRank { get; init; } } \ No newline at end of file diff --git a/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs b/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs index a3e770e0c..b188f134c 100644 --- a/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs +++ b/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs @@ -147,4 +147,5 @@ public static class StringExtensions var newString = str.UnescapeUnicodeCodePoint(); return newString; }); + } \ No newline at end of file diff --git a/src/NadekoBot/_common/_Extensions/NumberExtensions.cs b/src/NadekoBot/_common/_Extensions/NumberExtensions.cs index 640a8feae..fc3d59652 100644 --- a/src/NadekoBot/_common/_Extensions/NumberExtensions.cs +++ b/src/NadekoBot/_common/_Extensions/NumberExtensions.cs @@ -1,7 +1,30 @@ +using System.Globalization; + namespace NadekoBot.Extensions; public static class NumberExtensions { public static DateTimeOffset ToUnixTimestamp(this double number) => new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number); + + public static string ToShortString(this decimal value) + { + if (value <= 1_000) + return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + if (value <= 1_000_000) + return Math.Round(value, 1).ToString(CultureInfo.InvariantCulture); + var tokens = " MBtq"; + var i = 2; + while (true) + { + var num = (decimal)Math.Pow(1000, i); + if (num > value) + { + var num2 = (decimal)Math.Pow(1000, i - 1); + return $"{Math.Round((value / num2), 1)}{tokens[i - 1]}".Trim(); + } + + i++; + } + } } \ No newline at end of file diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 4143812e4..0408e579c 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1404,4 +1404,8 @@ cleanupguilddata: prompt: - prompt honeypot: - - honeypot \ No newline at end of file + - honeypot +coins: + - coins + - crypto + - cryptos \ No newline at end of file diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index c1fa73de1..f918657b8 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -4545,4 +4545,15 @@ honeypot: ex: - '' params: - - {} \ No newline at end of file + - {} +coins: + desc: |- + Shows a list of 10 crypto currencies ordered by market cap. + Shows their price, change in the last24h, market cap and circulating and total supply. + Paginated with 10 per page. + ex: + - '' + - '2' + params: + - page: + desc: "Page number to show. Starts at 1." \ No newline at end of file