public class EventListener extends ListenerAdapter {
@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
User author = event.getAuthor();
TextChannel channel = event.getChannel();
Message msg = event.getMessage();
//DONE use log4j
//logging
if (!DiscordBot.logExcemptID.contains(author.getId())) {
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();
//check perms
long perm = MiscUtils.getActivePerms(s, channel, cmd);
String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel); //pass it to the subcommand handler to handle instead
if(cmd.isDisabled()) perms = "DISABLED"; //override if disabled by code
+ msg.getContentDisplay() + (r.getRemarks() == null ? "." : ". (" + r.getRemarks() + ")")); //logging has to be before sendMessage, or else if no permission it will just quit
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
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
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();
"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
AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler();
if(ap != null) { //if not destroyed
ap.stopAndClearQueue(event.getGuild()); //can double fire on normal end; shouldnt be too big of a problem
}
}
}
@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();
"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();
"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
event.getGuild().getAudioManager().openAudioConnection(event.getChannelJoined()); //seems to need explicit reconnect on bot move, it gets stuck on attempting to reconnect otherwise; probably would be fixed soon but hot patch for now
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();
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();
public static final int DISABLED = 0x80000; //even if i use 0x80000000 and it overflows it should still work //DONE 0x200 is a new permission; JDA 3 will probably not recognize it so i have to circumvent over it manually //changed to 0x80000
private static final List<String> disableExempt = Arrays.asList("admin", "admin.settings", "admin.settings.perms");
public Settings() {
this.alias = Arrays.asList("setting", "set");
this.desc = "Change the settings of the bot for this guild!";
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid argument count specified.");
}
if(!words[1].matches("[a-zA-Z0-9.]+")) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified."); //prevent SQL injection; might be restricting though, but then again all command names are alphanumeric
try(Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){
if(channel.getGuild().getTextChannelById(words[0]) == null) throw new NumberFormatException();
} catch(NumberFormatException e) {
if(words[0].startsWith("#")) {
if(!msg.getMentionedChannels().isEmpty()) {
words[0] = msg.getMentionedChannels().get(0).getId(); //assuming its to order from left to right; but tbh if they try to trick the command with 2 channels it wont work anyways with the range check and node check
}
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid channel specified.");
}
}
try {
rs = s.executeQuery("SELECT \"" + node + "\" FROM perms_" + cat + " WHERE id = " + channel.getGuild().getId() + ";"); //hopefully this will be pretty fast
} catch(SQLException x) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified.");
}
EnumSet<Permission> def = null;
if(node.contains(".")) { //path must be valid coz or else SQL wouldve thrown an exception
if(sOrig.equals(node)) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified."); //SQL returns the actual string if the table doesnt have the column
for(String line : sOrig.split("\n")) {
String[] split = line.split(":");
int index = 0;
if(split.length < 2) { //channels would have colons
if(perm == null && disableExempt.contains(words[1].toLowerCase())) return new CommandResult(CommandResultType.INVALIDARGS, "You cannot disable this node."); //temporary patch
} catch(IllegalArgumentException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid permission specified.");
}
String[] lines = {""};
String type = "";
if(rs.next()) { //considerations: rs.next(); guild vs channel; allows; add or remove;
lines = rs.getString(1).split("\n");
if(lines[0].equals(node)) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified."); //SQL returns the actual string if the table doesnt have the column
int i = 0;
long orig = 0;
if(opType == 2) {
orig = Long.parseLong(lines[0]);
} else {
try {
for(i = 0; i <= lines.length; i++) {
if(lines[i].startsWith(words[0] + ":")) {
orig = Long.parseLong(lines[i].split(":")[1]);
break;
}
}
} catch(ArrayIndexOutOfBoundsException e) {
lines = Arrays.copyOf(lines, lines.length + 1); //to extend it; should be the same method as using arraylist
//orig = Permission.getRaw(def); //use default //actually should already have imposed global default if rs has next; adding default to this will only give channel the global perm, which makes no sense
type = split[0]; lines[i] = split[1]; //at this stage theres only 2 options: guild and a valid channel id
} else {
if(opType == 2) {
String[] split = imposePerms(null, Permission.getRaw(def), perm, allows); //initiate guild by imposing perm to default; since allow is in the lower 32 bit, i can just put def as the base
type = split[0]; lines[0] = split[1];
} else {
lines[0] = String.valueOf(Permission.getRaw(def)); //initiate guild with the default permission of the command
lines = Arrays.copyOf(lines, lines.length + 1);
String[] split = imposePerms(words[0], 0, perm, allows); //impose with perm base = 0
type = split[0]; lines[1] = split[1];
}
}
String fin = String.join("\n", lines) + "\n"; //last line must have \n
s.execute("INSERT INTO perms_" + cat + "(id, \"" + node + "\") VALUES (" + channel.getGuild().getId() + ", '" + fin + "') ON CONFLICT(id) DO UPDATE SET \"" + node + "\" = '" + fin + "';"); //no need blank update check, because it will never be the same
"Edit default perms required for commands for this guild!", Arrays.asList(
"Adding a `-` sign before a permission overrides the parent permission set,",
"Where `cat(guild > channel) > cmd(guild > channel) > subcmd(guild > channel)` (parent > child, where subcmd channel is of highest priority).",
"*For example, specifying `admin.settings.prefix` with `-MANAGE_SERVER` will override the requirement of `admin.settings`, and allow those without `MANAGE_SERVER` to use `admin.settings.prefix`*.\n",
"The permission nodes are as such: `<cat>[.cmd][.subcmd]`.",
"For permission names, please refer to https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/core/Permission.html,",
"Along with a special permission `DISABLED` for disabling the node.",
"Specifying the permission twice will remove it.\n",
"Use `wipe` if you want to reset the permissions for the node, and `wipeall` for resetting all perms in the whole category.",
"`list` and `wipe`/`wipeall` only accepts the first 2 parameters."), EnumSet.of(Permission.ADMINISTRATOR), -BotUserLevel.BOT_OWNER.ordinal());
registerSubCommand("prefix", Arrays.asList("pre"), (channel, user, msg, words) -> { //dont allow prefixes with markdown for now
String prefix;
if(words.length < 1) {
prefix = DiscordBot.prefix;
channel.sendMessage("No prefix entered. Using default prefix...").queue();
if(prefix.length() > 30) return new CommandResult(CommandResultType.INVALIDARGS, "Please do not enter prefixes that are too long."); //arbitrarily set; can change anytime
try (Connection con = DiscordBot.db.getConnection()) {
PreparedStatement s = con.prepareStatement("INSERT INTO settings(id, prefix) VALUES (" + channel.getGuild().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET prefix = ? WHERE prefix <> ?");
"Change the prefix for this guild!", Arrays.asList("*You can also include spaces with the use of `code block`s.*", "Leave blank to use the default (`" + DiscordBot.prefix + "`)."));
channel.sendMessage("Invalid index inputted. Defaulting to first result...").queue();
}
JSONObject anime, info;
try {
JSONObject main = new JSONObject(new JSONTokener(new URL("https://api.jikan.moe/v3/search/anime?q=" + URLEncoder.encode(search, "UTF-8")).openStream()));
info = new JSONObject(new JSONTokener(new URL("https://api.jikan.moe/v3/anime/" + anime.getInt("mal_id")).openStream()));
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
} catch (JSONException e) {
e.printStackTrace();
return new CommandResult(CommandResultType.INVALIDARGS, "There are not enough results for your specified index!");
}
EmbedBuilder em = new EmbedBuilder();
em.setTitle("Anime info for " + anime.getString("title") + " (" + anime.getString("type") + ")", anime.getString("url"));
em.setThumbnail(anime.getString("image_url"));
em.setDescription("JP: " + info.getString("title_japanese") + "\n\n" + (info.isNull("synopsis") ? "No synopsis information has been added to this title." : info.getString("synopsis")));
if(!source.contains("[Light novel]") && !source.contains("anga]")) source = source.replaceFirst("\\[(.*?)\\](.*)", "$1 ([Adaptation]$2)"); //something couldve adapted it if its original instead
this.remarks = Arrays.asList("Leave the search words blank for a random pic!", "If you have not specified the index, it will be a random result from the search.", "Include `-f` if you want to load the full picture!", "However, full pictures takes a while to load in.");
this.remarks = Arrays.asList("The URL should be a direct link to an image.", "You can also upload an image as an attachment while calling this command instead of using a url.",
" * Specify the `-d` parameter to do a depth search!", " * It is useful for cropped images and edited images, but makes the search much longer,", " * and can sometimes result in a related image instead of the actual source.");
}
@Override
public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { //allow people to not input url to check sauce of the most recent image like u/2dgt3d?
List<String> amend = new ArrayList<String>(Arrays.asList(args));
//check if image boards are found, if so return the first one found and discard similar results since imageboards are as standard as we can get
//it is possible but unlikely that we would be grabbing the wrong image's source in the lower sections even though there are good sources (that are not imageboards) at the top due to this
//gets second image coz usually the first one is just a sharper identical image
Element simThumb = yandex.select(osSimilar).get(1);
String similar = "https:" + (osSimilar.contains(".CbirSimilar") ? simThumb.attr("style").replaceAll(".*url\\((.*?)\\).*", "$1") : simThumb.attr("src")); //use yandex's thumbnails since the actual source might be broken already
//use other sizes if found - second most accurate result (unless theres edits that uses the image in it (memes for example), so still retain similar results)
if(yandex.select(".CbirOtherSizes-Wrapper").size() > 0) { //merge the sort algorithms? //sorting with ratio seems to perform bad for most crops so dont sort anymore
try { //search iqdb first for the tags; results usually more organized and formatted
Element iqdb = Jsoup.connect("https://iqdb.org/?url=" + URLEncoder.encode(url, "UTF-8") + "&service[]=1&service[]=2&service[]=3&service[]=4&service[]=5&service[]=11&service[]=13").post().body(); //services excluding eshuushuu since it has no tags and will fallback to saucenao anyways
//post instead of get due to the recent incident making iqdb and saucenao block imgur
if(iqdb.select(".err").size() > 0 && iqdb.select(".err").html().contains("HTTP")) //iqdb errors, most likely broken link so dont fallback
throw new IOException(iqdb.selectFirst(".err").ownText().split("\\.")[0]);
Elements tb = iqdb.select("th:contains(Best match)").get(0).parent().siblingElements();
try { //fallback to saucenao, usually pixiv source instead of image boards; also falls back if its anime scenes
Element saucenao = Jsoup.connect("https://saucenao.com/search.php").requestBody("url=" + URLEncoder.encode(url, "UTF-8")).post().body(); //same reason as iqdb post
Element result = saucenao.selectFirst(".resulttable");
//change line break tags to \n, and use first line as title
result.select("br").after("\\n");
String[] title = result.selectFirst(".resulttitle") == null ? new String[]{"No title"} : result.selectFirst(".resulttitle").wholeText().replaceAll("\\\\n", "\n").split("\n", 2); //there can be no titles, like 4chan sources
.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"))
+ (!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)
List<String> amend = new ArrayList<String>(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
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");
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!");
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.");
List<String> 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)
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
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
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
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."));
//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
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
String imgUrl = info.getJSONArray("photos").getJSONObject(0).getJSONArray("resolutions").getJSONObject(0).getString("url"); //seems to have dead urls, how fix
footer = "Weather info last updated: " + OffsetDateTime.parse(obs.getJSONObject("observationTime").getString("timestamp")).format(DateTimeFormatter.RFC_1123_DATE_TIME)
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
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();
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 info = details.getJSONObject("items").getJSONObject(String.valueOf(id)); //TODO make async
//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.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);
//if(cmd.isDisabled(channel.getGuild().getId())) content += "*" + cmdInfo.substring(0, cmdInfo.length() - 2) + "*\n"; //TODO migrate to SQLite //seems to have too much overhead
/*else*/ content += cmdInfo;
}
em.addField(entry.getKey(), content, false);
em.addBlankField(false);
}
em.getFields().remove(em.getFields().size() - 1);
em.setFooter("Do " + prefix + "help [cmdname] for more information about the command!",
null);
em.setColor(new Color(10,182,246));
- channel.sendMessage(em.build()).queue();
+ channel.sendMessageEmbeds(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
Command cmd = DiscordBot.commands.get(args[0]);
if (cmd == null) {
cmd = DiscordBot.aliases.get(args[0]);
if (cmd == null) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid command!");
}
}
String missing;
String name = cmd.getName(); String desc = "";
EmbedBuilder em = new EmbedBuilder(); //DONE show permission required
for(int i = 1; i < args.length; i++) {
cmd = cmd.getSubCommand(args[i]);
if(cmd == null) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid sub command! Check `" + prefix + "help " + String.join(" ", Arrays.copyOfRange(args, 0, i)) + "` for more info.");
this.remarks = Arrays.asList("Leave blank for your own user info!", "", "* Specify the `-a` parameter to get a larger profile picture instead of a thumbnail.");
}
//DONE merge with ID.java?
@Override
public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) {
List<User> userList = msg.getMentionedUsers();
User user = null;
List<String> params = new ArrayList<>(Arrays.asList(args));
boolean bigPfp = params.removeAll(Collections.singleton("-a")); //pass by ref so it removes from the list
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`.");
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?
//eb.setDescription("[Hover me for the spoiler!](https://discordapp.com/channels/" + channel.getGuild().getId() + "/" + channel.getId() + " \"" + lineSplit[1] + "\")");
try {
eb.setDescription("[Hover me for the spoiler!](https://hastebin.com/" + pasteToHastebin(message) + ".txt \"" + message + "\")");
} catch (JSONException | IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
eb.setColor(new Color(255, 255, 0));
eb.setFooter("Click on the link if you cannot see the hover, or are on Discord mobile!", null);
- channel.sendMessage(eb.build()).queue();
+ channel.sendMessageEmbeds(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.FAILURE, "The bot has no permission to delete messages, and making spoiler box with the actual spoilers still visible seems pointless, ain't it? :P");
final HttpInterfaceManager httpInterfaceManager; //package final since TrackScheduler will also access it //not anymore, but its fine leaving it as is
public ScheduledExecutorService ex = Executors.newScheduledThreadPool(2, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("audio-scheduler-%d").build());
static class TrackData {
private String url;
private String uDis;
private String fDur;
private String ytAp = null;
private List<String> votes = new ArrayList<>();
public String getUrl() {
return url;
}
public String getUserWithDiscriminator() {
return uDis;
}
public String getFormattedDuration() {
return fDur;
}
public String getYoutubeAutoplayParam() {
return ytAp;
}
public int voteSkip(User user, int req) {
if (votes.contains(user.getId()))
throw new UnsupportedOperationException("You have already voted!");
votes.add(user.getId());
if (votes.size() < req) {
return votes.size();
} else {
votes.clear();
return -1;
}
}
public TrackData(String url, User user, long durMillis) {
resFuture.complete(new CommandResult(CommandResultType.FAILURE, "There is already a pending playlist or livestream."));
return;
}
mm.vote(user.getUser(), "tracks", req); //self vote; should never return -1 (success) coz req > 1
channel.sendMessage("Due to the total duration of your requested tracks, it has been added to pending. It will be automatically removed if it has not been approved by the users in the channel for longer than 1 minute.\n" + "Others in the channel should use `!desp music approve` to vote.").queue();
}, (ex) -> resFuture.complete(ex.getStackTrace()[0].getMethodName().equals("readPlaylistName") ? new CommandResult(CommandResultType.FAILURE, "Cannot read the playlist specified. Is it private?") : new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex))));
exceptionally.accept(e); //so i dont lose my sanity over silenced errors
}
}
@Override
public void playlistLoaded(AudioPlaylist playlist) {
try {
if(playlist.getTracks().size() == 0) { //somehow its possible; do the same as noResult()
if(url.startsWith("ytsearch:")) load(user, url.replaceFirst("yt", "sc"), resultHandler, exceptionally); //searches in soundcloud, since apparently yt pretty frequently returns no result
else resultHandler.accept(null, new ArrayList<>());
return;
}
String plName = playlist.getName();
List<AudioTrack> tracks = playlist.getTracks();
String plId = "";
if(playlist.isSearchResult()) {
tracks = tracks.subList(0, 1); //only get first result if search
plName = null; //no actual playlist name
} else {
if(url.contains("://soundcloud.com") || url.contains("://www.youtube.com")) //add pl id if is playlist
tracks.add(0, tracks.remove(tracks.indexOf(playlist.getSelectedTrack()))); //shift selected track to first track
resultHandler.accept(plName, tracks.stream().filter(t -> t != null).collect(Collectors.toList()));
} catch (Exception e) {
exceptionally.accept(e); //so i dont lose my sanity over silenced errors
}
}
@Override
public void noMatches() {
if(url.startsWith("ytsearch:")) load(user, url.replaceFirst("yt", "sc"), resultHandler, exceptionally); //searches in soundcloud, since apparently yt pretty frequently returns no result
else resultHandler.accept(null, new ArrayList<>());
}
@Override
public void loadFailed(FriendlyException exception) {
public MessageBuilder queueCheck(Guild guild, int page) throws IllegalArgumentException {
GuildMusicManager mm = getGuildMusicManager(guild);
AudioTrack playing = mm.player.getPlayingTrack();
if(playing == null) return null;
MessageBuilder smsg = new MessageBuilder();
List<AudioTrack> tracks = mm.scheduler.findTracks(1, Integer.MAX_VALUE).stream().filter(a -> a != null).collect(Collectors.toList()); //get all tracks in queue
tracks.add(0, playing);
int maxPage = (int) Math.ceil(tracks.size() / 10f);
if(page > maxPage) throw new IllegalArgumentException("There is no such page.");
smsg.append("There is a total of `" + tracks.size() + "` tracks " + (mm.scheduler.loop.equals("loop") ? "looping" : "in autoplay") + ".\n\n");
} else {
long millis = 0;
for(AudioTrack track : tracks)
millis += track.getDuration();
smsg.append("There is a total of `" + tracks.size() + "` tracks queued" + ((millis == Long.MAX_VALUE || millis < 0) ? ".\n\n" : ", with a total duration of `" + MiscUtils.convertMillis(millis - playing.getPosition()) + "`.\n\n"));
} else { //remove autoplay queued track when disabling autoplay?
if (mm.scheduler.loop.equals("autoplay") && mm.scheduler.getQueueSize() == 1 && mm.scheduler.findTracks(1, 1).get(0).getUserData(TrackData.class).uDis.equals("Autoplay")) { //including autoplay, theres only 2 tracks; only remove tracks that is autoplayed
mm.scheduler.removeTrack(1);
}
mm.scheduler.loop = null;
return false;
}
}
public void stopAndClearQueue(Guild guild) {
GuildMusicManager mm = getGuildMusicManager(guild);
mm.pending = null;
mm.pendingCleanup = null;
mm.clearQueueCleanup = null;
mm.scheduler.loop = null;
mm.scheduler.clearSchedulerQueue();
mm.clearAllVotes();
mm.player.stopTrack();
mm.player.setPaused(false);
guild.getAudioManager().closeAudioConnection();
}
//should ALWAYS be called before discarding this instance
DiscordBot.lastMusicCmd.get(s).sendMessage("The music bot is going into maintenance and it will now disconnect. Sorry for the inconvenience.").queue();
}
});
musicManagers = null; //so further operations wont be possible even if i forgot to set this instance to null
+ (handler.toggleLoopQueue(c.getGuild(), "autoplay") ? "on.\nIt will end once someone joins the music channel." : "off."))
.queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You are currently not alone in the music channel, therefore this feature is disabled.");
}
}, "", null, "Auto-queues related tracks when you are alone!",
Arrays.asList(" * Currently only works if the last track in the queue is a youtube video.", " Note: it will autoplay indefinitely until you toggle it again or someone joins."));
registerSubCommand("loop", Arrays.asList("l", "loopqueue"), (c, u, m, a) -> {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter a valid timestamp with colons.");
} catch (IllegalArgumentException e) {
return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
}
}, "[+/-][hr:]<min:sec>", Arrays.asList("4:52:21", "2:00", "+30:00", "-1:23:45"), "Set the playing position in the current track!",
Arrays.asList("timestamps without signs are absolute, whereas `+`/`-` means go forward or backward from the current position for the amount of time specified respectively.", " * the person who requested the current track can always set the position, regardless of perms."),
registerSubCommand("removetrack", Arrays.asList("rt", "r", "skiptrack", "st"), (c, u, m, a) -> {
if(a.length > 0) {
try {
int num = Integer.parseInt(a[0]);
if(num == 1) return new CommandResult(CommandResultType.INVALIDARGS, "You cannot remove the current track from the queue with this command. Use !desp music skip instead.");
if(num < 1) return new CommandResult(CommandResultType.INVALIDARGS, "The index you entered is invalid.");
return new CommandResult(CommandResultType.INVALIDARGS, "Please valid indices.");
}
}, "<fromindex> <toindex>", Arrays.asList("2 3"), "Move a track to the specified index.", Arrays.asList(" * you can also run this command regardless of perms if you are alone with the bot.")
c.sendMessage("Successfully shuffled the queue.").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You are currently not alone in the music channel, therefore this feature is disabled.");
}
}, "", null, "Shuffle the tracks in the queue when you are alone!", null);
//TODO make vote system for this
registerSubCommand("clear", Arrays.asList("disconnect", "dc", "stop", "clearqueue"), (c, u, m, a) -> {
handler.stopAndClearQueue(c.getGuild());
c.sendMessage("The queue has been cleared.").queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "", null, "Clear the queue and stop the player.", Arrays.asList(" * you can also run this command regardless of perms if you are alone with the bot.")
return new CommandResult(CommandResultType.FAILURE, e.getMessage());
} catch(NullPointerException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "There is nothing playing currently! Please specify a song title to search the lyrics up.");
} catch(IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
}, "[search words]", Arrays.asList(""), "Search some lyrics up!", Arrays.asList(" * if you did not specify any search words, the bot will try to fetch the lyrics of the current track playing, if any."));
//TODO equalizer persistence?
//make this premium?
registerSubCommand("equalizer", Arrays.asList("eq"), (c, u, m, a) -> {
GuildMusicManager mm = handler.getGuildMusicManager(c.getGuild());
Float[] vals;
if(a.length == 0) {
vals = mm.getCurrentGain();
} else {
try {
try {
if(a.length != 15) throw new NumberFormatException();
vals = new Float[15];
for(int i = 0; i < a.length; i++) {
vals[i] = Float.parseFloat(a[i]);
}
mm.setGain(0, vals);
} catch(NumberFormatException e) {
vals = mm.setPresetGain(a[0]);
}
} catch(IllegalArgumentException e2) {
return new CommandResult(CommandResultType.INVALIDARGS, e2.getMessage());
}
}
//formatting
DecimalFormat df = new DecimalFormat("0.00");
df.setPositivePrefix("+");
EmbedBuilder eb = new EmbedBuilder();
eb.setTitle("Current equalizer graph");
eb.appendDescription("```\n");
for(double line = 0.25; line >= -0.25; line -= 0.05) {
return new CommandResult(CommandResultType.SUCCESS);
}, "[bandvalues/presetname]", Arrays.asList("", "0.09 0.07 0.07 0.01 0 0 -0.02 -0.02 0.03 0.03 0.05 0.07 0.09 0.1 0.1", "bassboost"), "Sets the equalizer for the music player in this guild!",
Arrays.asList("Accepts 15 bands with values ranging from -0.25 to 0.25, where -0.25 is muted and 0.25 is double volume.", "* presets include: `bassboost`, `default`, `rock`.", "Input nothing to return the current settings.", "",
"Note: you might experience some audio cracking for band values >0.1, since amplifying volumes remotely does not work well.", "It is recommended to use values from -0.25 to 0.1, and turning discord volume up instead.")
if(Arrays.asList("move", "clear").contains(sub.getName())) { //should always be connected to vc
if(channel.getGuild().getAudioManager().getConnectedChannel().getMembers().size() == 2) { //alone; no need to check if the other member is requester, since its checked before
CommandResult res = super.execute(channel, author, msg, args); //pass to subcommand handler for perms checking
if(res.getResultType() == CommandResultType.NOPERMS && Arrays.asList("forceskip", "skip", "setposition", "removetrack").contains(sub.getName())) { //add back info to command result
res = new CommandResult(CommandResultType.INVALIDARGS, res.getMessage().getContentRaw().split(" to execute")[0] + ", or have requested the track to execute this command.");
}
return res;
}
//maybe move the lyrics stuff elsewhere?
private final String geniusAuth = DiscordBot.tokens.getProperty("genius");
//annotated usually isnt actual lyrics, but the following are known to be some
//private ExecutorService executor = Executors.newCachedThreadPool(); //unfortunately i need to finish the api search to get the url to start the html scrape, which means i cannot use future for multithread here
public List<MessageEmbed> getLyrics(String search) throws IOException { //DONE dont splice words between embeds; make whole sentence to be spliced instead
//appends metadata portion to the given embed (should be the last one)
private void setFinalLyricsEmbed(EmbedBuilder eb, JSONObject main, Element html) { //change the format for the supplementary info in artists and albums from italic to sth else?
try {
String name = main.getJSONObject("primary_artist").getString("name");
JSONObject preload = new JSONObject(StringEscapeUtils.unescapeJson(temp));
JSONObject song = preload.getJSONObject("entities").getJSONObject("songs").getJSONObject(Integer.toString(preload.getJSONObject("songPage").getInt("song")));
//parse albums
try {
StringBuilder sb = new StringBuilder();
String album = IntStream.range(0, song.getJSONArray("trackingData").length()).mapToObj(i -> song.getJSONArray("trackingData").getJSONObject(i)).filter(obj -> obj.getString("key").equals("Primary Album")).findFirst().get().getString("value").trim();