Page MenuHomedesp's stash

No OneTemporary

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<String, String> 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<Attachment> 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<String> 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<String, String> 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 = "<address/search words>";
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<String, String> 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<Object> 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 + "&timestamp=" + 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<String> 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<String, GuildMusicManager> 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<String> 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<CommandResult> searchAndPlay(String search, String type, Member user, TextChannel channel) throws NoSuchElementException {
CompletableFuture<CommandResult> 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<String, List<AudioTrack>> resultHandler, Consumer<Throwable> 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<AudioTrack> 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<AudioTrack> 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<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("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<AudioTrack> 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<AudioTrack> 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<AudioTrack> 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<AudioTrack> 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<AudioTrack> 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

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 21, 3:59 PM (1 d, 15 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fc/c2/a70811c9f63515d49adef3cf3b47

Event Timeline