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 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); } }