diff --git a/src/me/despawningbone/discordbot/EventListener.java b/src/me/despawningbone/discordbot/EventListener.java index cb68936..88e9434 100644 --- a/src/me/despawningbone/discordbot/EventListener.java +++ b/src/me/despawningbone/discordbot/EventListener.java @@ -1,438 +1,441 @@ package me.despawningbone.discordbot; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.command.music.AudioTrackHandler; import me.despawningbone.discordbot.command.music.GuildMusicManager; import me.despawningbone.discordbot.command.music.Music; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.GuildVoiceState; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.VoiceChannel; import net.dv8tion.jda.api.entities.Message.Attachment; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMoveEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; import net.dv8tion.jda.api.events.user.UserActivityStartEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; public class EventListener extends ListenerAdapter { public static Multimap testAnimePicCache = ArrayListMultimap.create(); @Override public void onGuildMessageReceived(GuildMessageReceivedEvent event) { DiscordBot.mainJDA = event.getJDA(); // refresh it? //no need, everything's getJDA() is the same jda basis but audioplayer's still using it; need to migrate to store in guildmusicmanager //String[] words = event.getMessage().getContentDisplay().split(" "); User author = event.getAuthor(); TextChannel channel = event.getChannel(); Message msg = event.getMessage(); //checkAnimePics(event); //DONE use log4j //logging if (!DiscordBot.logExcemptID.contains(author.getId())) { String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]"; String payload = "[INFO] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full msg: " + msg.getContentDisplay(); List att = event.getMessage().getAttachments(); if (!att.isEmpty()) { payload += System.lineSeparator() + " Attachments:"; for (int i = 0; i < att.size(); i++) { payload += System.lineSeparator() + " " + att.get(i).getUrl(); } } DiscordBot.logger.trace(payload); //TODO log embeds and add user id too? } /*if (String.join("", words).equalsIgnoreCase("!despacito")) { channel.sendMessage("https://www.youtube.com/watch?v=W3GrSMYbkBE").queue(); return; }*/ CommandResult result = null; try (Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){ String prefix = MiscUtils.getPrefix(s, event.getGuild().getId()); if(msg.getContentDisplay().toLowerCase().startsWith(prefix.toLowerCase())) { String msgStripped = msg.getContentDisplay().substring(prefix.length()).replaceAll("\\s\\s+", " "); //merges space String[] args = msgStripped.split(" "); // base on command length? Command cmd = DiscordBot.commands.get(args[0].toLowerCase()); cmd = cmd == null ? DiscordBot.aliases.get(args[0].toLowerCase()) : cmd; if (cmd != null) { ResultSet uRs = s.executeQuery("SELECT reports FROM users WHERE id = " + author.getId() + ";"); if(uRs.next()) { //checks null at the same time if(uRs.getString(1).split("\n").length >= 5) { channel.sendMessage("You are banned from using the bot.").queue(); DiscordBot.logger.info("[WARN] " + author.getName() + " (" + author.getId() + ") tried to execute " + msg.getContentDisplay() + " but was banned."); return; } } uRs.close(); long perm = MiscUtils.getActivePerms(s, channel, cmd); //result = cmd.execute(channel, author, msg, args); String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel); //pass it to the subcommand handler to handle instead if(perms == null || event.getAuthor().getId().equals(DiscordBot.OwnerID)) { //owner overrides perms for convenience /*if(cmd.isDisabled(channel.getGuild().getId())) { channel.sendMessage("This command is disabled!").queue(); result = new CommandResult(CommandResultType.FAILURE, "Disabled command"); } else {*/ cmd.executeAsync(channel, author, msg, Arrays.copyOfRange(args, 1, args.length), r -> { //catch all exceptions? //should have actually DiscordBot.logger.info("[" + r.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed " + msg.getContentDisplay() + (r.getRemarks() == null ? "." : ". (" + r.getRemarks() + ")")); //logging has to be before sendMessage, or else if no permission it will just quit if(r.getMessage() != null) channel.sendMessage(r.getMessage()).queue(); }); //dont know if async will screw anything up //wont, TODO log date and which server executed the command also? return; //} } else if(perms.equals("DISABLED") || cmd.isDisabled()) { msg.addReaction("❎").queue(); - result = new CommandResult(CommandResultType.DISABLED); + result = new CommandResult(CommandResultType.DISABLED); } else { result = new CommandResult(CommandResultType.NOPERMS, perms); channel.sendMessage(result.getMessage()).queue(); } } else { result = new CommandResult(CommandResultType.FAILURE, "Invalid command"); //do more stuff? } } else if(msg.getContentRaw().matches("<@!?" + DiscordBot.BotID + ">")) result = greet(channel, author, prefix); } catch (SQLException e) { result = new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } if(result != null) DiscordBot.logger.info("[" + result.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed " + msg.getContentDisplay() + (result.getRemarks() == null ? "." : ". (" + result.getRemarks() + ")")); } @SuppressWarnings("unused") private void checkAnimePics(GuildMessageReceivedEvent event) { //TESTING CompletableFuture.runAsync(() -> { User author = event.getAuthor(); TextChannel channel = event.getChannel(); Message msg = event.getMessage(); List pics = new ArrayList<>(); if(channel.getId().equals("615396635383562272") && !DiscordBot.BotID.equals(author.getId())) { for(Attachment att : msg.getAttachments()) { if(att.isImage()) pics.add(att.getUrl()); } /*if(msg.getContentDisplay().contains("http")) { for(String s : msg.getContentDisplay().split("http")) { try { new URL("http" + s); pics.add("http" + s); } catch(MalformedURLException e) { ; } } }*/ try { Thread.sleep(1500); msg = channel.retrieveMessageById(msg.getId()).complete(); } catch (InterruptedException e2) { e2.printStackTrace(); } for(MessageEmbed em : msg.getEmbeds()) { //System.out.println(em.toJSONObject()); if(em.getThumbnail() != null) { System.out.println("thumb"); if(em.getSiteProvider() != null && em.getSiteProvider().getName().equals("pixiv")) { //pixiv doesnt show whole pic in embed pics.add(em.getUrl()); } else { pics.add(em.getThumbnail().getUrl()); } } else if(em.getImage() != null && !author.isBot()) { System.out.println("img"); pics.add(em.getImage().getUrl()); } else { System.out.println("url"); pics.add(em.getUrl()); } } //System.out.println(pics); //System.out.println(testAnimePicCache); for(String pic : pics) { //handle first since these mustnt be the same urls, therefore cache saucenao sauce instead String origPic = pic; if(!testAnimePicCache.containsValue(pic)) { Element saucenao; try { saucenao = Jsoup.connect("https://saucenao.com/search.php?url=" + pic).get().body(); Element result = saucenao.selectFirst(".resulttable"); try { //normal pixiv/deviantart handling if(result.parent().attr("class").equals("result hidden")) throw new NullPointerException(); Element source = result.selectFirst("strong:contains(ID:)").nextElementSibling(); pic = source.attr("href"); //will not run if the cache has the value || if the link specified aint a pic } catch (NullPointerException e1) { //weird saucenao card formatting (eg episode info), or no result; we dont handle these ; } } catch (IOException e) { e.printStackTrace(); } } System.out.println("next" + testAnimePicCache); if(testAnimePicCache.containsValue(pic)) { try { Multimap temp = ArrayListMultimap.create(); System.out.println("temp"); Message m = channel.retrieveMessageById(Multimaps.invertFrom(testAnimePicCache, temp).get(pic).toArray(new String[1])[0]).complete(); EmbedBuilder eb = new EmbedBuilder(); eb.setTitle("Repost detected!"); eb.setDescription("[This image](" + origPic + ")" + (origPic.equals(pic) ? "" : "([source](" + pic + "))") + " is a repost of [this message](" + m.getJumpUrl() + ") by `" + m.getAuthor().getName() + "#" + m.getAuthor().getDiscriminator() + "` at **" + m.getTimeCreated().format(DateTimeFormatter.RFC_1123_DATE_TIME) + "**.\n"); eb.setThumbnail(origPic); channel.sendMessage(eb.build()).queue(); continue; } catch(NullPointerException e) { //if the message is deleted e.printStackTrace(); } } else { System.out.println("put"); testAnimePicCache.put(msg.getId(), pic); } } } }).whenComplete((r, t) -> {if(t != null) t.printStackTrace();}); } private CommandResult greet(TextChannel channel, User author, String prefix) { MessageBuilder smsg = new MessageBuilder(); String nick = channel.getGuild().getMemberById(author.getId()).getNickname(); if (nick != null) { smsg.append("Yo " + nick + "!\n"); } else { smsg.append("Yo " + author.getName() + "!\n"); } if (author.getId().equals("237881229876133888")) { // easter eggs smsg.append("How art thou, my creator?"); } if (author.getId().equals("179824176847257600")) { smsg.append("Still stalking as usual, huh? :smirk:"); smsg.append("\n\"go fix your potatoes\" - pugger"); } if (author.getId().equals("165403578133905408")) { smsg.append( "You're probably still dead :D (your status), either that or you are playing with your doodle :P"); } if (author.getId().equals("187714189672841216")) { smsg.append("Hello, fearsome coder named Morgan :wink:"); } if (author.getId().equals("201768560345743360")) { smsg.append("Still need help to save you from the school? :D"); } if (author.getId().equals("257660112703979521")) { smsg.append("I like how you only uses me for `!desp roll 1`. :P"); } if (author.getId().equals("272712701988569090")) { smsg.append("This guy is generous :smile:"); } if (author.getId().equals("203861130995695616")) { smsg.append("He asked my owner to change it so here it is :smile:"); } if (author.getId().equals("206038522971422721")) { smsg.append("Appearently he loves !desp idiot :face_palm:"); } if (author.getId().equals("218377806994866176")) { smsg.append("Your potatoes seems better than Dank's :smirk:"); } if (author.getId().equals("139316803582033920")) { smsg.append("That ironic name tho :stuck_out_tongue:"); } if (author.getId().equals("237058431272484864")) { smsg.append(":b:oi"); } if (author.getId().equals("338258756762730496")) { smsg.append("**a e s t h e t i c**"); } EmbedBuilder eb = new EmbedBuilder(); eb.setColor(0x051153); //TODO version info based on git commits eb.appendDescription("This bot is running **despbot v1.5.0**, Shard `" + DiscordBot.mainJDA.getShardInfo().getShardString() + "`. ([invite me!](https://discordapp.com/oauth2/authorize?&client_id=311086271642599424&scope=bot&permissions=0))\n"); eb.appendDescription("Connected guilds: `" + DiscordBot.mainJDA.getGuildCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getGuildCache().size()).sum()) + "`; "); eb.appendDescription("Total members (cached): `" + DiscordBot.mainJDA.getUserCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getUserCache().size()).sum()) + "`\n"); eb.appendDescription("DM `despawningbone#4078` if you have any questions!\n"); eb.appendDescription("To get a list of commands, do `" + prefix + "help`."); smsg.setEmbed(eb.build()); Message fmsg = smsg.build(); channel.sendMessage(fmsg).queue(); return new CommandResult(CommandResultType.SUCCESS, null); } @Override public void onGuildMessageUpdate(GuildMessageUpdateEvent event) { //System.out.println("edit"); //debug Message msg = event.getMessage(); User author = event.getAuthor(); TextChannel channel = event.getChannel(); if (!DiscordBot.logExcemptID.contains(author.getId())) { String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]"; DiscordBot.logger.trace("[EDIT] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full edited msg: " + msg.getContentDisplay()); } } @Override public void onUserActivityStart(UserActivityStartEvent event) { //store presence for checking osu pp Activity osu = event.getNewActivity(); if (osu != null && osu.getName().equals("osu!") && osu.isRich() //if need to include other games and details, just remove this if clause and change sGame below && osu.asRichPresence().getDetails() != null) { String toolTip = osu.asRichPresence().getLargeImage().getText(); //DiscordBot.guildMemberPresence.put(event.getUser().getId(), game); //so that game update to nothing wont be logged /*System.out.println(event.getGuild().getName()); System.out.println(event.getUser().getName()); System.out.println(game.getName()); System.out.println(game.getUrl()); System.out.println(game.getType()); System.out.println(game.isRich() ? game.asRichPresence().getDetails() : "no rich"); System.out.println(game.isRich() ? game.asRichPresence().getState() : "no rich");*/ //OffsetDateTime timesent = OffsetDateTime.now(); //System.out.println(toolTip); String sGame = (toolTip.lastIndexOf(" (") != -1 ? toolTip.substring(0, toolTip.lastIndexOf(" (")) : toolTip) + "||" + osu.asRichPresence().getDetails(); //sGame = sGame.replaceAll("'", "''"); /*OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms");*/ try (Connection con = DiscordBot.db.getConnection()) { //DONE do i need to close the statement? PreparedStatement s = con.prepareStatement("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET game = ? WHERE game <> ?;" ); //prevent blank updates s.setString(1, sGame); s.setString(2, sGame); s.setString(3, sGame); s.execute(); //con.createStatement().execute("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", '" + sGame + "') ON CONFLICT(id) DO UPDATE SET game = '" + sGame + "' WHERE game <> '" + sGame + "';" ); //prevent blank updates } catch (SQLException e) { //FIXED if i make this not osu only, be aware of SQL injections through sGame (probably being paranoid tho) e.printStackTrace(); } /*timeReceived = OffsetDateTime.now(); ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms");*/ } } //TODO only update when not paused? //TODO update on member deafen? private void waitActivity(Guild guild) { AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); ap.getGuildMusicManager(guild).player.setPaused(true); ap.getGuildMusicManager(guild).clearQueueCleanup = ap.ex.schedule(() -> { ap.stopAndClearQueue(guild); DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("The queue has been cleared.").queue(); ap.getGuildMusicManager(guild).clearQueueCleanup = null; }, 1, TimeUnit.MINUTES); } private String updateActivity(Guild guild, VoiceChannel vc) { AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); GuildMusicManager mm = ap.getGuildMusicManager(guild); if(mm.clearQueueCleanup != null) { - ap.getGuildMusicManager(guild).player.setPaused(false); + mm.player.setPaused(false); mm.clearQueueCleanup.cancel(true); mm.clearQueueCleanup = null; } String type = mm.scheduler.loop; //mm cannot be null if (type != null && vc.getMembers().size() > 2 && vc.getMembers().contains(guild.getMemberById(DiscordBot.OwnerID))) { ap.toggleLoopQueue(guild, type); return type; } return null; } @Override public void onGuildVoiceLeave(GuildVoiceLeaveEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState(); - if (vs.inVoiceChannel()) { - if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) { + if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) { + if (vs.inVoiceChannel()) { if (vs.getChannel().equals(event.getChannelLeft()) && event.getChannelLeft().getMembers().size() < 2) { TextChannel channel = DiscordBot.lastMusicCmd.get(event.getGuild().getId()); channel.sendMessage( "All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } - } else { //got kicked - System.out.println("Got kicked"); - ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler().stopAndClearQueue(event.getGuild()); + } + } 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(); if (vs.inVoiceChannel()) { String id = event.getGuild().getId(); TextChannel channel = DiscordBot.lastMusicCmd.get(id); if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)) { if (vs.getChannel().equals(event.getChannelLeft())) { if (!event.getMember().getUser().getId().equals(DiscordBot.BotID) && event.getChannelLeft().getMembers().size() < 2) { channel.sendMessage( "All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } } else if (vs.getChannel().equals(event.getChannelJoined())) { String type = updateActivity(event.getGuild(), event.getChannelJoined()); if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue(); } } else { //moved bot to empty channel if(event.getChannelJoined().getMembers().size() < 2) { channel.sendMessage( "The bot has been moved to an empty channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } else { //moved bot to channel with ppl + 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(); String id = event.getGuild().getId(); TextChannel channel = DiscordBot.lastMusicCmd.get(id); if(channel != null) { if (vs.inVoiceChannel() && !event.getMember().getUser().getId().equals(DiscordBot.BotID)) { if (vs.getChannel().equals(event.getChannelJoined())) { String type = updateActivity(event.getGuild(), event.getChannelJoined()); if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue(); } } } } } diff --git a/src/me/despawningbone/discordbot/command/admin/Shutdown.java b/src/me/despawningbone/discordbot/command/admin/Shutdown.java index dbbc116..dd5bebe 100644 --- a/src/me/despawningbone/discordbot/command/admin/Shutdown.java +++ b/src/me/despawningbone/discordbot/command/admin/Shutdown.java @@ -1,35 +1,32 @@ package me.despawningbone.discordbot.command.admin; -import java.util.concurrent.TimeUnit; - 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.music.AudioTrackHandler; import me.despawningbone.discordbot.command.music.Music; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Shutdown extends Command { public Shutdown() { this.desc = "Shut down the bot"; this.usage = ""; this.botUserLevel = BotUserLevel.BOT_OWNER.ordinal(); } @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { channel.sendMessage("Bye!").queue(); System.out.println("\nShutting down the bot...\n"); + + //shut down audio system gracefully first if exists AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); - if(ap != null) { - ap.ex.schedule(() -> DiscordBot.mainJDA.shutdown(), 500, TimeUnit.MILLISECONDS); //delay needed to actually shutdown correctly; borrowing the scheduler lmao - ap.shutdown(); - } else { - DiscordBot.mainJDA.shutdown(); - } + if(ap != null) ap.shutdown(); + + new Thread(() -> DiscordBot.mainJDA.shutdown()).start(); //new thread needed to actually shutdown correctly since or else its stuck waiting for the event to finish return new CommandResult(CommandResultType.SUCCESS); } } diff --git a/src/me/despawningbone/discordbot/command/info/CityInfo.java b/src/me/despawningbone/discordbot/command/info/CityInfo.java index de57330..6a23d13 100644 --- a/src/me/despawningbone/discordbot/command/info/CityInfo.java +++ b/src/me/despawningbone/discordbot/command/info/CityInfo.java @@ -1,278 +1,278 @@ package me.despawningbone.discordbot.command.info; import java.awt.Color; import java.io.IOException; import java.net.URL; 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 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"); //private final String flickrAPI = DiscordBot.tokens.getProperty("flickr"); @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 { /*JSONTokener georesult = null; InputStream geostream = null; String search = URLEncoder.encode(sword, "UTF-8"); URL geocode = null; try { geocode = new URL("https://maps.googleapis.com/maps/api/geocode/json?address=" + search + "&key=" + AudioPlayer.GAPI + "&language=en"); geostream = geocode.openStream(); } catch (IOException e) { e.printStackTrace(); } georesult = new JSONTokener(geostream); JSONObject geomain = new JSONObject(georesult); JSONArray resultList = geomain.getJSONArray("results"); if(resultList.isNull(0)) { channel.sendMessage("Unfortunately there is no results :cry:").queue(); return new CommandResult(CommandResultType.NORESULT); } JSONObject firstResult = resultList.getJSONObject(0); JSONObject loc = firstResult.getJSONObject("geometry").getJSONObject("location"); //String formattedAddr = firstResult.getString("formatted_address"); JSONArray addrComponents = firstResult.getJSONArray("address_components"); String formattedAddr = "", addr = firstResult.getString("formatted_address"); String countryShort = null; String region = null; String locality = null; String country = null; String colarea = null; boolean stop = false; for(int i = 0; i < addrComponents.length(); i++) { JSONObject component = addrComponents.getJSONObject(i); String compname = component.getString("long_name"); if(!stop) { if(i == addrComponents.length() - 1) { formattedAddr += compname; } else { formattedAddr += compname + ", "; } } List types = component.getJSONArray("types").toList(); if(types.contains("country")) { countryShort = component.getString("short_name"); country = compname; if(i == 0) { channel.sendMessage("You need to specify which part of the country you want to get the info from.").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is a country"); } } else if(types.contains("continent")) { if(i == 0) { channel.sendMessage("You need to specify which part of the continent you want to get the info from.").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is a continent"); } } else if(types.contains("postal_code")) { if(i == 0) { formattedAddr = addr; stop = true; } } else if(types.contains("administrative_area_level_1")) { region = compname; } else if(types.contains("locality")) { locality = compname; } else if(types.contains("colloquial_area")) { colarea = compname; } else if(types.contains("natural_feature") && addrComponents.length() == 1) { channel.sendMessage("Search civilized locations please :joy:").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is natural"); } } if(region == null) { if(stop) { region = country; } else { if(locality.equals("Singapore")) { formattedAddr = addr; } region = colarea; if(locality != null) region = locality; } } double lat = loc.getDouble("lat"); double lng = loc.getDouble("lng"); JSONTokener timeresult = null; InputStream timestream = null; URL timezone = null; Timestamp timestamp = new Timestamp(System.currentTimeMillis()); long sec = Math.round(timestamp.getTime() / 1000.0); try { //can deprecate this since the new weather scrape has local time, but there wont be a name for the timezone anymore timezone = new URL("https://maps.googleapis.com/maps/api/timezone/json?location=" + lat + "," + lng + "×tamp=" + sec + "&key=" + AudioPlayer.GAPI + "&language=en"); timestream = timezone.openStream(); } catch (IOException e) { e.printStackTrace(); } timeresult = new JSONTokener(timestream); JSONObject timemain = new JSONObject(timeresult); String timeZoneName = timemain.getString("timeZoneName"); int rawOffset = timemain.getInt("rawOffset"); int dstOffset = timemain.getInt("dstOffset"); ZonedDateTime zone = ZonedDateTime.now(ZoneOffset.ofTotalSeconds(rawOffset + dstOffset)); int hours = (rawOffset + dstOffset) / 60 / 60;*/ boolean hasWeather = true; JSONObject info = null; String wQualifiedName = "", woeid = "", lng = "", lat = "", region = "", countryShort = ""; TimeZone timezone = null; /*try { URLConnection con = new URL("https://api.flickr.com/services/rest/?method=flickr.places.find&api_key=" + flickrAPI + "&query=" + URLEncoder.encode(sword, "UTF-8") + "&format=json&nojsoncallback=1").openConnection(); con.setRequestProperty("Accept-Language", "en"); JSONArray warray = new JSONObject(new JSONTokener(con.getInputStream())).getJSONObject("places").getJSONArray("place"); int index; for(index = 0; index < warray.length(); index++) { if(warray.getJSONObject(index).has("timezone")) { break; } } JSONObject wsearch = warray.getJSONObject(index); woeid = wsearch.getString("woeid"); //flickr api, using generated api key (will it expire?) //highest accuracy so far //wQualifiedName = wsearch.getString("_content"); //too short ArrayList pSplit = new ArrayList<>(Arrays.asList(URLDecoder.decode(wsearch.getString("place_url"), "UTF-8").substring(1).split("/"))); if(!pSplit.get(pSplit.size() - 1).equals(wsearch.getString("woe_name"))) { pSplit.add(wsearch.getString("woe_name")); } Collections.reverse(pSplit); wQualifiedName = String.join(", ", pSplit); timezone = TimeZone.getTimeZone(wsearch.getString("timezone")); lat = wsearch.getString("latitude"); lng = wsearch.getString("longitude"); String[] rSplit = wQualifiedName.split(", "); region = rSplit.length > 1 ? rSplit[rSplit.length - 2] : rSplit[0]; countryShort = countryCodes.get(rSplit[rSplit.length - 1].trim()); } catch(IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch (JSONException e) { //e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); }*/ //FLICKR DED try { JSONObject wsearch = new JSONObject(new JSONTokener(new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherSearch;text=" + URLEncoder.encode(sword, "UTF-8") + "?returnMeta=true").openStream())).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")); region = wsearch.getString("city"); - timezone = TimeZone.getTimeZone(new JSONObject(new JSONTokener(new URL("https://api.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")); + 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 info = new JSONObject(new JSONTokener(new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherService;woeids=[" + woeid + "]?lang=en-US&returnMeta=true").openStream())).getJSONObject("data").getJSONArray("weathers").getJSONObject(0); } catch(IOException e) { - e.printStackTrace(); + return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch(JSONException e) { e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); //hasWeather = false; } /*String ftimezone = timeZoneName + " (UTC" + (Math.signum(hours) == 1 || Math.signum(hours) == 0 ? "+" + hours : hours) + ")"; if(ftimezone.length() < 34) { ftimezone += String.join("", Collections.nCopies(34 - ftimezone.length(), " ")); }*/ StringBuffer unibuff = new StringBuffer(); if(countryShort != null) { char[] ch = countryShort.toLowerCase().toCharArray(); for(char c : ch) { int temp = (int)c; int temp_integer = 96; //for lower case if(temp<=122 & temp>=97) unibuff.append(Character.toChars(127461 + (temp-temp_integer))); } } else { unibuff.append("N/A"); } 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.setFooter("Weather info last updated: " + info.getString("lastBuildDate") , null); embedmsg.addField("Country", unibuff.toString(), 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"); 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: " + obs.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.sendMessage(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/music/AudioTrackHandler.java b/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java index 63c4c83..3d2e926 100644 --- a/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java +++ b/src/me/despawningbone/discordbot/command/music/AudioTrackHandler.java @@ -1,463 +1,463 @@ package me.despawningbone.discordbot.command.music; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.commons.lang3.exception.ExceptionUtils; import org.awaitility.Awaitility; import org.awaitility.core.ConditionTimeoutException; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.wrapper.spotify.SpotifyApi; import me.despawningbone.discordbot.DiscordBot; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.VoiceChannel; import net.dv8tion.jda.api.managers.AudioManager; import net.dv8tion.jda.internal.utils.PermissionUtil; public class AudioTrackHandler { private ConcurrentHashMap musicManagers; private AudioPlayerManager playerManager; final String GAPI = DiscordBot.tokens.getProperty("google"); //package final 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 List votes = new ArrayList<>(); public String getUrl() { return url; } public String getUserWithDiscriminator() { return uDis; } public String getFormattedDuration() { return fDur; } 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) { this.url = url; this.uDis = user.getName() + "#" + user.getDiscriminator(); this.fDur = MiscUtils.convertMillis(durMillis); } //overload for autoplay public TrackData(String url, String user, long durMillis) { this.url = url; this.uDis = user; this.fDur = MiscUtils.convertMillis(durMillis); } } public AudioTrackHandler() { this.musicManagers = new ConcurrentHashMap<>(); this.playerManager = new DefaultAudioPlayerManager(); playerManager.setFrameBufferDuration(1000); playerManager.getConfiguration().setFilterHotSwapEnabled(true); try { //register spotify source manager playerManager.registerSourceManager(new SpotifyAudioSourceManager(new SpotifyApi.Builder().setClientId(DiscordBot.tokens.getProperty("spotifyid")).setClientSecret(DiscordBot.tokens.getProperty("spotifysecret")).build(), playerManager, this)); } catch (Exception e) { e.printStackTrace(); } AudioSourceManagers.registerRemoteSources(playerManager); AudioSourceManagers.registerLocalSource(playerManager); } public GuildMusicManager getGuildMusicManager(Guild guild) { String guildId = guild.getId(); GuildMusicManager musicManager; musicManager = musicManagers.get(guildId); if (musicManager == null) { musicManager = new GuildMusicManager(guild, this, playerManager); musicManagers.put(guildId, musicManager); } guild.getAudioManager().setSendingHandler(musicManager.getSendHandler()); return musicManager; } public CompletableFuture searchAndPlay(String search, String type, Member user, TextChannel channel) throws NoSuchElementException { CompletableFuture resFuture = new CompletableFuture<>(); String url = search; try { new URL(url); } catch(MalformedURLException e) { url = fetchUrlFromSearch(search, type); } if (url == null) throw new NoSuchElementException(); load(user, url, (n, l) -> { if (l.size() > 100) { resFuture.complete(new CommandResult(CommandResultType.INVALIDARGS, "Cannot queue in a playlist of more than 100 tracks.")); return; } if(!user.getVoiceState().inVoiceChannel()) { resFuture.complete(new CommandResult(CommandResultType.FAILURE, "You are not in a voice channel.")); return; } if((channel.getGuild().getAudioManager().isConnected() && !channel.getGuild().getAudioManager().getConnectedChannel().getId().equals(user.getVoiceState().getChannel().getId()))) { resFuture.complete(new CommandResult(CommandResultType.FAILURE, "You are not in the same channel as the bot.")); return; } if (l.size() > 10 || l.stream().anyMatch(t -> t.getInfo().isStream)) { GuildMusicManager mm = getGuildMusicManager(user.getGuild()); int req = (int) Math.ceil(user.getVoiceState().getChannel().getMembers().stream().filter(mem -> !mem.getUser().isBot()).count() / 2.0); if(req > 1 && !DiscordBot.ModID.contains(user.getId())) { if(mm.pending != null) { 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(); mm.pending = l; mm.pendingCleanup = ex.schedule(() -> { mm.clearVotes("tracks"); mm.pending = null; mm.pendingCleanup = null; channel.sendMessage(user.getUser().getName() + "'s" + (l.size() > 1 ? " playlist " : " livestream ") + "request has timed out.").queue(); }, 1, TimeUnit.MINUTES); resFuture.complete(new CommandResult(CommandResultType.SUCCESS, "Pending approval")); return; } } try { if(!l.isEmpty()) { int startIndex = queueTracks(l, user) + 1; if (l.size() == 1){ channel.sendMessage("Adding `" + l.get(0).getInfo().title + "` (" + (l.get(0).getDuration() == Long.MAX_VALUE ? "N/A" : l.get(0).getUserData(TrackData.class).getFormattedDuration()) + ") to the queue. [`" + startIndex + "`]").queue(); } else if (l.size() > 1) { channel.sendMessage("Adding playlist `" + n + "` to the queue, queue now has a total of `" + (startIndex + l.size() - 1) + "` tracks.").queue(); channel.sendMessage((startIndex == 1 ? "Playing `" : "First track: `") + l.get(0).getInfo().title + "` (" + l.get(0).getUserData(TrackData.class).getFormattedDuration() + ") [`" + startIndex + "`].").queue(); } resFuture.complete(new CommandResult(CommandResultType.SUCCESS)); } else { resFuture.complete(new CommandResult(CommandResultType.NORESULT)); } } catch (UnsupportedOperationException e) { resFuture.complete(new CommandResult(CommandResultType.FAILURE, e.getMessage())); } }, (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)))); return resFuture; } private String fetchUrlFromSearch(String search, String type) { if (type.startsWith("youtube")) { type = type.substring(8, type.length()); if (type.equals("video")) { return "ytsearch:" + search; } else { //only use yt api when playlist (and autoplay) due to strict rate limit try { //NOADD check if theres a way to search for playlists and vids at the same time //using different commands now InputStream input = new URL("https://www.googleapis.com/youtube/v3/search?part=snippet%20&q=" + URLEncoder.encode(search, "UTF-8") + "%20&type=" + type + "%20&key=" + GAPI).openStream(); JSONObject result = new JSONObject(new JSONTokener(new InputStreamReader(input, "UTF-8"))).getJSONArray("items").getJSONObject(0); JSONObject id = result.getJSONObject("id"); String vidID = id.getString(type + "Id"); //can switch to pure playlist impl tbh i aint even using this api for normal vids return "https://www.youtube.com/" + (type.equals("video") ? "watch?v=" : "playlist?list=") + vidID; } catch (IOException e) { e.printStackTrace(); return null; } catch (JSONException e) { return null; } } } else if (type.equals("soundcloud")) { return "scsearch:" + search; } throw new UnsupportedOperationException("This provider is not implemented yet!"); } //package private void load(Member user, String url, BiConsumer> resultHandler, Consumer exceptionally) { playerManager.loadItemOrdered(getGuildMusicManager(user.getGuild()), url, new AudioLoadResultHandler() { @Override public void trackLoaded(AudioTrack track) { try { track.setUserData(new TrackData(track.getInfo().uri, user.getUser(), track.getDuration())); resultHandler.accept(null, Arrays.asList(track)); } catch(Exception e) { 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; } List tracks = playlist.isSearchResult() ? playlist.getTracks().subList(0, 1) : playlist.getTracks(); String plId = ""; if (!playlist.isSearchResult()) if(url.contains("://soundcloud.com") || url.contains("://www.youtube.com")) plId = url.contains("://soundcloud.com") ? "?in=" + url.split("soundcloud.com/")[1] : "&list=" + url.split("list=")[1].split("&")[0]; for (AudioTrack track : tracks) //TODO tell users that we skipped some tracks? if(track != null) track.setUserData(new TrackData(track.getInfo().uri + plId, user.getUser(), track.getDuration())); if (playlist.getSelectedTrack() != null) tracks.add(0, tracks.remove(tracks.indexOf(playlist.getSelectedTrack()))); //shift selected track to first track resultHandler.accept(playlist.getName(), tracks.stream().filter(t -> t != null).collect(Collectors.toList())); //only get first result if search } 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) { if(exception.getMessage().contains("Unknown file format.") && url.contains("open.spotify.com")) { resultHandler.accept(null, new ArrayList<>()); //TODO TEMPORARY FIX } else { exceptionally.accept(exception.getCause() != null ? exception.getCause() : exception); } } }); } public int queueTracks(List tracks, Member user) throws UnsupportedOperationException { Guild guild = user.getGuild(); GuildMusicManager musicManager = getGuildMusicManager(guild); int startIndex = musicManager.scheduler.getQueueSize() + (musicManager.player.getPlayingTrack() != null ? 1 : 0); if (user.getVoiceState().inVoiceChannel()) { if (!guild.getAudioManager().isConnected() && !guild.getAudioManager().isAttemptingToConnect()) { VoiceChannel voice = user.getVoiceState().getChannel(); if (PermissionUtil.checkPermission(voice, guild.getSelfMember(), Permission.VOICE_CONNECT, Permission.VOICE_SPEAK)) { guild.getAudioManager().openAudioConnection(voice); //already checked permissions so no need to try catch } else { throw new UnsupportedOperationException("The bot cannot play music in that channel."); } } } else { throw new UnsupportedOperationException("You are currently not in a voice channel."); } try { if (!guild.getAudioManager().isConnected()) { Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> guild.getAudioManager().isConnected()); } } catch (ConditionTimeoutException e) { throw new UnsupportedOperationException("Error while connecting to voice channel: The connection timed out."); } if (guild.getAudioManager().getConnectedChannel().equals(user.getVoiceState().getChannel())) { for (AudioTrack track : tracks) musicManager.scheduler.queue(track); //nulls should already be handled; if it aint its my fault lmao } else { throw new UnsupportedOperationException("You are currently not in the same channel as the bot."); } return startIndex; //if it successfully returned it means that nothing failed } public String skipTrack(Guild guild) { GuildMusicManager musicManager = getGuildMusicManager(guild); musicManager.scheduler.nextTrack(); musicManager.player.setPaused(false); //implicit resume try { return musicManager.player.getPlayingTrack().getInfo().title; } catch (NullPointerException e) { musicManager.scheduler.loop = null; return null; } } public String setTrackPosition(Guild guild, long hour, long min, long sec) throws IllegalArgumentException { GuildMusicManager mm = getGuildMusicManager(guild); Long millis = TimeUnit.HOURS.toMillis(hour) + TimeUnit.MINUTES.toMillis(min) + TimeUnit.SECONDS.toMillis(sec); AudioTrack track = mm.player.getPlayingTrack(); if (track.getDuration() > millis && !track.getInfo().isStream) { track.setPosition(millis); return MiscUtils.convertMillis(track.getPosition()); } else if (track.getInfo().isStream) { throw new IllegalArgumentException("You cannot set the track time in a stream!"); } else { throw new IllegalArgumentException("You cannot set the track time over the track duration."); } } //not zero-based public MessageBuilder getTrackInfo(Guild guild, int index) { //TODO add views, etc by storing them when getting with lavaplayer? GuildMusicManager mm = getGuildMusicManager(guild); if(index - 1 > mm.scheduler.getQueueSize() || index < 1) return null; AudioTrack track = index == 1 ? mm.player.getPlayingTrack() : mm.scheduler.findTracks(index - 1, 1).get(0); TrackData data = track.getUserData(TrackData.class); MessageBuilder smsg = new MessageBuilder(); String fpos = MiscUtils.convertMillis(track.getPosition()); String fdur = data.getFormattedDuration(); smsg.append((index == 1 ? "Current" : MiscUtils.ordinal(index)) + " track: `" + track.getInfo().title + "` (" + fpos + "/" + (track.getDuration() == Long.MAX_VALUE ? "???" : fdur) + ")\n"); smsg.append("Author: " + track.getInfo().author + "\n"); smsg.append("Requested by: `" + data.getUserWithDiscriminator() + "`\n"); String timeTag = ""; if(data.getUrl().startsWith("https://www.youtube.com")) timeTag = "&="; else if(data.getUrl().startsWith("https://soundcloud.com")) timeTag = "#="; smsg.append("URL: " + data.getUrl() + (timeTag.isEmpty() ? "" : timeTag + TimeUnit.MILLISECONDS.toSeconds(track.getPosition()))); return smsg; } //TODO migrate to embed? 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 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("The current queue (page " + page + "/" + maxPage + "): \n"); if (mm.scheduler.loop != null) { 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")); } int times = (page - 1) * 10; for (AudioTrack track : tracks.subList((page - 1) * 10, Math.min(tracks.size(), page * 10))) { times++; TrackData data = track.getUserData(TrackData.class); smsg.append("[" + times + "]: `" + track.getInfo().title + "` (" + (track.getDuration() == Long.MAX_VALUE ? "N/A" : data.getFormattedDuration()) + ") requested by `" + data.getUserWithDiscriminator() + "`\n"); } if(maxPage > page) smsg.append("\nDo `!desp music queue " + (page + 1) + "` to see the next page."); return smsg; } public boolean togglePause(Guild guild, boolean pause) throws IllegalStateException { GuildMusicManager mm = getGuildMusicManager(guild); if (mm.player.isPaused() == !pause) { mm.player.setPaused(pause); return mm.player.isPaused(); } else { throw new IllegalStateException("The player is already " + (mm.player.isPaused() ? "paused!" : "unpaused!")); } } public boolean toggleLoopQueue(Guild guild, String type) { type = type.toLowerCase(); GuildMusicManager mm = getGuildMusicManager(guild); if (mm.scheduler.loop == null || !mm.scheduler.loop.equals(type)) { mm.scheduler.loop = type; if (type != null && type.equals("autoplay") && mm.scheduler.getQueueSize() < 1) { mm.scheduler.queueAutoplay(mm.player.getPlayingTrack()); } return true; } 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(); } public void shutdown() { musicManagers.forEach((s, mm) -> { //System.out.println(DiscordBot.mainJDA.getGuildById(s).getName()); mm.player.destroy(); mm.scheduler.clearSchedulerQueue(); AudioManager man = DiscordBot.mainJDA.getGuildById(s).getAudioManager(); - if(man.isConnected()) { + if(man.isConnected() || man.isAttemptingToConnect()) { man.closeAudioConnection(); 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 playerManager.shutdown(); ex.shutdown(); } } diff --git a/src/me/despawningbone/discordbot/command/music/TrackScheduler.java b/src/me/despawningbone/discordbot/command/music/TrackScheduler.java index 603d3fd..eb08745 100644 --- a/src/me/despawningbone/discordbot/command/music/TrackScheduler.java +++ b/src/me/despawningbone/discordbot/command/music/TrackScheduler.java @@ -1,200 +1,200 @@ package me.despawningbone.discordbot.command.music; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import me.despawningbone.discordbot.DiscordBot; import me.despawningbone.discordbot.command.music.AudioTrackHandler.TrackData; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.json.JSONObject; import org.json.JSONTokener; /** * This class schedules tracks for the audio player. It contains the queue of * tracks. */ public class TrackScheduler extends AudioEventAdapter { private final AudioPlayer player; private final BlockingQueue queue; private AudioTrackHandler ap; private Guild guild; public String loop = null; //way too lazy to use getter setters lmao /** * @param player * The audio player this scheduler uses */ public TrackScheduler(Guild parent, AudioTrackHandler handler, AudioPlayer player) { this.player = player; this.queue = new LinkedBlockingQueue<>(); this.ap = handler; this.guild = parent; } /** * Add the next track to queue or play right away if nothing is in the * queue. * * @param track * The track to play or add to queue. */ public void queue(AudioTrack track) { // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If // something is playing, it returns false and does nothing. In that case the player was already playing so this // track goes to the queue instead. if (!player.startTrack(track, true)) { queue.offer(track); } else if (loop != null && loop.equals("autoplay")) { //i dont think this is needed as people need to play something before autoplay can be toggled anyways queueAutoplay(track); } } /** * Start the next track, stopping the current one if it is playing. */ public void nextTrack() { //DONE rewrite to not include q.remove here so that stuff like interrupted wont break the queue? // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are // giving null to startTrack, which is a valid argument and will simply stop the player. AudioTrack track = queue.poll(); player.startTrack(track, false); if(track == null) { //System.out.println("finished"); //debug loop = null; delayCloseConnection(player); //required because if not it will throw InterruptedException } } //seems to be called internally somehow; even when mayStartNext is false (REPLACED, STOPPED etc) this still fires //NVM ITS CALLED FROM AudioTrackHandler.skipTrack() LOL public void queueAutoplay(AudioTrack track) { //check duplicate please, some can get into a dead loop like cosMo@暴走P - WalpurgisNacht and Ice - 絶 //well randoming it works ap.ex.submit(() -> { //async so it can free up the event try { //System.out.println("autoplay"); - InputStream input = new URL("https://www.googleapis.com/youtube/v3/search?part=snippet%20&relatedToVideoId=" + track.getIdentifier() + "&type=video%20&key=" + ap.GAPI).openStream(); + InputStream input = new URL("https://www.googleapis.com/youtube/v3/search?part=snippet&relatedToVideoId=" + track.getIdentifier() + "&type=video&key=" + ap.GAPI).openStream(); JSONTokener result = new JSONTokener(new InputStreamReader(input, "UTF-8")); Member temp; try { temp = guild.getMemberByTag(track.getUserData(TrackData.class).getUserWithDiscriminator()); } catch (IllegalArgumentException e) { //track was autoplay queued before this temp = guild.getMemberById(DiscordBot.BotID); //fallback } final Member user = temp; int t = ThreadLocalRandom.current().nextInt(2); String url = "https://youtube.com/watch?v=" + new JSONObject(result).getJSONArray("items").getJSONObject(t).getJSONObject("id").getString("videoId"); ap.load(user, url, (n, l) -> { AudioTrack auto = l.get(0); auto.setUserData(new TrackData(auto.getInfo().uri, "Autoplay", auto.getDuration())); ap.queueTracks(l.subList(0, 1), user); }, ex -> { ex.printStackTrace(); DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track: " + ex.getMessage()).queue(); loop = null; }); } catch (Exception e) { e.printStackTrace(); DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track. Is the last track a youtube video?").queue(); loop = null; } }); } public List findTracks(int startIndex, int range) { AudioTrack[] a = queue.toArray(new AudioTrack[queue.size()]); int i = startIndex - 1 + range < 0 ? Integer.MAX_VALUE : startIndex - 1 + range; //prevent overflow AudioTrack[] t = Arrays.copyOfRange(a, startIndex - 1, Math.min(queue.size() + 1, i)); //accounts for first track player thats not in queue return Arrays.asList(t); } public AudioTrack removeTrack(int num) { Iterator i = queue.iterator(); num = num - 1; for (int times = 0; i.hasNext(); times++) { AudioTrack removed = i.next(); if (num == times) { i.remove(); return removed; } } return null; } public AudioTrack moveTrack(int from, int to) { List q = new ArrayList<>(Arrays.asList(queue.toArray(new AudioTrack[queue.size()]))); AudioTrack track = q.remove(from); q.add(to, track); synchronized (queue) { //obtain lock and operate before releasing or else queue might not be complete queue.clear(); for(AudioTrack t : q) queue.offer(t); } return track; } public void shuffleQueue() { List q = Arrays.asList(queue.toArray(new AudioTrack[queue.size()])); Collections.shuffle(q); synchronized (queue) { //obtain lock and operate before releasing or else queue might not be complete queue.clear(); for(AudioTrack t : q) queue.offer(t); } } public void clearSchedulerQueue() { queue.clear(); } public int getQueueSize() { return queue.size(); } @Override public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED or REPLACED) //System.out.println(endReason); //debug boolean mayStartNext = endReason.mayStartNext; if (mayStartNext) { //doesnt queue clone if skipped if (loop != null && loop.equals("loop")) { //so what the hecc if loop is null and i do loop.equals("Loop") it freezes the thread AudioTrack clone = track.makeClone(); TrackData origData = clone.getUserData(TrackData.class); clone.setUserData(new TrackData(origData.getUrl(), origData.getUserWithDiscriminator(), clone.getDuration())); //wipe votes queue.offer(clone); } nextTrack(); } if(mayStartNext || endReason == AudioTrackEndReason.REPLACED) { //queues new if skipped too if (loop != null && loop.equals("autoplay") && queue.size() < 1) { queueAutoplay(player.getPlayingTrack()); } } } private void delayCloseConnection(AudioPlayer player) { ap.ex.schedule(() -> guild.getAudioManager().closeAudioConnection(), 1, TimeUnit.MILLISECONDS); } } \ No newline at end of file