diff --git a/src/me/despawningbone/discordbot/command/anime/Waifu.java b/src/me/despawningbone/discordbot/command/anime/Waifu.java index 9bb0b3a..029f7d9 100644 --- a/src/me/despawningbone/discordbot/command/anime/Waifu.java +++ b/src/me/despawningbone/discordbot/command/anime/Waifu.java @@ -1,182 +1,186 @@ package me.despawningbone.discordbot.command.anime; import java.awt.Color; import java.io.IOException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; 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.jsoup.Connection.Response; import org.jsoup.Jsoup; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Waifu extends Command { public Waifu() { this.desc = "Find information about your waifu/husbando!"; //, or leave it blank for a random one!"; this.usage = " [| index]"; this.examples = Arrays.asList("neptune", "ryuZU"); this.alias = Arrays.asList("husbando"); } @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { channel.sendTyping().queue(); String[] stripped = String.join(" ", args).split("\\|"); if(args.length < 1) return new CommandResult(CommandResultType.FAILURE, "Please enter something to search for!"); - JSONObject main; + JSONObject main, props; try { //get required cookies - Response con = Jsoup.connect("https://mywaifulist.moe/dash/").execute(); - String cookie = String.join(";", con.header("Set-Cookie").split("; expires.*?,.*?,")).split("; expires")[0]; - String csrf = con.parse().selectFirst("meta[name=\"csrf-token\"]").attr("content"); - String res = Jsoup.connect("https://mywaifulist.moe/api/waifu/search") //advancedsearch is no longer a thing - .userAgent("Mozilla/4.0") - .header("Cookie", cookie) - .header("X-CSRF-Token", csrf) + //Mozilla/4.0 actually bypasses the need for CSRF, but breaks /api/search/all since it requires X-Requested-With (/browse?name= still works but doesn't handle multiple types at once) + Response con = Jsoup.connect("https://mywaifulist.moe/").userAgent("Mozilla/5.0").execute(); + String res = Jsoup.connect("https://mywaifulist.moe/api/search/all") //advancedsearch is no longer a thing + .userAgent("Mozilla/5.0") .header("X-Requested-With", "XMLHttpRequest") - .header("Accept", "application/json, text/plain, */*") - .header("Content-Type", "application/json;charset=utf-8") - .followRedirects(true).ignoreContentType(true) + .header("X-XSRF-TOKEN", URLDecoder.decode(con.cookie("XSRF-TOKEN"), "UTF-8")) //base64 might be encoded - if it is, then laravel will return 419 page expired + .header("Content-Type", "application/json") //needed or the endpoint can't parse the query json + .header("Referer", "https://mywaifulist.moe/") //needed to avoid internal server error for some reason + .cookies(con.cookies()) + .ignoreContentType(true) .requestBody("{\"query\":\"" + stripped[0].trim() + "\"}").post().text(); //check index int index = 0; JSONArray jarr = new JSONArray(new JSONTokener(res)); try { //System.out.println(arr.length()); if(stripped.length > 1) index = Integer.parseInt(stripped[1].trim()) - 1; if(jarr.length() <= index && index != 0) throw new NumberFormatException(); //if set index is out of bounds } catch (NumberFormatException e) { channel.sendMessage("Invalid index inputted. Defaulting to first result.").queue(); channel.sendTyping().queue(); index = 0; } //sort search results for fetching with specified index Pattern wholeWord = Pattern.compile("(?i).*\\b" + stripped[0].trim() + "\\b.*"); List arr = new ArrayList<>(); for(Object obj : jarr) arr.add((JSONObject) obj); - arr = arr.stream().filter(o -> !o.isNull("type") && (o.getString("type").equalsIgnoreCase("waifu") || o.getString("type").equalsIgnoreCase("husbando"))) //filter only characters + arr = arr.stream().filter(o -> !o.isNull("entity_type") && (o.getString("entity_type").equalsIgnoreCase("waifu") || o.getString("entity_type").equalsIgnoreCase("husbando"))) //filter only characters .sorted((a, b) -> Integer.compare( //combine likes and trash to get popularity - sort by popularity since the search result sucks ass (b.has("likes") ? b.getInt("likes") : 0) + (b.has("trash") ? b.getInt("trash") : 0), (a.has("likes") ? a.getInt("likes") : 0) + (a.has("trash") ? a.getInt("trash") : 0))) .sorted((a, b) -> wholeWord.matcher(a.getString("name")).matches() && !wholeWord.matcher(b.getString("name")).matches() ? -1 : 0) //move whole word matches up only if last one was not matched .collect(Collectors.toList()); //fetch - main = new JSONObject(new JSONTokener(Jsoup.connect("https://mywaifulist.moe/api/waifu/" + arr.get(index).getInt("id")) + props = new JSONObject(new JSONTokener(Jsoup.connect("https://mywaifulist.moe/waifu/" + arr.get(index).getString("slug")) .userAgent("Mozilla/4.0") - .header("Cookie", cookie) - .header("X-CSRF-Token", csrf) - .header("X-Requested-With", "XMLHttpRequest") - .followRedirects(true).ignoreContentType(true).get().text())).getJSONObject("data"); - } catch (IndexOutOfBoundsException e) { - //e.printStackTrace(); + .followRedirects(true) + .get().selectFirst("#app").attr("data-page"))).getJSONObject("props"); + main = props.getJSONObject("waifu"); + } catch (IndexOutOfBoundsException | JSONException e) { + e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); } catch (IOException e) { if(e.toString().contains("Status=422")) return new CommandResult(CommandResultType.NORESULT); //search scope too broad(?) return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } EmbedBuilder em = new EmbedBuilder(); em.setColor(new Color(44, 62, 80)); em.setTitle((main.getBoolean("husbando") ? "Husbando" : "Waifu") + " info of " + main.getString("name"), "https://mywaifulist.moe/waifu/" + main.getString("slug")); //series - JSONObject series = main.getJSONObject("series"); + JSONObject series = main.getJSONArray("appearances").getJSONObject(0); em.setAuthor("Series: " + series.getString("name"), "https://mywaifulist.moe/series/" + series.getString("slug")); - if(!main.getJSONObject("series").isNull("display_picture") && !main.getJSONObject("series").getString("display_picture").isEmpty()) - em.setThumbnail(main.getJSONObject("series").getString("display_picture")); + if(!series.isNull("display_picture") && !series.getString("display_picture").isEmpty()) + em.setThumbnail(series.getString("display_picture")); try { em.setDescription(main.getString("description").substring(0, Math.min(main.getString("description").length(), 2040)) + (main.getString("description").length() > 2040 ? "..." : "")); } catch (IllegalArgumentException e) { return new CommandResult(CommandResultType.TOOLONG); } em.setImage(main.getString("display_picture").replaceAll("\\\\", "")); //series appearance + series description ArrayList appearances = new ArrayList<>(); int totalName = 0; for(Object obj : main.getJSONArray("appearances")) { JSONObject jobj = ((JSONObject) obj); appearances.add(jobj); totalName += jobj.getString("name").length() + jobj.getString("slug").length() + 43; //magic number for giving enough leeway } final int avg = (1024 - totalName) / appearances.size(); //get max description average length em.addField("Appearances", appearances.stream().map(jobj -> "[" + jobj.getString("name") + "](https://mywaifulist.moe/series/" + jobj.getString("slug") + (!jobj.isNull("description") ? (" \"" + jobj.getString("description").substring(0, Math.min(jobj.getString("description").length(), avg)).replaceAll("\"", "”")) //trim desc to max length, replacing double quotes since it will interfere with markdown + (jobj.getString("description").length() > avg ? "..." : "") + "\")" : ")")) //append ... if its not finished (only if desc is non null will desc be printed) .collect(Collectors.joining(", ")), false); //aliases if(!main.isNull("original_name") && !main.getString("original_name").isEmpty()) em.addField("Also known as", main.getString("original_name") + (main.isNull("romaji_name") || main.getString("romaji_name").isEmpty() ? "" : ", " + main.getString("romaji_name")), false); em.addBlankField(false); //optionally existing info if (!main.isNull("origin")) em.addField("Origin", main.getString("origin"), true); if (!main.isNull("height")) em.addField("Height", String.valueOf(main.getDouble("height")), true); if (!main.isNull("weight")) em.addField("Weight", String.valueOf(main.getDouble("weight")), true); if (!main.isNull("bust")) em.addField("Bust", String.valueOf(main.getDouble("bust")), true); if (!main.isNull("hip")) em.addField("Hip", String.valueOf(main.getDouble("hip")), true); if (!main.isNull("waist")) em.addField("Waist", String.valueOf(main.getDouble("waist")), true); if (!main.isNull("blood_type")) em.addField("Blood type", main.getString("blood_type"), true); if (!main.isNull("birthday_day") && main.getInt("birthday_day") != 0 && !main.isNull("birthday_month") && !main.getString("birthday_month").isEmpty()) em.addField("Birthday", main.getString("birthday_month") + " " + String.valueOf(main.getInt("birthday_day")) + (main.isNull("birthday_year") || main.getString("birthday_year").isEmpty() ? "" : ", " + main.getString("birthday_year")), true); //only add blank on 5 fields for formatting if (em.getFields().size() == 5) { em.addBlankField(true); } //tags is back! (for some entries at least) - String tags = main.getJSONArray("tags").toList().stream().map(o -> o.toString().replaceAll("\\{name=", "").replaceAll(", id=.*\\}", "")).collect(Collectors.joining(", ")); + String tags = props.getJSONArray("tags").toList().stream().map(o -> o.toString().replaceAll("\\{name=", "").replaceAll(", id=.*\\}", "")).collect(Collectors.joining(", ")); if(!tags.isEmpty()) em.addField("Tags", tags, false); //popularity stats if(em.getFields().size() > 2) em.addBlankField(false); em.addField("Likes", main.getInt("likes") + " (#" + (main.isNull("like_rank") ? "N/A" : main.getInt("like_rank")) + ")", true); em.addField("Popularity rank", "#" + (main.isNull("popularity_rank") ? "N/A" : main.getInt("popularity_rank")), true); em.addField("Trash", main.getInt("trash") + " (#" + (main.isNull("trash_rank") ? "N/A" : main.getInt("trash_rank")) + ")", true); - em.setFooter("Created by " + main.getJSONObject("creator").getString("name") + " | MyWaifuList.moe", null); + //creator name no longer exists + em.setFooter("Created at " + DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneId.of("Z")).format(Instant.parse(main.getString("created_at"))) + " | MyWaifuList.moe", null); channel.sendMessageEmbeds(em.build()).queue(); return new CommandResult(CommandResultType.SUCCESS); } -} +} \ No newline at end of file diff --git a/src/me/despawningbone/discordbot/command/games/Osu.java b/src/me/despawningbone/discordbot/command/games/Osu.java index 3619d56..5526fbc 100644 --- a/src/me/despawningbone/discordbot/command/games/Osu.java +++ b/src/me/despawningbone/discordbot/command/games/Osu.java @@ -1,1159 +1,1159 @@ 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.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; 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.PlayParameters; 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 static final String osuAPI = DiscordBot.tokens.getProperty("osu"); private static final DecimalFormat df = new DecimalFormat("#.##"); private static final String[] modes = new String[] {"osu!", "osu!taiko", "osu!catch", "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) -> { List amend = new ArrayList(Arrays.asList(words)); int wParamIndex = amend.indexOf("-w"); //parse weight param and uid; doesnt work if -w is immediately followed by pp params like 100x etc String uid = null; boolean weight = wParamIndex != -1; if(weight) { try { int wParamLength = 1; uid = amend.get(wParamIndex + 1); if(!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://"))) { //url thats not user url means its most likely a beatmap, aka no username param uid = getPlayer(user); } else { wParamLength = 2; //has username } amend.subList(wParamIndex, wParamIndex + wParamLength).clear(); //remove } catch (IndexOutOfBoundsException e) { uid = getPlayer(user); } } String initmap; try { initmap = amend.get(0); } catch (IndexOutOfBoundsException e) { initmap = "null"; //dud } CompletableFuture msgId = new CompletableFuture<>(); //check if no map input, use discord rich presence if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) { //get map name from status 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 -> msgId.complete(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(); //parse map name and search 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); also breaks if difficulty has " in it 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 } } else { return new CommandResult(CommandResultType.FAILURE, "There is no account of your rich presence, therefore I cannot get the beatmap from your status."); } } //if still dud aka no presence nor url if(initmap.equals("null")) { //shouldnt throw at all return new CommandResult(CommandResultType.FAILURE, "You haven't played any maps I can recognize yet!"); } //parse beatmap Map beatmap = null; try { 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 (beatmap.title.isEmpty()) { return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap."); } //parse pp params double dacc = 100; int combo = beatmap.max_combo(), mods = 0, miss = 0; try { for (int i = 0; i < amend.size(); i++) { String param = amend.get(i); if (param.startsWith("+")) { mods = Koohii.mods_from_str(param.substring(1).toUpperCase()); //invalid mods just get ignored } else if (param.toLowerCase().endsWith("m")) { miss = Integer.parseInt(param.substring(0, param.length() - 1)); if(miss < 0 || miss > beatmap.objects.size()) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid miss count specified."); } else if (param.endsWith("%")) { dacc = Double.parseDouble(param.substring(0, param.length() - 1)); if(dacc < 0) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid accuracy specified."); } else if (param.toLowerCase().endsWith("x")) { combo = Integer.parseInt(param.substring(0, param.length() - 1)); if(combo < 0 || combo > beatmap.max_combo()) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid combo specified."); } } } catch (NumberFormatException e) { return new CommandResult(CommandResultType.INVALIDARGS, "Invalid params specified."); } //compute pp Accuracy acc = null; JSONObject jbm = null; try { acc = new Accuracy(dacc, miss, beatmap); jbm = computePP(beatmap, initmap.substring(initmap.lastIndexOf("/") + 1).split("&")[0], mods, acc.n50, acc.n100, acc.n300, miss, combo); } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch (IllegalArgumentException e) { return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage()); } //build embed EmbedBuilder eb = new EmbedBuilder(); if(jbm.has("source") && !jbm.getString("source").isEmpty()) eb.setTitle("Source: " + jbm.getString("source")); eb.setDescription("Mode: " + modes[beatmap.mode]); eb.setAuthor("PP information for " + (beatmap.title_unicode.isEmpty() ? beatmap.title : beatmap.title_unicode), initmap, "https://b.ppy.sh/thumb/" + jbm.getString("beatmapset_id") + ".jpg"); eb.addField("Artist", (beatmap.artist_unicode.isEmpty() ? beatmap.artist : beatmap.artist_unicode), true); eb.addField("Created by", beatmap.creator, true); String totaldur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("total_length") / jbm.getDouble("speed")))); String draindur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("hit_length") / jbm.getDouble("speed")))); eb.addField("Duration", totaldur + " | Drain " + draindur, true); eb.addField("Difficulty", beatmap.version, true); eb.addField("Mods", (mods == 0 ? "None" : Koohii.mods_str(mods)), true); eb.addField("BPM", df.format(jbm.getDouble("bpm") * jbm.getDouble("speed")), true); eb.addField("Accuracy", df.format(jbm.getDouble("accVal") * 100) + "%", true); eb.addField("Combo", String.valueOf(combo) + "x", true); eb.addField("Misses", String.valueOf(miss), true); eb.addField("300", String.valueOf(acc.n300), true); eb.addField("100", String.valueOf(acc.n100), true); eb.addField("50", String.valueOf(acc.n50), true); eb.addField("PP", df.format(jbm.getDouble("ppVal")), true); if(weight) { try { JSONObject res = getPlayerData("get_user_best", uid, beatmap.mode, user); if(res.has("result")) { String gain = df.format(weightForPP(jbm.getDouble("ppVal"), jbm.getString("beatmap_id"), res.getJSONArray("result"))); eb.addField("Actual pp gain for " + uid, (gain.equals("-1") ? "User has a better score than this" : gain + "pp"), true); } else { channel.sendMessage("Unknown user `" + res.getString("search") + "` specified for pp checking, ignoring...").queue(); } } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } eb.setFooter("Stars: " + df.format(jbm.getDouble("starsVal")) + " | CS: " + df.format(jbm.getDouble("cs")) + " | HP: " + df.format(jbm.getDouble("hp")) + " | AR: " + df.format(jbm.getDouble("ar")) + " | OD: " + df.format(jbm.getDouble("od")), null); eb.setColor(new Color(239, 109, 167)); //remove retrieve msg if sent if(msgId.isDone()) { msgId.thenAccept(m -> channel.deleteMessageById(m).queue()); } channel.sendMessageEmbeds(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) -> { List params = new ArrayList<>(Arrays.asList(words)); int modeId = getMode(params); final double precision = 1e-4, iterations = 500; //get user 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); } //get user top JSONArray arr = null; try { JSONObject res = getPlayerData("get_user_best", uid.trim(), modeId, user); if(!res.has("result")) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`."); arr = res.getJSONArray("result"); } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } double limit = arr.getJSONObject(0).getDouble("pp") + 100; //fricc those who wanna get pp over 100pp more than they can lmao //perform bisection search try { double targetPP = params.size() > index ? Double.parseDouble(params.get(index)) : 1; int i = 0; double minPP = 0, maxPP = limit, mid = 0; while(i < iterations && Math.abs(maxPP - minPP) > precision) { mid = (maxPP + minPP) / 2; double temp = weightForPP(mid, null, arr); if(temp > targetPP) { maxPP = mid; } else { minPP = mid; } i++; } if(!df.format(mid).equals(df.format(limit))) { channel.sendMessage("For " + uid + " to actually gain " + df.format(targetPP) + "pp in " + modes[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."); } }, "[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).")); //DONE NEED EXTREME TIDYING UP //DONE 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; //ignore unsupported modes and default to standard 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) //parse params String[] split = String.join(" ", params).split("\\|"); 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!"); } //fetch recent plays JSONArray array; String name; try { JSONObject res = getPlayerData("get_user_recent", search, modeId, user); if(!res.has("result")) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "` or the player has not been playing in the last 24h."); array = res.getJSONArray("result"); if(array.length() == 0) return new CommandResult(CommandResultType.FAILURE, "You have no recent plays in this 24h!"); //set name according to supported formats name = res.getBoolean("isId") ? //isId might return true on cases inputted as https://osu.ppy.sh/users/despawningbone for example, which would make the fetching redundant but still works getPlayerData("get_user", search, 0, user).getJSONArray("result").getJSONObject(0).getString("username"): res.getString("search"); } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } //get which play to return 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 bid = mostRecent.getString("beatmap_id"); //get consecutive tries int i; for(i = nrecent + 1; i < array.length(); i++) { if(!array.getJSONObject(i).getString("beatmap_id").equals(bid)) { break; } } //if we reached the end of the list, there might be more that we missed since we can only fetch 50 recent plays String more = ""; if(i == array.length()) { more = " (or more)"; } //parse map of the recent play and compute pp int n50 = mostRecent.getInt("count50"), n100 = mostRecent.getInt("count100"), n300 = mostRecent.getInt("count300"), miss = mostRecent.getInt("countmiss"), combo = mostRecent.getInt("maxcombo"), mods = mostRecent.getInt("enabled_mods"); Map beatmap = null; JSONObject jbm = null; try { beatmap = new Koohii.Parser() .map(new BufferedReader(new InputStreamReader(getMap(bid), "UTF-8"))); jbm = computePP(beatmap, bid, mods, n50, n100, n300, miss, combo); } 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()); } //build embed String modStr = Koohii.mods_str(mods); EmbedBuilder eb = new EmbedBuilder(); eb.setColor(new Color(0, 0, 0)); eb.setTitle((beatmap.artist_unicode.isEmpty() ? beatmap.artist : beatmap.artist_unicode) + " - " + (beatmap.title_unicode.isEmpty() ? beatmap.title : beatmap.title_unicode) + " [" + beatmap.version + "]" + (modStr.isEmpty() ? "" : " +" + modStr) + " (" + df.format(jbm.getDouble("starsVal")) + "*)" , "https://osu.ppy.sh/beatmaps/" + bid); eb.setAuthor(MiscUtils.ordinal(nrecent + 1) + " most recent " + modes[modeId] + " play by " + name, "https://osu.ppy.sh/users/" + mostRecent.getString("user_id"), "https://a.ppy.sh/" + mostRecent.getString("user_id")); eb.setDescription(MiscUtils.ordinal(i - nrecent) + more + " consecutive try\n" + "Ranking: " + mostRecent.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + (mostRecent.getString("rank").equals("F") ? " (Estimated completion percentage: " + df.format((n50 + n100 + n300 + miss) * 100.0 / beatmap.objects.size()) + "%)" : "")); eb.setThumbnail("https://b.ppy.sh/thumb/" + jbm.getString("beatmapset_id") + ".jpg"); eb.addField("Score", String.valueOf(mostRecent.getInt("score")), true); eb.addField("Accuracy", df.format(jbm.getDouble("accVal") * 100) + " (" + n300 + "/" + n100 + "/" + n50 + "/" + miss + ")", true); eb.addField("Combo", (mostRecent.getInt("perfect") == 0 ? combo + "x / " + beatmap.max_combo() + "x" : "FC (" + combo + "x)"), true); eb.addField("PP", df.format(jbm.getDouble("ppVal")) + "pp / " + df.format(jbm.getDouble("ppMax")) + "pp", true); eb.setFooter("Score submitted", null); eb.setTimestamp(OffsetDateTime.parse(mostRecent.getString("date").replace(" ", "T") + "+00:00")); channel.sendMessageEmbeds(eb.build()).queue(); return new CommandResult(CommandResultType.SUCCESS); }, "[-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).")); //DONE 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)); } //get all usernames String[] split = String.join(" ", params).split("\\|"); String concName = String.join(" | ", split); //init chart XYChart chart = new XYChartBuilder().width(800).height(600).title("Top PP plays for " + concName + " (" + modes[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().setChartTitlePadding(12); chart.getStyler().setChartPadding(12); chart.getStyler().setChartBackgroundColor(new Color(200, 220, 230)); //plot each user's top plays onto chart for(String name : split) { List pps = new ArrayList<>(); try { JSONObject res = getPlayerData("get_user_best", name.trim(), modeId, user); if(!res.has("result")) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`."); JSONArray array = res.getJSONArray("result"); for(int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); pps.add(obj.getDouble("pp")); } } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } Collections.reverse(pps); //so it orders from low to high left to right chart.addSeries(name.trim() + "'s PP", pps); } //write to png 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"), + }, "[mode] [username] [| usernames]...", Arrays.asList("", "-t", "FlyingTuna | Rafis | despawningbone"), "Get a graph with the users' top plays!", Arrays.asList( - " * You can specify up to 30 players at once, seperated by `|`." + " * You can specify up to 30 players at once, seperated by `|`.\n" + " * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard).")); //TODO reaction based paging? 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."); } String id = "", set = ""; try { new URL(words[0]); } catch (MalformedURLException e) { //if not url, search 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 = 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 e1) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1)); } catch (JSONException e1) { return new CommandResult(CommandResultType.NORESULT); } } //empty id means words[0] was a valid url - accept and parse 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 { //old urls if (set.contains("/b/") || set.contains("#")) { return new CommandResult(CommandResultType.INVALIDARGS, "This is a specific beatmap, not a beatmap set."); } id = id.split("&")[0]; } } //get beatmap set info InputStream stream = null; try { stream = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id).openStream(); } catch (IOException ex) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex)); } JSONArray arr = new JSONArray(new JSONTokener(stream)); //convert it to a list of object for streaming List obj = new ArrayList(); for (int i = 0; i < arr.length(); i++) { obj.add(arr.getJSONObject(i)); } //group by mode sorted by difficulty and flatten back to a list 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; //use first object's metadata for universal info like author try { fo = fobj.get(0); } catch (IndexOutOfBoundsException e) { return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap URL."); } if(page > Math.ceil((double) fobj.size() / 3)) { //each page is 3 entries return new CommandResult(CommandResultType.INVALIDARGS, "The beatmap set does not have this many difficulties!"); } //build embed page heading 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); //build each entry int n = ((page - 1) * 3); for (int i = n; (n + 3 <= fobj.size() ? i < n + 3 : i < fobj.size()); i++) { JSONObject json = fobj.get(i); em.addField("Difficulty", "[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")" + " ([Preview](https://jmir.xyz/osu/#" + json.getString("beatmap_id") + "))", false); //bloodcat is no longer a thing but someone hosted a copy for previewing here 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", modes[json.getInt("mode")], true); } //build footer and send em.setFooter("Page: " + String.valueOf(page) + "/" + String.valueOf((int) Math.ceil((double) fobj.size() / 3)), null); channel.sendMessageEmbeds(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); //fetch user data JSONObject usr; try { JSONObject res = getPlayerData("get_user", search, modeId, user); if(!res.has("result")) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`."); usr = res.getJSONArray("result").getJSONObject(0); } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } String id = usr.getString("user_id"); //no matter if its url or not user_id is still gonna be the most accurate one //build embed EmbedBuilder em = new EmbedBuilder(); em.setAuthor("User info of " + usr.getString("username") + " (" + modes[modeId] + ")", "https://osu.ppy.sh/users/" + id); //check if pfp exists, if not fallback to blank 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); //if user doesnt have pp, there wont be any data pretty much - also might be a result of inactivity (?) if(usr.isNull("pp_raw")) return new CommandResult(CommandResultType.FAILURE, "This user has not played any map in " + modes[modeId] + " before."); em.appendDescription("Joined " + usr.getString("join_date")); 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); em.addField("Rank", (usr.getInt("pp_rank") == 0 ? "N/A" : "#" + usr.getString("pp_rank")) + " | " + MiscUtils.countryNameToUnicode(usr.getString("country")) + " #" + usr.getString("pp_country_rank"), true); em.addField("Play count", usr.getString("playcount") + " (" + df.format(Long.parseLong(usr.getString("total_seconds_played")) / 3600.0) + "h)", true); em.addField("Total score", usr.getString("total_score"), true); em.addField("Ranked score", usr.getString("ranked_score") + " (" + df.format((usr.getDouble("ranked_score") / usr.getDouble("total_score")) * 100) + "%)", true); em.addField("Level", df.format(usr.getDouble("level")), true); em.addField("S+", usr.getString("count_rank_sh"), true); em.addField("SS+", usr.getString("count_rank_ssh"), true); em.addField("A", usr.getString("count_rank_a"), true); em.addField("S", usr.getString("count_rank_s"), true); em.addField("SS", usr.getString("count_rank_ss"), 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.sendMessageEmbeds(em.build()).queue(); return new CommandResult(CommandResultType.SUCCESS); }, "[gamemode] [user URL|keyword]", Arrays.asList("", "despawningbone", "-t 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; //get page number 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 } //get user id then fetch top plays from XHR req JSONArray array; String name, id; try { JSONObject res = getPlayerData("get_user", search, modeId, user); if(!res.has("result")) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`."); JSONObject usr = res.getJSONArray("result").getJSONObject(0); name = usr.getString("username"); id = usr.getString("user_id"); //top plays internal api use different names for modes than listed String[] m = new String[]{"osu", "taiko", "fruits", "mania"}; InputStream stream = new URL("https://osu.ppy.sh/users/" + id + "/scores/best" + "?mode=" + m[modeId] + "&offset=" + (page - 1) * 10 + "&limit=10").openStream(); if (stream.available() < 4) { return new CommandResult(CommandResultType.FAILURE, "This player does not have this many plays in this gamemode!"); } array = new JSONArray(new JSONTokener(stream)); } catch (IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } //build embed EmbedBuilder eb = new EmbedBuilder(); eb.setAuthor(name + "'s top plays (" + modes[modeId] + ")", "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id); DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for(int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); String mods = obj.getJSONArray("mods").join("").replaceAll("\"", ""); 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)); channel.sendMessageEmbeds(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("compare", Arrays.asList("c", "comp"), (channel, user, msg, words) -> { String map = "", name = "", img = "", mode = ""; //get which card to compare with, starting from newest to oldest 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(); } //iterate through recent 100 messsages 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(); //our own cards if(card.getAuthor().getId().equals(DiscordBot.BotID)) { //recent 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]; //pp } 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!", ""); } //owobot support } else if(card.getAuthor().getId().equals("289066747443675143")) { //new score tracker 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(); //>rs } else if(card.getContentDisplay().contains("Recent ")) { map = embed.getAuthor().getUrl(); name = author.substring(0, author.lastIndexOf("+")); img = embed.getThumbnail().getUrl(); mode = card.getContentDisplay().contains("Mania") ? "mania" : card.getContentDisplay().split("Recent ")[1].split(" ")[0]; //ye fucking blame owobot for being so inconsistent //map url response } 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 card hit check index and decrement + reset if neded if(index > 1 && !map.isEmpty()) { index--; map = ""; } //short circuit if found if(!map.isEmpty()) break; } } //yet another set of mode names so we have to parse 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 still doesnt exist after iteration, fail if(map.isEmpty()) return new CommandResult(CommandResultType.FAILURE, "There are no recent map/score cards to compare with in the past 100 messages!"); //if name or id is provided in the command use it else fallback to requester String uid = param[0].isEmpty() ? getPlayer(user) : param[0].replaceAll(" ", "%20"); //get scores to compare 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())); //build embed 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); //iterate through existing scores for this beatmap and add it to the embed 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(modeId) * 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.sendMessageEmbeds(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) { double net = 0, change = -1; List pps = new ArrayList<>(); //get new top pp list, replacing old score if needed 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 JSONObject obj = userBest.getJSONObject(i); double v = obj.getDouble("pp"); String bid = obj.getString("beatmap_id"); if(bid.equals(ibid)) { if(v >= pp) { return -1; } else { change = v; } } if(v <= pp && !pps.contains(pp)) { //insert on first smaller occurence pps.add(pp); } pps.add(v); } //doesnt even get on top list - no pp gain if(pps.indexOf(pp) == -1) { return 0; } //calculate net weighted gain 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)); if(i == pps.indexOf(pp)) { net += w; } else { net += c*(Math.pow(0.95, i - 1)) * (0.95 - 1); } } 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; } //note: stripped ids are considered usernames - if you are sure that its an id (eg result from api) then wrap it in an url private static JSONObject getPlayerData(String api, String input, int modeId, User requester) throws IOException { //check if user url provided, if not assume username String id = ""; boolean noturl = false; try { new URL(input); id = input.substring(input.lastIndexOf("/") + 1); } catch (MalformedURLException e) { noturl = true; id = input; } //if empty fallback to own if(id.isEmpty()) { id = getPlayer(requester); } //set params and fetch String addparam = ""; if (noturl) { //search as usernames addparam = "&type=string"; } if (modeId != 0) { //change mode addparam += ("&m=" + modeId); } //limit either gets ignored or truncated to highest - is fine if i leave it as 100 (highest of any apis i use) JSONArray a = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/" + api + "?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100").openStream())); //always return the val we did the search with, and only result if it exists JSONObject wrap = new JSONObject("{\"search\": \"" + id + "\", \"isId\": " + !noturl + "}"); if (a.length() > 0) { wrap.put("result", a); } return wrap; } //TODO find a way to parallelize map fetching and meta fetching if possible? private static JSONObject computePP(Map beatmap, String bid, int mods, int n50, int n100, int n300, int miss, int combo) throws IllegalArgumentException, IOException { //set osu-wide beatmap params (non mode specific) PlayParameters p = new Koohii.PlayParameters(); p.beatmap = beatmap; p.n50 = n50; p.n100 = n100; p.n300 = n300; p.nmiss = miss; p.nobjects = p.n300 + p.n100 + p.n50 + p.nmiss; p.mods = mods; if (combo > 0) { p.combo = combo; } else { p.combo = p.beatmap.max_combo(); } //get beatmap metadata String addparam = ""; if(p.beatmap.mode == 1) { //these mods alter metadata that taiko needs if((mods & Koohii.MODS_HT) != 0) addparam = "&mods=256"; if((mods & (Koohii.MODS_DT | Koohii.MODS_NC)) != 0) if(addparam.isEmpty()) addparam = "&mods=64"; else addparam = ""; } URL url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + bid + addparam); JSONObject jbm = new JSONArray(new JSONTokener(url.openStream())).getJSONObject(0); //finally calc pp and put it in metadata MapStats stats = new MapStats(p.beatmap); switch(p.beatmap.mode) { case 0: //osu //std specific params - calc diff and acc DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods); p.speed_stars = stars.speed; p.aim_stars = stars.aim; PPv2 oPP = new PPv2(p); jbm.put("ppVal", oPP.total); jbm.put("accVal", oPP.computed_accuracy.value(0)); //use computed jbm.put("starsVal", stars.total); jbm.put("ppMax", constructPP(stars.aim, stars.speed, p.beatmap, mods).total); Koohii.mods_apply(mods, stats, 1|2|4|8); break; case 1: //taiko //taiko specific params - get diff from metadata since theres no calculating mechanism p.speed_stars = jbm.getDouble("difficultyrating"); TaikoPP tPP = new TaikoPP(p); jbm.put("ppVal", tPP.pp); jbm.put("accVal", tPP.acc); jbm.put("starsVal", p.speed_stars); jbm.put("ppMax", new TaikoPP(p.speed_stars, p.max_combo, mods, 1, 0, p.beatmap).pp); tPP.taiko_mods_apply(mods, stats); break; default: throw new UnsupportedOperationException("This gamemode is not yet supported."); } //mods_apply updates the values, so we cant use beatmap.cs etc and we gotta give back via jbm jbm.put("cs", stats.cs); jbm.put("hp", stats.hp); jbm.put("ar", stats.ar); jbm.put("od", stats.od); jbm.put("speed", stats.speed); return jbm; } private static PPv2 constructPP(double aim_stars, double speed_stars, Map b, int mods) { PlayParameters p = new PlayParameters(); 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; params.replaceAll(String::toLowerCase); //all searching should be case insensitive anyways, is fine if we change the entire thing //pass by "ref" so it removes from the list; //iterates over all mode params and remove, giving priority according to descending id and defaulting to standard (0) if(params.removeAll(Collections.singleton("-t"))) modeId = 1; if(params.removeAll(Collections.singleton("-c"))) modeId = 2; if(params.removeAll(Collections.singleton("-m"))) modeId = 3; return modeId; } private static InputStream getMap(String origURL) throws IOException { //get beatmap id String id = origURL.substring(origURL.lastIndexOf("/") + 1); if (origURL.contains("/beatmapsets/")) { //new urls with this format must include # to specify beatmap, or else its a set if (!origURL.contains("#")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL."); } else { //either /beatmap/ or old url /b/ /s/, latter of which we dont want 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 } //fetch map URLConnection mapURL; try { mapURL = new URL("https://osu.ppy.sh/osu/" + id).openConnection(); //no longer has any IOExceptions, always return 200 no matter what mapURL.setRequestProperty("User-Agent", "Mozilla/4.0"); InputStream stream = mapURL.getInputStream(); if (stream.available() < 4) throw new IOException(); //to fallback return stream; } catch (IOException e) { //try fallback regardless of exception reason //fallback to bloodcat successor - much more convoluted and slower to fetch a single map now //get map info mapURL = new URL("https://api.chimu.moe/v1/map/" + id).openConnection(); mapURL.setRequestProperty("User-Agent", "Mozilla/4.0"); JSONObject mapData = new JSONObject(new JSONTokener(mapURL.getInputStream())).getJSONObject("data"); if(mapData.length() == 0) throw new IOException(e); //invalid beatmap String mapName = mapData.getString("OsuFile").replaceAll("[\\\\/:*?\"<>|]", ""); //apparently the name returned isnt windows safe, so sanitize //fetch beatmapset mapURL = new URL("https://chimu.moe" + mapData.getString("DownloadPath")).openConnection(); mapURL.setRequestProperty("User-Agent", "Mozilla/4.0"); ZipInputStream set = new ZipInputStream(mapURL.getInputStream()); //iterate to get correct map ZipEntry map = null; while((map = set.getNextEntry()) != null) //assume always have the file specified in map info if(map.getName().equals(mapName)) return set; //found at input stream current pointer throw new IOException(e); //not catching this, if even this doesnt work just throw something went wrong. } } } diff --git a/src/me/despawningbone/discordbot/command/info/CityInfo.java b/src/me/despawningbone/discordbot/command/info/CityInfo.java index fac821b..db8ba2e 100644 --- a/src/me/despawningbone/discordbot/command/info/CityInfo.java +++ b/src/me/despawningbone/discordbot/command/info/CityInfo.java @@ -1,140 +1,149 @@ package me.despawningbone.discordbot.command.info; import java.awt.Color; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.text.DecimalFormat; import java.text.NumberFormat; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Date; -import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; +import org.jsoup.Connection.Response; +import org.jsoup.Jsoup; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; 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.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; public class CityInfo extends Command { public CityInfo() { this.alias = Arrays.asList("ci", "weather"); this.desc = "Search for info about a city!"; //"Search for info about the city the address is in!"; this.usage = "
"; - for (String country : Locale.getISOCountries()) { - Locale locale = new Locale("en", country); - countryCodes.put(locale.getDisplayCountry(Locale.ENGLISH), locale.getCountry()); - } this.examples = Arrays.asList("hong kong", "tokyo"); //"HK", "akihabara"); } - HashMap countryCodes = new HashMap<>(); - NumberFormat formatter = new DecimalFormat("#0.00"); @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { if(args.length < 1) { return new CommandResult(CommandResultType.INVALIDARGS, "Please input a city name."); //or a address."); } else { channel.sendTyping().queue(); String sword = String.join(" ", args); try { boolean hasWeather = true; JSONObject info = null; String wQualifiedName = "", woeid = "", lng = "", lat = "", region = "", countryShort = ""; TimeZone timezone = null; try { - URLConnection sCon = new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherSearch;text=" + URLEncoder.encode(sword, "UTF-8") + "?returnMeta=true").openConnection(); + //www.yahoo.com changed its endpoint - there's no longer a AJAX API for weather info, and the search autocomplete is basically just a prefix search which is way inferior; so we use ca.news.yahoo.com instead + URLConnection sCon = new URL("https://ca.news.yahoo.com/_td/api/resource/WeatherSearch;text=" + URLEncoder.encode(sword, "UTF-8") + "?returnMeta=true").openConnection(); sCon.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"); JSONObject wsearch = new JSONObject(new JSONTokener(sCon.getInputStream())).getJSONArray("data").getJSONObject(0); woeid = String.valueOf(wsearch.getInt("woeid")); //yahoo scrape lat = String.valueOf(wsearch.getDouble("lat")); lng = String.valueOf(wsearch.getDouble("lon")); wQualifiedName = wsearch.getString("qualifiedName"); - countryShort = countryCodes.get(wsearch.getString("country")); + countryShort = wQualifiedName.substring(wQualifiedName.lastIndexOf(",") + 1); //the display name from yahoo is not always consistent with Java's Locale display name, so we extract from qualified name instead region = wQualifiedName.split(",")[wQualifiedName.split(",").length - 2]; //get second highest level, highest should always be country code timezone = TimeZone.getTimeZone(new JSONObject(new JSONTokener(new URL("https://api.internal.teleport.org/api/locations/" + lat + "," + lng + "/?embed=location:nearest-cities/location:nearest-city/city:timezone").openStream())).getJSONObject("_embedded").getJSONArray("location:nearest-cities").getJSONObject(0).getJSONObject("_embedded").getJSONObject("location:nearest-city").getJSONObject("_embedded").getJSONObject("city:timezone").getString("iana_name")); //can use metaweather, but not accurate enough //can also broaden the scope for yahoo scrape for it to work better //JSONObject wsearch = new JSONObject(new JSONTokener(new URL("https://api.flickr.com/services/rest/?method=flickr.places.findByLatLon&api_key=bdaafeafab62267931d920dda27a4f90&lat=" + lat + "&lon=" + lng + "&format=json&nojsoncallback=1").openStream())).getJSONObject("places").getJSONArray("place").getJSONObject(0); //gonna use flickr find instead - URLConnection iCon = new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherService;woeids=[" + woeid + "]?lang=en-US&returnMeta=true").openConnection(); - iCon.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"); - info = new JSONObject(new JSONTokener(iCon.getInputStream())).getJSONObject("data").getJSONArray("weathers").getJSONObject(0); + + //get cookies and crumb for WeatherService + Response mainCon = Jsoup.connect("https://ca.news.yahoo.com/weather").execute(); + String mainPage = mainCon.body(); + int mainData = mainPage.indexOf("root.App.main = "); + String crumb = new JSONObject(mainPage.substring(mainData + 16, mainPage.indexOf(";\n", mainData))).getJSONObject("context").getJSONObject("dispatcher").getJSONObject("stores").getJSONObject("WeatherStore").getString("crumb"); + + info = new JSONObject(new JSONTokener( + Jsoup.connect("https://ca.news.yahoo.com/_td/api/resource/WeatherService;crumb=" + URLEncoder.encode(crumb, "UTF-8") + ";woeids=[" + woeid + "]?lang=en-US&returnMeta=true") + .cookies(mainCon.cookies()) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0") + .ignoreContentType(true) + .execute().bodyStream() + )).getJSONObject("data").getJSONArray("weathers").getJSONObject(0); + + hasWeather = info.getJSONObject("observation").getJSONObject("temperature").length() != 0; } catch(IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch(JSONException e) { e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); //hasWeather = false; } Date date = new Date(); //System.out.println(info); EmbedBuilder embedmsg = new EmbedBuilder(); embedmsg.setAuthor("Info for " + wQualifiedName, null, null); embedmsg.setColor(new Color(100, 0, 255)); embedmsg.addField("Country", MiscUtils.countryNameToUnicode(countryShort), true); embedmsg.addField("Region", region, true); embedmsg.addField("Current time", OffsetDateTime.now(timezone.toZoneId()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ssa").withLocale(Locale.ENGLISH)).trim(), true); long hours = (timezone.getOffset(date.getTime())/1000/60/60); embedmsg.addField("Timezone" , timezone.getDisplayName(timezone.inDaylightTime(date), TimeZone.LONG, Locale.ENGLISH) + " (UTC" + (Math.signum(hours) == 1 || Math.signum(hours) == 0 ? "+" + hours : hours) + ")" + "\u1160", true); //FLICKR DED String footer = "Weather info not available"; if (hasWeather) { //use another api if weather info not available? JSONObject obs = info.getJSONObject("observation"); JSONObject temp = obs.getJSONObject("temperature"); JSONObject forecast = info.getJSONObject("forecasts").getJSONArray("daily").getJSONObject(0); if(temp.has("now")) { embedmsg.addField("Temperature", fToC(temp.getInt("now")) + "°C (↑" + fToC(temp.getInt("high")) + "°C | ↓" + fToC(temp.getInt("low")) + "°C)", true); embedmsg.addField("Humidity", obs.getInt("humidity") + "% (Chance of rain: " + forecast.getInt("precipitationProbability") + "%)", true); embedmsg.addField("Visibility", miToKm(obs.getDouble("visibility")) + "km (" + obs.getString("conditionDescription") + ")", true); embedmsg.addField("Atmospheric pressure", formatter.format(obs.getDouble("barometricPressure") / 0.029530) + "millibars", true); embedmsg.addField("Wind speed", miToKm(obs.getDouble("windSpeed")) + "km/h", true); embedmsg.addField("Wind direction", obs.getInt("windDirection") + "° (" + obs.getString("windDirectionCode") + ")", true); embedmsg.addField("Feels Like", fToC(temp.getInt("feelsLike")) + "°C", true); embedmsg.addField("UV index", obs.getInt("uvIndex") + " (" + obs.getString("uvDescription") + ")", true); embedmsg.addField("Sunrise", MiscUtils.convertMillis(info.getJSONObject("sunAndMoon").getLong("sunrise") * 1000).substring(0, 5), true); embedmsg.addField("Sunset", MiscUtils.convertMillis(info.getJSONObject("sunAndMoon").getLong("sunset") * 1000).substring(0, 5), true); String imgUrl = info.getJSONArray("photos").getJSONObject(0).getJSONArray("resolutions").getJSONObject(0).getString("url"); //seems to have dead urls, how fix embedmsg.setThumbnail(imgUrl.split(":\\/\\/").length > 2 ? "https://" + imgUrl.split(":\\/\\/")[2] : imgUrl); footer = "Weather info last updated: " + OffsetDateTime.parse(obs.getJSONObject("observationTime").getString("timestamp")).format(DateTimeFormatter.RFC_1123_DATE_TIME) .replace("GMT", timezone.getDisplayName(timezone.inDaylightTime(date), TimeZone.SHORT, Locale.ENGLISH)); //+ " | " + wQualifiedName; //add weather provider to footer? } } embedmsg.addField("Latitude", lat, true); embedmsg.addField("Longitude", lng, true); embedmsg.setFooter(footer, null); try { MessageEmbed fmsg = embedmsg.build(); channel.sendMessageEmbeds(fmsg).queue(); } catch(InsufficientPermissionException e2) { return new CommandResult(CommandResultType.FAILURE, "Unfortunately, the bot is missing the permission `MESSAGE_EMBED_LINKS` which is required for this command to work."); } return new CommandResult(CommandResultType.SUCCESS); } catch (Exception e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } } private String fToC(int f) { //fucking no metric ree return formatter.format((f-32)*5.0/9); } private String miToKm(double mile) { return formatter.format(mile*1.609344); } } diff --git a/src/me/despawningbone/discordbot/command/info/Fandom.java b/src/me/despawningbone/discordbot/command/info/Fandom.java index 3ba8585..43fc72a 100644 --- a/src/me/despawningbone/discordbot/command/info/Fandom.java +++ b/src/me/despawningbone/discordbot/command/info/Fandom.java @@ -1,106 +1,106 @@ package me.despawningbone.discordbot.command.info; import java.awt.Color; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.time.Instant; import java.util.Arrays; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Fandom extends Command { public Fandom() { this.desc = "Search across all the fan-made wikis!"; this.alias = Arrays.asList("wikia", "gamepedia"); //gamepedia is now fandom basically this.usage = ": [| index]"; this.examples = Arrays.asList("zelda: gate of time", "clockwork planet: ryuZU", "angel beats: kanade"); } @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { channel.sendTyping().queue(); if (args.length < 1) { return new CommandResult(CommandResultType.INVALIDARGS, "Invalid input, check help for more info."); } try { String[] init = String.join(" ", args).split(":", 2); if(init.length < 2) { return new CommandResult(CommandResultType.INVALIDARGS, "Please use a colon to seperate the wiki and the search query!"); } //get index String[] split = init[1].split(" \\| "); String sword = split[0]; int num = 0; if (split.length > 1) { try { num = Integer.parseInt(split[1]) - 1; if(num < 0) throw new NumberFormatException(); } catch (NumberFormatException e) { channel.sendMessage("Invalid index inputted. Defaulting to the first result...").queue(); num = 0; } } //bruh the gamepedia merge killed a ton of the actual good UCP/nirvana API controller endpointss //now i gotta use wack substitutions //search - since SearchApiController is now gone i gotta use the other ones String search = URLEncoder.encode(sword.trim(), "UTF-8"); HttpURLConnection url = null; String wiki = init[0].replaceAll("[^\\p{L}\\p{N} ]+", "").replaceAll(" ", "-").toLowerCase(); - //the wikia domain is still in use; no need to swap to fandom.com for now - //alternative search endpoint (more of an autocomplete only but much faster): "https://" + wiki + ".wikia.com/wikia.php?controller=LinkSuggest&method=getLinkSuggestions&format=json&query=" + search - url = (HttpURLConnection) new URL("https://" + wiki + ".wikia.com/api.php?action=query&format=json&list=search&srsearch=" + search).openConnection(); + //newer wikis does not have an entry under wikia.com anymore + //alternative search endpoint (more of an autocomplete only but much faster): "https://" + wiki + ".fandom.com/wikia.php?controller=UnifiedSearchSuggestions&method=getSuggestions&format=json&scope=internal&query=" + search + url = (HttpURLConnection) new URL("https://" + wiki + ".fandom.com/api.php?action=query&format=json&list=search&srsearch=" + search).openConnection(); //sometimes this has no results (new wikis?) if(url.getResponseCode() == 404) { return new CommandResult(CommandResultType.FAILURE, "Unknown wiki name!"); //404 means unknown wiki now } //get result int id; try { JSONObject result = new JSONObject(new JSONTokener(url.getInputStream())); id = result.getJSONObject("query").getJSONArray("search").getJSONObject(num).getInt("pageid"); } catch(JSONException e) { return new CommandResult(CommandResultType.NORESULT, "it in the " + init[0] + " wiki"); } //fetch details about page; way worse formatting than AsSimpleJson but hey its gone what can i do - JSONObject details = new JSONObject(new JSONTokener(new URL("https://" + wiki + ".wikia.com/api/v1/Articles/Details?abstract=500&ids=" + id).openStream())); + JSONObject details = new JSONObject(new JSONTokener(new URL("https://" + wiki + ".fandom.com/api/v1/Articles/Details?abstract=500&ids=" + id).openStream())); JSONObject info = details.getJSONObject("items").getJSONObject(String.valueOf(id)); //TODO make async EmbedBuilder eb = new EmbedBuilder(); eb.setTitle(info.getString("title"), details.getString("basepath") + info.getString("url")); eb.setAuthor(StringUtils.capitalize(init[0]) + " wiki", details.getString("basepath")); //only use until the last full stop before table of content or end for slightly better formatting //there might be false positives for table of content detection since its just checking 1 after full stop, but honestly rarely less details > commonly being ugly af String desc = info.getString("abstract").replaceAll("^(?:(.*?\\.) ?1 .*|(.*\\.) .*?)$", "$1$2"); //greedy if table of content is present, else lazy to get the last eb.setDescription(desc.matches(".*\\.$") ? desc : (desc + "...")); //if everything fails (aka last char aint a full stop) give it the good ol ... treatment if(info.has("comments")) eb.addField("Comments", String.valueOf(info.getInt("comments")), false); if(!info.isNull("thumbnail")) eb.setThumbnail(info.getString("thumbnail").substring(0, info.getString("thumbnail").indexOf("/revision/"))); //get full img by trimming revision path eb.setFooter("Last edited by " + info.getJSONObject("revision").getString("user"), null); eb.setTimestamp(Instant.ofEpochSecond(Long.parseLong(info.getJSONObject("revision").getString("timestamp")))); eb.setColor(new Color(0, 42, 50)); channel.sendMessageEmbeds(eb.build()).queue(); return new CommandResult(CommandResultType.SUCCESS); //add searched result name? } catch (Exception e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } } diff --git a/src/me/despawningbone/discordbot/command/info/Wikipedia.java b/src/me/despawningbone/discordbot/command/info/Wikipedia.java index 6549563..ebecfd6 100644 --- a/src/me/despawningbone/discordbot/command/info/Wikipedia.java +++ b/src/me/despawningbone/discordbot/command/info/Wikipedia.java @@ -1,88 +1,89 @@ package me.despawningbone.discordbot.command.info; import java.io.IOException; import java.net.URL; import java.net.URLEncoder; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Locale; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; +import org.jsoup.Jsoup; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Wikipedia extends Command { public Wikipedia() { this.desc = "Ask the almighty Wikipedia!"; this.alias = Arrays.asList("wiki"); this.usage = "[-lang] [| index]"; this.remarks = Arrays.asList("You can search a specific language's Wikipedia with the parameter, as long as there is a valid subdomain for that language,", "e.g. `en.wikipedia.org` or `ja.wikipedia.org`."); this.examples = Arrays.asList("C++", "ping | 4", "-ja 秋葉原"); } @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { //DONE make use of XHR instead? if (args.length < 1) { return new CommandResult(CommandResultType.INVALIDARGS, "Search something :joy:"); } try { String lang = "en"; if(args.length > 1 && args[0].startsWith("-")) { lang = args[0].substring(1); args = Arrays.copyOfRange(args, 1, args.length); } String[] split = String.join(" ", args).split(" \\|"); String sword = split[0]; int index = 0; try { if(split.length > 1) { index = Integer.parseInt(split[1].trim()) - 1; if(index < 1 || index > 10) throw new NumberFormatException(); } } catch (NumberFormatException e) { channel.sendMessage("Invalid index inputted. Defaulting to first result...").queue(); } String search = URLEncoder.encode(sword, "UTF-8"); String title; try { JSONObject s = new JSONObject(new JSONTokener(new URL("https://" + lang + ".wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=" + search).openStream())); title = URLEncoder.encode(s.getJSONObject("query").getJSONArray("search").getJSONObject(index).getString("title"), "UTF-8").replaceAll("\\+", "%20"); } catch(IOException e) { //usually caused by wrong language wiki return new CommandResult(CommandResultType.INVALIDARGS, "Unknown wiki language version specified."); } catch(JSONException e) { return new CommandResult(CommandResultType.INVALIDARGS, "There are not enough results for your specified index!"); } JSONObject result = new JSONObject(new JSONTokener(new URL("https://" + lang + ".wikipedia.org/api/rest_v1/page/summary/" + title).openStream())); //TODO do something with type = disambiguition? EmbedBuilder eb = new EmbedBuilder(); eb.setAuthor(new Locale(result.getString("lang")).getDisplayName(Locale.ENGLISH) + " Wikipedia"); //DONE add support for other languages? if(result.has("thumbnail")) eb.setThumbnail(result.getJSONObject("thumbnail").getString("source")); - eb.setTitle(result.getString("displaytitle").replaceAll("", "*"), result.getJSONObject("content_urls").getJSONObject("desktop").getString("page")); + eb.setTitle(Jsoup.parse(result.getString("displaytitle")).text(), result.getJSONObject("content_urls").getJSONObject("desktop").getString("page")); eb.setDescription(result.getString("extract")); eb.setFooter("Last revision id " + result.getString("revision"), "https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png"); eb.setTimestamp(OffsetDateTime.parse(result.getString("timestamp"))); channel.sendMessageEmbeds(eb.build()).queue(); return new CommandResult(CommandResultType.SUCCESS); //add searched result name? } catch (Exception e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } }