diff --git a/pom.xml b/pom.xml
index 252adec..688081f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,161 +1,165 @@
4.0.0
Bot
Bot
1.0-SNAPSHOT
despbot
A discord bot
+
+ 1.8
+ 1.8
+
src
maven-compiler-plugin
3.5.1
1.8
org.awaitility
awaitility
3.0.0
org.slf4j
slf4j-api
1.8.0-alpha2
org.slf4j
slf4j-log4j12
1.8.0-alpha2
log4j
apache-log4j-extras
1.2.17
commons-logging
commons-logging
1.2
com.sedmelluq
lavaplayer
1.3.50
com.sedmelluq
jda-nas
1.1.0
se.michaelthelin.spotify
spotify-web-api-java
6.0.0
commons-io
commons-io
2.5
com.vdurmont
emoji-java
3.3.0
net.java.dev.jna
jna
4.5.0
org.apache.commons
commons-lang3
3.6
commons-io
commons-io
2.5
org.apache.httpcomponents
httpcore
4.4.6
org.json
json
20170516
net.objecthunter
exp4j
0.4.8
net.dv8tion
JDA
4.1.1_155
org.seleniumhq.selenium
selenium-java
3.11.0
org.reflections
reflections
0.9.11
org.apache.commons
commons-math3
3.6.1
org.knowm.xchart
xchart
3.6.4
org.xerial
sqlite-jdbc
3.25.2
com.zaxxer
HikariCP
3.3.1
central
bintray
https://jcenter.bintray.com
diff --git a/src/me/despawningbone/discordbot/EventListener.java b/src/me/despawningbone/discordbot/EventListener.java
index b4e0a3a..cb68936 100644
--- a/src/me/despawningbone/discordbot/EventListener.java
+++ b/src/me/despawningbone/discordbot/EventListener.java
@@ -1,445 +1,438 @@
package me.despawningbone.discordbot;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.command.music.AudioTrackHandler;
import me.despawningbone.discordbot.command.music.GuildMusicManager;
import me.despawningbone.discordbot.command.music.Music;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.Message.Attachment;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMoveEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent;
import net.dv8tion.jda.api.events.user.UserActivityStartEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
public class EventListener extends ListenerAdapter {
public static Multimap testAnimePicCache = ArrayListMultimap.create();
@Override
public void onGuildMessageReceived(GuildMessageReceivedEvent event) {
DiscordBot.mainJDA = event.getJDA(); // refresh it? //no need, everything's getJDA() is the same jda basis but audioplayer's still using it; need to migrate to store in guildmusicmanager
//String[] words = event.getMessage().getContentDisplay().split(" ");
User author = event.getAuthor();
TextChannel channel = event.getChannel();
Message msg = event.getMessage();
//checkAnimePics(event);
- /*if(author.getId().equals("411350223625912323")) {
- channel.sendMessage("| ||\n|| |_").queue();
- }*/
- //if(event.getMessage().getAttachments().stream().anyMatch(p -> p.getUrl().equals("https://cdn.discordapp.com/attachments/419464220304867348/432108442036207617/TinyPlainAnemoneshrimp-size_restricted.gif"))) event.getMessage().delete().queue();
- if(event.getMessage().getAttachments().stream().anyMatch(p -> p.getUrl().contains("DeepFryer_20191122_160935.jpg"))) event.getMessage().delete().queue();
- //if(event.getGuild().getId().equals("398140934035996687")) channel.getGuild().getRolesByName("Actual robutt role, this one has perms don't fuck with it", true).get(0).getPermissions().forEach(p -> System.out.println(p));
-
//DONE use log4j
//logging
if (!DiscordBot.logExcemptID.contains(author.getId())) {
String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]";
String payload = "[INFO] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full msg: " + msg.getContentDisplay();
List att = event.getMessage().getAttachments();
if (!att.isEmpty()) {
payload += System.lineSeparator() + " Attachments:";
for (int i = 0; i < att.size(); i++) {
payload += System.lineSeparator() + " " + att.get(i).getUrl();
}
}
DiscordBot.logger.trace(payload); //TODO log embeds and add user id too?
}
/*if (String.join("", words).equalsIgnoreCase("!despacito")) {
channel.sendMessage("https://www.youtube.com/watch?v=W3GrSMYbkBE").queue();
return;
}*/
CommandResult result = null;
try (Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){
String prefix = MiscUtils.getPrefix(s, event.getGuild().getId());
if(msg.getContentDisplay().toLowerCase().startsWith(prefix.toLowerCase())) {
String msgStripped = msg.getContentDisplay().substring(prefix.length()).replaceAll("\\s\\s+", " "); //merges space
String[] args = msgStripped.split(" "); // base on command length?
Command cmd = DiscordBot.commands.get(args[0].toLowerCase());
cmd = cmd == null ? DiscordBot.aliases.get(args[0].toLowerCase()) : cmd;
if (cmd != null) {
ResultSet uRs = s.executeQuery("SELECT reports FROM users WHERE id = " + author.getId() + ";");
if(uRs.next()) { //checks null at the same time
if(uRs.getString(1).split("\n").length >= 5) {
channel.sendMessage("You are banned from using the bot.").queue();
DiscordBot.logger.info("[WARN] " + author.getName() + " (" + author.getId() + ") tried to execute " + msg.getContentDisplay() + " but was banned.");
return;
}
}
uRs.close();
long perm = MiscUtils.getActivePerms(s, channel, cmd);
//result = cmd.execute(channel, author, msg, args);
String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel); //pass it to the subcommand handler to handle instead
if(perms == null || event.getAuthor().getId().equals(DiscordBot.OwnerID)) { //owner overrides perms for convenience
/*if(cmd.isDisabled(channel.getGuild().getId())) {
channel.sendMessage("This command is disabled!").queue();
result = new CommandResult(CommandResultType.FAILURE, "Disabled command");
} else {*/
cmd.executeAsync(channel, author, msg, Arrays.copyOfRange(args, 1, args.length), r -> { //catch all exceptions? //should have actually
DiscordBot.logger.info("[" + r.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed "
+ msg.getContentDisplay() + (r.getRemarks() == null ? "." : ". (" + r.getRemarks() + ")")); //logging has to be before sendMessage, or else if no permission it will just quit
if(r.getMessage() != null) channel.sendMessage(r.getMessage()).queue();
}); //dont know if async will screw anything up //wont, TODO log date and which server executed the command also?
return;
//}
} else if(perms.equals("DISABLED") || cmd.isDisabled()) {
msg.addReaction("❎").queue();
result = new CommandResult(CommandResultType.DISABLED);
} else {
result = new CommandResult(CommandResultType.NOPERMS, perms);
channel.sendMessage(result.getMessage()).queue();
}
} else {
result = new CommandResult(CommandResultType.FAILURE, "Invalid command");
//do more stuff?
}
} else if(msg.getContentRaw().matches("<@!?" + DiscordBot.BotID + ">")) result = greet(channel, author, prefix);
} catch (SQLException e) {
result = new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
if(result != null) DiscordBot.logger.info("[" + result.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed "
+ msg.getContentDisplay() + (result.getRemarks() == null ? "." : ". (" + result.getRemarks() + ")"));
}
@SuppressWarnings("unused")
private void checkAnimePics(GuildMessageReceivedEvent event) {
//TESTING
CompletableFuture.runAsync(() -> {
User author = event.getAuthor();
TextChannel channel = event.getChannel();
Message msg = event.getMessage();
List pics = new ArrayList<>();
if(channel.getId().equals("615396635383562272") && !DiscordBot.BotID.equals(author.getId())) {
for(Attachment att : msg.getAttachments()) {
if(att.isImage()) pics.add(att.getUrl());
}
/*if(msg.getContentDisplay().contains("http")) {
for(String s : msg.getContentDisplay().split("http")) {
try {
new URL("http" + s);
pics.add("http" + s);
} catch(MalformedURLException e) {
;
}
}
}*/
try {
Thread.sleep(1500);
msg = channel.retrieveMessageById(msg.getId()).complete();
} catch (InterruptedException e2) {
e2.printStackTrace();
}
for(MessageEmbed em : msg.getEmbeds()) {
//System.out.println(em.toJSONObject());
if(em.getThumbnail() != null) {
System.out.println("thumb");
if(em.getSiteProvider() != null && em.getSiteProvider().getName().equals("pixiv")) { //pixiv doesnt show whole pic in embed
pics.add(em.getUrl());
} else {
pics.add(em.getThumbnail().getUrl());
}
} else if(em.getImage() != null && !author.isBot()) {
System.out.println("img");
pics.add(em.getImage().getUrl());
} else {
System.out.println("url");
pics.add(em.getUrl());
}
}
//System.out.println(pics);
//System.out.println(testAnimePicCache);
for(String pic : pics) { //handle first since these mustnt be the same urls, therefore cache saucenao sauce instead
String origPic = pic;
if(!testAnimePicCache.containsValue(pic)) {
Element saucenao;
try {
saucenao = Jsoup.connect("https://saucenao.com/search.php?url=" + pic).get().body();
Element result = saucenao.selectFirst(".resulttable");
try { //normal pixiv/deviantart handling
if(result.parent().attr("class").equals("result hidden")) throw new NullPointerException();
Element source = result.selectFirst("strong:contains(ID:)").nextElementSibling();
pic = source.attr("href"); //will not run if the cache has the value || if the link specified aint a pic
} catch (NullPointerException e1) { //weird saucenao card formatting (eg episode info), or no result; we dont handle these
;
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("next" + testAnimePicCache);
if(testAnimePicCache.containsValue(pic)) {
try {
Multimap temp = ArrayListMultimap.create();
System.out.println("temp");
Message m = channel.retrieveMessageById(Multimaps.invertFrom(testAnimePicCache, temp).get(pic).toArray(new String[1])[0]).complete();
EmbedBuilder eb = new EmbedBuilder();
eb.setTitle("Repost detected!");
eb.setDescription("[This image](" + origPic + ")" + (origPic.equals(pic) ? "" : "([source](" + pic + "))") + " is a repost of [this message](" + m.getJumpUrl() + ") by `" + m.getAuthor().getName() + "#" + m.getAuthor().getDiscriminator() + "` at **" + m.getTimeCreated().format(DateTimeFormatter.RFC_1123_DATE_TIME) + "**.\n");
eb.setThumbnail(origPic);
channel.sendMessage(eb.build()).queue();
continue;
} catch(NullPointerException e) { //if the message is deleted
e.printStackTrace();
}
} else {
System.out.println("put");
testAnimePicCache.put(msg.getId(), pic);
}
}
}
}).whenComplete((r, t) -> {if(t != null) t.printStackTrace();});
}
private CommandResult greet(TextChannel channel, User author, String prefix) {
MessageBuilder smsg = new MessageBuilder();
String nick = channel.getGuild().getMemberById(author.getId()).getNickname();
if (nick != null) {
smsg.append("Yo " + nick + "!\n");
} else {
smsg.append("Yo " + author.getName() + "!\n");
}
if (author.getId().equals("237881229876133888")) { // easter eggs
smsg.append("How art thou, my creator?");
}
if (author.getId().equals("179824176847257600")) {
smsg.append("Still stalking as usual, huh? :smirk:");
smsg.append("\n\"go fix your potatoes\" - pugger");
}
if (author.getId().equals("165403578133905408")) {
smsg.append(
"You're probably still dead :D (your status), either that or you are playing with your doodle :P");
}
if (author.getId().equals("187714189672841216")) {
smsg.append("Hello, fearsome coder named Morgan :wink:");
}
if (author.getId().equals("201768560345743360")) {
smsg.append("Still need help to save you from the school? :D");
}
if (author.getId().equals("257660112703979521")) {
smsg.append("I like how you only uses me for `!desp roll 1`. :P");
}
if (author.getId().equals("272712701988569090")) {
smsg.append("This guy is generous :smile:");
}
if (author.getId().equals("203861130995695616")) {
smsg.append("He asked my owner to change it so here it is :smile:");
}
if (author.getId().equals("206038522971422721")) {
smsg.append("Appearently he loves !desp idiot :face_palm:");
}
if (author.getId().equals("218377806994866176")) {
smsg.append("Your potatoes seems better than Dank's :smirk:");
}
if (author.getId().equals("139316803582033920")) {
smsg.append("That ironic name tho :stuck_out_tongue:");
}
if (author.getId().equals("237058431272484864")) {
smsg.append(":b:oi");
}
if (author.getId().equals("338258756762730496")) {
smsg.append("**a e s t h e t i c**");
}
EmbedBuilder eb = new EmbedBuilder();
eb.setColor(0x051153); //TODO version info based on git commits
eb.appendDescription("This bot is running **despbot v1.5.0**, Shard `" + DiscordBot.mainJDA.getShardInfo().getShardString() + "`. ([invite me!](https://discordapp.com/oauth2/authorize?&client_id=311086271642599424&scope=bot&permissions=0))\n");
eb.appendDescription("Connected guilds: `" + DiscordBot.mainJDA.getGuildCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getGuildCache().size()).sum()) + "`; ");
eb.appendDescription("Total members (cached): `" + DiscordBot.mainJDA.getUserCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getUserCache().size()).sum()) + "`\n");
eb.appendDescription("DM `despawningbone#4078` if you have any questions!\n");
eb.appendDescription("To get a list of commands, do `" + prefix + "help`.");
smsg.setEmbed(eb.build());
Message fmsg = smsg.build();
channel.sendMessage(fmsg).queue();
return new CommandResult(CommandResultType.SUCCESS, null);
}
@Override
public void onGuildMessageUpdate(GuildMessageUpdateEvent event) {
//System.out.println("edit"); //debug
Message msg = event.getMessage();
User author = event.getAuthor();
TextChannel channel = event.getChannel();
if (!DiscordBot.logExcemptID.contains(author.getId())) {
String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]";
DiscordBot.logger.trace("[EDIT] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full edited msg: " + msg.getContentDisplay());
}
}
@Override
public void onUserActivityStart(UserActivityStartEvent event) { //store presence for checking osu pp
Activity osu = event.getNewActivity();
if (osu != null && osu.getName().equals("osu!") && osu.isRich() //if need to include other games and details, just remove this if clause and change sGame below
&& osu.asRichPresence().getDetails() != null) {
String toolTip = osu.asRichPresence().getLargeImage().getText();
//DiscordBot.guildMemberPresence.put(event.getUser().getId(), game); //so that game update to nothing wont be logged
/*System.out.println(event.getGuild().getName());
System.out.println(event.getUser().getName());
System.out.println(game.getName());
System.out.println(game.getUrl());
System.out.println(game.getType());
System.out.println(game.isRich() ? game.asRichPresence().getDetails() : "no rich");
System.out.println(game.isRich() ? game.asRichPresence().getState() : "no rich");*/
//OffsetDateTime timesent = OffsetDateTime.now();
//System.out.println(toolTip);
String sGame = (toolTip.lastIndexOf(" (") != -1 ? toolTip.substring(0, toolTip.lastIndexOf(" (")) : toolTip) + "||" + osu.asRichPresence().getDetails();
//sGame = sGame.replaceAll("'", "''");
/*OffsetDateTime timeReceived = OffsetDateTime.now();
long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
System.out.println("Time taken: " + ms + "ms");*/
try (Connection con = DiscordBot.db.getConnection()) { //DONE do i need to close the statement?
PreparedStatement s = con.prepareStatement("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET game = ? WHERE game <> ?;" ); //prevent blank updates
s.setString(1, sGame); s.setString(2, sGame); s.setString(3, sGame);
s.execute();
//con.createStatement().execute("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", '" + sGame + "') ON CONFLICT(id) DO UPDATE SET game = '" + sGame + "' WHERE game <> '" + sGame + "';" ); //prevent blank updates
} catch (SQLException e) { //FIXED if i make this not osu only, be aware of SQL injections through sGame (probably being paranoid tho)
e.printStackTrace();
}
/*timeReceived = OffsetDateTime.now();
ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
System.out.println("Time taken: " + ms + "ms");*/
}
}
//TODO only update when not paused?
//TODO update on member deafen?
private void waitActivity(Guild guild) {
AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
ap.getGuildMusicManager(guild).player.setPaused(true);
ap.getGuildMusicManager(guild).clearQueueCleanup = ap.ex.schedule(() -> {
ap.stopAndClearQueue(guild);
DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("The queue has been cleared.").queue();
ap.getGuildMusicManager(guild).clearQueueCleanup = null;
}, 1, TimeUnit.MINUTES);
}
private String updateActivity(Guild guild, VoiceChannel vc) {
AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
GuildMusicManager mm = ap.getGuildMusicManager(guild);
if(mm.clearQueueCleanup != null) {
ap.getGuildMusicManager(guild).player.setPaused(false);
mm.clearQueueCleanup.cancel(true);
mm.clearQueueCleanup = null;
}
String type = mm.scheduler.loop; //mm cannot be null
if (type != null
&& vc.getMembers().size() > 2
&& vc.getMembers().contains(guild.getMemberById(DiscordBot.OwnerID))) {
ap.toggleLoopQueue(guild, type);
return type;
}
return null;
}
@Override
public void onGuildVoiceLeave(GuildVoiceLeaveEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this
GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState();
if (vs.inVoiceChannel()) {
if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
if (vs.getChannel().equals(event.getChannelLeft())
&& event.getChannelLeft().getMembers().size() < 2) {
TextChannel channel = DiscordBot.lastMusicCmd.get(event.getGuild().getId());
channel.sendMessage(
"All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.")
.queue();
waitActivity(event.getGuild());
}
} else { //got kicked
System.out.println("Got kicked");
((Music) DiscordBot.commands.get("music")).getAudioTrackHandler().stopAndClearQueue(event.getGuild());
}
}
}
@Override
public void onGuildVoiceMove(GuildVoiceMoveEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this
GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState();
if (vs.inVoiceChannel()) {
String id = event.getGuild().getId();
TextChannel channel = DiscordBot.lastMusicCmd.get(id);
if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
if (vs.getChannel().equals(event.getChannelLeft())) {
if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)
&& event.getChannelLeft().getMembers().size() < 2) {
channel.sendMessage(
"All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.")
.queue();
waitActivity(event.getGuild());
}
} else if (vs.getChannel().equals(event.getChannelJoined())) {
String type = updateActivity(event.getGuild(), event.getChannelJoined());
if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
}
} else { //moved bot to empty channel
if(event.getChannelJoined().getMembers().size() < 2) {
channel.sendMessage(
"The bot has been moved to an empty channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.")
.queue();
waitActivity(event.getGuild());
} else { //moved bot to channel with ppl
String type = updateActivity(event.getGuild(), event.getChannelJoined());
if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
}
}
}
}
@Override
public void onGuildVoiceJoin(GuildVoiceJoinEvent event) {
GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState();
String id = event.getGuild().getId();
TextChannel channel = DiscordBot.lastMusicCmd.get(id);
if(channel != null) {
if (vs.inVoiceChannel() && !event.getMember().getUser().getId().equals(DiscordBot.BotID)) {
if (vs.getChannel().equals(event.getChannelJoined())) {
String type = updateActivity(event.getGuild(), event.getChannelJoined());
if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue();
}
}
}
}
}
diff --git a/src/me/despawningbone/discordbot/command/games/Osu.java b/src/me/despawningbone/discordbot/command/games/Osu.java
index 8780662..ee9fc26 100644
--- a/src/me/despawningbone/discordbot/command/games/Osu.java
+++ b/src/me/despawningbone/discordbot/command/games/Osu.java
@@ -1,1174 +1,1174 @@
package me.despawningbone.discordbot.command.games;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.BitmapEncoder.BitmapFormat;
import org.knowm.xchart.XYSeries.XYSeriesRenderStyle;
import org.knowm.xchart.style.Styler.LegendPosition;
import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.command.games.Koohii.Accuracy;
import me.despawningbone.discordbot.command.games.Koohii.DiffCalc;
import me.despawningbone.discordbot.command.games.Koohii.Map;
import me.despawningbone.discordbot.command.games.Koohii.PPv2;
import me.despawningbone.discordbot.command.games.Koohii.PPv2Parameters;
import me.despawningbone.discordbot.command.games.Koohii.MapStats;
import me.despawningbone.discordbot.command.games.Koohii.TaikoPP;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
public class Osu extends Command {
private final static String osuAPI = DiscordBot.tokens.getProperty("osu");
private static DecimalFormat df = new DecimalFormat("#.##");
private final static HashMap modes = new HashMap<>();
static {
modes.put(0, "osu!");
modes.put(1, "osu!taiko");
modes.put(2, "osu!catch");
modes.put(3, "osu!mania");
}
public Osu() { //TODO add back the todos to respective sub command, automate desc for subcmds, add back typing; CLOSE ALL STREAMS
this.desc = "All the info you need with osu!";
this.usage = "";
//TODO add a command to parse replays?
registerSubCommand("pp", Arrays.asList("map"), (channel, user, msg, words) -> {
//OffsetDateTime timesent = OffsetDateTime.now();
List amend = new ArrayList(Arrays.asList(words));
int temp = amend.indexOf("-w");
String uid = null;
boolean weight = temp != -1;
if(weight) {
try {
uid = amend.get(temp + 1);
if(!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://"))) {
uid = getPlayer(user);
}
amend.subList(temp, temp + 1).clear();
} catch (IndexOutOfBoundsException e) {
uid = getPlayer(user);
}
}
String initmap;
try {
initmap = amend.get(0);
} catch (IndexOutOfBoundsException e) {
initmap = "null";
}
List mid = new ArrayList();
if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) { //check if no map input, use discord rich presence
String details = null;
try (Connection con = DiscordBot.db.getConnection()) {
ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
if(rs.next()) {
details = rs.getString(1).substring(rs.getString(1).indexOf("||"));
}
rs.close();
} catch (SQLException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
channel.sendMessage("Trying to retrieve map from discord status...").queue(m -> mid.add(m.getId())); //DONE custom status masks presence now, will not be as effective; any way to get manually? //JDA 4 API change fixed this
channel.sendTyping().queue();
/*OffsetDateTime timeReceived = OffsetDateTime.now();
long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
System.out.println("Time taken: " + ms + "ms");*/
if (details != null) { //TODO if name is sth like `Feryquitous - (S).0ngs//---::compilation.[TQR-f3] [-[//mission:#FC.0011-excindell.defer.abferibus]-]` it breaks (but reasonable break tbh)
//if(game.getName().equals("osu!") && game.isRich()) {
try {
String title = URLEncoder.encode(details.substring(details.indexOf(" - ") + 3, details.lastIndexOf("[")).trim(), "UTF-8");
String diff = URLEncoder.encode(details.substring(details.lastIndexOf("[") + 1, details.lastIndexOf("]")).trim(), "UTF-8");
String url = "https://osusearch.com/query/?title=" + title + "&diff_name=" + diff + "&query_order=play_count&offset=0";
URLConnection stream = new URL(url).openConnection();
stream.addRequestProperty("User-Agent", "Mozilla/4.0");
JSONTokener tokener = new JSONTokener(stream.getInputStream());
initmap = "https://osu.ppy.sh/beatmaps/" + new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmap_id");
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
} catch (JSONException e) {
e.printStackTrace();
System.out.println(details);
return new CommandResult(CommandResultType.NORESULT); //returns if osusearch isnt updated fast enough
}
/*SELENIUM DEPRECATED*/
/*String url = "http://osusearch.com/search/?title=" + title + "&diff_name=" + diff
+ "&query_order=play_count";
System.out.println(url);
//MsgListener.driver.manage().timeouts().implicitlyWait(2000, TimeUnit.MILLISECONDS);
DiscordBot.driver.get(url);
WebDriverWait wait = new WebDriverWait(DiscordBot.driver, 10);
wait.until(ExpectedConditions
.elementToBeClickable(By.cssSelector("div[class~=beatmap-list]")));
Document document = Jsoup.parse(DiscordBot.driver.getPageSource());
//System.out.println(document.toString());
initmap = document.body()
.select("div[class~=beatmap-list]")
.first()
.child(0)
.select("div.truncate.beatmap-title a").get(0).attr("href");*/
} else {
return new CommandResult(CommandResultType.FAILURE, "There is no account of your rich presence, therefore I cannot get the beatmap from your status.");
}
/*
* } else { throw new
* IllegalArgumentException("Rich presence is not an instance of osu!, therefore I cannot get the beatmap from your status."
* ); }
*/
}
if(initmap.equals("null")) { //shouldnt throw at all
return new CommandResult(CommandResultType.FAILURE, "You haven't played any maps I can recognize yet!");
}
List params = Arrays.asList(words);
double dacc = 100;
int combo = -1, mods = 0, miss = 0;
PPv2Parameters p = new Koohii.PPv2Parameters();
try {
p.beatmap = new Koohii.Parser()
.map(new BufferedReader(new InputStreamReader(getMap(initmap), "UTF-8")));
} catch (IOException e1) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
} catch (UnsupportedOperationException e1) {
return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
} catch (IllegalArgumentException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
}
if (p.beatmap.title.isEmpty()) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
}
try {
for (int i = 0; i < params.size(); i++) {
String param = params.get(i);
if (param.startsWith("+")) {
mods = Koohii.mods_from_str(param.substring(1).toUpperCase());
} else if (param.toLowerCase().endsWith("m")) {
miss = Integer.parseInt(param.substring(0, param.length() - 1));
} else if (param.endsWith("%")) {
dacc = Double.parseDouble(param.substring(0, param.length() - 1));
} else if (param.toLowerCase().endsWith("x")) {
combo = Integer.parseInt(param.substring(0, param.length() - 1));
}
}
} catch (NumberFormatException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid value inputted.");
}
DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
Accuracy acc = new Accuracy(dacc, p.beatmap.objects.size(), miss);
p.n50 = acc.n50;
p.n100 = acc.n100;
p.n300 = acc.n300;
p.nmiss = miss;
p.aim_stars = stars.aim;
p.speed_stars = stars.speed;
p.mods = mods;
if (combo > 0) {
p.combo = combo;
} else {
p.combo = p.beatmap.max_combo();
}
EmbedBuilder eb = new EmbedBuilder();
JSONTokener result = null;
InputStream stream = null;
URL url = null;
try {
String addparam = "";
if(p.beatmap.mode == 1) {
if((mods & 1<<8) != 0) addparam = "&mods=256";
if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64";
else addparam = "";
}
url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + initmap.substring(initmap.lastIndexOf("/") + 1).split("&")[0] + addparam);
stream = url.openStream();
} catch (IOException ex) {
ex.printStackTrace();
}
result = new JSONTokener(stream);
JSONObject jbm = new JSONArray(result).getJSONObject(0);
String setid = jbm.getString("beatmapset_id");
double accVal, ppVal, starsVal;
MapStats stats = new MapStats(p.beatmap);
try {
PPv2 pp = new PPv2(p);
ppVal = pp.total;
accVal = acc.value(); //somehow real_acc aint correct, pretty sure i screwed sth up
starsVal = stars.total;
Koohii.mods_apply(mods, stats, 1|2|4|8);
} catch (UnsupportedOperationException e) { //should always only be taiko
starsVal = jbm.getDouble("difficultyrating");
TaikoPP pp = new TaikoPP(starsVal, dacc, p);
ppVal = pp.pp;
accVal = pp.acc;
Koohii.mods_apply(mods, stats, 0); //want nothing but speed to be changed
}
String totaldur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("total_length") / stats.speed)));
String draindur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("hit_length") / stats.speed)));
if(jbm.has("source") && !jbm.getString("source").isEmpty()) eb.setTitle("Source: " + jbm.getString("source"));
eb.setDescription("Mode: " + modes.get(p.beatmap.mode));
eb.setAuthor("PP information for " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode),
initmap, "https://b.ppy.sh/thumb/" + setid + ".jpg");
eb.addField("Artist", (p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode),
true);
eb.addField("Created by", p.beatmap.creator, true);
eb.addField("Duration", totaldur + " | Drain " + draindur, true);
eb.addField("Difficulty", p.beatmap.version, true);
eb.addField("Mods", (p.mods == 0 ? "None" : Koohii.mods_str(p.mods)), true);
eb.addField("BPM", df.format(jbm.getDouble("bpm") * stats.speed), true);
eb.addField("Accuracy", df.format(accVal * 100), true);
eb.addField("Combo", String.valueOf(p.combo), true);
eb.addField("Misses", String.valueOf(p.nmiss), true);
eb.addField("300", String.valueOf(p.n300), true);
eb.addField("100", String.valueOf(p.n100), true);
eb.addField("50", String.valueOf(p.n50), true);
eb.addField("PP", df.format(ppVal), true);
if(weight) {
try {
String gain = df.format(weightForPP(ppVal, jbm.getString("beatmap_id"), getUserBest(uid, p.beatmap.mode)));
eb.addField("Actual pp gain for " + uid, (gain.equals("-1") ? "User has a better score than this" : gain + "pp"), true);
} catch (IllegalArgumentException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
}
}
eb.setFooter("Stars: " + df.format(starsVal) + " | CS: " + df.format(stats.cs) + " | HP: " + df.format(stats.hp)
+ " | AR: " + df.format(stats.ar) + " | OD: " + df.format(stats.od), null);
//e.setColor(new Color(248, 124, 248));
eb.setColor(new Color(239, 109, 167));
if(!mid.isEmpty()) {
channel.deleteMessageById(mid.get(0)).queue();
}
//channel.sendMessage(e.build()).queue();
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[beatmap URL] [+|%|x|m] [-w [username]]", Arrays.asList("-w", "+DT", "https://osu.ppy.sh/b/1817768 93.43% -w", "https://osu.ppy.sh/beatmaps/1154509 +HDDT 96.05% 147x 2m -w despawningbone"),
"Check PP information about that beatmap!", Arrays.asList(
" * Available parameters: +[mod alias], [accuracy]%, [combo]x, [misses]m",
" * specify the `-w` parameter with a username (or none for your own) to check how many actual pp the play will get them!",
" * You can also not input the URL, if you are currently playing osu! and discord sensed it."));
registerSubCommand("weight", Arrays.asList("w"), (channel, user, msg, words) -> {
//System.out.println(weightForPP(95.91, null, null));
//String debugMax = "", debugMin = "", debugData = "", debugAll = "";
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
final double precision = 1e-4, iterations = 500;
String uid;
int index = params.size() < 2 ? 0 : 1;
try {
uid = params.get(index - 1);
if(index == 0 || (!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://")))) {
uid = getPlayer(user);
}
} catch (IndexOutOfBoundsException e) {
uid = getPlayer(user);
}
JSONArray arr = null;
try {
arr = getUserBest(uid, modeId);
} catch (IllegalArgumentException e) {
return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
}
double maxPP = arr.getJSONObject(0).getDouble("pp") + 100; //fricc those who wanna get pp over 100pp more than they can lmao
try {
//double basePP = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + uid).openStream())).getJSONObject(0).getDouble("pp_raw");
double targetPP = params.size() > index ? Double.parseDouble(params.get(index)) : 1;
int i = 0; double minPP = 0, mid = 0;
/*for(i = 0; i < 600; i++) {
//debugData += weightForPP(i, null, arr) + "\n";
debugData += i + " " + weightForPP(i, null, arr) + "\n";
}*/
//System.out.println(debugData);
i = 0;
while(i < iterations && Math.abs(maxPP - minPP) > precision) {
mid = (maxPP + minPP) / 2;
double temp = weightForPP(mid, null, arr);
//System.out.println(i + ", " + mid + ", " + temp);
if(temp > targetPP) {
maxPP = mid;
} else {
minPP = mid;
}
i++;
//System.out.println(maxPP + " " + minPP);
//debugAll += maxPP + " " + minPP + "\n";
//debugMax += maxPP + "";
//debugMin += minPP + " ";
}
//System.out.println(debugAll);
//System.out.println(debugMax);
//System.out.println(debugMin);
if(!df.format(mid).equals(df.format(arr.getJSONObject(0).getDouble("pp") + 100))) {
channel.sendMessage("For " + uid + " to actually gain " + df.format(targetPP) + "pp in " + modes.get(modeId) + ", they have to play a map worth approximately **" + df.format(mid) + "pp** raw.").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You can't really achieve such large pp jumps without people thinking you are hacking :P");
}
} catch (NumberFormatException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid target pp specified.");
}
//return "";
}, "[username] [wished pp gain]", Arrays.asList("", "10", "despawningbone 20"),
"Estimate how much raw pp a map needs to have in order to gain you pp!",
Arrays.asList(" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
//TODO NEED EXTREME TIDYING UP
//TODO merge parts of the code with getRecent() and getPP();
registerSubCommand("recent", Arrays.asList("r"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
if(modeId > 1) modeId = 0;
boolean passOnly = params.removeAll(Collections.singleton("-p")); //no need space, as spaced ones are split (if a player has a name with spaced -p it might be a problem)
String[] split = String.join(" ", params).split("\\|");
- String search = split[0];
+ String search = split[0].trim();
int nrecent = 0;
try {
nrecent = Integer.parseInt(split[1].trim()) - 1;
} catch (NumberFormatException e) {
//print to channel?
} catch (ArrayIndexOutOfBoundsException e) {
; //nothing special about this
}
if(nrecent > 50) {
return new CommandResult(CommandResultType.FAILURE, "Only the 50 most recent plays can be recalled!");
}
String id = "";
boolean noturl = false;
try {
new URL(search);
id = search.substring(search.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
id = search;
}
if(id.isEmpty()) {
id = getPlayer(user);
}
JSONTokener result = null;
InputStream stream = null;
URL url = null;
try {
String addparam = "";
if (noturl) {
addparam = "&type=string";
}
if (modeId != 0) {
addparam += ("&m=" + modeId);
}
url = new URL(
"https://osu.ppy.sh/api/get_user_recent?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=50");
stream = url.openStream();
if (stream.available() < 4) {
return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + id + "` or the player has not been playing in the last 24h.");
}
} catch (IOException ex) {
ex.printStackTrace();
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
}
result = new JSONTokener(stream);
JSONArray array = new JSONArray(result);
//System.out.println(array);
if(array.length() > 0) {
JSONObject mostRecent = null;
if(passOnly) {
int times = 0;
for(int i = 0; i < array.length(); i++) {
if(!array.getJSONObject(i).getString("rank").equals("F")) {
times++;
if(times - 1 >= nrecent) {
mostRecent = array.getJSONObject(i);
nrecent = i;
break;
}
}
}
if(mostRecent == null) {
return new CommandResult(CommandResultType.INVALIDARGS, "You did not have this much passed plays in the recent 50 plays!");
}
} else {
try {
mostRecent = array.getJSONObject(nrecent);
} catch (JSONException e) {
return new CommandResult(CommandResultType.FAILURE, "You only played " + array.length() + " times recently!");
}
}
String beatmap = mostRecent.getString("beatmap_id");
String name;
if (noturl) {
name = id;
id = mostRecent.getString("user_id");
} else {
try {
Long.parseLong(id);
//get more info from here?
name = new JSONObject(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id).openStream())).getString("username");
} catch (NumberFormatException e) {
name = id;
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
}
int i;
String more = "";
for(i = nrecent + 1; i < array.length(); i++) {
if(!array.getJSONObject(i).getString("beatmap_id").equals(beatmap)) {
break;
}
if(i == array.length() - 1) {
more = " (or more)";
}
}
if(i == array.length()) {
more = " (or more)";
}
PPv2Parameters p = new Koohii.PPv2Parameters();
try {
p.beatmap = new Koohii.Parser()
.map(new BufferedReader(new InputStreamReader(getMap(beatmap), "UTF-8")));
} catch (IOException e1) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
} catch (UnsupportedOperationException e1) {
return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
} catch (IllegalArgumentException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
}
int mods = mostRecent.getInt("enabled_mods");
if (p.beatmap.title.isEmpty()) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
}
DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
p.n50 = mostRecent.getInt("count50");
p.n100 = mostRecent.getInt("count100");
p.n300 = mostRecent.getInt("count300");
p.nmiss = mostRecent.getInt("countmiss");
//System.out.println(p.n300);
p.nobjects = p.n300 + p.n100 + p.n50 + p.nmiss;
//System.out.println(p.nobjects);
p.aim_stars = stars.aim;
p.speed_stars = stars.speed;
p.mods = mods;
p.combo = mostRecent.getInt("maxcombo");
EmbedBuilder eb = new EmbedBuilder();
//System.out.println(mods);
String modStr = Koohii.mods_str(mods);
eb.setColor(new Color(0, 0, 0));
double ppVal, ppMax, starVal, accVal;
JSONObject obj;
try {
String addparam = "";
if(p.beatmap.mode == 1) {
if((mods & 1<<8) != 0) addparam = "&mods=256";
if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64";
else addparam = "";
}
obj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + mostRecent.getInt("beatmap_id") + addparam).openStream())).getJSONObject(0);
} catch (JSONException | IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
try {
PPv2 pp = new PPv2(p);
starVal = stars.total;
accVal = pp.computed_accuracy.value();
ppVal = pp.total;
ppMax = constructPP(stars.aim, stars.speed, p.beatmap, mods).total;
} catch (UnsupportedOperationException e) {
starVal = obj.getDouble("difficultyrating");
TaikoPP pp = new TaikoPP(starVal, p);
ppVal = pp.pp;
accVal = pp.acc;
ppMax = new TaikoPP(starVal, p.max_combo, mods, 1, 0, p.beatmap).pp;
}
eb.setTitle((p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode) + " - " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode) + " [" + p.beatmap.version + "]" + (modStr.isEmpty() ? "" : " +" + modStr) + " (" + df.format(starVal) + "*)", "https://osu.ppy.sh/beatmaps/" + beatmap);
eb.setAuthor(MiscUtils.ordinal(nrecent + 1) + " most recent " + modes.get(modeId) + " play by " + name, "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
eb.setDescription(MiscUtils.ordinal(i - nrecent) + more + " consecutive try\nRanking: " + mostRecent.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + (mostRecent.getString("rank").equals("F") ? " (Estimated completion percentage: " + df.format(p.nobjects * 100.0 / p.beatmap.objects.size()) + "%)" : ""));
eb.setThumbnail("https://b.ppy.sh/thumb/" + obj.getString("beatmapset_id") + ".jpg");
eb.addField("Score", String.valueOf(mostRecent.getInt("score")), true);
eb.addField("Accuracy", df.format(accVal * 100) + " (" + p.n300 + "/" + p.n100 + "/" + p.n50 + "/" + p.nmiss + ")", true);
eb.addField("Combo", (mostRecent.getInt("perfect") == 0 ? p.combo + "x / " + p.beatmap.max_combo() + "x" : "FC (" + p.combo + "x)"), true);
eb.addField("PP", df.format(ppVal) + "pp / " + df.format(ppMax) + "pp", true);
eb.setFooter("Score submitted", null);
eb.setTimestamp(OffsetDateTime.parse(mostRecent.getString("date").replace(" ", "T") + "+00:00"));
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.FAILURE, "You have no recent plays in this 24h!");
}
}, "[-p] [username] [| index]", Arrays.asList("", "-p | 2", "-p despawningbone"),
"Check a user's recent play!", Arrays.asList(
" * Specifying the `-p` parameter will only search for plays that did not fail.",
" * Supports `-t` for taiko (Defaults to standard)."));
//TODO tidy up first part, merge with getTopPlays()?
registerSubCommand("ppchart", Arrays.asList("ppc"), (channel, user, msg, words) -> {
if(words.length < 30) {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
if(params.size() < 1) {
params.add(getPlayer(user));
}
String[] split = String.join(" ", params).split("\\|");
String concName = String.join(" | ", split);
XYChart chart = new XYChartBuilder().width(800).height(600).title("Top PP plays for " + concName + " (" + modes.get(modeId) + ")").yAxisTitle("PP").xAxisTitle("Plays (100 = top)").build();
chart.getStyler().setDefaultSeriesRenderStyle(XYSeriesRenderStyle.Scatter);
chart.getStyler().setLegendPosition(LegendPosition.InsideSE);
chart.getStyler().setPlotContentSize(0.91);
chart.getStyler().setMarkerSize(7);
//chart.getStyler().setAxisTitlePadding(24);
chart.getStyler().setChartTitlePadding(12);
chart.getStyler().setChartPadding(12);
chart.getStyler().setChartBackgroundColor(new Color(200, 220, 230));
//ThreadLocalRandom ran = ThreadLocalRandom.current();
for(String name : split) {
List pps = new ArrayList<>();
try {
JSONArray array = getUserBest(name.trim(), modeId);
for(int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
pps.add(obj.getDouble("pp"));
}
} catch (IllegalArgumentException e) {
return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
}
Collections.reverse(pps);
/*XYSeries series =*/ chart.addSeries(name.trim() + "'s PP", pps);
//use series.setXYSeriesRenderStyle() for average line
//series.setMarkerColor(new Color(ran.nextInt(1, 255), ran.nextInt(1, 255), ran.nextInt(1, 255)));
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
BitmapEncoder.saveBitmap(chart, os, BitmapFormat.PNG);
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
channel.sendFile(new ByteArrayInputStream(os.toByteArray()), "ppchart.png").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "Please do not include so many players at once.");
}
}, "[mode] [username] [| usernames]...", Arrays.asList("", "taiko", "FlyingTuna | Rafis | despawningbone"),
"Get a graph with the users' top plays!", Arrays.asList(
" * You can specify up to 30 players at once, seperated by `|`."
+ " * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
registerSubCommand("set", Arrays.asList("s"), (channel, user, msg, words) -> {
int page = 1;
if (words.length < 1) {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter search words or an URL.");
}/* else if (words.length > 2) {
try {
page = Integer.parseInt(words[2]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Please enter a valid page number.");
}
}*/
String id = "", set = "";
try {
new URL(words[0]);
} catch (MalformedURLException e) {
//throw new IllgalArgumentException("Invalid URL."); //NOFIX: for some reason, discord seems to continue the typing after this has been sent //its because of queue()
try {
String[] split = String.join(" ", words).split("\\|");
String search = URLEncoder.encode(split[0].trim(), "UTF-8");
try {
if(split.length > 1) page = Integer.parseInt(split[1].trim());
} catch (NumberFormatException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter a valid page number.");
}
String url = "https://osusearch.com/query/?title=" + search + "&query_order=play_count&offset=0";
URLConnection stream;
stream = new URL(url).openConnection();
stream.addRequestProperty("User-Agent", "Mozilla/4.0");
JSONTokener tokener = new JSONTokener(stream.getInputStream());
id = String.valueOf(new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmapset_id"));
set = "https://osu.ppy.sh/beatmapset/" + id;
} catch (IOException | JSONException e1) {
if(e1 instanceof JSONException) {
return new CommandResult(CommandResultType.NORESULT);
} else {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
}
}
}
if(id.equals("")) {
set = words[0];
id = set.substring(set.lastIndexOf("/") + 1);
if (set.contains("/beatmapsets/")) {
id = set.substring(set.lastIndexOf("/beatmapsets/"));
id = id.substring(id.lastIndexOf("/") + 1);
} else {
if (set.contains("/b/") || set.contains("#")) {
return new CommandResult(CommandResultType.INVALIDARGS, "This is a specific beatmap, not a beatmap set.");
}
id = id.split("&")[0];
}
}
JSONTokener result = null;
InputStream stream = null;
URL url = null;
try {
url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id);
stream = url.openStream();
} catch (IOException ex) {
ex.printStackTrace();
}
result = new JSONTokener(stream);
JSONArray arr = new JSONArray(result);
List obj = new ArrayList();
for (int i = 0; i < arr.length(); i++) {
obj.add(arr.getJSONObject(i));
}
List fobj = obj.stream().sorted(Comparator.comparing(e -> e.getDouble("difficultyrating")))
.collect(Collectors.groupingBy(e -> e.getInt("mode"))).values().stream().flatMap(List::stream)
.collect(Collectors.toList());
EmbedBuilder em = new EmbedBuilder();
JSONObject fo;
try {
fo = fobj.get(0);
} catch (IndexOutOfBoundsException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap URL.");
}
if(page > Math.ceil((double) fobj.size() / 3)) {
return new CommandResult(CommandResultType.INVALIDARGS, "The beatmap set does not have this many difficulties!");
}
em.setAuthor("Beatmap set information of " + fo.getString("title"), set,
"https://b.ppy.sh/thumb/" + id + ".jpg");
em.setDescription("Author: " + fo.getString("artist"));
em.addField("BPM", fo.getString("bpm"), true);
em.addField("Creator", fo.getString("creator"), true);
em.addField("Approved/Ranked", (fo.getInt("approved") >= 1 ? "Yes" : "No"), true);
em.addBlankField(false);
int n = ((page - 1) * 3);
//String desc = "";
for (int i = n; (n + 3 <= fobj.size() ? i < n + 3 : i < fobj.size()); i++) {
JSONObject json = fobj.get(i);
String mode = modes.get(json.getInt("mode"));
/*desc += "**[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")** (" + mode + ")\n";
desc += df.format(json.getDouble("difficultyrating")) + "* " + (json.get("max_combo").toString() + "*").replaceAll("null*", "N/A") + "";*/
em.addField("Difficulty", "[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")" + " ([Preview](http://bloodcat.com/osu/preview.html#" + json.getString("beatmap_id") + "))", false);
/*em.addField("URL", "https://osu.ppy.sh/b/" + json.getString("beatmap_id"), true);
em.addBlankField(true);*/
em.addField("Max combo", json.get("max_combo").toString().replaceAll("null", "N/A"), true);
em.addField("Stars", df.format(json.getDouble("difficultyrating")), true);
em.addField("Mode", mode, true);
}
em.setFooter("Page: " + String.valueOf(page) + "/" + String.valueOf((int) Math.ceil((double) fobj.size() / 3)),
null);
channel.sendMessage(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, " [| page]\n", Arrays.asList("big black", "high free spirits | 2"),
"Check info about a beatmap set!", Arrays.asList(
" * Useful to get the specific difficulty URL for !desp osu pp."));
//TODO add userset for setting username to db so getPlayer wont be wrong?
registerSubCommand("user", Arrays.asList("u"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
String search = String.join(" ", params);
String id = "";
boolean noturl = false;
try {
new URL(search);
id = search.substring(search.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
id = search;
}
if(id.isEmpty()) {
id = getPlayer(user);
}
JSONTokener result = null;
InputStream stream = null;
URL url = null;
try {
String addparam = "";
if (noturl) {
addparam = "&type=string";
}
if (modeId != 0) {
addparam += ("&m=" + modeId);
}
url = new URL(
"https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam);
stream = url.openStream();
if (stream.available() < 4) {
return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
}
} catch (IOException ex) {
ex.printStackTrace();
}
result = new JSONTokener(stream);
JSONObject usr = new JSONArray(result).getJSONObject(0);
id = usr.getString("user_id"); //no matter if its url or not user_id is still gonna be the most accurate one
EmbedBuilder em = new EmbedBuilder();
em.setAuthor("User info of " + usr.getString("username") + " (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id);
String img = "https://a.ppy.sh/" + id;
try {
new URL(img).openStream();
} catch (IOException e) {
img = "https://s.ppy.sh/images/blank.jpg";
}
em.setThumbnail(img);
try {
em.addField("PP", usr.getDouble("pp_raw") == 0 ? "No Recent Plays" : usr.getString("pp_raw"), true);
em.addField("Accuracy", df.format(usr.getDouble("accuracy")), true);
} catch (JSONException e) {
return new CommandResult(CommandResultType.FAILURE, "This user has not played any map in " + modes.get(modeId) + " before.");
}
StringBuffer unibuff = new StringBuffer();
char[] ch = usr.getString("country").toLowerCase().toCharArray();
for (char c : ch) {
int temp = (int) c;
int temp_integer = 96; //for lower case
if (temp <= 122 & temp >= 97)
unibuff.append(Character.toChars(127461 + (temp - temp_integer)));
}
em.addField("Rank", (usr.getInt("pp_rank") == 0 ? "N/A" : "#" + usr.getString("pp_rank")) + " | Country #" + usr.getString("pp_country_rank"), true);
em.addField("Country", unibuff.toString(), true);
em.addField("Level", df.format(usr.getDouble("level")), true);
em.addField("Play count", usr.getString("playcount"), true);
em.addField("SS+", usr.getString("count_rank_ssh"), true);
em.addField("SS", usr.getString("count_rank_ss"), true);
double total = usr.getDouble("total_score");
em.addField("Total score", String.valueOf((long) total), true);
em.addField("S+", usr.getString("count_rank_sh"), true);
em.addField("S", usr.getString("count_rank_s"), true);
double ranked = usr.getDouble("ranked_score");
em.addField("Ranked score", String.valueOf((long) ranked) + " (" + df.format((ranked / total) * 100) + "%)", true);
em.setColor(new Color(66, 134, 244));
em.setFooter("300: " + usr.getString("count300") + " | 100: " + usr.getString("count100") + " | 50: "
+ usr.getString("count50"), null); //TODO display percentage?
channel.sendMessage(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[gamemode] [user URL|keyword]", Arrays.asList("", "despawningbone", "taiko despawningbone"),
"Check info about a user!", Arrays.asList(
" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
//DONE api demanding; need to add cooldown //nvm i changed it to use XHR now
//parts mergeable with getRecent()
registerSubCommand("topplays", Arrays.asList("top", "t"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params), page;
String[] split = String.join(" ", params).split("\\|");
String search = split[0];
try {
page = Integer.parseInt(split[1].trim());
if(page > 10) {
return new CommandResult(CommandResultType.INVALIDARGS, "You can only request your top 100 plays!");
}
} catch (NumberFormatException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid page number!");
} catch (ArrayIndexOutOfBoundsException e) {
page = 1; //its normal for people to not input index
}
String id = "";
boolean noturl = false;
try {
new URL(search);
id = search.substring(search.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
id = search;
}
if(id.isEmpty()) {
id = getPlayer(user);
}
JSONTokener result = null;
InputStream stream = null;
String name, addparam = "";
try {
if (noturl) {
addparam = "&type=string";
}
JSONObject userObj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id + addparam).openStream())).getJSONObject(0);
name = userObj.getString("username");
id = userObj.getString("user_id");
URLConnection con = new URL("https://osu.ppy.sh/users/" + id + "/scores/best?mode=" + (modeId == 0 ? "osu" : modeId == 1 ? "taiko" : modeId == 2 ? "fruits" : modeId == 3 ? "mania" : "osu") + "&offset=" + (page - 1) * 10 + "&limit=10").openConnection();
stream = con.getInputStream();
if (stream.available() < 4) {
return new CommandResult(CommandResultType.FAILURE, "This player does not have this many plays in this gamemode!");
}
} catch (IOException ex) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
} catch (JSONException ex) {
return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
}
result = new JSONTokener(stream);
JSONArray array = new JSONArray(result);
EmbedBuilder eb = new EmbedBuilder();
eb.setAuthor(name + "'s top plays (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
for(int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
String mods = obj.getJSONArray("mods").join("").replaceAll("\"", "");
/*bInfo = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + bId).openStream())).getJSONObject(0);
String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "** "
+ df.format(bInfo.getDouble("difficultyrating")) + "*"
+ (!mods.isEmpty() ? " **" + mods + "** " : " ")
+ " ([link](https://osu.ppy.sh/beatmap/" + bId + "))"
+ "\n" + (obj.getDouble("pp") + "pp (" + obj.getInt("count300") + "/" + obj.getInt("count100") + "/" + obj.getInt("count50") + "/" + obj.getInt("countmiss") + ")")
+ " " + (obj.getInt("perfect") == 1 ? "FC " + obj.getInt("maxcombo") + "x" : obj.getInt("maxcombo") + (!bInfo.isNull("max_combo") ? "/" + bInfo.getInt("max_combo") + "x" : "x"))
+ "\nPlayed on " + obj.getString("date");
eb.addField((i + 1) + ". " + bInfo.getString("artist") + " - " + bInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false); //no unicode unfortunately*/
JSONObject stats = obj.getJSONObject("statistics");
JSONObject bInfo = obj.getJSONObject("beatmap");
JSONObject bsInfo = obj.getJSONObject("beatmapset");
int bId = bInfo.getInt("id");
String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "**"
+ (!mods.isEmpty() ? " **" + mods + "** " : " ")
+ df.format(bInfo.getDouble("difficulty_rating")) + "*"
+ " ([map link](https://osu.ppy.sh/beatmaps/" + bId + "))"
+ "\nScore: " + obj.getInt("score")
+ "\n**" + (obj.getDouble("pp") + "pp** | Weighted " + df.format(obj.getJSONObject("weight").getDouble("pp")) + "pp (" + df.format(obj.getJSONObject("weight").getDouble("percentage")) + "%)"
+ "\n" + df.format(obj.getDouble("accuracy") * 100) + "% " + (obj.getBoolean("perfect") ? "FC " + obj.getInt("max_combo") + "x" : obj.getInt("max_combo") + "x") + " (" + stats.getInt("count_300") + "/" + stats.getInt("count_100") + "/" + stats.getInt("count_50") + "/" + stats.getInt("count_miss") + ")")
+ "\nPlayed on " + format.format(DatatypeConverter.parseDateTime(obj.getString("created_at")).getTime());
eb.addField(((page - 1) * 10 + (i + 1)) + ". " + bsInfo.getString("artist") + " - " + bsInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false); //no unicode unfortunately*/
}
eb.setFooter("Page: " + page, null);
eb.setColor(new Color(5, 255, 162));
} catch (JSONException e1) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
}
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[username] [|page]", Arrays.asList("", "| 2", "despawningbone", "-m despawningbone"),
"Get the top plays of a user!", Arrays.asList(
" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
/*registerSubCommand("search", Arrays.asList("s"), (channel, user, msg, words) -> { //need to deprecate google search //no longer works at all
String search = String.join(" ", Arrays.asList(words));
List> url = new ArrayList<>();
try {
url = GoogleSearch
.search(URLEncoder.encode("site:https://osu.ppy.sh/ " + search, "UTF-8"), 10);
} catch (IOException e1) {
e1.printStackTrace();
}
if (!url.isEmpty()) {
String list = "";
Stream> stream = url.stream();
if (words[0].equals("mapsearch")) {
stream = stream.filter(n -> n.getValue().contains("/b/") || n.getValue().contains("/s/")
|| n.getValue().contains("/beatmapsets/"));
} else if (words[0].equals("usersearch")) {
stream = stream.filter(n -> n.getValue().contains("/u/") || n.getValue().contains("/users/"));
}
list = "Search results for " + search + ":\n\n";
list += stream
.map(e -> (e.getKey() + "\n" + e.getValue() + "\n").replaceAll("", "").replaceAll("", ""))
.collect(Collectors.joining("\n"));
if (!list.isEmpty()) {
channel.sendMessage(list).queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.NORESULT);
}
} else {
return new CommandResult(CommandResultType.NORESULT);
}
}, "search|usersearch|mapsearch [keywords]", Arrays.asList("usersearch cookiezi", "search big black"),
"Search the osu website!", null);*/
registerSubCommand("compare", Arrays.asList("c", "comp"), (channel, user, msg, words) -> {
String map = "", name = "", img = "", mode = "";
int index = 1;
String[] param = String.join(" ", words).split("\\|");
try {
if(param.length > 1) index = Integer.parseInt(param[1].trim());
} catch(NumberFormatException e) {
channel.sendMessage("Invalid index specified. Defaulting to the most recent scorecard...").queue();
}
for(Message card : channel.getHistory().retrievePast(100).complete()) {
List embeds = card.getEmbeds();
if(embeds.size() > 0) {
MessageEmbed embed = embeds.get(0);
if(embed.getAuthor() == null) continue;
String author = embed.getAuthor().getName();
if(card.getAuthor().getId().equals(DiscordBot.BotID)) {
if(author.contains("most recent osu!")) {
map = embed.getUrl();
name = embed.getTitle().lastIndexOf("+") == -1 ? embed.getTitle().substring(0, embed.getTitle().lastIndexOf("(")) : embed.getTitle().substring(0, embed.getTitle().lastIndexOf("+"));
img = embed.getThumbnail().getUrl();
mode = author.split("most recent osu!")[1].split(" play")[0];
} else if(author.startsWith("PP information for")) {
map = embed.getAuthor().getUrl();
name = author.split("PP information for ")[1] + " [" +embed.getFields().get(3).getValue() + "]";
img = embed.getAuthor().getIconUrl();
mode = embed.getDescription().replace("Mode: osu!", "");
}
} else if(card.getAuthor().getId().equals("289066747443675143")) { //owobot support
if(author.startsWith("New") && author.contains("in osu!")) {
String markdown = embed.getDescription().substring(7, embed.getDescription().indexOf("\n")).trim();
map = markdown.substring(markdown.lastIndexOf("(") + 1, markdown.length() - 2);
name = markdown.substring(0, markdown.indexOf("__**]"));
img = embed.getThumbnail().getUrl();
mode = author.substring(author.lastIndexOf("in osu! "), author.length()).trim();
} else if(card.getContentDisplay().contains("Most Recent ")) {
map = embed.getAuthor().getUrl();
name = author.substring(0, author.lastIndexOf("+"));
img = embed.getThumbnail().getUrl();
mode = card.getContentDisplay().contains("Mania") ? "mania" : card.getContentDisplay().split("Most Recent ")[1].split(" ")[0]; //ye fucking blame owobot for being so inconsistent
} else if(card.getContentDisplay().contains("map(s).")) {
map = embed.getAuthor().getUrl();
map = map.substring(0, map.length() - (map.endsWith("/") ? 1 : 0)); //fucking hell the url actually changes according to how ppl trigger it
name = author.split(" – ")[1];
String[] split = embed.getFields().get(0).getName().split("__", 3);
name = name.substring(0, name.lastIndexOf(" by ")) + " [" + split[1] + "]";
img = "https://b.ppy.sh/thumb/" + embed.getDescription().split("\\[map\\]\\(https:\\/\\/osu\\.ppy\\.sh\\/d\\/", 2)[1].split("\\)", 2)[0] + ".jpg";
mode = split[2].isEmpty() ? "std" : split[2].substring(2, split[2].lastIndexOf("]") + 1);
}
}
if(index > 1) {
index--;
map = "";
}
if(!map.isEmpty()) break;
}
}
int modeId = 0;
if(mode.equalsIgnoreCase("taiko")) modeId = 1;
else if(mode.equalsIgnoreCase("catch") || mode.equals("ctb")) modeId = 2;
else if(mode.equalsIgnoreCase("mania")) modeId = 3;
if(map.isEmpty()) return new CommandResult(CommandResultType.FAILURE, "There are no recent map/score cards to compare with in the past 100 messages!");
String uid = param[0].isEmpty() ? getPlayer(user) : param[0].replaceAll(" ", "%20");
try {
URL url = new URL("https://osu.ppy.sh/api/get_scores?k=" + osuAPI + "&b=" + map.substring(map.lastIndexOf("/") + 1) + "&u=" + uid + "&m=" + modeId);
JSONArray arr = new JSONArray(new JSONTokener(url.openStream()));
EmbedBuilder eb = new EmbedBuilder();
eb.setTitle("Top plays for " + arr.getJSONObject(0).getString("username") + " on " + name);
eb.setColor(new Color(237, 154, 70));
eb.setThumbnail(img);
for(int i = 0; i < arr.length(); i++) {
JSONObject score = arr.getJSONObject(i);
String mods = Koohii.mods_str(score.getInt("enabled_mods"));
Accuracy acc = new Accuracy(score.getInt("count300"), score.getInt("count100"), score.getInt("count50"), score.getInt("countmiss"));
String info = "**`" + (mods.isEmpty() ? "No mods" : mods) + "` Score:\n"
+ score.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + " " + (score.isNull("pp") ? "No " : score.getDouble("pp")) + "pp** | Score **" + score.getInt("score") + "**\n"
+ df.format(acc.value() * 100) + "% " + (score.getInt("perfect") == 1 ? "FC " + score.getInt("maxcombo") + "x" : score.getInt("maxcombo") + "x") + " (" + acc.n300 + "/" + acc.n100 + "/" + acc.n50 + "/" + acc.nmisses + ")\n"
+ "Played on " + score.getString("date") + (score.getInt("replay_available") == 1 ? " (Replay available)" : "");
eb.appendDescription(info + "\n\n");
}
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
} catch (JSONException e) {
e.printStackTrace();
return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + uid + "` or the player has no scores on this map.");
}
}, "[username] [| index]", null, "Compare your score with the most recent map/score card in the channel!", Arrays.asList(" * Specify the index to skip to the nth recent score card in the channel."));
}
private static double weightForPP(double pp, String ibid, JSONArray userBest) {
//String[] dataset = test.split(" ");
//List datas = Arrays.asList(dataset).stream().map(s -> Double.parseDouble(s)).collect(Collectors.toList());
double net = 0, change = -1;
List pps = new ArrayList<>();
for(int i = 0; i < userBest.length(); i++) { //something like a treemap might work more elegantly, but i need to add pp to the list anyways so its fine i guess
//for(int i = 0; i < datas.size(); i++) {
JSONObject obj = userBest.getJSONObject(i);
double v = obj.getDouble("pp");
//double v = datas.get(i);
//System.out.println("raw" + v);
String bid = obj.getString("beatmap_id");
if(bid.equals(ibid)) {
if(v >= pp) {
return -1;
} else {
change = v;
}
}
if(v <= pp && !pps.contains(pp)) {
pps.add(pp);
}
pps.add(v);
}
if(pps.indexOf(pp) == -1) {
return 0;
}
//System.out.println(pps.indexOf(pp));
if(change == -1) {
double last = pps.size() > 100 ? pps.remove(100) * (Math.pow(0.95, 99)): 0;
for(int i = pps.indexOf(pp); i < pps.size(); i++) {
double c = pps.get(i);
double w = c*(Math.pow(0.95, i));
//System.out.println("pp" + w);
if(i == pps.indexOf(pp)) {
net += w;
} else {
net += c*(Math.pow(0.95, i - 1)) * (0.95 - 1);
}
//System.out.println("t1" + net);
}
//System.out.println("last" + last);
net -= last; //because the last one is completely not counted, not just shifted to a lower value
} else {
int old = pps.indexOf(change) - 1;
pps.remove(change);
for(int i = pps.indexOf(pp); i <= old; i++) {
double c = pps.get(i);
double w = c*(Math.pow(0.95, i));
if(c == pp) {
net += w - change*(Math.pow(0.95, old));
} else {
net += w * (0.95 - 1);
}
}
}
return net;
}
private static JSONArray getUserBest(String id, int mode) {
boolean noturl = false;
try {
new URL(id);
id = id.substring(id.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
}
JSONTokener result = null;
InputStream stream = null;
URL url = null;
try {
String addparam = "";
if (noturl) {
addparam = "&type=string";
}
//System.out.println("https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
url = new URL(
"https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
stream = url.openStream();
if (stream.available() < 4) {
throw new IllegalArgumentException("Unknown player `" + id + "`.");
}
} catch (IOException ex) {
ex.printStackTrace();
}
result = new JSONTokener(stream);
return new JSONArray(result);
}
private static PPv2 constructPP(double aim_stars, double speed_stars, Map b, int mods) {
PPv2Parameters p = new PPv2Parameters();
p.aim_stars = aim_stars;
p.speed_stars = speed_stars;
p.beatmap = b;
p.nobjects = b.objects.size();
p.mods = mods;
return new PPv2(p);
}
private static String getPlayer(User user) { //say im getting from presence/name?
try (Connection con = DiscordBot.db.getConnection()){
ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
String player = rs.next() ? player = rs.getString(1).substring(0, rs.getString(1).indexOf("||")) : user.getName();
rs.close();
return player;
} catch (SQLException e) {
return user.getName();
}
}
private static int getMode(List params) {
int modeId = 0; String mode = "";
params.replaceAll(String::toLowerCase);
if(params.contains("-t")) {
mode = "-t";
modeId = 1;
}
if(params.contains("-c")) {
mode = "-c";
modeId = 2;
}
if(params.contains("-m")) {
mode = "-m";
modeId = 3;
}
params.removeAll(Collections.singleton(mode)); //pass by ref so it removes from the list
return modeId;
}
private static InputStream getMap(String origURL) throws IOException {
String id = origURL.substring(origURL.lastIndexOf("/") + 1);
if (origURL.contains("/beatmapsets/")) {
if (!origURL.contains("#")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
} else {
if (origURL.contains("/s/")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
id = id.split("&")[0]; //remove things like &m=0, which is no need
}
URLConnection mapURL;
try {
mapURL = new URL("https://osu.ppy.sh/osu/" + id).openConnection();
mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");
} catch (IOException e) {
//e.printStackTrace();
throw new IllegalArgumentException("Invalid beatmap URL.");
}
try {
return mapURL.getInputStream();
} catch (IOException e) {
if(e.getMessage().contains("503")) { //most likely cloudflare, unless they change the request method which is very unlikely
mapURL = new URL("https://bloodcat.com/osu/b/" + id).openConnection();
mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");
return mapURL.getInputStream(); //not catching this, if even this doesnt work just throw something went wrong.
} else {
throw e;
}
}
}
}
diff --git a/src/me/despawningbone/discordbot/command/info/Calculator.java b/src/me/despawningbone/discordbot/command/info/Calculator.java
index 9f1ec83..b0fe808 100644
--- a/src/me/despawningbone/discordbot/command/info/Calculator.java
+++ b/src/me/despawningbone/discordbot/command/info/Calculator.java
@@ -1,115 +1,218 @@
package me.despawningbone.discordbot.command.info;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
import java.text.DecimalFormat;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.EmptyStackException;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import javax.imageio.ImageIO;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.math3.special.Gamma;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import com.neovisionaries.ws.client.WebSocket;
+import com.neovisionaries.ws.client.WebSocketAdapter;
+import com.neovisionaries.ws.client.WebSocketException;
+import com.neovisionaries.ws.client.WebSocketFactory;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.operator.Operator;
public class Calculator extends Command {
public Calculator() {
this.alias = Arrays.asList("calc");
this.desc = "Do some calculations!";
- this.usage = "";
+ this.usage = "[-w] ";
this.remarks = Arrays.asList("This calculator is using exp4j.",
"You can get the built in operators and functions at:", "http://projects.congrace.de/exp4j/",
- "`!` - factorials", "`logx(base, num)` - logarithm (base x)", "are supported too.");
+ "`!` - factorials", "`logx(base, num)` - logarithm (base x)", "are supported too.",
+ "\nYou can also query [Wolfram Alpha](https://www.wolframalpha.com/) using the `-w` switch.");
this.examples = Arrays.asList("3*4-2", "4 * (sin(3 - 5)) + 5!", "log(e) + logx(10, 100)");
}
Function logb = new Function("logx", 2) {
@Override
public double apply(double... args) {
return Math.log(args[1]) / Math.log(args[0]);
}
};
Function digamma = new Function("digamma", 1) {
@Override
public double apply(double... args) {
return Gamma.digamma(args[0]);
}
};
Operator factorial = new Operator("!", 2, true, Operator.PRECEDENCE_POWER + 1) {
@Override
public double apply(double... args) {
final long arg = (long) args[0];
if ((double) arg != args[0]) {
throw new IllegalArgumentException("Operand for factorial has to be an integer");
}
if (arg < 0) {
throw new IllegalArgumentException("The operand of the factorial can not " + "be " + "less than zero");
}
double result = 1;
for (int i = 1; i <= arg; i++) {
result *= i;
}
return result;
}
};
@Override
public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) {
- if (args.length < 1) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Please enter an operation.");
- }
- String splitted = String.join(" ", args);
- if (msg.getContentDisplay().toLowerCase().contains("the meaning of life, the universe, and everything")) { // easter egg
- channel.sendMessage("The answer is: `42`").queue();
- return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
- }
- if (args[0].equals("9+10") || args[0].equals("9 + 10")) { // easter egg
- channel.sendMessage("The answer is: `21`\n\n *i am smart ;)*").queue();
- return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
- }
- if (args[0].equals("666") || splitted.equals("333")) { // easter egg
- channel.sendMessage("No you don't").queue();
- return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
+ List params = new ArrayList<>(Arrays.asList(args));
+ if(params.removeAll(Collections.singleton("-w"))) {
+ return queryWolfram(String.join(" ", params), channel);
} else {
- String operation = String.join("", args);
- if (operation.contains("!")) {
- int index = operation.indexOf("!");
- while (index >= 0) { // indexOf returns -1 if no match found
- operation = new StringBuilder(operation).insert(index + 1, "(1)").toString();
- index = operation.indexOf("!", index + 1);
- }
+ if (args.length < 1) {
+ return new CommandResult(CommandResultType.INVALIDARGS, "Please enter an operation.");
}
- DecimalFormat format = new DecimalFormat();
- format.setDecimalSeparatorAlwaysShown(false);
- // System.out.println(operation);
- Expression e = null;
- String ans = null;
- double planckConstant = 6.62607004 * Math.pow(10, -34);
- double eulerMascheroni = 0.57721566490153286060651209008240243104215933593992;
- try {
- e = new ExpressionBuilder(operation).variable("h").variable("γ").function(digamma).function(logb).operator(factorial).build()
- .setVariable("h", planckConstant).setVariable("γ", eulerMascheroni);
- ans = Double.toString(e.evaluate());
- // String ans = format.format(e.evaluate());
- } catch (EmptyStackException e1) {
- return new CommandResult(CommandResultType.INVALIDARGS, "You have imbalanced parentheses.");
- } catch (ArithmeticException e1) {
- return new CommandResult(CommandResultType.INVALIDARGS, "You cannot divide by zero.");
- } catch (IllegalArgumentException e1) {
- return new CommandResult(CommandResultType.FAILURE, "An error has occured: " + e1.getMessage());
+ String splitted = String.join(" ", args);
+ if (msg.getContentDisplay().toLowerCase().contains("the meaning of life, the universe, and everything")) { // easter egg
+ channel.sendMessage("The answer is: `42`").queue();
+ return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
}
- if (ans.equals("NaN")) {
- ans = "Undefined";
+ if (splitted.equals("9+10") || splitted.equals("9 + 10")) { // easter egg
+ channel.sendMessage("The answer is: `21`\n\n *i am smart ;)*").queue();
+ return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
}
- channel.sendMessage("The answer is: `" + ans + "`").queue();
- return new CommandResult(CommandResultType.SUCCESS);
+ if (splitted.equals("666") || splitted.equals("333")) { // easter egg
+ channel.sendMessage("No you don't").queue();
+ return new CommandResult(CommandResultType.SUCCESS, "Executed easter egg");
+ } else {
+ String operation = String.join("", args);
+ if (operation.contains("!")) {
+ int index = operation.indexOf("!");
+ while (index >= 0) { // indexOf returns -1 if no match found
+ operation = new StringBuilder(operation).insert(index + 1, "(1)").toString();
+ index = operation.indexOf("!", index + 1);
+ }
+ }
+ DecimalFormat format = new DecimalFormat();
+ format.setDecimalSeparatorAlwaysShown(false);
+ // System.out.println(operation);
+ Expression e = null;
+ String ans = null;
+ double planckConstant = 6.62607004 * Math.pow(10, -34);
+ double eulerMascheroni = 0.57721566490153286060651209008240243104215933593992;
+ try {
+ e = new ExpressionBuilder(operation).variable("h").variable("γ").function(digamma).function(logb).operator(factorial).build()
+ .setVariable("h", planckConstant).setVariable("γ", eulerMascheroni);
+ ans = Double.toString(e.evaluate());
+ // String ans = format.format(e.evaluate());
+ } catch (EmptyStackException e1) {
+ return new CommandResult(CommandResultType.INVALIDARGS, "You have imbalanced parentheses.");
+ } catch (ArithmeticException e1) {
+ return new CommandResult(CommandResultType.INVALIDARGS, "You cannot divide by zero.");
+ } catch (IllegalArgumentException e1) {
+ return new CommandResult(CommandResultType.FAILURE, "An error has occured: " + e1.getMessage());
+ }
+ if (ans.equals("NaN")) {
+ ans = "Undefined";
+ }
+ channel.sendMessage("The answer is: `" + ans + "`").queue();
+ return new CommandResult(CommandResultType.SUCCESS);
+ }
+ }
+ }
+
+ public CommandResult queryWolfram(String operation, TextChannel channel) {
+ try {
+ CompletableFuture result = new CompletableFuture<>();
+ WebSocket socket = new WebSocketFactory().createSocket("wss://www.wolframalpha.com/n/v1/api/fetcher/results");
+ ArrayList pos = new ArrayList<>(); //prevent duplicate
+ socket.addListener(new WebSocketAdapter() {
+ @Override
+ public void onTextMessage(WebSocket websocket, String message) {
+ try {
+ //System.out.println(message);
+ JSONObject resp = new JSONObject(new JSONTokener(message));
+ switch(resp.getString("type")) {
+ case "pods":
+ for(Object obj : resp.getJSONArray("pods")) { //pods might have multiple values
+ JSONObject pod = (JSONObject) obj;
+
+ if(!pos.contains(pod.getInt("position"))) { //check dupe
+ //build big image for each pods
+ ArrayList images = new ArrayList<>();
+ int width = 0, height = 0;
+
+ if(!pod.has("subpods")) continue; //ignore empty pods that doesnt have image, usually fixed somewhere else
+
+ for(Object subobj : pod.getJSONArray("subpods")) {
+ JSONObject subpodImg = ((JSONObject) subobj).getJSONObject("img");
+ images.add(ImageIO.read(new URL(subpodImg.getString("src"))));
+
+ if(subpodImg.getInt("width") > width) width = subpodImg.getInt("width"); //get widest image and use it as final width
+ height += subpodImg.getInt("height"); //add all images
+ }
+
+ //create final image
+ BufferedImage podImg = new BufferedImage(width + 20, height + 20, BufferedImage.TYPE_INT_RGB); //padding
+ Graphics g = podImg.getGraphics();
+ g.setColor(new Color(255, 255, 255)); //fill as white first
+ g.fillRect(0, 0, width + 20, height + 20);
+
+ int y = 10;
+ for(BufferedImage img : images) {
+ g.drawImage(img, 10, y, null);
+ y += img.getHeight();
+ }
+
+ //send each pod as an individual message
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ImageIO.write(podImg, "png", os);
+ channel.sendMessage(pod.getString("title") + ":")
+ .addFile(new ByteArrayInputStream(os.toByteArray()), "result.png").queue();
+
+ pos.add(pod.getInt("position")); //update to prevent dupes
+ }
+ }
+ break;
+ case "didyoumean":
+ result.complete(new CommandResult(CommandResultType.INVALIDARGS, "No results found :cry:\nDid you mean `" + resp.getJSONArray("didyoumean").getJSONObject(0).getString("val") + "`?"));
+ break;
+ case "queryComplete":
+ websocket.disconnect();
+ result.complete(new CommandResult(CommandResultType.SUCCESS)); //if its preceded by anything it wouldnt update; thats how complete() works
+ break;
+ }
+ } catch(Exception e) {
+ result.complete(new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)));
+ }
+ }
+ });
+ //System.out.println("{\"type\":\"init\",\"lang\":\"en\",\"exp\":" + System.currentTimeMillis() + ",\"displayDebuggingInfo\":false,\"messages\":[{\"type\":\"newQuery\",\"locationId\":\"hipuj\",\"language\":\"en\",\"displayDebuggingInfo\":false,\"yellowIsError\":false,\"input\":\"" + operation + "\",\"assumption\":[],\"file\":null}],\"input\":\"" + operation + "\",\"assumption\":[],\"file\":null}");
+ socket.connect();
+ socket.sendText("{\"type\":\"init\",\"lang\":\"en\",\"exp\":" + System.currentTimeMillis() + ",\"displayDebuggingInfo\":false,\"messages\":[{\"type\":\"newQuery\",\"locationId\":\"hipuj\",\"language\":\"en\",\"displayDebuggingInfo\":false,\"yellowIsError\":false,\"input\":\"" + operation + "\",\"assumption\":[],\"file\":null}],\"input\":\"" + operation + "\",\"assumption\":[],\"file\":null}");
+ return result.get();
+ } catch (IOException | WebSocketException | InterruptedException | ExecutionException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
}
}
diff --git a/src/me/despawningbone/discordbot/command/misc/Choose.java b/src/me/despawningbone/discordbot/command/misc/Choose.java
index a881b6c..652d408 100644
--- a/src/me/despawningbone/discordbot/command/misc/Choose.java
+++ b/src/me/despawningbone/discordbot/command/misc/Choose.java
@@ -1,45 +1,41 @@
package me.despawningbone.discordbot.command.misc;
import java.util.Arrays;
import java.util.Random;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.utils.MarkdownSanitizer;
public class Choose extends Command {
public Choose() {
this.desc = "Let me choose the best for you!";
this.usage = " | [...]";
this.examples = Arrays.asList("eggs | ham | sausage | tomato");
}
@Override //tags might get echoed if someone did "markdown injection" lmao //nvm fixed
public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) {
if (args.length < 1) {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter something for me to choose from lol");
}
- //String stripped = String.join(" ", args);
- String stripped = msg.getContentStripped().replaceAll("\\s\\s+", " ").split(" ", 3)[2];
- if (stripped.contains("|")) {
- String[] split = stripped.split(" *\\| *");
- Random randomno = new Random();
- //System.out.println(split.length);
- if (split.length < 2 || (split.length == 2 && (split[0].isEmpty() || split[1].isEmpty()))) {
- return new CommandResult(CommandResultType.INVALIDARGS, "At least put 2 choices for me to choose from lol");
- } else if (Arrays.asList(split).contains("")) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Please do not input empty choices.");
- }
- int ran = randomno.nextInt(split.length);
- channel.sendMessage("<@!" + author.getId() + ">: If I were you, I would have chosen `" + split[ran]
- + "` :stuck_out_tongue:").queue();
- return new CommandResult(CommandResultType.SUCCESS);
- } else {
+ String stripped = MarkdownSanitizer.sanitize(String.join(" ", args));
+ String[] split = stripped.split(" *\\| *");
+ Random randomno = new Random();
+ //System.out.println(split.length);
+ if (split.length < 2 || (split.length == 2 && (split[0].isEmpty() || split[1].isEmpty()))) {
return new CommandResult(CommandResultType.INVALIDARGS, "At least put 2 choices for me to choose from lol");
+ } else if (Arrays.asList(split).contains("")) {
+ return new CommandResult(CommandResultType.INVALIDARGS, "Please do not input empty choices.");
}
+ int ran = randomno.nextInt(split.length);
+ channel.sendMessage("<@!" + author.getId() + ">: If I were you, I would have chosen `" + split[ran]
+ + "` :stuck_out_tongue:").queue();
+ return new CommandResult(CommandResultType.SUCCESS);
}
}