diff --git a/NadekoBot.sln b/NadekoBot.sln
index fd630f8d..096b6db8 100644
--- a/NadekoBot.sln
+++ b/NadekoBot.sln
@@ -41,32 +41,32 @@ Global
{7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
{7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
- {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.NadekoRelease|Any CPU.ActiveCfg = NadekoRelease|Any CPU
- {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.NadekoRelease|Any CPU.Build.0 = NadekoRelease|Any CPU
+ {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.NadekoRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.NadekoRelease|Any CPU.Build.0 = Release|Any CPU
{7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.Build.0 = Release|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
- {8D71A857-879A-4A10-859E-5FF824ED6688}.NadekoRelease|Any CPU.ActiveCfg = NadekoRelease|Any CPU
- {8D71A857-879A-4A10-859E-5FF824ED6688}.NadekoRelease|Any CPU.Build.0 = NadekoRelease|Any CPU
+ {8D71A857-879A-4A10-859E-5FF824ED6688}.NadekoRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {8D71A857-879A-4A10-859E-5FF824ED6688}.NadekoRelease|Any CPU.Build.0 = Release|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.Build.0 = Release|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
- {3091164F-66AE-4543-A63D-167C1116241D}.NadekoRelease|Any CPU.ActiveCfg = NadekoRelease|Any CPU
- {3091164F-66AE-4543-A63D-167C1116241D}.NadekoRelease|Any CPU.Build.0 = NadekoRelease|Any CPU
+ {3091164F-66AE-4543-A63D-167C1116241D}.NadekoRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {3091164F-66AE-4543-A63D-167C1116241D}.NadekoRelease|Any CPU.Build.0 = Release|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
- {1B5603B4-6F8F-4289-B945-7BAAE523D740}.NadekoRelease|Any CPU.ActiveCfg = NadekoRelease|Any CPU
- {1B5603B4-6F8F-4289-B945-7BAAE523D740}.NadekoRelease|Any CPU.Build.0 = NadekoRelease|Any CPU
+ {1B5603B4-6F8F-4289-B945-7BAAE523D740}.NadekoRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {1B5603B4-6F8F-4289-B945-7BAAE523D740}.NadekoRelease|Any CPU.Build.0 = Release|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.Build.0 = Release|Any CPU
{45B2545D-C612-4919-B34C-D65EA1371C51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
diff --git a/NadekoBot/Modules/Searches/Commands/OsuCommands.cs b/NadekoBot/Modules/Searches/Commands/OsuCommands.cs
new file mode 100644
index 00000000..876c1369
--- /dev/null
+++ b/NadekoBot/Modules/Searches/Commands/OsuCommands.cs
@@ -0,0 +1,269 @@
+ο»Ώusing Discord.Commands;
+using NadekoBot.Classes;
+using Newtonsoft.Json.Linq;
+using System;
+using System.IO;
+using System.Net;
+using System.Text.RegularExpressions;
+
+namespace NadekoBot.Modules.Searches.Commands
+{
+ internal class OsuCommands : DiscordCommand
+ {
+ public OsuCommands(DiscordModule module) : base(module)
+ {
+ }
+
+ internal override void Init(CommandGroupBuilder cgb)
+ {
+ cgb.CreateCommand(Module.Prefix + "osu u")
+ .Description("Shows osu stats for a player.\n**Usage**:~osu u Name")
+ .Parameter("usr", ParameterType.Required)
+ .Parameter("mode", ParameterType.Unparsed)
+ .Do(async e =>
+ {
+ if (string.IsNullOrWhiteSpace(e.GetArg("usr")))
+ return;
+
+ using (WebClient cl = new WebClient())
+ {
+ try
+ {
+ var m = 0;
+ if (!string.IsNullOrWhiteSpace(e.GetArg("mode")))
+ {
+ m = ResolveGameMode(e.GetArg("mode"));
+ }
+
+ cl.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
+ cl.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/5.0 (Windows NT 6.2; Win64; x64)");
+ cl.DownloadDataAsync(new Uri($"http://lemmmy.pw/osusig/sig.php?uname={ e.GetArg("usr") }&flagshadow&xpbar&xpbarhex&pp=2&mode={m}"));
+ cl.DownloadDataCompleted += async (s, cle) =>
+ {
+ try
+ {
+ await e.Channel.SendFile($"{e.GetArg("usr")}.png", new MemoryStream(cle.Result)).ConfigureAwait(false);
+ await e.Channel.SendMessage($"`Profile Link:`https://osu.ppy.sh/u/{Uri.EscapeDataString(e.GetArg("usr"))}\n`Image provided by https://lemmmy.pw/osusig`").ConfigureAwait(false);
+ }
+ catch { }
+ };
+ }
+ catch
+ {
+ await e.Channel.SendMessage("π’ Failed retrieving osu signature :\\").ConfigureAwait(false);
+ }
+ }
+ });
+
+ cgb.CreateCommand(Module.Prefix + "osu b")
+ .Description("Shows information about an osu beatmap.\n**Usage**:~osu b https://osu.ppy.sh/s/127712")
+ .Parameter("map", ParameterType.Unparsed)
+ .Do(async e =>
+ {
+ if (string.IsNullOrWhiteSpace(NadekoBot.Creds.OsuAPIKey))
+ {
+ await e.Channel.SendMessage("π’ An osu! API key is required.").ConfigureAwait(false);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(e.GetArg("map")))
+ return;
+
+ try
+ {
+ var mapId = ResolveMap(e.GetArg("map"));
+ var reqString = $"https://osu.ppy.sh/api/get_beatmaps?k={NadekoBot.Creds.OsuAPIKey}&{mapId}";
+ var obj = JArray.Parse(await SearchHelper.GetResponseStringAsync(reqString).ConfigureAwait(false))[0];
+ var sb = new System.Text.StringBuilder();
+ var starRating = Math.Round(Double.Parse($"{obj["difficultyrating"]}"), 2);
+ var time = TimeSpan.FromSeconds(Double.Parse($"{obj["total_length"]}")).ToString(@"mm\:ss");
+ sb.AppendLine($"{obj["artist"]} - {obj["title"]}, mapped by {obj["creator"]}. https://osu.ppy.sh/s/{obj["beatmapset_id"]}");
+ sb.AppendLine($"{starRating} stars, {obj["bpm"]} BPM | AR{obj["diff_approach"]}, CS{obj["diff_size"]}, OD{obj["diff_overall"]} | Length: {time}");
+ await e.Channel.SendMessage(sb.ToString()).ConfigureAwait(false);
+ }
+ catch
+ {
+ await e.Channel.SendMessage("Something went wrong.");
+ }
+ });
+
+ cgb.CreateCommand(Module.Prefix + "osu top5")
+ .Description("Displays a user's top 5 plays. \n**Usage**:~osu top5 Name")
+ .Parameter("usr", ParameterType.Required)
+ .Parameter("mode", ParameterType.Unparsed)
+ .Do(async e =>
+ {
+ if (string.IsNullOrWhiteSpace(NadekoBot.Creds.OsuAPIKey))
+ {
+ await e.Channel.SendMessage("π’ An osu! API key is required.").ConfigureAwait(false);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(e.GetArg("usr")))
+ {
+ await e.Channel.SendMessage("π’ Please provide a username.").ConfigureAwait(false);
+ return;
+ }
+
+ try
+ {
+ var m = 0;
+ if (!string.IsNullOrWhiteSpace(e.GetArg("mode")))
+ {
+ m = ResolveGameMode(e.GetArg("mode"));
+ }
+
+ var reqString = $"https://osu.ppy.sh/api/get_user_best?k={NadekoBot.Creds.OsuAPIKey}&u={Uri.EscapeDataString(e.GetArg("usr"))}&type=string&limit=5&m={m}";
+ var obj = JArray.Parse(await SearchHelper.GetResponseStringAsync(reqString).ConfigureAwait(false));
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine($"Top 5 plays for {e.GetArg("usr")}:");
+ sb.AppendLine("");
+ foreach (var item in obj)
+ {
+ var mapReqString = $"https://osu.ppy.sh/api/get_beatmaps?k={NadekoBot.Creds.OsuAPIKey}&b={item["beatmap_id"]}";
+ var map = JArray.Parse(await SearchHelper.GetResponseStringAsync(mapReqString).ConfigureAwait(false))[0];
+ var pp = Math.Round(Double.Parse($"{item["pp"]}"), 2);
+ var acc = CalculateAcc(item, m);
+ var mods = ResolveMods(Int32.Parse($"{item["enabled_mods"]}"));
+ if (mods != "+")
+ sb.AppendLine($"{pp}pp | {acc}% | {map["artist"]} - {map["title"]} ({map["version"]}) **{mods}** | /b/{item["beatmap_id"]}");
+ else
+ sb.AppendLine($"{pp}pp | {acc}% | {map["artist"]} - {map["title"]} ({map["version"]}) | /b/{item["beatmap_id"]}");
+ }
+ await e.Channel.SendMessage(sb.ToString()).ConfigureAwait(false);
+ }
+ catch
+ {
+ await e.Channel.SendMessage("Something went wrong.");
+ }
+ });
+ }
+
+ //https://osu.ppy.sh/wiki/Accuracy
+ private static Double CalculateAcc(JToken play, int mode)
+ {
+ if (mode == 0)
+ {
+ var hitPoints = Double.Parse($"{play["count50"]}") * 50 + Double.Parse($"{play["count100"]}") * 100 + Double.Parse($"{play["count300"]}") * 300;
+ var totalHits = Double.Parse($"{play["count50"]}") + Double.Parse($"{play["count100"]}") + Double.Parse($"{play["count300"]}") + Double.Parse($"{play["countmiss"]}");
+ totalHits *= 300;
+ return Math.Round(hitPoints / totalHits * 100, 2);
+ }
+ else if (mode == 1)
+ {
+ var hitPoints = Double.Parse($"{play["countmiss"]}") * 0 + Double.Parse($"{play["count100"]}") * 0.5 + Double.Parse($"{play["count300"]}") * 1;
+ var totalHits = Double.Parse($"{play["countmiss"]}") + Double.Parse($"{play["count100"]}") + Double.Parse($"{play["count300"]}");
+ hitPoints *= 300;
+ totalHits *= 300;
+ return Math.Round(hitPoints / totalHits * 100, 2);
+ }
+ else if (mode == 2)
+ {
+ var fruitsCaught = Double.Parse($"{play["count50"]}") + Double.Parse($"{play["count100"]}") + Double.Parse($"{play["count300"]}");
+ var totalFruits = Double.Parse($"{play["countmiss"]}") + Double.Parse($"{play["count50"]}") + Double.Parse($"{play["count100"]}") + Double.Parse($"{play["count300"]}") + Double.Parse($"{play["countkatu"]}");
+ return Math.Round(fruitsCaught / totalFruits * 100, 2);
+ }
+ else
+ {
+ var hitPoints = Double.Parse($"{play["count50"]}") * 50 + Double.Parse($"{play["count100"]}") * 100 + Double.Parse($"{play["countkatu"]}") * 200 + (Double.Parse($"{play["count300"]}") + Double.Parse($"{play["countgeki"]}")) * 300;
+ var totalHits = Double.Parse($"{play["countmiss"]}") + Double.Parse($"{play["count50"]}") + Double.Parse($"{play["count100"]}") + Double.Parse($"{play["countkatu"]}") + Double.Parse($"{play["count300"]}") + Double.Parse($"{play["countgeki"]}");
+ totalHits *= 300;
+ return Math.Round(hitPoints / totalHits * 100, 2);
+ }
+ }
+
+ private static string ResolveMap(string mapLink)
+ {
+ Match s = new Regex(@"osu.ppy.sh\/s\/", RegexOptions.IgnoreCase).Match(mapLink);
+ Match b = new Regex(@"osu.ppy.sh\/b\/", RegexOptions.IgnoreCase).Match(mapLink);
+ Match p = new Regex(@"osu.ppy.sh\/p\/", RegexOptions.IgnoreCase).Match(mapLink);
+ Match m = new Regex(@"&m=", RegexOptions.IgnoreCase).Match(mapLink);
+ if (s.Success)
+ {
+ var mapId = mapLink.Substring(mapLink.IndexOf("/s/") + 3);
+ return $"s={mapId}";
+ }
+ else if (b.Success)
+ {
+ if (m.Success)
+ return $"b={mapLink.Substring(mapLink.IndexOf("/b/") + 3, mapLink.IndexOf("&m") - (mapLink.IndexOf("/b/") + 3))}";
+ else
+ return $"b={mapLink.Substring(mapLink.IndexOf("/b/") + 3)}";
+ }
+ else if (p.Success)
+ {
+ if (m.Success)
+ return $"b={mapLink.Substring(mapLink.IndexOf("?b=") + 3, mapLink.IndexOf("&m") - (mapLink.IndexOf("?b=") + 3))}";
+ else
+ return $"b={mapLink.Substring(mapLink.IndexOf("?b=") + 3)}";
+ }
+ else
+ {
+ return $"s={mapLink}"; //just a default incase an ID number was provided by itself (non-url)?
+ }
+ }
+
+ private static int ResolveGameMode(string mode)
+ {
+ switch (mode.ToLower())
+ {
+ case "std":
+ case "standard":
+ return 0;
+ case "taiko":
+ return 1;
+ case "ctb":
+ case "catchthebeat":
+ return 2;
+ case "mania":
+ case "osu!mania":
+ return 3;
+ default:
+ return 0;
+ }
+ }
+
+ //https://github.com/ppy/osu-api/wiki#mods
+ private static string ResolveMods(int mods)
+ {
+ var modString = $"+";
+
+ if (IsBitSet(mods, 0))
+ modString += "NF";
+ if (IsBitSet(mods, 1))
+ modString += "EZ";
+ if (IsBitSet(mods, 8))
+ modString += "HT";
+
+ if (IsBitSet(mods, 3))
+ modString += "HD";
+ if (IsBitSet(mods, 4))
+ modString += "HR";
+ if (IsBitSet(mods, 6) && !IsBitSet(mods, 9))
+ modString += "DT";
+ if (IsBitSet(mods, 9))
+ modString += "NC";
+ if (IsBitSet(mods, 10))
+ modString += "FL";
+
+ if (IsBitSet(mods, 5))
+ modString += "SD";
+ if (IsBitSet(mods, 14))
+ modString += "PF";
+
+ if (IsBitSet(mods, 7))
+ modString += "RX";
+ if (IsBitSet(mods, 11))
+ modString += "AT";
+ if (IsBitSet(mods, 12))
+ modString += "SO";
+ return modString;
+ }
+
+ private static bool IsBitSet(int mods, int pos)
+ {
+ return (mods & (1 << pos)) != 0;
+ }
+
+ }
+}
diff --git a/NadekoBot/Modules/Searches/SearchesModule.cs b/NadekoBot/Modules/Searches/SearchesModule.cs
index 7c0750a0..7220f295 100644
--- a/NadekoBot/Modules/Searches/SearchesModule.cs
+++ b/NadekoBot/Modules/Searches/SearchesModule.cs
@@ -30,6 +30,7 @@ namespace NadekoBot.Modules.Searches
commands.Add(new RedditCommand(this));
commands.Add(new WowJokeCommand(this));
commands.Add(new CalcCommand(this));
+ commands.Add(new OsuCommands(this));
rng = new Random();
}
@@ -257,38 +258,6 @@ $@"π **Weather for** γ{obj["target"]}γ
}
});
- cgb.CreateCommand(Prefix + "osu")
- .Description("Shows osu stats for a player.\n**Usage**:~osu Name")
- .Parameter("usr", ParameterType.Unparsed)
- .Do(async e =>
- {
- if (string.IsNullOrWhiteSpace(e.GetArg("usr")))
- return;
-
- using (WebClient cl = new WebClient())
- {
- try
- {
- cl.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
- cl.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/5.0 (Windows NT 6.2; Win64; x64)");
- cl.DownloadDataAsync(new Uri($"http://lemmmy.pw/osusig/sig.php?uname={ e.GetArg("usr") }&flagshadow&xpbar&xpbarhex&pp=2"));
- cl.DownloadDataCompleted += async (s, cle) =>
- {
- try
- {
- await e.Channel.SendFile($"{e.GetArg("usr")}.png", new MemoryStream(cle.Result)).ConfigureAwait(false);
- await e.Channel.SendMessage($"`Profile Link:`https://osu.ppy.sh/u/{Uri.EscapeDataString(e.GetArg("usr"))}\n`Image provided by https://lemmmy.pw/osusig`").ConfigureAwait(false);
- }
- catch { }
- };
- }
- catch
- {
- await e.Channel.SendMessage("π’ Failed retrieving osu signature :\\").ConfigureAwait(false);
- }
- }
- });
-
cgb.CreateCommand(Prefix + "ud")
.Description("Searches Urban Dictionary for a word.\n**Usage**:~ud Pineapple")
.Parameter("query", ParameterType.Unparsed)
diff --git a/NadekoBot/NadekoBot.cs b/NadekoBot/NadekoBot.cs
index 1f3cd52f..d203f30d 100644
--- a/NadekoBot/NadekoBot.cs
+++ b/NadekoBot/NadekoBot.cs
@@ -105,6 +105,9 @@ namespace NadekoBot
Console.WriteLine(string.IsNullOrWhiteSpace(Creds.SoundCloudClientID)
? "No soundcloud Client ID found. Soundcloud streaming is disabled."
: "SoundCloud streaming enabled.");
+ Console.WriteLine(string.IsNullOrWhiteSpace(Creds.OsuAPIKey)
+ ? "No osu! api key found. Song & top score lookups will not work. User lookups still available."
+ : "osu! API key provided.");
BotMention = $"<@{Creds.BotId}>";
diff --git a/NadekoBot/NadekoBot.csproj b/NadekoBot/NadekoBot.csproj
index 904f2bf3..9c2114f7 100644
--- a/NadekoBot/NadekoBot.csproj
+++ b/NadekoBot/NadekoBot.csproj
@@ -148,6 +148,7 @@
+
diff --git a/NadekoBot/_Models/JSONModels/_JSONModels.cs b/NadekoBot/_Models/JSONModels/_JSONModels.cs
index d5e57d41..ae7241bd 100644
--- a/NadekoBot/_Models/JSONModels/_JSONModels.cs
+++ b/NadekoBot/_Models/JSONModels/_JSONModels.cs
@@ -16,6 +16,7 @@ namespace NadekoBot.Classes.JSONModels
public string LOLAPIKey { get; set; } = "";
public string TrelloAppKey { get; set; } = "";
public string CarbonKey { get; set; } = "";
+ public string OsuAPIKey { get; set; } = "";
}
[DebuggerDisplay("{items[0].id.playlistId}")]
public class YoutubePlaylistSearch
diff --git a/NadekoBot/bin/Debug/credentials_example.json b/NadekoBot/bin/Debug/credentials_example.json
index d8d8fb33..245b1141 100644
--- a/NadekoBot/bin/Debug/credentials_example.json
+++ b/NadekoBot/bin/Debug/credentials_example.json
@@ -11,5 +11,6 @@
"MashapeKey": "",
"LOLAPIKey": "",
"TrelloAppKey": "",
- "CarbonKey": ""
+ "CarbonKey": "",
+ "OsuAPIKey": ""
}
\ No newline at end of file
diff --git a/commandlist.md b/commandlist.md
index 9c71b275..3c581b68 100644
--- a/commandlist.md
+++ b/commandlist.md
@@ -248,8 +248,10 @@ Command and aliases | Description | Usage
`~i` | Pulls the first image found using a search parameter. Use ~ir for different results. | ~i cute kitten
`~ir` | Pulls a random image using a search parameter. | ~ir cute kitten
`~lmgtfy` | Google something for an idiot.
-`~hs` | Searches for a Hearthstone card and shows its image. Takes a while to complete. | ~hs Ysera
-`~osu` | Shows osu stats for a player. | ~osu Name
+`~hs` | Searches for a Hearthstone card and shows its image. Takes a while to complete. | ~hs Ysera
+`~osu u` | Shows osu stats for a player. Optional mode. | ~osu u rrtyui std
+`~osu b` | Shows osu stats for a beatmap. | ~osu b https://osu.ppy.sh/b/992685
+`~osu top5` | Shows an osu player's top 5 plays. Optional mode. | ~osu top5 Dusk ctb
`~ud` | Searches Urban Dictionary for a word. | ~ud Pineapple
`~#` | Searches Tagdef.com for a hashtag. | ~# ff
`~quote` | Shows a random quote.