diff --git a/pom.xml b/pom.xml
index f7049409..97d9c501 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,7 @@
com.sedmelluq
lavaplayer
- 1.3.19
+ 1.3.20
org.apache.commons
@@ -75,7 +75,7 @@
net.dv8tion
JDA
- 3.8.3_462
+ 4.0.0_39
compile
@@ -96,7 +96,7 @@
se.michaelthelin.spotify
spotify-web-api-java
- 2.1.2
+ 2.2.0
org.hibernate
diff --git a/resources/current-version.txt b/resources/current-version.txt
index 460b6d89..308b6faa 100644
--- a/resources/current-version.txt
+++ b/resources/current-version.txt
@@ -1 +1 @@
-1.6.1.4
\ No newline at end of file
+1.6.2
\ No newline at end of file
diff --git a/resources/versions.xml b/resources/versions.xml
index f6e8b042..48c223c3 100644
--- a/resources/versions.xml
+++ b/resources/versions.xml
@@ -1,6 +1,19 @@
-
+
+ **new command parser that is smarter at interpreting arguments**
+ - makes using arguments less strict and inline arguments may now be used wherever you want in the command and they are treated as regular arguments with the input to their right as value
+ - *meaning `insert track $to listName $at position` could now also be written `insert track $at position $to listName` or even `insert $to=listName $at=position track`*
+ - enables using escape characters and quotes to escape meta characters e.g. `play $spotify \$trackname` or `play $spotify "$trackname"`
+ - enables argument values containing whitespace by using quotes like `command $arg="some value"`
+ **enabled selecting several options comma separated when asked a question**
+ **added an option to select all options when asked a question by certain commands**
+ **added property to customise argument prefix**
+ **added monthly charts to charts command**
+ **help command examples now use the custom prefixes**
+ **upgrade to JDA 4**
+
+
make Spotify redirect smarter
add some handling for rare cases when PlaylistTracks have a null track
diff --git a/resources/xml-contributions/commandInterceptors.xml b/resources/xml-contributions/commandInterceptors.xml
index d330cbc5..e43365c4 100644
--- a/resources/xml-contributions/commandInterceptors.xml
+++ b/resources/xml-contributions/commandInterceptors.xml
@@ -3,8 +3,11 @@
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/resources/xml-contributions/commands.xml b/resources/xml-contributions/commands.xml
index 18f3b1d5..c90e9562 100644
--- a/resources/xml-contributions/commands.xml
+++ b/resources/xml-contributions/commands.xml
@@ -2,110 +2,111 @@
- $botify add $spotify $own from the inside $to my list.
- $botify add $queue my list
- $botify add http://someurl.com $to linkin park
+ %sadd $spotify $own from the inside $to my list.
+ %sadd $queue my list
+ %sadd http://someurl.com $to linkin park
- $botify answer 2
+ %sanswer 2
+ %sanswer 2,4,6
- $botify create my list
+ %screate my list
- $botify delete my list
+ %sdelete my list
- $botify export my list
+ %sexport my list
- $botify help play
+ %shelp play
- $botify login
+ %slogin
- $botify play
- $botify play numb
- $botify play from the inside artist:linkin park album:meteora
- $botify play someurl.com
- $botify play $youtube youtube rewind 2018
- $botify play $youtube $list $limit=5 memes
- $botify play $youtube $list important videos
- $botify play $spotify $list this is linkin park
- $botify play $spotify $list $own goat
+ %splay
+ %splay numb
+ %splay from the inside artist:linkin park album:meteora
+ %splay someurl.com
+ %splay $youtube youtube rewind 2018
+ %splay $youtube $list $limit=5 memes
+ %splay $youtube $list important videos
+ %splay $spotify $list this is linkin park
+ %splay $spotify $list $own goat
- $botify queue
- $botify queue numb
- $botify queue from the inside artist:linkin park album:meteora
- $botify queue $list my list
- $botify queue $spotify $list this is linkin park
- $botify queue $list $spotify $own favs
- $botify queue $youtube $list memes
- $botify queue $youtube $list $limit=5 memes
+ %squeue
+ %squeue numb
+ %squeue from the inside artist:linkin park album:meteora
+ %squeue $list my list
+ %squeue $spotify $list this is linkin park
+ %squeue $list $spotify $own favs
+ %squeue $youtube $list memes
+ %squeue $youtube $list $limit=5 memes
- $botify remove numb $from my list
- $botify remove http://someurl.com/video1 $from my list
- $botify remove $index 3 $from my list
- $botify remove $index 13-19 $from my list
+ %sremove numb $from my list
+ %sremove http://someurl.com/video1 $from my list
+ %sremove $index 3 $from my list
+ %sremove $index 13-19 $from my list
- $botify rename Patrice
+ %srename Patrice
- $botify repeat
- $botify repeat $one
+ %srepeat
+ %srepeat $one
- $botify rewind
- $botify rewind 6
+ %srewind
+ %srewind 6
- $botify search $list
- $botify search $list my list
- $botify search $spotify numb artist:linkin park album:meteora
- $botify search $spotify $list this is linkin park
- $botify search $youtube $list memes
- $botify search $youtube $list $limit=6 memes
+ %ssearch $list
+ %ssearch $list my list
+ %ssearch $spotify numb artist:linkin park album:meteora
+ %ssearch $spotify $list this is linkin park
+ %ssearch $youtube $list memes
+ %ssearch $youtube $list $limit=6 memes
- $botify skip
- $botify skip 6
+ %sskip
+ %sskip 6
- $botify upload my list
+ %supload my list
- $botify permission $grant play $to playbackmanager
- $botify permission $deny add $for playbackmanager
- $botify permission $clear shuffle
- $botify permission $grant $all manager
- $botify permission $grant $category playback $to playbackmanager
- $botify permission $lock add
+ %spermission $grant play $to playbackmanager
+ %spermission $deny add $for playbackmanager
+ %spermission $clear shuffle
+ %spermission $grant $all manager
+ %spermission $grant $category playback $to playbackmanager
+ %spermission $lock add
@@ -115,49 +116,49 @@
description="Clear the current queue of all tracks (except the currently playing track)."/>
- $botify forward 110
- $botify forward $minutes 2
+ %sforward 110
+ %sforward $minutes 2
- $botify reverse 110
- $botify reverse $minutes 2
+ %sreverse 110
+ %sreverse $minutes 2
- $botify preset add %s $to favs $as fav
- $botify preset play $list %s $as pl
- $botify preset forward %s $as ff
- $botify preset search $list $as list
- $botify preset play $spotify $own $list %s $as psol
- $botify preset
- $botify preset $delete psol
+ description="Create or delete a command preset or show all saved presets. Command presets can be used as shortcuts for lengthy commands or creating an alias for a command. Presets han hold one variable marked by "%%s" that may be assigned a value when using the preset. Syntax to create a preset: [preset] $as [name]. Mind that if your preset contains arguments you either need to put the preset in quotation marks or escape the argument prefixes using the escape character '\\'; see the examples for references.">
+ %spreset "add %%s $to favs" $as fav
+ %spreset "play $list %%s" $as pl
+ %spreset forward %%s $as ff
+ %spreset search \\$list $as list
+ %spreset "play $spotify $own $list %%s" $as psol
+ %spreset
+ %spreset $delete psol
- $botify prefix .
+ %sprefix .
- $botify move 5 $to 10 $on my list
- $botify move 4-6 $to 10 $on my list
- $botify move 14-16 $to 10 $on my list
+ description="Move one or several items in a botify playlist to a different index. When moving items down the playlist the items will end up behind the track that is currently at the target index or before when moving items upwards. When entering an index range to move it includes the start and end index. Indices are human, meaning they start at 1. To view full playlists with all indices search the list (%ssearch $list my list) and then click the view full list link.">
+ %smove 5 $to 10 $on my list
+ %smove 4-6 $to 10 $on my list
+ %smove 14-16 $to 10 $on my list
- $botify insert $spotify $own $list goat $to my list $at 1
- $botify insert $youtube $list favs $to my list $at 10
+ %sinsert $spotify $own $list goat $to my list $at 1
+ %sinsert $youtube $list favs $to my list $at 10
- $botify property
- $botify property color $set blue
- $botify property color $set #1DB954
- $botify property $toggle playback notification
- $botify property default source $set youtube
- $botify property default list source $set local
+ %sproperty
+ %sproperty color $set blue
+ %sproperty color $set #1DB954
+ %sproperty $toggle playback notification
+ %sproperty default source $set youtube
+ %sproperty default list source $set local
diff --git a/resources/xml-contributions/guildProperties.xml b/resources/xml-contributions/guildProperties.xml
index 853b1ab4..0e77caef 100644
--- a/resources/xml-contributions/guildProperties.xml
+++ b/resources/xml-contributions/guildProperties.xml
@@ -7,4 +7,5 @@
+
\ No newline at end of file
diff --git a/src/main/java/net/robinfriedli/botify/Botify.java b/src/main/java/net/robinfriedli/botify/Botify.java
index d44d670e..8ab73769 100644
--- a/src/main/java/net/robinfriedli/botify/Botify.java
+++ b/src/main/java/net/robinfriedli/botify/Botify.java
@@ -6,9 +6,9 @@
import org.slf4j.LoggerFactory;
import com.wrapper.spotify.SpotifyApi;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.OnlineStatus;
-import net.dv8tion.jda.core.hooks.ListenerAdapter;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.OnlineStatus;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.command.SecurityManager;
@@ -105,7 +105,7 @@ public static void shutdownListeners() {
* method waits for those threads to finish, causing a deadlock.
*
* @param millisToWait time to wait for pending actions to complete in milliseconds, after this time the bot will
- * quit either way
+ * quit either way
*/
public static void shutdown(long millisToWait) {
Botify botify = get();
diff --git a/src/main/java/net/robinfriedli/botify/audio/AudioManager.java b/src/main/java/net/robinfriedli/botify/audio/AudioManager.java
index ceb61f0a..85588e69 100644
--- a/src/main/java/net/robinfriedli/botify/audio/AudioManager.java
+++ b/src/main/java/net/robinfriedli/botify/audio/AudioManager.java
@@ -13,10 +13,11 @@
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.VoiceChannel;
-import net.dv8tion.jda.core.exceptions.InsufficientPermissionException;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.VoiceChannel;
+import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
+import net.robinfriedli.botify.audio.spotify.SpotifyService;
import net.robinfriedli.botify.audio.youtube.YouTubeService;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.command.widgets.NowPlayingWidget;
@@ -55,7 +56,15 @@ public AudioManager(YouTubeService youTubeService, SessionFactory sessionFactory
guildManager.setAudioManager(this);
}
- public void playTrack(Guild guild, @Nullable VoiceChannel channel) {
+ public void startPlayback(Guild guild, @Nullable VoiceChannel channel) {
+ playTrack(guild, channel, false);
+ }
+
+ public void startOrResumePlayback(Guild guild, @Nullable VoiceChannel channel) {
+ playTrack(guild, channel, true);
+ }
+
+ public void playTrack(Guild guild, @Nullable VoiceChannel channel, boolean resumePaused) {
AudioPlayback playback = getPlaybackForGuild(guild);
if (channel != null) {
@@ -64,15 +73,12 @@ public void playTrack(Guild guild, @Nullable VoiceChannel channel) {
throw new InvalidCommandException("Not in a voice channel");
}
- QueueIterator currentQueueIterator = playback.getCurrentQueueIterator();
- Playable current = playback.getAudioQueue().getCurrent();
- if (!playback.isPaused()
- || (currentQueueIterator != null && !current.matches(currentQueueIterator.getCurrentlyPlaying()))) {
+ if (playback.isPaused() && resumePaused) {
+ playback.unpause();
+ } else {
QueueIterator queueIterator = new QueueIterator(playback, this);
playback.setCurrentQueueIterator(queueIterator);
queueIterator.playNext();
- } else {
- playback.unpause();
}
}
@@ -97,8 +103,8 @@ public AudioPlayerManager getPlayerManager() {
return playerManager;
}
- public PlayableFactory createPlayableFactory(Guild guild) {
- return new PlayableFactory(urlAudioLoader, youTubeService, guildManager.getContextForGuild(guild).getTrackLoadingExecutor());
+ public PlayableFactory createPlayableFactory(Guild guild, SpotifyService spotifyService) {
+ return new PlayableFactory(spotifyService, urlAudioLoader, youTubeService, guildManager.getContextForGuild(guild).getTrackLoadingExecutor());
}
void createHistoryEntry(Playable playable, Guild guild) {
diff --git a/src/main/java/net/robinfriedli/botify/audio/AudioPlayback.java b/src/main/java/net/robinfriedli/botify/audio/AudioPlayback.java
index 5d5ac30f..2f50f8d5 100644
--- a/src/main/java/net/robinfriedli/botify/audio/AudioPlayback.java
+++ b/src/main/java/net/robinfriedli/botify/audio/AudioPlayback.java
@@ -5,10 +5,10 @@
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.VoiceChannel;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.VoiceChannel;
/**
* There is exactly one AudioPlayback per guild instantiated when initializing the guild. This class holds all information
diff --git a/src/main/java/net/robinfriedli/botify/audio/AudioPlayerSendHandler.java b/src/main/java/net/robinfriedli/botify/audio/AudioPlayerSendHandler.java
index 17146aa3..df73bb58 100644
--- a/src/main/java/net/robinfriedli/botify/audio/AudioPlayerSendHandler.java
+++ b/src/main/java/net/robinfriedli/botify/audio/AudioPlayerSendHandler.java
@@ -1,10 +1,13 @@
package net.robinfriedli.botify.audio;
+import java.nio.ByteBuffer;
+
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
-import net.dv8tion.jda.core.audio.AudioSendHandler;
+import net.dv8tion.jda.api.audio.AudioSendHandler;
public class AudioPlayerSendHandler implements AudioSendHandler {
+
private final AudioPlayer audioPlayer;
private AudioFrame lastFrame;
@@ -19,8 +22,8 @@ public boolean canProvide() {
}
@Override
- public byte[] provide20MsAudio() {
- return lastFrame.getData();
+ public ByteBuffer provide20MsAudio() {
+ return ByteBuffer.wrap(lastFrame.getData());
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/audio/AudioQueue.java b/src/main/java/net/robinfriedli/botify/audio/AudioQueue.java
index abf007a7..2cf6a0cc 100644
--- a/src/main/java/net/robinfriedli/botify/audio/AudioQueue.java
+++ b/src/main/java/net/robinfriedli/botify/audio/AudioQueue.java
@@ -8,8 +8,8 @@
import java.util.stream.IntStream;
import com.google.common.collect.Lists;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.discord.properties.ColorSchemeProperty;
import net.robinfriedli.botify.entities.GuildSpecification;
@@ -167,10 +167,10 @@ public EmbedBuilder buildMessageEmbed(AudioPlayback playback, Guild guild) {
String currentPosition = Util.normalizeMillis(playback.getCurrentPositionMs());
Playable current = getCurrent();
- String duration = Util.normalizeMillis(current.getDurationMsInterruptible());
+ String duration = Util.normalizeMillis(current.durationMs());
embedBuilder.addField(
"Current",
- "| " + current.getDisplayInterruptible() + " - " + currentPosition + " / " + duration,
+ "| " + current.display() + " - " + currentPosition + " / " + duration,
false
);
@@ -278,7 +278,7 @@ public void clear() {
* Clear the current tracks in this queue
*
* @param retainCurrent keeps the track that is referenced by the currentTrack index in the queue, this is used
- * when the track is currently being played
+ * when the track is currently being played
*/
public void clear(boolean retainCurrent) {
if (!isEmpty() && retainCurrent) {
@@ -367,7 +367,7 @@ public void randomize() {
* Generates the random queue order when enabling the shuffle option
*
* @param protectCurrent if true this makes sure that the current track will remain in the same position, used
- * when the playback is currently playing
+ * when the playback is currently playing
*/
public void randomize(boolean protectCurrent) {
randomizedOrder.clear();
@@ -420,8 +420,8 @@ private void appendIcon(StringBuilder builder, String unicode, boolean enabled)
}
private void appendPlayable(StringBuilder trackListBuilder, Playable playable) {
- String display = playable.getDisplayInterruptible();
- long durationMs = playable.getDurationMsInterruptible();
+ String display = playable.display();
+ long durationMs = playable.durationMs();
trackListBuilder.append("| ").append(display).append(" - ").append(Util.normalizeMillis(durationMs)).append(System.lineSeparator());
}
diff --git a/src/main/java/net/robinfriedli/botify/audio/Playable.java b/src/main/java/net/robinfriedli/botify/audio/Playable.java
index 85306c37..b568d13f 100644
--- a/src/main/java/net/robinfriedli/botify/audio/Playable.java
+++ b/src/main/java/net/robinfriedli/botify/audio/Playable.java
@@ -6,9 +6,10 @@
import javax.annotation.Nullable;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.entities.Playlist;
import net.robinfriedli.botify.entities.PlaylistItem;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import org.hibernate.Session;
/**
@@ -18,81 +19,117 @@ public interface Playable {
/**
* @return The url of the music file to stream
- * @throws InterruptedException if the thread loading the data asynchronously gets interrupted
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
*/
- String getPlaybackUrl() throws InterruptedException;
+ String getPlaybackUrl() throws UnavailableResourceException;
/**
* @return an id that uniquely identifies this playable together with {@link #getSource()}
*/
- String getId() throws InterruptedException;
+ String getId() throws UnavailableResourceException;
/**
* @return The title of the Playable. For Spotify it's the track name, for YouTube it's the video title and for other
* URLs it's either the tile or the URL depending on whether or not a title could be found
- * @throws InterruptedException if the thread loading the data asynchronously gets interrupted
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
*/
- String getDisplay() throws InterruptedException;
+ String getDisplay() throws UnavailableResourceException;
/**
- * @return the display of the Playable, showing interrupted Playables as "[UNAVAILABLE]"
+ * @return the display of the Playable, showing cancelled Playables as "[UNAVAILABLE]"
*/
- default String getDisplayInterruptible() {
+ default String display() {
try {
return getDisplay();
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
return "[UNAVAILABLE]";
}
}
/**
* @return the display of the Playable
- * @throws InterruptedException if the thread loading the data asynchronously gets interrupted
- * @throws TimeoutException if the data is not loaded in time
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
+ * @throws TimeoutException if the data is not loaded in time
*/
- String getDisplay(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException;
+ String getDisplay(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException;
/**
- * @return the display of the Playable, showing interrupted Playables as "[UNAVAILABLE]" and Playables that couldn't
+ * @return the display of the Playable, showing cancelled Playables as "[UNAVAILABLE]" and Playables that couldn't
* load in time as "Loading..."
*/
- default String getDisplayInterruptible(long timeOut, TimeUnit unit) {
+ default String display(long timeOut, TimeUnit unit) {
try {
return getDisplay(timeOut, unit);
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
return "[UNAVAILABLE]";
} catch (TimeoutException e) {
return "Loading...";
}
}
+ /**
+ * Return the display of the playable getting the current value without waiting for completion of the value,
+ * returning the provided alternativeValue instead if the playable is not completed.
+ *
+ * @param alternativeValue the value to return instead if the value has not been loaded yet
+ * @return the display
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
+ */
+ String getDisplayNow(String alternativeValue) throws UnavailableResourceException;
+
+ default String getDisplayNow() {
+ try {
+ return getDisplayNow("Loading...");
+ } catch (UnavailableResourceException e) {
+ return "[UNAVAILABLE]";
+ }
+ }
+
/**
* @return The duration of the audio track in milliseconds
- * @throws InterruptedException if loading the data asynchronously was interrupted
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
*/
- long getDurationMs() throws InterruptedException;
+ long getDurationMs() throws UnavailableResourceException;
/**
- * @return The duration of the audio track in milliseconds or 0 if loading the Playable was interrupted
+ * @return The duration of the audio track in milliseconds or 0 if loading the Playable was cancelled
*/
- default long getDurationMsInterruptible() {
+ default long durationMs() {
try {
return getDurationMs();
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
return 0;
}
}
/**
- * @return The duration of the audio track in milliseconds or 0 if loading the Playable was interrupted or did
+ * @return The duration of the audio track in milliseconds or 0 if loading the Playable was cancelled or did
* not load in time
*/
- long getDurationMs(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException;
+ long getDurationMs(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException;
- default long getDurationMsInterruptible(long timeOut, TimeUnit unit) {
+ default long durationMs(long timeOut, TimeUnit unit) {
try {
return getDurationMs(timeOut, unit);
- } catch (InterruptedException | TimeoutException e) {
+ } catch (UnavailableResourceException | TimeoutException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the duration now without waiting for the playable to finish, returning the alternativeValue instead if not
+ * done
+ *
+ * @param alternativeValue the value to return if the value has not been loaded yet
+ * @return the display
+ * @throws UnavailableResourceException if loading the item was cancelled due to being unavailable or cancelled
+ */
+ long getDurationNow(long alternativeValue) throws UnavailableResourceException;
+
+ default long getDurationNow() {
+ try {
+ return getDurationNow(0);
+ } catch (UnavailableResourceException e) {
return 0;
}
}
@@ -101,7 +138,7 @@ default long getDurationMsInterruptible(long timeOut, TimeUnit unit) {
* Exports this playable as a persistable {@link PlaylistItem}
*
* @param playlist the playlist this item will be a part of
- * @param user the user that added the item
+ * @param user the user that added the item
* @return the create item (not persisted yet)
*/
PlaylistItem export(Playlist playlist, User user, Session session);
@@ -125,17 +162,4 @@ default long getDurationMsInterruptible(long timeOut, TimeUnit unit) {
*/
void setCached(AudioTrack audioTrack);
- default boolean matches(Playable playable) {
- if (playable == null) {
- return false;
- }
-
- try {
- return getSource().equals(playable.getSource()) && getId().equals(playable.getId());
- } catch (InterruptedException ignored) {
- }
-
- return false;
- }
-
}
diff --git a/src/main/java/net/robinfriedli/botify/audio/PlayableFactory.java b/src/main/java/net/robinfriedli/botify/audio/PlayableFactory.java
index 60e10abc..33970cea 100644
--- a/src/main/java/net/robinfriedli/botify/audio/PlayableFactory.java
+++ b/src/main/java/net/robinfriedli/botify/audio/PlayableFactory.java
@@ -18,6 +18,8 @@
import com.wrapper.spotify.exceptions.SpotifyWebApiException;
import com.wrapper.spotify.exceptions.detailed.BadRequestException;
import com.wrapper.spotify.exceptions.detailed.NotFoundException;
+import com.wrapper.spotify.model_objects.specification.AlbumSimplified;
+import com.wrapper.spotify.model_objects.specification.PlaylistSimplified;
import com.wrapper.spotify.model_objects.specification.Track;
import net.robinfriedli.botify.audio.spotify.SpotifyService;
import net.robinfriedli.botify.audio.spotify.TrackWrapper;
@@ -26,6 +28,7 @@
import net.robinfriedli.botify.audio.youtube.YouTubeService;
import net.robinfriedli.botify.audio.youtube.YouTubeVideo;
import net.robinfriedli.botify.concurrent.GuildTrackLoadingExecutor;
+import net.robinfriedli.botify.concurrent.Invoker;
import net.robinfriedli.botify.entities.UrlTrack;
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.botify.exceptions.NoResultsFoundException;
@@ -38,14 +41,18 @@
*/
public class PlayableFactory {
+ private final SpotifyService spotifyService;
private final UrlAudioLoader urlAudioLoader;
private final YouTubeService youTubeService;
private final GuildTrackLoadingExecutor trackLoadingExecutor;
+ private final Invoker invoker;
- public PlayableFactory(UrlAudioLoader urlAudioLoader, YouTubeService youTubeService, GuildTrackLoadingExecutor trackLoadingExecutor) {
+ public PlayableFactory(SpotifyService spotifyService, UrlAudioLoader urlAudioLoader, YouTubeService youTubeService, GuildTrackLoadingExecutor trackLoadingExecutor) {
+ this.spotifyService = spotifyService;
this.urlAudioLoader = urlAudioLoader;
this.youTubeService = youTubeService;
this.trackLoadingExecutor = trackLoadingExecutor;
+ this.invoker = new Invoker();
}
/**
@@ -53,8 +60,8 @@ public PlayableFactory(UrlAudioLoader urlAudioLoader, YouTubeService youTubeServ
* necessary.
*
* @param redirectSpotify if true the matching YouTube video is loaded to play the full track using
- * {@link YouTubeService#redirectSpotify(HollowYouTubeVideo)}, else a {@link TrackWrapper} is created to play the
- * preview mp3 provided by Spotify
+ * {@link YouTubeService#redirectSpotify(HollowYouTubeVideo)}, else a {@link TrackWrapper} is created to play the
+ * preview mp3 provided by Spotify
*/
public Playable createPlayable(boolean redirectSpotify, Object track) {
if (track instanceof Playable) {
@@ -76,44 +83,69 @@ public Playable createPlayable(boolean redirectSpotify, Object track) {
}
}
- public List createPlayables(boolean redirectSpotify, Collection> tracks) {
- return createPlayables(redirectSpotify, tracks, true);
+ public List createPlayables(boolean redirectSpotify, Collection> items) {
+ return createPlayables(redirectSpotify, items, true);
+ }
+
+ public List createPlayables(boolean redirectSpotify, Object item) {
+ return createPlayables(redirectSpotify, item, true);
+ }
+
+ public List createPlayables(boolean redirectSpotify, Object item, boolean mayInterrupt) {
+ if (item instanceof Collection) {
+ return createPlayables(redirectSpotify, (Collection) item, mayInterrupt);
+ } else {
+ return createPlayables(redirectSpotify, Lists.newArrayList(item), mayInterrupt);
+ }
}
/**
* Creates Playables for a Collection of Objects; YouTube videos or Spotify Tracks.
*
- * @param redirectSpotify f true the matching YouTube video is loaded to play the full track using
- * {@link YouTubeService#redirectSpotify(HollowYouTubeVideo)}, else a {@link TrackWrapper} is created to play the
- * preview mp3 provided by Spotify
- * @param tracks the objects to create a Playable for
- * @param mayInterrupt determines whether loading the playables should be interrupted if the same
- * {@link GuildTrackLoadingExecutor} loads different playables where mayInterrupt is also true. This is used for
- * the play command so that when the user starts playing different tracks the bot can stop wasting resources on loading
- * the playables that won't be needed anymore. This should never be true for command like the Queue or AddCommand
- * where invoking the command again should not cancel out the last command.
- * @return the create Playables
+ * @param redirectSpotify if true the matching YouTube video is loaded to play the full track using
+ * {@link YouTubeService#redirectSpotify(HollowYouTubeVideo)}, else a {@link TrackWrapper} is created to play the
+ * preview mp3 provided by Spotify
+ * @param items the objects to create a Playable for
+ * @param mayInterrupt determines whether loading the playables should be interrupted if the same
+ * {@link GuildTrackLoadingExecutor} loads different playables where mayInterrupt is also true. This is used for
+ * the play command so that when the user starts playing different tracks the bot can stop wasting resources on loading
+ * the playables that won't be needed anymore. This should never be true for commands like the Queue or AddCommand
+ * where invoking the command again should not cancel out the last command.
+ * @return the created Playables
*/
- public List createPlayables(boolean redirectSpotify, Collection> tracks, boolean mayInterrupt) {
+ public List createPlayables(boolean redirectSpotify, Collection> items, boolean mayInterrupt) {
List playables = Lists.newArrayList();
List tracksToRedirect = Lists.newArrayList();
+ List youTubePlaylistsToLoad = Lists.newArrayList();
- for (Object track : tracks) {
- if (track instanceof Playable) {
- playables.add((Playable) track);
- } else if (track instanceof Track) {
- if (redirectSpotify) {
- HollowYouTubeVideo youTubeVideo = new HollowYouTubeVideo(youTubeService, (Track) track);
- tracksToRedirect.add(youTubeVideo);
- playables.add(youTubeVideo);
- } else {
- playables.add(new TrackWrapper((Track) track));
+ try {
+ for (Object item : items) {
+ if (item instanceof Playable) {
+ playables.add((Playable) item);
+ } else if (item instanceof Track) {
+ handleTrack((Track) item, redirectSpotify, tracksToRedirect, playables);
+ } else if (item instanceof UrlTrack) {
+ playables.add(((UrlTrack) item).asPlayable());
+ } else if (item instanceof YouTubePlaylist) {
+ YouTubePlaylist youTubePlaylist = ((YouTubePlaylist) item);
+ playables.addAll(youTubePlaylist.getVideos());
+ youTubePlaylistsToLoad.add(youTubePlaylist);
+ } else if (item instanceof PlaylistSimplified) {
+ List t = invoker.runWithCredentials(spotifyService.getSpotifyApi(), () -> spotifyService.getPlaylistTracks((PlaylistSimplified) item));
+ for (Track track : t) {
+ handleTrack(track, redirectSpotify, tracksToRedirect, playables);
+ }
+ } else if (item instanceof AlbumSimplified) {
+ List t = invoker.runWithCredentials(spotifyService.getSpotifyApi(), () -> spotifyService.getAlbumTracks((AlbumSimplified) item));
+ for (Track track : t) {
+ handleTrack(track, redirectSpotify, tracksToRedirect, playables);
+ }
+ } else if (item != null) {
+ throw new UnsupportedOperationException("Unsupported playable " + item.getClass());
}
- } else if (track instanceof UrlTrack) {
- playables.add(((UrlTrack) track).asPlayable());
- } else if (track != null) {
- throw new UnsupportedOperationException("Unsupported playable " + track.getClass());
}
+ } catch (Exception e) {
+ throw new RuntimeException("Exception while creating Playables", e);
}
if (!tracksToRedirect.isEmpty()) {
@@ -125,12 +157,30 @@ public List createPlayables(boolean redirectSpotify, Collection> tra
}
youTubeService.redirectSpotify(youTubeVideo);
}
+
+ for (YouTubePlaylist youTubePlaylist : youTubePlaylistsToLoad) {
+ if (Thread.currentThread().isInterrupted()) {
+ youTubePlaylistsToLoad.forEach(YouTubePlaylist::cancelLoading);
+ }
+
+ youTubeService.populateList(youTubePlaylist);
+ }
}, mayInterrupt);
}
return playables;
}
+ private void handleTrack(Track track, boolean redirectSpotify, List tracksToRedirect, List playables) {
+ if (redirectSpotify) {
+ HollowYouTubeVideo youTubeVideo = new HollowYouTubeVideo(youTubeService, track);
+ tracksToRedirect.add(youTubeVideo);
+ playables.add(youTubeVideo);
+ } else {
+ playables.add(new TrackWrapper(track));
+ }
+ }
+
public List createPlayables(YouTubePlaylist youTubePlaylist) {
return createPlayables(youTubePlaylist, true);
}
@@ -140,8 +190,8 @@ public List createPlayables(YouTubePlaylist youTubePlaylist) {
* them as Playables
*
* @param youTubePlaylist the YouTube playlist to load the videos for
- * @param mayInterrupt whether or not loading the tracks can be interrupted by a similar action,
- * see {@link #createPlayables(boolean, Collection, boolean)}
+ * @param mayInterrupt whether or not loading the tracks can be interrupted by a similar action,
+ * see {@link #createPlayables(boolean, Collection, boolean)}
* @return the {@link HollowYouTubeVideo}s as Playables
*/
public List createPlayables(YouTubePlaylist youTubePlaylist, boolean mayInterrupt) {
@@ -212,6 +262,16 @@ public Playable createPlayable(String url, SpotifyApi spotifyApi, boolean redire
}
}
+ /**
+ * Create Playables for any URL.
+ *
+ * @param url the url that points to a playable track or playlist
+ * @param spotifyApi the SpotifyApi instance
+ * @param redirectSpotify if true the loaded Spotify tracks will get directed to YouTube videos
+ * @param mayInterrupt whether or not loading the tracks can be interrupted by a similar action,
+ * see {@link #createPlayables(boolean, Collection, boolean)}
+ * @return the created playables
+ */
public List createPlayables(String url, SpotifyApi spotifyApi, boolean redirectSpotify, boolean mayInterrupt) {
List playables;
@@ -291,7 +351,7 @@ private List createPlayablesFromSpotifyUrl(URI uri, SpotifyApi spotify
List playlistTracks = spotifyService.getPlaylistTracks(playlistId);
return createPlayables(redirectSpotify, playlistTracks, mayInterrupt);
} catch (NotFoundException e) {
- throw new NoResultsFoundException("No playlist found for id " + playlistId);
+ throw new NoResultsFoundException(String.format("No Spotify playlist found for id '%s'", playlistId));
} catch (IOException | SpotifyWebApiException e) {
throw new RuntimeException("Exception during Spotify request", e);
} finally {
@@ -309,7 +369,7 @@ private List createPlayablesFromSpotifyUrl(URI uri, SpotifyApi spotify
Track track = spotifyApi.getTrack(trackId).build().execute();
return Lists.newArrayList(createPlayable(redirectSpotify, track));
} catch (NotFoundException e) {
- throw new NoResultsFoundException("No track found for id " + trackId);
+ throw new NoResultsFoundException(String.format("No Spotify track found for id '%s'", trackId));
} catch (IOException | SpotifyWebApiException e) {
throw new RuntimeException("Exception during Spotify request", e);
} finally {
@@ -327,7 +387,7 @@ private List createPlayablesFromSpotifyUrl(URI uri, SpotifyApi spotify
List albumTracks = spotifyService.getAlbumTracks(albumId);
return createPlayables(redirectSpotify, albumTracks, mayInterrupt);
} catch (BadRequestException e) {
- throw new NoResultsFoundException("No album found for id " + albumId);
+ throw new NoResultsFoundException(String.format("No album found for id '%s'", albumId));
} catch (IOException | SpotifyWebApiException e) {
throw new RuntimeException("Exception during Spotify request", e);
} finally {
diff --git a/src/main/java/net/robinfriedli/botify/audio/QueueIterator.java b/src/main/java/net/robinfriedli/botify/audio/QueueIterator.java
index 673d74de..c2d19892 100644
--- a/src/main/java/net/robinfriedli/botify/audio/QueueIterator.java
+++ b/src/main/java/net/robinfriedli/botify/audio/QueueIterator.java
@@ -8,14 +8,15 @@
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.discord.MessageService;
import net.robinfriedli.botify.discord.properties.AbstractGuildProperty;
import net.robinfriedli.botify.discord.properties.ColorSchemeProperty;
import net.robinfriedli.botify.entities.GuildSpecification;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import net.robinfriedli.botify.util.EmojiConstants;
import net.robinfriedli.botify.util.PropertiesLoadingService;
import net.robinfriedli.botify.util.StaticSessionProvider;
@@ -116,7 +117,7 @@ void playNext() {
String playbackUrl;
try {
playbackUrl = track.getPlaybackUrl();
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
++retryCount;
iterateQueue(playback, queue, true);
return;
@@ -173,7 +174,7 @@ private void iterateQueue(AudioPlayback playback, AudioQueue queue, boolean igno
private void sendError(Playable track, Throwable e) {
if (retryCount == 0) {
EmbedBuilder embedBuilder = new EmbedBuilder();
- embedBuilder.setTitle("Could not load track " + track.getDisplayInterruptible());
+ embedBuilder.setTitle("Could not load track " + track.display());
embedBuilder.setDescription(e.getMessage());
embedBuilder.setColor(Color.RED);
@@ -188,10 +189,10 @@ private void sendError(Playable track, Throwable e) {
private void sendCurrentTrackNotification(Playable currentTrack) {
MessageService messageService = new MessageService();
EmbedBuilder embedBuilder = new EmbedBuilder();
- embedBuilder.addField("Now playing", currentTrack.getDisplayInterruptible(), false);
+ embedBuilder.addField("Now playing", currentTrack.display(), false);
if (queue.hasNext()) {
- embedBuilder.addField("Next", queue.getNext().getDisplayInterruptible(), false);
+ embedBuilder.addField("Next", queue.getNext().display(), false);
}
StringBuilder footerBuilder = new StringBuilder();
diff --git a/src/main/java/net/robinfriedli/botify/audio/UrlPlayable.java b/src/main/java/net/robinfriedli/botify/audio/UrlPlayable.java
index b912f306..4934de4b 100644
--- a/src/main/java/net/robinfriedli/botify/audio/UrlPlayable.java
+++ b/src/main/java/net/robinfriedli/botify/audio/UrlPlayable.java
@@ -5,7 +5,7 @@
import javax.annotation.Nullable;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.entities.Playlist;
import net.robinfriedli.botify.entities.PlaylistItem;
import net.robinfriedli.botify.entities.UrlTrack;
@@ -57,6 +57,11 @@ public String getDisplay(long timeOut, TimeUnit unit) {
return getDisplay();
}
+ @Override
+ public String getDisplayNow(String alternativeValue) {
+ return getDisplay();
+ }
+
@Override
public long getDurationMs() {
return duration;
@@ -67,6 +72,11 @@ public long getDurationMs(long timeOut, TimeUnit unit) {
return getDurationMs();
}
+ @Override
+ public long getDurationNow(long alternativeValue) {
+ return getDurationMs();
+ }
+
@Override
public PlaylistItem export(Playlist playlist, User user, Session session) {
return new UrlTrack(this, user, playlist);
diff --git a/src/main/java/net/robinfriedli/botify/audio/spotify/SpotifyService.java b/src/main/java/net/robinfriedli/botify/audio/spotify/SpotifyService.java
index 6389bca1..b01857ac 100644
--- a/src/main/java/net/robinfriedli/botify/audio/spotify/SpotifyService.java
+++ b/src/main/java/net/robinfriedli/botify/audio/spotify/SpotifyService.java
@@ -148,6 +148,10 @@ public List getAlbumTracks(String albumId) throws IOException, SpotifyWeb
return tracks;
}
+ public List getAlbumTracks(AlbumSimplified albumSimplified) throws IOException, SpotifyWebApiException {
+ return getAlbumTracks(albumSimplified.getId());
+ }
+
public SpotifyApi getSpotifyApi() {
return spotifyApi;
}
diff --git a/src/main/java/net/robinfriedli/botify/audio/spotify/TrackWrapper.java b/src/main/java/net/robinfriedli/botify/audio/spotify/TrackWrapper.java
index f67ef9ee..e0524058 100644
--- a/src/main/java/net/robinfriedli/botify/audio/spotify/TrackWrapper.java
+++ b/src/main/java/net/robinfriedli/botify/audio/spotify/TrackWrapper.java
@@ -4,7 +4,7 @@
import com.wrapper.spotify.model_objects.specification.ArtistSimplified;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.audio.AbstractSoftCachedPlayable;
import net.robinfriedli.botify.audio.Playable;
import net.robinfriedli.botify.audio.youtube.HollowYouTubeVideo;
@@ -50,6 +50,11 @@ public String getDisplay(long timeOut, TimeUnit unit) {
return getDisplay();
}
+ @Override
+ public String getDisplayNow(String alternativeValue) {
+ return getDisplay();
+ }
+
@Override
public long getDurationMs() {
return track.getDurationMs();
@@ -60,6 +65,11 @@ public long getDurationMs(long timeOut, TimeUnit unit) {
return getDurationMs();
}
+ @Override
+ public long getDurationNow(long alternativeValue) {
+ return getDurationMs();
+ }
+
@Override
public PlaylistItem export(Playlist playlist, User user, Session session) {
return new Song(track, user, playlist, session);
diff --git a/src/main/java/net/robinfriedli/botify/audio/youtube/HollowYouTubeVideo.java b/src/main/java/net/robinfriedli/botify/audio/youtube/HollowYouTubeVideo.java
index 75b56ba6..8b4dc46f 100644
--- a/src/main/java/net/robinfriedli/botify/audio/youtube/HollowYouTubeVideo.java
+++ b/src/main/java/net/robinfriedli/botify/audio/youtube/HollowYouTubeVideo.java
@@ -11,6 +11,7 @@
import com.wrapper.spotify.model_objects.specification.Track;
import net.robinfriedli.botify.audio.AbstractSoftCachedPlayable;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
/**
* YouTube video when the data has not been loaded yet. This is used for YouTube playlist elements or Spotify tracks that
@@ -39,7 +40,7 @@ public HollowYouTubeVideo(YouTubeService youTubeService, @Nullable Track redirec
}
@Override
- public String getTitle() throws InterruptedException {
+ public String getTitle() throws UnavailableResourceException {
return getCompleted(title);
}
@@ -48,17 +49,22 @@ public void setTitle(String title) {
}
@Override
- public String getTitle(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException {
+ public String getTitle(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException {
return getWithTimeout(title, timeOut, unit);
}
@Override
- public String getVideoId() throws InterruptedException {
+ public String getDisplayNow(String alternativeValue) throws UnavailableResourceException {
+ return getNow(title, alternativeValue);
+ }
+
+ @Override
+ public String getVideoId() throws UnavailableResourceException {
return getCompleted(id);
}
@Override
- public String getId() throws InterruptedException {
+ public String getId() throws UnavailableResourceException {
return redirectedSpotifyTrack != null ? redirectedSpotifyTrack.getId() : getVideoId();
}
@@ -67,12 +73,7 @@ public void setId(String id) {
}
@Override
- public String getVideoId(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException {
- return getWithTimeout(id, timeOut, unit);
- }
-
- @Override
- public long getDuration() throws InterruptedException {
+ public long getDuration() throws UnavailableResourceException {
return getCompleted(duration);
}
@@ -81,10 +82,15 @@ public void setDuration(long duration) {
}
@Override
- public long getDuration(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException {
+ public long getDuration(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException {
return getWithTimeout(duration, timeOut, unit);
}
+ @Override
+ public long getDurationNow(long alternativeValue) throws UnavailableResourceException {
+ return getNow(duration, alternativeValue);
+ }
+
@Nullable
@Override
public Track getRedirectedSpotifyTrack() {
@@ -120,7 +126,7 @@ public String getSource() {
return redirectedSpotifyTrack != null ? "Spotify" : "YouTube";
}
- private E getCompleted(CompletableFuture future) throws InterruptedException {
+ private E getCompleted(CompletableFuture future) throws UnavailableResourceException {
try {
if (!future.isDone() && redirectedSpotifyTrack != null) {
youTubeService.redirectSpotify(this);
@@ -132,17 +138,28 @@ private E getCompleted(CompletableFuture future) throws InterruptedExcept
} catch (TimeoutException e) {
throw new RuntimeException("Video loading timed out", e);
} catch (CancellationException e) {
- throw new InterruptedException();
+ throw new UnavailableResourceException();
}
}
- private E getWithTimeout(CompletableFuture future, long time, TimeUnit unit) throws InterruptedException, TimeoutException {
+ private E getWithTimeout(CompletableFuture future, long time, TimeUnit unit) throws UnavailableResourceException, TimeoutException {
try {
return future.get(time, unit);
- } catch (ExecutionException e) {
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (CancellationException e) {
+ throw new UnavailableResourceException();
+ }
+ }
+
+ private E getNow(CompletableFuture future, E alternativeValue) throws UnavailableResourceException {
+ try {
+ return future.getNow(alternativeValue);
+ } catch (CompletionException e) {
throw new RuntimeException(e);
} catch (CancellationException e) {
- throw new InterruptedException();
+ throw new UnavailableResourceException();
}
}
+
}
diff --git a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeService.java b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeService.java
index d108763f..a96989cd 100644
--- a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeService.java
+++ b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeService.java
@@ -27,6 +27,7 @@
import net.robinfriedli.botify.command.commands.PlayCommand;
import net.robinfriedli.botify.command.commands.QueueCommand;
import net.robinfriedli.botify.exceptions.NoResultsFoundException;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import net.robinfriedli.stringlist.StringList;
import net.robinfriedli.stringlist.StringListImpl;
@@ -196,7 +197,7 @@ private List searchVideos(long limit, String searchTerm) throws IO
List items = search.execute().getItems();
if (items.isEmpty()) {
- throw new NoResultsFoundException("No YouTube video found for " + searchTerm);
+ throw new NoResultsFoundException(String.format("No YouTube video found for '%s'", searchTerm));
}
return items;
@@ -215,7 +216,8 @@ private List getAllVideos(List videoIds) throws IOException {
VideoListResponse response = query.execute();
videos.addAll(response.getItems());
nextPageToken = response.getNextPageToken();
- } while (nextPageToken != null);
+ query.setPageToken(nextPageToken);
+ } while (!Strings.isNullOrEmpty(nextPageToken));
return videos;
}
@@ -301,7 +303,7 @@ private List searchPlaylists(long limit, String searchTerm) throws
List items = playlistSearch.execute().getItems();
if (items.isEmpty()) {
- throw new NoResultsFoundException("No YouTube playlist found for " + searchTerm);
+ throw new NoResultsFoundException(String.format("No YouTube playlist found for '%s'", searchTerm));
}
return items;
@@ -388,8 +390,8 @@ private void loadDurationsAsync(List videos) {
String id;
try {
id = hollowYouTubeVideo.getVideoId();
- } catch (InterruptedException e) {
- return;
+ } catch (UnavailableResourceException e) {
+ continue;
}
videoIds.add(id);
}
@@ -402,8 +404,8 @@ private void loadDurationsAsync(List videos) {
Long duration;
try {
duration = durationMillis.get(video.getVideoId());
- } catch (InterruptedException e) {
- return;
+ } catch (UnavailableResourceException e) {
+ continue;
}
video.setDuration(duration != null ? duration : 0);
}
@@ -422,7 +424,7 @@ private void loadDurationsAsync(List videos) {
* but it's necessary if shuffle is enabled when loading a large playlist as the populateList methods might take a
* while until the items towards the end of the list are loaded.
*
- * @param index the index of the item to load
+ * @param index the index of the item to load
* @param playlist the playlist the item is a part of
* @deprecated deprecated as of 1.2.1 since the method is unreliable when the playlist contains unavailable items and
* very inefficient for minimal gain
@@ -492,7 +494,7 @@ public YouTubeVideo videoForId(String id) {
List items = videoRequest.execute().getItems();
if (items.isEmpty()) {
- throw new NoResultsFoundException("No YouTube video found for id " + id);
+ throw new NoResultsFoundException(String.format("No YouTube video found for id '%s'", id));
}
Video video = items.get(0);
@@ -511,7 +513,7 @@ public YouTubePlaylist playlistForId(String id) {
List items = playlistRequest.execute().getItems();
if (items.isEmpty()) {
- throw new NoResultsFoundException("No YouTube playlist found for id " + id);
+ throw new NoResultsFoundException(String.format("No YouTube playlist found for id '%s'", id));
}
Playlist playlist = items.get(0);
diff --git a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideo.java b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideo.java
index 13b873d2..846dda56 100644
--- a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideo.java
+++ b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideo.java
@@ -6,11 +6,12 @@
import javax.annotation.Nullable;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.audio.Playable;
import net.robinfriedli.botify.entities.Playlist;
import net.robinfriedli.botify.entities.PlaylistItem;
import net.robinfriedli.botify.entities.Video;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import org.hibernate.Session;
/**
@@ -22,72 +23,76 @@ public interface YouTubeVideo extends Playable {
/**
* @return the title of the YouTube video, or in case of a cancelled {@link HollowYouTubeVideo} throw an
- * {@link InterruptedException} to signal that loading the tracks has been interrupted, the checked method
- * {@link Playable#getDisplayInterruptible()} will then show the video as "[UNAVAILABLE]"
+ * {@link UnavailableResourceException} to signal that loading the tracks has been cancelled, the checked method
+ * {@link Playable#display()} will then show the video as "[UNAVAILABLE]"
*/
- String getTitle() throws InterruptedException;
+ String getTitle() throws UnavailableResourceException;
/**
* like {@link #getTitle()} but throws a {@link TimeoutException} after the specified time limit if the
- * {@link HollowYouTubeVideo} is not loaded in time, the method {@link Playable#getDisplayInterruptible(long, TimeUnit)}
+ * {@link HollowYouTubeVideo} is not loaded in time, the method {@link Playable#display(long, TimeUnit)}
* will then show the video as "Loading..."
*/
- String getTitle(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException;
+ String getTitle(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException;
@Override
- default String getDisplay() throws InterruptedException {
+ default String getDisplay() throws UnavailableResourceException {
return getTitle();
}
@Override
- default String getDisplay(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException {
+ default String getDisplay(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException {
return getTitle(timeOut, unit);
}
- /**
- * @return the id of the YouTube video, throwing an {@link InterruptedException} if cancelled,
- * see {@link #getTitle()}
- */
- String getVideoId() throws InterruptedException;
+ @Override
+ default String getDisplayNow(String alternativeValue) throws UnavailableResourceException {
+ return getDisplay();
+ }
/**
- * @return the id of the YouTube video, throwing a {@link TimeoutException} if loading takes longer than the provided
- * amount of time, see {@link #getTitle(long, TimeUnit)}
+ * @return the id of the YouTube video, throwing an {@link UnavailableResourceException} if cancelled,
+ * see {@link #getTitle()}
*/
- String getVideoId(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException;
+ String getVideoId() throws UnavailableResourceException;
@Override
- default String getId() throws InterruptedException {
+ default String getId() throws UnavailableResourceException {
return getVideoId();
}
@Override
- default String getPlaybackUrl() throws InterruptedException {
+ default String getPlaybackUrl() throws UnavailableResourceException {
return String.format("https://www.youtube.com/watch?v=%s", getVideoId());
}
/**
- * @return the duration of the YouTube video in milliseconds or throw an {@link InterruptedException} if cancelled,
+ * @return the duration of the YouTube video in milliseconds or throw an {@link UnavailableResourceException} if cancelled,
* see {@link #getTitle()}
*/
- long getDuration() throws InterruptedException;
+ long getDuration() throws UnavailableResourceException;
/**
* @return the duration of the YouTube video in milliseconds or throw a {@link TimeoutException} if loading takes
* longer that the provided amount of time, see {@link #getTitle(long, TimeUnit)}
*/
- long getDuration(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException;
+ long getDuration(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException;
@Override
- default long getDurationMs() throws InterruptedException {
+ default long getDurationMs() throws UnavailableResourceException {
return getDuration();
}
@Override
- default long getDurationMs(long timeOut, TimeUnit unit) throws InterruptedException, TimeoutException {
+ default long getDurationMs(long timeOut, TimeUnit unit) throws UnavailableResourceException, TimeoutException {
return getDuration(timeOut, unit);
}
+ @Override
+ default long getDurationNow(long alternativeValue) throws UnavailableResourceException {
+ return getDuration();
+ }
+
@Override
default PlaylistItem export(Playlist playlist, User user, Session session) {
return new Video(this, user, playlist);
diff --git a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideoImpl.java b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideoImpl.java
index 0ea618fc..47e3555d 100644
--- a/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideoImpl.java
+++ b/src/main/java/net/robinfriedli/botify/audio/youtube/YouTubeVideoImpl.java
@@ -37,11 +37,6 @@ public String getVideoId() {
return id;
}
- @Override
- public String getVideoId(long timeOut, TimeUnit unit) {
- return getVideoId();
- }
-
@Override
public long getDuration() {
return duration;
diff --git a/src/main/java/net/robinfriedli/botify/boot/Launcher.java b/src/main/java/net/robinfriedli/botify/boot/Launcher.java
index df4e350e..89b169a1 100644
--- a/src/main/java/net/robinfriedli/botify/boot/Launcher.java
+++ b/src/main/java/net/robinfriedli/botify/boot/Launcher.java
@@ -12,11 +12,11 @@
import com.google.common.base.Strings;
import com.wrapper.spotify.SpotifyApi;
import com.wrapper.spotify.SpotifyHttpManager;
-import net.dv8tion.jda.core.AccountType;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.JDABuilder;
-import net.dv8tion.jda.core.OnlineStatus;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.AccountType;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.JDABuilder;
+import net.dv8tion.jda.api.OnlineStatus;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.youtube.YouTubeService;
@@ -106,7 +106,6 @@ public static void main(String[] args) {
// setup JDA
JDA jda = new JDABuilder(AccountType.BOT)
.setToken(discordToken)
- .setCorePoolSize(10)
.setStatus(OnlineStatus.IDLE)
.build()
.awaitReady();
diff --git a/src/main/java/net/robinfriedli/botify/boot/tasks/MigrateGuildSpecificationsTask.java b/src/main/java/net/robinfriedli/botify/boot/tasks/MigrateGuildSpecificationsTask.java
index b70987ff..1e809070 100644
--- a/src/main/java/net/robinfriedli/botify/boot/tasks/MigrateGuildSpecificationsTask.java
+++ b/src/main/java/net/robinfriedli/botify/boot/tasks/MigrateGuildSpecificationsTask.java
@@ -8,9 +8,9 @@
import com.google.common.base.Splitter;
import com.google.common.io.Files;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Role;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Role;
import net.robinfriedli.botify.boot.StartupTask;
import net.robinfriedli.botify.entities.AccessConfiguration;
import net.robinfriedli.botify.entities.GrantedRole;
diff --git a/src/main/java/net/robinfriedli/botify/boot/tasks/MigratePlaylistsTask.java b/src/main/java/net/robinfriedli/botify/boot/tasks/MigratePlaylistsTask.java
index 7eb962ba..e0390ba9 100644
--- a/src/main/java/net/robinfriedli/botify/boot/tasks/MigratePlaylistsTask.java
+++ b/src/main/java/net/robinfriedli/botify/boot/tasks/MigratePlaylistsTask.java
@@ -9,8 +9,8 @@
import com.google.common.io.Files;
import com.wrapper.spotify.SpotifyApi;
import com.wrapper.spotify.exceptions.SpotifyWebApiException;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.boot.StartupTask;
import net.robinfriedli.botify.entities.Playlist;
import net.robinfriedli.botify.entities.PlaylistItem;
diff --git a/src/main/java/net/robinfriedli/botify/boot/tasks/SetRedirectedSpotifyTrackNameTask.java b/src/main/java/net/robinfriedli/botify/boot/tasks/SetRedirectedSpotifyTrackNameTask.java
index e3aaa549..f9557e4b 100644
--- a/src/main/java/net/robinfriedli/botify/boot/tasks/SetRedirectedSpotifyTrackNameTask.java
+++ b/src/main/java/net/robinfriedli/botify/boot/tasks/SetRedirectedSpotifyTrackNameTask.java
@@ -11,8 +11,8 @@
import com.wrapper.spotify.exceptions.SpotifyWebApiException;
import com.wrapper.spotify.model_objects.credentials.ClientCredentials;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.boot.StartupTask;
import net.robinfriedli.botify.util.PropertiesLoadingService;
import net.robinfriedli.jxp.api.JxpBackend;
diff --git a/src/main/java/net/robinfriedli/botify/boot/tasks/VersionUpdateAlertTask.java b/src/main/java/net/robinfriedli/botify/boot/tasks/VersionUpdateAlertTask.java
index bf73a7d9..f239a057 100644
--- a/src/main/java/net/robinfriedli/botify/boot/tasks/VersionUpdateAlertTask.java
+++ b/src/main/java/net/robinfriedli/botify/boot/tasks/VersionUpdateAlertTask.java
@@ -8,9 +8,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.boot.StartupTask;
import net.robinfriedli.botify.discord.MessageService;
import net.robinfriedli.jxp.api.JxpBackend;
diff --git a/src/main/java/net/robinfriedli/botify/command/AbstractCommand.java b/src/main/java/net/robinfriedli/botify/command/AbstractCommand.java
index 6e5e9333..b689e9df 100644
--- a/src/main/java/net/robinfriedli/botify/command/AbstractCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/AbstractCommand.java
@@ -13,16 +13,17 @@
import org.apache.commons.lang3.tuple.Pair;
import com.google.api.client.util.Sets;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.MessageBuilder;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.MessageEmbed;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.spotify.SpotifyService;
import net.robinfriedli.botify.command.commands.AnswerCommand;
import net.robinfriedli.botify.command.interceptor.CommandInterceptorChain;
+import net.robinfriedli.botify.command.parser.CommandParser;
import net.robinfriedli.botify.concurrent.CheckedRunnable;
import net.robinfriedli.botify.concurrent.Invoker;
import net.robinfriedli.botify.discord.GuildManager;
@@ -48,18 +49,19 @@ public abstract class AbstractCommand {
private final CommandManager commandManager;
private final ArgumentContribution argumentContribution;
private final MessageService messageService;
+ private final String commandBody;
private final String identifier;
private final String description;
private final Category category;
private boolean requiresInput;
- private String commandBody;
+ private String commandInput;
// used to prevent onSuccess being called when no exception has been thrown but the command failed anyway
private boolean isFailed;
public AbstractCommand(CommandContribution commandContribution,
CommandContext context,
CommandManager commandManager,
- String commandString,
+ String commandBody,
boolean requiresInput,
String identifier,
String description,
@@ -68,13 +70,13 @@ public AbstractCommand(CommandContribution commandContribution,
this.context = context;
this.commandManager = commandManager;
this.requiresInput = requiresInput;
+ this.commandBody = commandBody;
this.identifier = identifier;
this.description = description;
this.category = category;
this.argumentContribution = setupArguments();
this.messageService = new MessageService();
-
- processCommand(commandString);
+ commandInput = "";
}
/**
@@ -100,11 +102,11 @@ public void withUserResponse(Object chosenOption) throws Exception {
}
public void verify() {
- if (requiresInput() && getCommandBody().isBlank()) {
+ if (requiresInput() && getCommandInput().isBlank()) {
throw new InvalidCommandException("That command requires more input!");
}
- getArgumentContribution().complete();
+ getArgumentContribution().verify();
}
/**
@@ -134,6 +136,10 @@ public CommandContext getContext() {
return context;
}
+ public MessageService getMessageService() {
+ return messageService;
+ }
+
/**
* Run code after setting the spotify access token to the one of the current user's login. This is required for
* commands that do not necessarily require a login, in which case the access token will always be set before
@@ -159,8 +165,22 @@ protected boolean argumentSet(String argument) {
return argumentContribution.argumentSet(argument);
}
+ protected String getArgumentValue(String argument) {
+ return getArgumentValue(argument, String.class);
+ }
+
protected E getArgumentValue(String argument, Class type) {
- return argumentContribution.getArgument(argument).getValue(type);
+ if (!argumentSet(argument)) {
+ throw new InvalidCommandException("Expected argument: " + argument);
+ }
+
+ ArgumentContribution.Argument arg = argumentContribution.require(argument, IllegalArgumentException.class);
+
+ if (!arg.hasValue()) {
+ throw new InvalidCommandException("Argument '" + argument + "' requires an assigned value!");
+ }
+
+ return arg.getValue(type);
}
protected boolean isPartitioned() {
@@ -171,10 +191,10 @@ protected boolean isPartitioned() {
* askQuestion implementation that uses the index of the option as its key and accepts a function to get the display
* for each option
*
- * @param options the available options
+ * @param options the available options
* @param displayFunc the function that returns the display for each option (e.g. for a Track it should be a function
- * that returns the track's name and artists)
- * @param the type of options
+ * that returns the track's name and artists)
+ * @param the type of options
*/
protected void askQuestion(List options, Function displayFunc) {
ClientQuestionEvent question = new ClientQuestionEvent(this);
@@ -182,6 +202,8 @@ protected void askQuestion(List options, Function displayFunc)
O option = options.get(i);
question.mapOption(String.valueOf(i), option, displayFunc.apply(option));
}
+
+ question.mapOption("all", options, "All of the above");
askQuestion(question);
}
@@ -259,11 +281,17 @@ protected void sendToActiveGuilds(MessageEmbed message) {
* Used for any command with an A $to B syntax.
*
* @return both halves of the logical two sided statement
+ * @deprecated replaced by the {@link CommandParser}; inline arguments are now treated as regular arguments with their
+ * right side up to the next argument as value. This has the benefit the the order and location of inline arguments
+ * no longer matters. Meaning 'insert a $to b $at c' could also be written 'insert a $at c $to b' or even
+ * 'insert $to=b $at=c a'
*/
+ @Deprecated
protected Pair splitInlineArgument(String argument) {
- return splitInlineArgument(getCommandBody(), argument);
+ return splitInlineArgument(getCommandInput(), argument);
}
+ @Deprecated
protected Pair splitInlineArgument(String part, String argument) {
StringList words = StringListImpl.create(part, " ");
@@ -287,6 +315,14 @@ protected Pair splitInlineArgument(String part, String argument)
return Pair.of(left.trim(), right.trim());
}
+ /**
+ * @return the string following the command identifier. This represents the command string used to parse the
+ * arguments and command input
+ */
+ public String getCommandBody() {
+ return commandBody;
+ }
+
/**
* @return the String this command gets referenced with
*/
@@ -306,8 +342,12 @@ public CommandManager getManager() {
return commandManager;
}
- public String getCommandBody() {
- return commandBody;
+ public String getCommandInput() {
+ return commandInput;
+ }
+
+ public void setCommandInput(String commandInput) {
+ this.commandInput = commandInput;
}
public boolean isFailed() {
@@ -330,6 +370,11 @@ public SpotifyService getSpotifyService() {
return context.getSpotifyService();
}
+ /**
+ * @param commandString the string following the command identifier
+ * @deprecated replaced by the {@link CommandParser}
+ */
+ @Deprecated
private void processCommand(String commandString) {
StringList words = StringListImpl.separateString(commandString, " ");
@@ -355,7 +400,7 @@ private void processCommand(String commandString) {
commandBodyIndex += word.length();
}
- commandBody = words.toString().substring(commandBodyIndex).trim();
+ commandInput = words.toString().substring(commandBodyIndex).trim();
}
public enum Category {
diff --git a/src/main/java/net/robinfriedli/botify/command/AbstractWidget.java b/src/main/java/net/robinfriedli/botify/command/AbstractWidget.java
index fb8c3028..65935a2e 100644
--- a/src/main/java/net/robinfriedli/botify/command/AbstractWidget.java
+++ b/src/main/java/net/robinfriedli/botify/command/AbstractWidget.java
@@ -3,18 +3,18 @@
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageReaction;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
-import net.dv8tion.jda.core.exceptions.ErrorResponseException;
-import net.dv8tion.jda.core.exceptions.InsufficientPermissionException;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageReaction;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.exceptions.ErrorResponseException;
+import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
import net.robinfriedli.botify.command.widgets.AbstractWidgetAction;
+import net.robinfriedli.botify.discord.MessageService;
import net.robinfriedli.botify.exceptions.UserException;
/**
@@ -61,7 +61,7 @@ public void setup() {
int finalI = i;
message.addReaction(action.getEmojiUnicode()).queue(aVoid -> {
if (finalI == actions.size() - 1 && !futureException.isDone()) {
- futureException.complete(null);
+ futureException.cancel(false);
}
}, throwable -> {
if (throwable instanceof InsufficientPermissionException || throwable instanceof ErrorResponseException) {
@@ -74,10 +74,19 @@ public void setup() {
});
}
- RuntimeException e = futureException.get();
- if (e != null) {
- throw e;
- }
+ futureException.thenAccept(e -> {
+ MessageService messageService = new MessageService();
+ if (e instanceof InsufficientPermissionException) {
+ messageService.sendError("Bot is missing permission: "
+ + ((InsufficientPermissionException) e).getPermission().getName(), message.getChannel());
+ } else if (e instanceof ErrorResponseException) {
+ if (((ErrorResponseException) e).getErrorCode() == 50013) {
+ messageService.sendError("Bot is missing permission to add reactions", message.getChannel());
+ } else {
+ logger.warn("Could not add reaction to message " + message, e);
+ }
+ }
+ });
} catch (InsufficientPermissionException e) {
// exception is actually never thrown when it should be, remove completable future hack if this ever changes
throw new UserException("Bot is missing permission: " + e.getPermission().getName(), e);
@@ -87,8 +96,6 @@ public void setup() {
} else {
logger.warn("Could not add reaction to message " + message, e);
}
- } catch (InterruptedException | ExecutionException e) {
- logger.warn("Unexpected exception while adding reaction", e);
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/ArgumentContribution.java b/src/main/java/net/robinfriedli/botify/command/ArgumentContribution.java
index 21acaaad..bfdc037f 100644
--- a/src/main/java/net/robinfriedli/botify/command/ArgumentContribution.java
+++ b/src/main/java/net/robinfriedli/botify/command/ArgumentContribution.java
@@ -1,14 +1,16 @@
package net.robinfriedli.botify.command;
+import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
-import com.google.common.collect.Lists;
+import org.apache.commons.collections4.map.CaseInsensitiveMap;
+
+import com.google.api.client.util.Lists;
import com.google.common.collect.Sets;
+import net.robinfriedli.botify.exceptions.InvalidArgumentException;
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.jxp.api.StringConverter;
import net.robinfriedli.jxp.exceptions.ConversionException;
@@ -21,8 +23,7 @@
public class ArgumentContribution {
private final AbstractCommand sourceCommand;
- private List arguments = Lists.newArrayList();
- private Map setArguments = new HashMap<>();
+ private final Map definedArguments = new CaseInsensitiveMap<>();
public ArgumentContribution(AbstractCommand sourceCommand) {
this.sourceCommand = sourceCommand;
@@ -38,7 +39,37 @@ public ArgumentContribution(AbstractCommand sourceCommand) {
*/
public Argument map(String arg) {
Argument argument = new Argument(arg);
- arguments.add(argument);
+ definedArguments.put(arg, argument);
+ return argument;
+ }
+
+ /**
+ * @param arg the identifier of the argument, case insensitive
+ * @return the found argument definition
+ */
+ public Argument get(String arg) {
+ return definedArguments.get(arg);
+ }
+
+ /**
+ * same as {@link #get(String)} but throws an exception if no argument definition was found
+ */
+ public Argument require(String arg) {
+ return require(arg, InvalidArgumentException.class);
+ }
+
+ public Argument require(String arg, Class extends RuntimeException> toThrow) {
+ Argument argument = get(arg);
+
+ if (argument == null) {
+ try {
+ throw toThrow.getConstructor(String.class)
+ .newInstance(String.format("Undefined argument '%s' on command '%s'.", arg, sourceCommand.getIdentifier()));
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Could not instantiate " + toThrow);
+ }
+ }
+
return argument;
}
@@ -46,12 +77,13 @@ public Argument map(String arg) {
* @return true if the user used the provided argument when calling the command
*/
public boolean argumentSet(String argument) {
- return setArguments.containsKey(argument);
+ Argument arg = require(argument, IllegalArgumentException.class);
+ return arg.isSet();
}
/**
* Add an argument used by the user to the setArguments when processing the command, verifying whether that argument
- * exists and meets all defined rules happens when {@link #complete()} is called.
+ * exists and meets all defined rules happens when {@link #verify()} is called.
*/
public void setArgument(String argument) {
setArgument(argument, "");
@@ -61,33 +93,25 @@ public void setArgument(String argument) {
* Same as {@link #setArgument(String)} but assigns the given value provided by the user
*/
public void setArgument(String argument, String value) {
- setArguments.put(argument, value);
+ Argument arg = require(argument);
+ arg.setSet(true);
+ arg.setValue(value);
}
/**
- * Completes the argument contribution by verifying the defined rules and assigning the provided values to the
- * arguments.
+ * Verifies all set argument rules
*/
- public void complete() {
- for (String setArgument : setArguments.keySet()) {
- if (arguments.stream().noneMatch(arg -> arg.getArgument().equalsIgnoreCase(setArgument))) {
- throw new InvalidCommandException(String.format("Unexpected argument '%s'", setArgument));
- }
+ public void verify() {
+ for (Argument value : definedArguments.values()) {
+ value.check();
}
-
- List selectedArguments = arguments
- .stream()
- .filter(arg -> setArguments.containsKey(arg.getArgument()))
- .collect(Collectors.toList());
- selectedArguments.forEach(arg -> arg.setValue(setArguments.get(arg.getArgument())));
- selectedArguments.forEach(arg -> arg.check(selectedArguments));
}
/**
* @return all arguments that may be used with this command
*/
public List getArguments() {
- return arguments;
+ return Lists.newArrayList(definedArguments.values());
}
/**
@@ -97,26 +121,11 @@ public AbstractCommand getSourceCommand() {
return sourceCommand;
}
- /**
- * @return the given argument definition for the provided name
- */
- public Argument getArgument(String argument) {
- List matches = arguments.stream().filter(arg -> arg.getArgument().equals(argument)).collect(Collectors.toList());
-
- if (matches.size() == 1) {
- return matches.get(0);
- } else if (matches.isEmpty()) {
- throw new IllegalArgumentException("Undefined argument " + argument);
- } else {
- throw new IllegalArgumentException("Duplicate argument definition for " + argument);
- }
- }
-
/**
* @return true if the ArgumentContribution for this command does not contain any arguments
*/
public boolean isEmpty() {
- return arguments.isEmpty();
+ return definedArguments.isEmpty();
}
/**
@@ -124,17 +133,18 @@ public boolean isEmpty() {
*/
public class Argument {
- private final String argument;
+ private final String identifier;
private String value;
private Set excludedArguments;
private Set neededArguments;
private String description;
+ private boolean set;
private boolean requiresValue;
// useful for commands that only require input if a certain argument is set
private boolean requiresInput;
- Argument(String argument) {
- this.argument = argument;
+ Argument(String identifier) {
+ this.identifier = identifier;
this.excludedArguments = Sets.newHashSet();
this.neededArguments = Sets.newHashSet();
description = "";
@@ -143,8 +153,8 @@ public class Argument {
/**
* @return the string identifier for this argument description
*/
- public String getArgument() {
- return argument;
+ public String getIdentifier() {
+ return identifier;
}
/**
@@ -227,35 +237,42 @@ public void setDescription(String description) {
/**
* Verify that this argument matches all configured rules
- *
- * @param selectedArguments the list of all arguments that were used
*/
- void check(List selectedArguments) {
- List setArguments = selectedArguments.stream().map(Argument::getArgument).collect(Collectors.toList());
- for (String selectedArgument : setArguments) {
- if (excludedArguments.contains(selectedArgument)) {
- String message = String.format("Conflicting arguments! %s may not be set when %s is set.", selectedArgument, argument);
- throw new InvalidCommandException(message);
+ void check() {
+ if (isSet()) {
+ for (String neededArgument : getNeededArguments()) {
+ Argument argument = require(neededArgument);
+ if (!argument.isSet()) {
+ throw new InvalidCommandException(String.format("Argument '%s' may only be set if argument '%s' is set.",
+ getIdentifier(), argument.getIdentifier()));
+ }
}
- }
- for (String neededArgument : neededArguments) {
- if (!setArguments.contains(neededArgument)) {
- String message = String.format("Invalid argument! %s may only be set if %s is set.", argument, neededArgument);
- throw new InvalidCommandException(message);
+ for (String excludedArgument : getExcludedArguments()) {
+ Argument argument = require(excludedArgument);
+ if (argument.isSet()) {
+ throw new InvalidCommandException(String.format("Argument '%s' can not be set if '%s' is set.",
+ getIdentifier(), argument.getIdentifier()));
+ }
}
- }
- if (requiresValue && !hasValue()) {
- throw new InvalidCommandException("Argument " + argument + " requires an assigned value!");
- } else if (!requiresValue && hasValue()) {
- throw new InvalidCommandException("Argument " + argument + " does not require an assigned value!");
- }
+ if (requiresValue && !hasValue()) {
+ throw new InvalidCommandException("Argument " + identifier + " requires an assigned value!");
+ }
- if (requiresInput && getSourceCommand().getCommandBody().isBlank()) {
- throw new InvalidCommandException("Argument " + argument + " requires additional command input.");
+ if (requiresInput && getSourceCommand().getCommandInput().isBlank()) {
+ throw new InvalidCommandException("Argument " + identifier + " requires additional command input.");
+ }
}
}
+
+ public boolean isSet() {
+ return set;
+ }
+
+ public void setSet(boolean set) {
+ this.set = set;
+ }
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/ClientQuestionEvent.java b/src/main/java/net/robinfriedli/botify/command/ClientQuestionEvent.java
index d9da2bef..5cd5afb7 100644
--- a/src/main/java/net/robinfriedli/botify/command/ClientQuestionEvent.java
+++ b/src/main/java/net/robinfriedli/botify/command/ClientQuestionEvent.java
@@ -5,9 +5,9 @@
import java.util.Timer;
import java.util.TimerTask;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.discord.MessageService;
import net.robinfriedli.botify.discord.properties.ColorSchemeProperty;
import net.robinfriedli.botify.exceptions.InvalidCommandException;
@@ -44,7 +44,7 @@ public Object get(String key) {
Option chosenOption = options.get(key);
if (chosenOption == null) {
- throw new InvalidCommandException("Unknown option " + key);
+ throw new InvalidCommandException(String.format("Unknown option '%s'. Make sure to copy the value from the 'key' column.", key));
}
return chosenOption.getOption();
@@ -95,7 +95,7 @@ public Guild getGuild() {
return getCommandContext().getGuild();
}
- public class Option {
+ public static class Option {
private final Object option;
private final String display;
diff --git a/src/main/java/net/robinfriedli/botify/command/CommandContext.java b/src/main/java/net/robinfriedli/botify/command/CommandContext.java
index fa731e1b..e4082607 100644
--- a/src/main/java/net/robinfriedli/botify/command/CommandContext.java
+++ b/src/main/java/net/robinfriedli/botify/command/CommandContext.java
@@ -5,11 +5,14 @@
import javax.annotation.Nullable;
import com.wrapper.spotify.SpotifyApi;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.GuildVoiceState;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.VoiceChannel;
import net.robinfriedli.botify.audio.spotify.SpotifyService;
import net.robinfriedli.botify.concurrent.CommandExecutionThread;
import net.robinfriedli.botify.discord.GuildContext;
@@ -60,6 +63,21 @@ public User getUser() {
return message.getAuthor();
}
+ public Member getMember() {
+ return message.getMember();
+ }
+
+ @Nullable
+ public VoiceChannel getVoiceChannel() {
+ GuildVoiceState voiceState = getMember().getVoiceState();
+
+ if (voiceState != null) {
+ return voiceState.getChannel();
+ }
+
+ return null;
+ }
+
public Guild getGuild() {
return message.getGuild();
}
diff --git a/src/main/java/net/robinfriedli/botify/command/SecurityManager.java b/src/main/java/net/robinfriedli/botify/command/SecurityManager.java
index 8983f023..f852afaf 100644
--- a/src/main/java/net/robinfriedli/botify/command/SecurityManager.java
+++ b/src/main/java/net/robinfriedli/botify/command/SecurityManager.java
@@ -2,9 +2,9 @@
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Member;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.discord.GuildManager;
import net.robinfriedli.botify.entities.AccessConfiguration;
import net.robinfriedli.botify.exceptions.ForbiddenCommandException;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/AbstractQueueLoadingCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/AbstractQueueLoadingCommand.java
index d273ce62..f747b95c 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/AbstractQueueLoadingCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/AbstractQueueLoadingCommand.java
@@ -27,6 +27,7 @@
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.botify.exceptions.NoResultsFoundException;
import net.robinfriedli.botify.exceptions.NoSpotifyResultsFoundException;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import net.robinfriedli.botify.util.SearchEngine;
import net.robinfriedli.stringlist.StringListImpl;
@@ -59,9 +60,9 @@ public void doRun() throws Exception {
AudioPlayback playback = audioManager.getPlaybackForGuild(getContext().getGuild());
playback.setCommunicationChannel(getContext().getChannel());
- if (UrlValidator.getInstance().isValid(getCommandBody())) {
+ if (UrlValidator.getInstance().isValid(getCommandInput())) {
loadUrlItems(audioManager, playback);
- } else if (SpotifyUri.isSpotifyUri(getCommandBody())) {
+ } else if (SpotifyUri.isSpotifyUri(getCommandInput())) {
loadSpotifyUri(audioManager);
} else if (argumentSet("list")) {
Source source = getSource();
@@ -87,8 +88,8 @@ public void doRun() throws Exception {
protected abstract void handleResults(List playables);
private void loadUrlItems(AudioManager audioManager, AudioPlayback playback) {
- PlayableFactory playableFactory = audioManager.createPlayableFactory(playback.getGuild());
- List playables = playableFactory.createPlayables(getCommandBody(), getContext().getSpotifyApi(), !argumentSet("preview"), mayInterrupt);
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(playback.getGuild(), getSpotifyService());
+ List playables = playableFactory.createPlayables(getCommandInput(), getContext().getSpotifyApi(), !argumentSet("preview"), mayInterrupt);
if (playables.isEmpty()) {
throw new NoResultsFoundException("Result is empty!");
}
@@ -97,18 +98,18 @@ private void loadUrlItems(AudioManager audioManager, AudioPlayback playback) {
}
private void loadSpotifyUri(AudioManager audioManager) throws Exception {
- SpotifyUri spotifyUri = SpotifyUri.parse(getCommandBody());
+ SpotifyUri spotifyUri = SpotifyUri.parse(getCommandInput());
SpotifyService spotifyService = getContext().getSpotifyService();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = spotifyUri.loadPlayables(playableFactory, spotifyService, !argumentSet("preview"), mayInterrupt);
handleResults(playables);
loadedAmount = playables.size();
}
private void loadLocalList(AudioManager audioManager) throws Exception {
- Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandBody(), isPartitioned(), getContext().getGuild().getId());
+ Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandInput(), isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local playlist found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No local playlist found for '%s'", getCommandInput()));
}
List items = runWithCredentials(() -> playlist.getTracks(getContext().getSpotifyApi()));
@@ -117,7 +118,7 @@ private void loadLocalList(AudioManager audioManager) throws Exception {
throw new NoResultsFoundException("Playlist is empty");
}
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = playableFactory.createPlayables(!argumentSet("preview"), items, mayInterrupt);
handleResults(playables);
loadedLocalList = playlist;
@@ -125,7 +126,7 @@ private void loadLocalList(AudioManager audioManager) throws Exception {
private void loadYouTubeList(AudioManager audioManager) {
YouTubeService youTubeService = audioManager.getYouTubeService();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
if (argumentSet("limit")) {
int limit = getArgumentValue("limit", Integer.class);
@@ -133,19 +134,19 @@ private void loadYouTubeList(AudioManager audioManager) {
throw new InvalidCommandException("Limit must be between 1 and 10");
}
- List playlists = youTubeService.searchSeveralPlaylists(limit, getCommandBody());
+ List playlists = youTubeService.searchSeveralPlaylists(limit, getCommandInput());
if (playlists.size() == 1) {
YouTubePlaylist playlist = playlists.get(0);
List playables = playableFactory.createPlayables(playlist, mayInterrupt);
handleResults(playables);
loadedYouTubePlaylist = playlist;
} else if (playlists.isEmpty()) {
- throw new NoResultsFoundException("No playlist found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No YouTube playlist found for '%s'", getCommandInput()));
} else {
askQuestion(playlists, YouTubePlaylist::getTitle, YouTubePlaylist::getChannelTitle);
}
} else {
- YouTubePlaylist youTubePlaylist = youTubeService.searchPlaylist(getCommandBody());
+ YouTubePlaylist youTubePlaylist = youTubeService.searchPlaylist(getCommandInput());
List playables = playableFactory.createPlayables(youTubePlaylist, mayInterrupt);
handleResults(playables);
loadedYouTubePlaylist = youTubePlaylist;
@@ -156,20 +157,20 @@ private void loadSpotifyList(AudioManager audioManager) throws Exception {
Callable callable = () -> {
List found;
if (argumentSet("own")) {
- found = getSpotifyService().searchOwnPlaylist(getCommandBody());
+ found = getSpotifyService().searchOwnPlaylist(getCommandInput());
} else {
- found = getSpotifyService().searchPlaylist(getCommandBody());
+ found = getSpotifyService().searchPlaylist(getCommandInput());
}
if (found.size() == 1) {
PlaylistSimplified playlist = found.get(0);
List playlistTracks = getSpotifyService().getPlaylistTracks(playlist);
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = playableFactory.createPlayables(!argumentSet("preview"), playlistTracks, mayInterrupt);
handleResults(playables);
loadedSpotifyPlaylist = playlist;
} else if (found.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No playlist found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify playlist found for '%s'", getCommandInput()));
} else {
askQuestion(found, PlaylistSimplified::getName, p -> p.getOwner().getDisplayName());
}
@@ -185,7 +186,7 @@ private void loadSpotifyList(AudioManager audioManager) throws Exception {
}
private void loadSpotifyAlbum(AudioManager audioManager) throws Exception {
- Callable> albumLoadCallable = () -> getSpotifyService().searchAlbum(getCommandBody(), argumentSet("own"));
+ Callable> albumLoadCallable = () -> getSpotifyService().searchAlbum(getCommandInput(), argumentSet("own"));
List albums;
if (argumentSet("own")) {
albums = runWithLogin(albumLoadCallable);
@@ -196,19 +197,19 @@ private void loadSpotifyAlbum(AudioManager audioManager) throws Exception {
if (albums.size() == 1) {
AlbumSimplified album = albums.get(0);
List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(album.getId()));
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = playableFactory.createPlayables(!argumentSet("preview"), tracks, mayInterrupt);
handleResults(playables);
loadedAlbum = album;
} else if (albums.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No albums found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No albums found for '%s'", getCommandInput()));
} else {
askQuestion(albums, AlbumSimplified::getName, album -> StringListImpl.create(album.getArtists(), ArtistSimplified::getName).toSeparatedString(", "));
}
}
private void loadTrack(AudioManager audioManager) throws Exception {
- Callable> loadTrackCallable = () -> getSpotifyService().searchTrack(getCommandBody(), argumentSet("own"));
+ Callable> loadTrackCallable = () -> getSpotifyService().searchTrack(getCommandInput(), argumentSet("own"));
List found;
if (argumentSet("own")) {
found = runWithLogin(loadTrackCallable);
@@ -217,12 +218,12 @@ private void loadTrack(AudioManager audioManager) throws Exception {
}
if (found.size() == 1) {
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
Playable track = playableFactory.createPlayable(!argumentSet("preview"), found.get(0));
handleResults(Lists.newArrayList(track));
loadedTrack = track;
} else if (found.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No Spotify track found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify track found for '%s'", getCommandInput()));
} else {
askQuestion(found, track -> {
String artistString = StringListImpl.create(track.getArtists(), ArtistSimplified::getName).toSeparatedString(", ");
@@ -239,25 +240,25 @@ private void loadYouTubeVideo(AudioManager audioManager) {
throw new InvalidCommandException("Limit must be between 1 and 10");
}
- List youTubeVideos = youTubeService.searchSeveralVideos(limit, getCommandBody());
+ List youTubeVideos = youTubeService.searchSeveralVideos(limit, getCommandInput());
if (youTubeVideos.size() == 1) {
Playable playable = youTubeVideos.get(0);
handleResults(Lists.newArrayList(playable));
loadedTrack = playable;
} else if (youTubeVideos.isEmpty()) {
- throw new NoResultsFoundException("No YouTube video found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No YouTube video found for '%s'", getCommandInput()));
} else {
askQuestion(youTubeVideos, youTubeVideo -> {
try {
return youTubeVideo.getTitle();
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
// Unreachable since only HollowYouTubeVideos might get interrupted
throw new RuntimeException(e);
}
});
}
} else {
- YouTubeVideo youTubeVideo = youTubeService.searchVideo(getCommandBody());
+ YouTubeVideo youTubeVideo = youTubeService.searchVideo(getCommandInput());
audioManager.getQueue(getContext().getGuild()).add(youTubeVideo);
handleResults(Lists.newArrayList(youTubeVideo));
loadedTrack = youTubeVideo;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/AddCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/AddCommand.java
index 1ca1ecd2..0833fe82 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/AddCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/AddCommand.java
@@ -1,11 +1,11 @@
package net.robinfriedli.botify.command.commands;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.validator.routines.UrlValidator;
import com.google.common.base.Strings;
@@ -35,6 +35,7 @@
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.botify.exceptions.NoResultsFoundException;
import net.robinfriedli.botify.exceptions.NoSpotifyResultsFoundException;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import net.robinfriedli.botify.util.PropertiesLoadingService;
import net.robinfriedli.botify.util.SearchEngine;
import net.robinfriedli.stringlist.StringListImpl;
@@ -57,31 +58,31 @@ public void doRun() {
Playlist playlist = SearchEngine.searchLocalList(session, getToAddString(), isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + getToAddString());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", getToAddString()));
}
List tracks = queue.getTracks();
invoke(() -> addPlayables(playlist, tracks));
} else {
- Pair pair = splitInlineArgument(getToAddString(), "to");
- Playlist playlist = SearchEngine.searchLocalList(session, pair.getRight(), isPartitioned(), getContext().getGuild().getId());
+ String playlistName = getArgumentValue("to");
+ Playlist playlist = SearchEngine.searchLocalList(session, playlistName, isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + pair.getRight());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", playlistName));
}
invoke(() -> {
- if (UrlValidator.getInstance().isValid(pair.getLeft())) {
- addUrl(playlist, pair.getLeft());
- } else if (SpotifyUri.isSpotifyUri(pair.getLeft())) {
- addSpotifyUri(playlist, SpotifyUri.parse(pair.getLeft()));
+ if (UrlValidator.getInstance().isValid(getCommandInput())) {
+ addUrl(playlist, getCommandInput());
+ } else if (SpotifyUri.isSpotifyUri(getCommandInput())) {
+ addSpotifyUri(playlist, SpotifyUri.parse(getCommandInput()));
} else {
if (argumentSet("list")) {
- addList(playlist, pair.getLeft());
+ addList(playlist, getCommandInput());
} else if (argumentSet("album")) {
- addSpotifyAlbum(playlist, pair.getLeft());
+ addSpotifyAlbum(playlist, getCommandInput());
} else {
- addSpecificTrack(playlist, pair.getLeft());
+ addSpecificTrack(playlist, getCommandInput());
}
}
});
@@ -123,14 +124,14 @@ private void addPlayables(Playlist playlist, List playables) {
private void addUrl(Playlist playlist, String url) {
AudioManager audioManager = Botify.get().getAudioManager();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = playableFactory.createPlayables(url, getContext().getSpotifyApi(), false, false);
addPlayables(playlist, playables);
}
private void addSpotifyUri(Playlist playlist, SpotifyUri spotifyUri) throws Exception {
AudioManager audioManager = Botify.get().getAudioManager();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
List playables = spotifyUri.loadPlayables(playableFactory, getContext().getSpotifyService(), false, false);
addPlayables(playlist, playables);
}
@@ -163,7 +164,7 @@ private void addSpotifyList(Playlist playlist, String searchTerm) throws Excepti
.collect(Collectors.toList());
addToList(playlist, songs);
} else if (playlists.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No Spotify playlists found for " + searchTerm);
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify playlists found for '%s'", searchTerm));
} else {
askQuestion(playlists, PlaylistSimplified::getName, p -> p.getOwner().getDisplayName());
}
@@ -191,7 +192,7 @@ private void addYouTubeList(Playlist playlist, String searchTerm) {
YouTubePlaylist youTubePlaylist = playlists.get(0);
loadYouTubeList(youTubePlaylist, playlist, youTubeService);
} else if (playlists.isEmpty()) {
- throw new NoResultsFoundException("No YouTube playlists found for " + searchTerm);
+ throw new NoResultsFoundException(String.format("No YouTube playlists found for '%s'", searchTerm));
} else {
askQuestion(playlists, YouTubePlaylist::getTitle, YouTubePlaylist::getChannelTitle);
}
@@ -205,7 +206,7 @@ private void addLocalList(Playlist playlist, String searchTerm) {
Playlist targetList = SearchEngine.searchLocalList(getContext().getSession(), searchTerm, isPartitioned(), getContext().getGuild().getId());
if (targetList == null) {
- throw new InvalidCommandException("No local playlist found for " + searchTerm);
+ throw new InvalidCommandException(String.format("No local playlist found for '%s'", searchTerm));
}
List items = targetList.getItemsSorted().stream().map(item -> item.copy(playlist)).collect(Collectors.toList());
@@ -244,7 +245,7 @@ private void addSpotifyAlbum(Playlist playlist, String searchTerm) throws Except
.collect(Collectors.toList());
addToList(playlist, songs);
} else if (albums.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No Spotify album found for " + searchTerm);
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify album found for '%s'", searchTerm));
} else {
askQuestion(
albums,
@@ -267,12 +268,12 @@ private void addSpecificTrack(Playlist playlist, String searchTerm) throws Excep
if (youTubeVideos.size() == 1) {
addToList(playlist, new Video(youTubeVideos.get(0), getContext().getUser(), playlist));
} else if (youTubeVideos.isEmpty()) {
- throw new NoResultsFoundException("No YouTube videos found for " + searchTerm);
+ throw new NoResultsFoundException(String.format("No YouTube videos found for '%s'", searchTerm));
} else {
askQuestion(youTubeVideos, youTubeVideo -> {
try {
return youTubeVideo.getTitle();
- } catch (InterruptedException e) {
+ } catch (UnavailableResourceException e) {
// Unreachable since only HollowYouTubeVideos might get interrupted
throw new RuntimeException(e);
}
@@ -294,7 +295,7 @@ private void addSpecificTrack(Playlist playlist, String searchTerm) throws Excep
if (tracks.size() == 1) {
addToList(playlist, new Song(tracks.get(0), getContext().getUser(), playlist, getContext().getSession()));
} else if (tracks.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No Spotify track found for " + searchTerm);
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify track found for '%s'", searchTerm));
} else {
askQuestion(tracks, track -> {
String artistString = StringListImpl.create(track.getArtists(), ArtistSimplified::getName).toSeparatedString(", ");
@@ -322,36 +323,48 @@ public void onSuccess() {
@Override
public void withUserResponse(Object option) {
Session session = getContext().getSession();
- Pair pair = splitInlineArgument(getToAddString(), "to");
+ String playlistName = getArgumentValue("to");
- Playlist playlist = SearchEngine.searchLocalList(session, pair.getRight(), isPartitioned(), getContext().getGuild().getId());
+ Playlist playlist = SearchEngine.searchLocalList(session, playlistName, isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + getToAddString());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", getToAddString()));
}
invoke(() -> {
- if (option instanceof Track) {
- addToList(playlist, new Song((Track) option, getContext().getUser(), playlist, session));
- } else if (option instanceof YouTubeVideo) {
- addToList(playlist, new Video((YouTubeVideo) option, getContext().getUser(), playlist));
- } else if (option instanceof PlaylistSimplified) {
- List playlistTracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks((PlaylistSimplified) option));
- List songs = playlistTracks.stream().map(t -> new Song(t, getContext().getUser(), playlist, session)).collect(Collectors.toList());
- addToList(playlist, songs);
- } else if (option instanceof YouTubePlaylist) {
- YouTubeService youTubeService = Botify.get().getAudioManager().getYouTubeService();
- loadYouTubeList((YouTubePlaylist) option, playlist, youTubeService);
- } else if (option instanceof AlbumSimplified) {
- List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(((AlbumSimplified) option).getId()));
- List songs = tracks.stream().map(t -> new Song(t, getContext().getUser(), playlist, session)).collect(Collectors.toList());
- addToList(playlist, songs);
+ if (option instanceof Collection) {
+ for (Object o : ((Collection) option)) {
+ handleOption(o, playlist, session);
+ }
+ } else {
+ handleOption(option, playlist, session);
}
});
}
+ private void handleOption(Object option, Playlist playlist, Session session) throws Exception {
+ if (option instanceof Track) {
+ addToList(playlist, new Song((Track) option, getContext().getUser(), playlist, session));
+ } else if (option instanceof YouTubeVideo) {
+ addToList(playlist, new Video((YouTubeVideo) option, getContext().getUser(), playlist));
+ } else if (option instanceof PlaylistSimplified) {
+ List playlistTracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks((PlaylistSimplified) option));
+ List songs = playlistTracks.stream().map(t -> new Song(t, getContext().getUser(), playlist, session)).collect(Collectors.toList());
+ addToList(playlist, songs);
+ } else if (option instanceof YouTubePlaylist) {
+ YouTubeService youTubeService = Botify.get().getAudioManager().getYouTubeService();
+ loadYouTubeList((YouTubePlaylist) option, playlist, youTubeService);
+ } else if (option instanceof AlbumSimplified) {
+ List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(((AlbumSimplified) option).getId()));
+ List songs = tracks.stream().map(t -> new Song(t, getContext().getUser(), playlist, session)).collect(Collectors.toList());
+ addToList(playlist, songs);
+ }
+ }
+
@Override
public ArgumentContribution setupArguments() {
ArgumentContribution argumentContribution = new ArgumentContribution(this);
+ argumentContribution.map("to")
+ .setDescription("Mandatory argument to define the playlist to which you want to add the tracks.");
argumentContribution.map("youtube").excludesArguments("spotify")
.setDescription("Add specific video from YouTube. Note that this argument is only required when searching, not when entering a URL.");
argumentContribution.map("spotify").excludesArguments("youtube")
@@ -372,7 +385,7 @@ public ArgumentContribution setupArguments() {
}
protected String getToAddString() {
- return getCommandBody();
+ return getCommandInput();
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/AnalyticsCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/AnalyticsCommand.java
index cf649149..173ad36c 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/AnalyticsCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/AnalyticsCommand.java
@@ -2,9 +2,9 @@
import java.util.List;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/AnswerCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/AnswerCommand.java
index dfd7cd4f..6ea86606 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/AnswerCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/AnswerCommand.java
@@ -1,5 +1,11 @@
package net.robinfriedli.botify.command.commands;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.base.Splitter;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
@@ -20,7 +26,25 @@ public void doRun() {
getContext().getGuildContext().getQuestion(getContext()).ifPresentOrElse(question -> {
sourceCommand = question.getSourceCommand();
- Object option = question.get(getCommandBody());
+ String commandInput = getCommandInput();
+ Splitter commaSplitter = Splitter.on(",").trimResults().omitEmptyStrings();
+ List options = commaSplitter.splitToList(commandInput);
+ Object option;
+ if (options.size() == 1) {
+ option = question.get(options.get(0));
+ } else {
+ Set chosenOptions = new LinkedHashSet<>();
+ for (String o : options) {
+ Object chosen = question.get(o);
+ if (chosen instanceof Collection) {
+ chosenOptions.addAll((Collection) chosen);
+ } else {
+ chosenOptions.add(chosen);
+ }
+ }
+
+ option = chosenOptions;
+ }
try {
sourceCommand.withUserResponse(option);
question.destroy();
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/ChartsCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/ChartsCommand.java
index bf63f8ec..661485d0 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/ChartsCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/ChartsCommand.java
@@ -1,14 +1,16 @@
package net.robinfriedli.botify.command.commands;
import java.math.BigInteger;
+import java.sql.Date;
+import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.Playable;
@@ -37,37 +39,66 @@ public void doRun() throws Exception {
Session session = getContext().getSession();
Guild guild = getContext().getGuild();
+ Date dateAtStartOfMonth = Date.valueOf(LocalDateTime.now().withDayOfMonth(1).toLocalDate());
Query globalQuery = session.createQuery("select source, trackId, count(*) as c from " + PlaybackHistory.class.getName()
+ " where trackId is not null group by trackId, source order by c desc", Object[].class)
.setMaxResults(5);
Query guildQuery = session.createQuery("select source, trackId, count(*) as c from " + PlaybackHistory.class.getName()
+ " where trackId is not null and guildId = '" + guild.getId() + "' group by trackId, source order by c desc", Object[].class)
.setMaxResults(5);
+
+ Query globalQueryMonthly = session.createQuery("select source, trackId, count(*) as c from " + PlaybackHistory.class.getName()
+ + " where trackId is not null and timestamp > '" + dateAtStartOfMonth.toString() + "' group by trackId, source order by c desc", Object[].class)
+ .setMaxResults(5);
+ Query guildQueryMonthly = session.createQuery("select source, trackId, count(*) as c from " + PlaybackHistory.class.getName()
+ + " where trackId is not null and timestamp > '" + dateAtStartOfMonth.toString() + "' and guildId = '" + guild.getId() + "' group by trackId, source order by c desc", Object[].class)
+ .setMaxResults(5);
+
List globalResults = globalQuery.getResultList();
List guildResults = guildQuery.getResultList();
+ List globalMonthlyResults = globalQueryMonthly.getResultList();
+ List guildMonthlyResults = guildQueryMonthly.getResultList();
@SuppressWarnings("unchecked")
Query globalArtistQuery = session.createSQLQuery("select artists_pk, count(*) as c " +
"from playback_history_artist group by artists_pk order by c desc limit 3");
@SuppressWarnings("unchecked")
Query guildArtistQuery = session.createSQLQuery("select artists_pk, count(*) as c from " +
- "(select * from playback_history_artist where playbackhistory_pk in(select pk from playback_history where guild_id = '" + guild.getId() + "')) as foo " +
+ "playback_history_artist as p where p.playbackhistory_pk in(select pk from playback_history where guild_id = '" + guild.getId() + "') " +
+ "group by artists_pk order by c desc limit 3");
+
+ @SuppressWarnings("unchecked")
+ Query globalArtistMonthlyQuery = session.createSQLQuery("select artists_pk, count(*) as c " +
+ "from playback_history_artist as p " +
+ "where p.playbackhistory_pk in(select pk from playback_history where timestamp > '" + dateAtStartOfMonth.toString() + "') " +
"group by artists_pk order by c desc limit 3");
+ @SuppressWarnings("unchecked")
+ Query guildArtistMonthlyQuery = session.createSQLQuery("select artists_pk, count(*) as c " +
+ "from playback_history_artist where playbackhistory_pk in(select pk from playback_history " +
+ "where timestamp > '" + dateAtStartOfMonth.toString() + "' and guild_id = '" + guild.getId() + "') " +
+ "group by artists_pk order by c desc limit 3");
+
List globalArtists = globalArtistQuery.getResultList();
List guildArtists = guildArtistQuery.getResultList();
+ List globalArtistsMonthly = globalArtistMonthlyQuery.getResultList();
+ List guildArtistMonthly = guildArtistMonthlyQuery.getResultList();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.addField("Global", "Shows the charts across all guilds", false);
- addTrackCharts(globalResults, embedBuilder);
- addArtists(globalArtists, embedBuilder);
+ addTrackCharts(globalResults, embedBuilder, "All time");
+ addArtists(globalArtists, embedBuilder, "All time");
+ addTrackCharts(globalMonthlyResults, embedBuilder, "Monthly");
+ addArtists(globalArtistsMonthly, embedBuilder, "Monthly");
embedBuilder.addBlankField(false);
embedBuilder.addField("Guild", "Shows the charts for this guild", false);
- addTrackCharts(guildResults, embedBuilder);
- addArtists(guildArtists, embedBuilder);
+ addTrackCharts(guildResults, embedBuilder, "All time");
+ addArtists(guildArtists, embedBuilder, "All time");
+ addTrackCharts(guildMonthlyResults, embedBuilder, "Monthly");
+ addArtists(guildArtistMonthly, embedBuilder, "Monthly");
sendMessage(embedBuilder);
}
- private void addTrackCharts(List queryResults, EmbedBuilder embedBuilder) throws Exception {
+ private void addTrackCharts(List queryResults, EmbedBuilder embedBuilder, String period) throws Exception {
Map tracksWithPlayedAmount = new HashMap<>();
List tracks = Lists.newArrayList();
for (Object[] record : queryResults) {
@@ -77,20 +108,21 @@ private void addTrackCharts(List queryResults, EmbedBuilder embedBuild
tracks.add(track);
}
- String title = "Count - Track";
+ String title = period + " - Track Charts";
if (!tracks.isEmpty()) {
Util.appendEmbedList(
embedBuilder,
tracks,
- track -> tracksWithPlayedAmount.get(track) + " - " + track.getDisplayInterruptible(),
- title
+ track -> tracksWithPlayedAmount.get(track) + " - " + track.display(),
+ title,
+ true
);
} else {
- embedBuilder.addField(title, "No data", false);
+ embedBuilder.addField(title, "No data", true);
}
}
- private void addArtists(List records, EmbedBuilder embedBuilder) {
+ private void addArtists(List records, EmbedBuilder embedBuilder, String period) {
Session session = getContext().getSession();
Map artistsWithPlayedAmount = new HashMap<>();
List artists = Lists.newArrayList();
@@ -102,16 +134,17 @@ private void addArtists(List records, EmbedBuilder embedBuilder) {
artistsWithPlayedAmount.put(artist, playedCount);
}
- String title = "Count - Artist";
+ String title = period + " - Artist Charts";
if (!artists.isEmpty()) {
Util.appendEmbedList(
embedBuilder,
artists,
artist -> artistsWithPlayedAmount.get(artist) + " - " + artist.getName(),
- title
+ title,
+ true
);
} else {
- embedBuilder.addField(title, "No data", false);
+ embedBuilder.addField(title, "No data", true);
}
}
@@ -130,7 +163,7 @@ private Playable getTrackForRecord(Object[] record) throws Exception {
return youTubeService.videoForId(id);
case "Url":
AudioManager audioManager = Botify.get().getAudioManager();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
return playableFactory.createPlayable(id, getContext().getSpotifyApi(), false);
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/CreateCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/CreateCommand.java
index 3faef265..c6e4394d 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/CreateCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/CreateCommand.java
@@ -20,10 +20,10 @@ public CreateCommand(CommandContribution commandContribution, CommandContext con
@Override
public void doRun() {
Session session = getContext().getSession();
- Playlist existingPlaylist = SearchEngine.searchLocalList(session, getCommandBody(), isPartitioned(), getContext().getGuild().getId());
+ Playlist existingPlaylist = SearchEngine.searchLocalList(session, getCommandInput(), isPartitioned(), getContext().getGuild().getId());
if (existingPlaylist != null) {
- throw new InvalidCommandException("Playlist " + getCommandBody() + " already exists");
+ throw new InvalidCommandException("Playlist " + getCommandInput() + " already exists");
}
String playlistCountMax = PropertiesLoadingService.loadProperty("PLAYLIST_COUNT_MAX");
@@ -36,7 +36,7 @@ public void doRun() {
}
}
- Playlist playlist = new Playlist(getCommandBody(), getContext().getUser(), getContext().getGuild());
+ Playlist playlist = new Playlist(getCommandInput(), getContext().getUser(), getContext().getGuild());
invoke(() -> session.persist(playlist));
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/DeleteCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/DeleteCommand.java
index 5ff2cd36..ddcc8cc3 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/DeleteCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/DeleteCommand.java
@@ -18,10 +18,10 @@ public DeleteCommand(CommandContribution commandContribution, CommandContext con
@Override
public void doRun() {
Session session = getContext().getSession();
- Playlist playlist = SearchEngine.searchLocalList(session, getCommandBody(), isPartitioned(), getContext().getGuild().getId());
+ Playlist playlist = SearchEngine.searchLocalList(session, getCommandInput(), isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", getCommandInput()));
}
invoke(() -> {
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/ExportCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/ExportCommand.java
index 6bd1cf2a..b1c41173 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/ExportCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/ExportCommand.java
@@ -3,8 +3,8 @@
import java.util.List;
import com.google.common.base.Strings;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioQueue;
import net.robinfriedli.botify.audio.Playable;
@@ -36,9 +36,9 @@ public void doRun() {
throw new InvalidCommandException("Queue is empty");
}
- Playlist existingPlaylist = SearchEngine.searchLocalList(session, getCommandBody(), isPartitioned(), guild.getId());
+ Playlist existingPlaylist = SearchEngine.searchLocalList(session, getCommandInput(), isPartitioned(), guild.getId());
if (existingPlaylist != null) {
- throw new InvalidCommandException("Playlist " + getCommandBody() + " already exists");
+ throw new InvalidCommandException("Playlist " + getCommandInput() + " already exists");
}
List tracks = queue.getTracks();
@@ -61,7 +61,7 @@ public void doRun() {
}
User createUser = getContext().getUser();
- Playlist playlist = new Playlist(getCommandBody(), createUser, guild);
+ Playlist playlist = new Playlist(getCommandInput(), createUser, guild);
invoke(() -> {
session.persist(playlist);
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/ForwardCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/ForwardCommand.java
index d0d1376e..f49af7d4 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/ForwardCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/ForwardCommand.java
@@ -29,12 +29,12 @@ public void doRun() {
long toForwardMs;
try {
if (argumentSet("minutes")) {
- toForwardMs = Integer.parseInt(getCommandBody()) * 60000;
+ toForwardMs = Integer.parseInt(getCommandInput()) * 60000;
} else {
- toForwardMs = Integer.parseInt(getCommandBody()) * 1000;
+ toForwardMs = Integer.parseInt(getCommandInput()) * 1000;
}
} catch (NumberFormatException e) {
- throw new InvalidCommandException("'" + getCommandBody() + "' is not convertible to type integer. " +
+ throw new InvalidCommandException("'" + getCommandInput() + "' is not convertible to type integer. " +
"Please enter a valid number.");
}
@@ -43,7 +43,7 @@ public void doRun() {
}
long newPosition = playback.getCurrentPositionMs() + toForwardMs;
- long duration = playback.getAudioQueue().getCurrent().getDurationMsInterruptible();
+ long duration = playback.getAudioQueue().getCurrent().durationMs();
if (newPosition > duration) {
throw new InvalidCommandException("New position too high! Current track duration: " + Util.normalizeMillis(duration) +
", new position: " + Util.normalizeMillis(newPosition));
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/HelpCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/HelpCommand.java
index b4e910f3..6424c729 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/HelpCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/HelpCommand.java
@@ -5,16 +5,19 @@
import java.util.List;
import java.util.stream.Collectors;
+import com.google.common.base.Strings;
import com.google.common.collect.LinkedHashMultimap;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Role;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Role;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
import net.robinfriedli.botify.entities.AccessConfiguration;
+import net.robinfriedli.botify.entities.GuildSpecification;
import net.robinfriedli.botify.entities.xml.CommandContribution;
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.jxp.api.XmlElement;
@@ -30,7 +33,7 @@ public HelpCommand(CommandContribution commandContribution, CommandContext conte
@Override
public void doRun() {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
listCommands();
} else {
showCommandHelp();
@@ -38,10 +41,24 @@ public void doRun() {
}
private void showCommandHelp() {
- getManager().getCommand(getContext(), getCommandBody()).ifPresentOrElse(command -> {
+ getManager().getCommand(getContext(), getCommandInput()).ifPresentOrElse(command -> {
+ String prefix;
+ GuildSpecification specification = getContext().getGuildContext().getSpecification();
+ String setPrefix = specification.getPrefix();
+ String botName = specification.getBotName();
+ if (!Strings.isNullOrEmpty(setPrefix)) {
+ prefix = setPrefix;
+ } else if (!Strings.isNullOrEmpty(botName)) {
+ prefix = botName + " ";
+ } else {
+ prefix = "$botify ";
+ }
+
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setTitle("Command " + command.getIdentifier() + ":");
- embedBuilder.setDescription(command.getDescription());
+ String descriptionFormat = command.getDescription();
+ String descriptionText = descriptionFormat.contains("%s") ? String.format(descriptionFormat, prefix) : descriptionFormat;
+ embedBuilder.setDescription(descriptionText);
Guild guild = getContext().getGuild();
AccessConfiguration accessConfiguration = Botify.get().getGuildManager().getAccessConfiguration(command.getIdentifier(), guild);
if (accessConfiguration != null) {
@@ -58,25 +75,30 @@ private void showCommandHelp() {
}
ArgumentContribution argumentContribution = command.setupArguments();
if (!argumentContribution.isEmpty()) {
- embedBuilder.addField("__Arguments__", "Keywords that alter the command behavior or define a search scope", false);
+ embedBuilder.addField("__Arguments__", "Keywords that alter the command behavior or define a search scope.", false);
+ char argumentPrefix = ArgumentPrefixProperty.getForCurrentContext();
for (ArgumentContribution.Argument argument : argumentContribution.getArguments()) {
- embedBuilder.addField("$" + argument.getArgument(), argument.getDescription(), false);
+ embedBuilder.addField(argumentPrefix + argument.getIdentifier(), argument.getDescription(), false);
}
}
List examples = command.getCommandContribution().query(tagName("example")).collect();
if (!examples.isEmpty()) {
- embedBuilder.addField("__Examples__", "Practical usage examples for this command. Note that '$botify' can be exchanged for your custom prefix or bot name.", false);
+ embedBuilder.addField("__Examples__", "Practical usage examples for this command.", false);
for (XmlElement example : examples) {
- embedBuilder.addField(example.getAttribute("title").getValue(), example.getTextContent(), false);
+ String exampleFormat = example.getTextContent();
+ String exampleText = exampleFormat.contains("%s") ? String.format(exampleFormat, prefix) : exampleFormat;
+ String titleFormat = example.getAttribute("title").getValue();
+ String titleText = titleFormat.contains("%s") ? String.format(titleFormat, prefix) : titleFormat;
+ embedBuilder.addField(titleText, exampleText, false);
}
}
sendMessage(embedBuilder);
}, () -> {
- throw new InvalidCommandException("No command found for " + getCommandBody());
+ throw new InvalidCommandException(String.format("No command found for '%s'", getCommandInput()));
});
}
@@ -98,7 +120,7 @@ private void listCommands() {
for (Category category : categories) {
Iterator commandIterator = commandsByCategory.get(category).iterator();
StringBuilder commandString = new StringBuilder();
- for (int i = 0; commandIterator.hasNext(); i++) {
+ while (commandIterator.hasNext()) {
AbstractCommand c = commandIterator.next();
commandString.append(c.getIdentifier());
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/InsertCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/InsertCommand.java
index bae78afe..46afe0ec 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/InsertCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/InsertCommand.java
@@ -2,8 +2,7 @@
import java.util.List;
-import org.apache.commons.lang3.tuple.Pair;
-
+import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.entities.Playlist;
@@ -22,14 +21,8 @@ public InsertCommand(CommandContribution commandContribution, CommandContext con
@Override
public void doRun() {
- Pair toAddStringWithIndex = splitInlineArgument("at");
- toAddString = toAddStringWithIndex.getLeft();
-
- try {
- targetIndex = Integer.parseInt(toAddStringWithIndex.getRight());
- } catch (NumberFormatException e) {
- throw new InvalidCommandException(String.format("'%s' is not an integer", toAddStringWithIndex.getRight()));
- }
+ targetIndex = getArgumentValue("at", Integer.class);
+ toAddString = getCommandInput();
super.doRun();
}
@@ -62,4 +55,12 @@ protected void addToList(Playlist playlist, List items) {
protected String getToAddString() {
return toAddString;
}
+
+ @Override
+ public ArgumentContribution setupArguments() {
+ ArgumentContribution argumentContribution = super.setupArguments();
+ argumentContribution.map("at").setRequiresValue(true)
+ .setDescription("Mandatory argument to define the index at which to insert the tracks.");
+ return argumentContribution;
+ }
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/LoginCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/LoginCommand.java
index de097634..a51b20ef 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/LoginCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/LoginCommand.java
@@ -7,8 +7,8 @@
import java.util.concurrent.TimeoutException;
import com.wrapper.spotify.requests.authorization.authorization_code.AuthorizationCodeUriRequest;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.CommandContext;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/LogoutCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/LogoutCommand.java
index b3055cc9..ef7b4189 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/LogoutCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/LogoutCommand.java
@@ -1,6 +1,6 @@
package net.robinfriedli.botify.command.commands;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.CommandContext;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/MoveCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/MoveCommand.java
index 6a22a5b8..35719046 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/MoveCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/MoveCommand.java
@@ -2,11 +2,10 @@
import java.util.List;
-import org.apache.commons.lang3.tuple.Pair;
-
import com.google.common.base.Splitter;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.entities.Playlist;
@@ -29,18 +28,17 @@ public MoveCommand(CommandContribution commandContribution, CommandContext conte
public void doRun() {
Guild guild = getContext().getGuild();
Session session = getContext().getSession();
- Pair indicesWithPlaylistName = splitInlineArgument("on");
- Playlist playlist = SearchEngine.searchLocalList(session, indicesWithPlaylistName.getRight(), isPartitioned(), guild.getId());
+ String playlistName = getArgumentValue("on");
+ Playlist playlist = SearchEngine.searchLocalList(session, playlistName, isPartitioned(), guild.getId());
if (playlist == null) {
- throw new NoResultsFoundException("No playlist found for " + indicesWithPlaylistName.getRight());
+ throw new NoResultsFoundException(String.format("No local playlist found for '%s'", playlistName));
} else if (playlist.isEmpty()) {
throw new InvalidCommandException("Playlist is empty");
}
- Pair sourceAndTargetIndices = splitInlineArgument(indicesWithPlaylistName.getLeft(), "to");
- String sourceIndex = sourceAndTargetIndices.getLeft();
- int targetIndex = parse(sourceAndTargetIndices.getRight());
+ String sourceIndex = getCommandInput();
+ int targetIndex = getArgumentValue("to", Integer.class);
checkIndex(targetIndex, playlist);
if (sourceIndex.contains("-")) {
@@ -178,4 +176,14 @@ public void onSuccess() {
sendSuccess(successMessageBuilder.toString());
}
}
+
+ @Override
+ public ArgumentContribution setupArguments() {
+ ArgumentContribution argumentContribution = new ArgumentContribution(this);
+ argumentContribution.map("to").setRequiresValue(true)
+ .setDescription("Mandatory argument to specify the target index.");
+ argumentContribution.map("on").setRequiresValue(true)
+ .setDescription("Mandatory argument to define the playlist where you want to move the tracks.");
+ return argumentContribution;
+ }
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PauseCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PauseCommand.java
index 403ddffa..39dee408 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PauseCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PauseCommand.java
@@ -1,6 +1,6 @@
package net.robinfriedli.botify.command.commands;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.AbstractCommand;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PermissionCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PermissionCommand.java
index 0dc3219a..0c7a7a6d 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PermissionCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PermissionCommand.java
@@ -5,13 +5,11 @@
import java.util.Set;
import java.util.stream.Collectors;
-import org.apache.commons.lang3.tuple.Pair;
-
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Role;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Role;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
@@ -83,12 +81,11 @@ private void grantPermissions() {
Set selectedCommands;
Set selectedRoles;
if (argumentSet("all")) {
- selectedCommands = Sets.newHashSet(getManager().getAllCommandContributions());
- selectedRoles = getSelectedRoles(getCommandBody());
+ selectedCommands = getAllCommands();
+ selectedRoles = getSelectedRoles(getCommandInput());
} else {
- Pair pair = splitInlineArgument("to");
- selectedCommands = getSelectedCommands(pair.getLeft());
- selectedRoles = getSelectedRoles(pair.getRight());
+ selectedCommands = getSelectedCommands(getCommandInput());
+ selectedRoles = getSelectedRoles(getArgumentValue("to"));
}
boolean addedAnything = invoke(() -> {
@@ -135,12 +132,11 @@ private void denyPermissions() {
Set selectedCommands;
Set selectedRoles;
if (argumentSet("all")) {
- selectedCommands = Sets.newHashSet(getManager().getAllCommandContributions());
- selectedRoles = getSelectedRoles(getCommandBody());
+ selectedCommands = getAllCommands();
+ selectedRoles = getSelectedRoles(getCommandInput());
} else {
- Pair pair = splitInlineArgument("for");
- selectedCommands = getSelectedCommands(pair.getLeft());
- selectedRoles = getSelectedRoles(pair.getRight());
+ selectedCommands = getSelectedCommands(getCommandInput());
+ selectedRoles = getSelectedRoles(getArgumentValue("for"));
}
Session session = getContext().getSession();
@@ -174,7 +170,7 @@ private void denyPermissions() {
private void clearCommands() {
GuildSpecification specification = getContext().getGuildContext().getSpecification();
- Set selectedCommands = getSelectedCommands(getCommandBody());
+ Set selectedCommands = getSelectedCommands(getCommandInput());
Session session = getContext().getSession();
boolean removedAnything = invoke(() -> {
@@ -199,7 +195,7 @@ private void clearCommands() {
private void lockCommands() {
GuildSpecification specification = getContext().getGuildContext().getSpecification();
- Set selectedCommands = getSelectedCommands(getCommandBody());
+ Set selectedCommands = getSelectedCommands(getCommandInput());
Session session = getContext().getSession();
boolean lockedAnything = invoke(() -> {
@@ -226,7 +222,7 @@ private void lockCommands() {
});
if (!lockedAnything) {
- sendError("All commands are already only available to the guild owner");
+ sendError("All selected commands are already only available to the guild owner");
}
}
@@ -248,7 +244,7 @@ private Set getSelectedRoles(String roleString) {
private Set getSelectedCommands(String commandString) {
if (argumentSet("all")) {
- return Sets.newHashSet(getManager().getAllCommandContributions());
+ return getAllCommands();
} else if (argumentSet("category")) {
Set selectedCommands = getManager()
.getAllCommands(getContext())
@@ -276,6 +272,13 @@ private Set getSelectedCommands(String commandString) {
}
}
+ private Set getAllCommands() {
+ return getManager().getAllCommands(getContext()).stream()
+ .filter(c -> c.getCategory() != Category.ADMIN)
+ .map(AbstractCommand::getCommandContribution)
+ .collect(Collectors.toSet());
+ }
+
@Override
public void onSuccess() {
// success message sent by interceptor
@@ -284,6 +287,10 @@ public void onSuccess() {
@Override
public ArgumentContribution setupArguments() {
ArgumentContribution argumentContribution = new ArgumentContribution(this);
+ argumentContribution.map("to").needsArguments("grant")
+ .setDescription("Specify the roles to grant access for.");
+ argumentContribution.map("for").excludesArguments("to").needsArguments("deny")
+ .setDescription("Specify the roles to remove access privileges from when using the $deny argument.");
argumentContribution.map("grant").setRequiresInput(true).excludesArguments("deny")
.setDescription("Grant the selected commands to the selected roles. Limits the command to the selected roles " +
"if no roles were previously defined.");
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PlayCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PlayCommand.java
index 65994a1f..f149b181 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PlayCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PlayCommand.java
@@ -2,21 +2,15 @@
import java.util.List;
-import com.wrapper.spotify.model_objects.specification.AlbumSimplified;
-import com.wrapper.spotify.model_objects.specification.PlaylistSimplified;
-import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Member;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.VoiceChannel;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.VoiceChannel;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.audio.AudioQueue;
import net.robinfriedli.botify.audio.Playable;
import net.robinfriedli.botify.audio.PlayableFactory;
-import net.robinfriedli.botify.audio.youtube.YouTubePlaylist;
-import net.robinfriedli.botify.audio.youtube.YouTubeVideo;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
@@ -34,16 +28,15 @@ public PlayCommand(CommandContribution commandContribution, CommandContext conte
public void doRun() throws Exception {
CommandContext context = getContext();
Guild guild = context.getGuild();
- Member member = guild.getMember(context.getUser());
- VoiceChannel channel = member.getVoiceState().getChannel();
+ VoiceChannel channel = context.getVoiceChannel();
AudioManager audioManager = Botify.get().getAudioManager();
MessageChannel messageChannel = getContext().getChannel();
AudioPlayback playbackForGuild = audioManager.getPlaybackForGuild(guild);
playbackForGuild.setCommunicationChannel(messageChannel);
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
if (playbackForGuild.isPaused() || !audioManager.getQueue(guild).isEmpty()) {
- audioManager.playTrack(guild, channel);
+ audioManager.startOrResumePlayback(guild, channel);
} else {
throw new InvalidCommandException("Queue is empty. Specify a song you want to play.");
}
@@ -58,13 +51,12 @@ protected void handleResults(List playables) {
throw new NoResultsFoundException("Result is empty!");
}
Guild guild = getContext().getGuild();
- Member member = guild.getMember(getContext().getUser());
- VoiceChannel channel = member.getVoiceState().getChannel();
+ VoiceChannel channel = getContext().getVoiceChannel();
AudioManager audioManager = Botify.get().getAudioManager();
AudioPlayback playback = getContext().getGuildContext().getPlayback();
playback.getAudioQueue().set(playables);
- audioManager.playTrack(guild, channel);
+ audioManager.startPlayback(guild, channel);
}
@Override
@@ -73,30 +65,17 @@ public void onSuccess() {
}
@Override
- public void withUserResponse(Object chosenOption) throws Exception {
+ public void withUserResponse(Object option) {
AudioManager audioManager = Botify.get().getAudioManager();
Guild guild = getContext().getGuild();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(guild);
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(guild, getSpotifyService());
AudioPlayback playback = audioManager.getPlaybackForGuild(guild);
AudioQueue queue = playback.getAudioQueue();
- if (chosenOption instanceof Track || chosenOption instanceof YouTubeVideo) {
- queue.set(playableFactory.createPlayable(!argumentSet("preview"), chosenOption));
- } else if (chosenOption instanceof PlaylistSimplified) {
- PlaylistSimplified playlist = (PlaylistSimplified) chosenOption;
- List tracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks(playlist));
- queue.set(playableFactory.createPlayables(!argumentSet("preview"), tracks));
- } else if (chosenOption instanceof YouTubePlaylist) {
- YouTubePlaylist youTubePlaylist = (YouTubePlaylist) chosenOption;
- queue.set(playableFactory.createPlayables(youTubePlaylist));
- } else if (chosenOption instanceof AlbumSimplified) {
- List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(((AlbumSimplified) chosenOption).getId()));
- queue.set(playableFactory.createPlayables(!argumentSet("preview"), tracks));
- }
+ List playables = playableFactory.createPlayables(!argumentSet("preview"), option);
+ queue.set(playables);
- Member member = guild.getMember(getContext().getUser());
- VoiceChannel channel = member.getVoiceState().getChannel();
- audioManager.playTrack(guild, channel);
+ audioManager.startPlayback(guild, getContext().getVoiceChannel());
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PrefixCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PrefixCommand.java
index ec924d45..cd208ee4 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PrefixCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PrefixCommand.java
@@ -18,11 +18,11 @@ public PrefixCommand(CommandContribution commandContribution, CommandContext con
public void doRun() {
GuildManager guildManager = Botify.get().getGuildManager();
- if (getCommandBody().length() < 1 || getCommandBody().length() > 5) {
+ if (getCommandInput().length() < 1 || getCommandInput().length() > 5) {
throw new InvalidCommandException("Length should be 1 - 5 characters");
}
- guildManager.setPrefix(getContext().getGuild(), getCommandBody());
+ getContext().getGuildContext().setPrefix(getCommandInput());
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PresetCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PresetCommand.java
index 103fe022..e86bb56c 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PresetCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PresetCommand.java
@@ -2,13 +2,13 @@
import java.util.List;
-import org.apache.commons.lang3.tuple.Pair;
-
-import net.dv8tion.jda.core.EmbedBuilder;
+import net.dv8tion.jda.api.EmbedBuilder;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
+import net.robinfriedli.botify.command.parser.CommandParser;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
import net.robinfriedli.botify.entities.Preset;
import net.robinfriedli.botify.entities.xml.CommandContribution;
import net.robinfriedli.botify.exceptions.InvalidCommandException;
@@ -27,14 +27,14 @@ public void doRun() {
Session session = getContext().getSession();
String guildId = getContext().getGuild().getId();
if (argumentSet("delete")) {
- Preset preset = SearchEngine.searchPreset(session, getCommandBody(), guildId);
+ Preset preset = SearchEngine.searchPreset(session, getCommandInput(), guildId);
if (preset == null) {
- throw new InvalidCommandException("No preset found for " + getCommandBody());
+ throw new InvalidCommandException(String.format("No preset found for '%s'", getCommandInput()));
}
invoke(() -> session.delete(preset));
- } else if (getCommandBody().isBlank()) {
+ } else if (getCommandInput().isBlank()) {
List presets = session.createQuery("from " + Preset.class.getName() + " where guild_id = '" + guildId + "'", Preset.class).getResultList();
EmbedBuilder embedBuilder = new EmbedBuilder();
if (presets.isEmpty()) {
@@ -47,9 +47,8 @@ public void doRun() {
}
sendMessage(embedBuilder);
} else {
- Pair pair = splitInlineArgument("as");
- String presetString = pair.getLeft();
- String name = pair.getRight();
+ String presetString = getCommandInput();
+ String name = getArgumentValue("as");
Preset existingPreset = SearchEngine.searchPreset(getContext().getSession(), name, getContext().getGuild().getId());
if (existingPreset != null) {
throw new InvalidCommandException("Preset " + name + " already exists");
@@ -58,6 +57,8 @@ public void doRun() {
Preset preset = new Preset(name, presetString, getContext().getGuild(), getContext().getUser());
String testString = presetString.contains("%s") ? name + " test" : name;
AbstractCommand abstractCommand = preset.instantiateCommand(getManager(), getContext(), testString);
+ CommandParser commandParser = new CommandParser(abstractCommand, ArgumentPrefixProperty.getForCurrentContext());
+ commandParser.parse();
abstractCommand.verify();
invoke(() -> session.persist(preset));
}
@@ -71,8 +72,11 @@ public void onSuccess() {
@Override
public ArgumentContribution setupArguments() {
ArgumentContribution argumentContribution = new ArgumentContribution(this);
+ argumentContribution.map("as").setRequiresValue(true).excludesArguments("delete")
+ .setDescription("Defines the name that will be used to call this preset. This argument is mandatory except when using the delete argument.");
argumentContribution.map("delete").setRequiresInput(true)
.setDescription("Delete an existing preset by its name.");
return argumentContribution;
}
+
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/PropertyCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/PropertyCommand.java
index 9fd3bb0a..4a4bf7a3 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/PropertyCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/PropertyCommand.java
@@ -2,9 +2,7 @@
import java.util.List;
-import org.apache.commons.lang3.tuple.Pair;
-
-import net.dv8tion.jda.core.EmbedBuilder;
+import net.dv8tion.jda.api.EmbedBuilder;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
@@ -26,7 +24,7 @@ public PropertyCommand(CommandContribution commandContribution, CommandContext c
@Override
public void doRun() {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
listProperties();
} else {
if (argumentSet("toggle")) {
@@ -38,17 +36,16 @@ public void doRun() {
}
private void setProperty() {
- Pair pair = splitInlineArgument("set");
- AbstractGuildProperty property = Botify.get().getGuildPropertyManager().getPropertyByName(pair.getLeft());
+ AbstractGuildProperty property = Botify.get().getGuildPropertyManager().getPropertyByName(getCommandInput());
if (property != null) {
- property.set(pair.getRight());
+ property.set(getArgumentValue("set"));
} else {
- throw new InvalidCommandException("No such property '" + pair.getLeft() + "'");
+ throw new InvalidCommandException("No such property '" + getCommandInput() + "'");
}
}
private void toggleProperty() {
- AbstractGuildProperty property = Botify.get().getGuildPropertyManager().getPropertyByName(getCommandBody());
+ AbstractGuildProperty property = Botify.get().getGuildPropertyManager().getPropertyByName(getCommandInput());
if (property != null) {
Object value = property.get();
if (value instanceof Boolean) {
@@ -58,7 +55,7 @@ private void toggleProperty() {
throw new InvalidCommandException("Value of property '" + property.getName() + "' is not a boolean");
}
} else {
- throw new InvalidCommandException("No such property '" + getCommandBody() + "'");
+ throw new InvalidCommandException("No such property '" + getCommandInput() + "'");
}
}
@@ -93,6 +90,9 @@ public ArgumentContribution setupArguments() {
ArgumentContribution argumentContribution = new ArgumentContribution(this);
argumentContribution.map("toggle")
.setDescription("Toggles a property with a boolean value (e.g. \"playback notification\") to its opposite value");
+ argumentContribution.map("set").setRequiresValue(true).excludesArguments("toggle")
+ .setDescription("Set a property to the specified value this argument is required when not using the toggle argument. " +
+ "E.g. property default source $set youtube.");
return argumentContribution;
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/QueueCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/QueueCommand.java
index 20c87df3..d77b1667 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/QueueCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/QueueCommand.java
@@ -1,13 +1,16 @@
package net.robinfriedli.botify.command.commands;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
+import com.google.common.collect.Lists;
import com.wrapper.spotify.model_objects.specification.AlbumSimplified;
import com.wrapper.spotify.model_objects.specification.PlaylistSimplified;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
@@ -31,7 +34,7 @@ public QueueCommand(CommandContribution commandContribution, CommandContext cont
@Override
public void doRun() throws Exception {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
listQueue();
} else {
super.doRun();
@@ -60,7 +63,7 @@ private void listQueue() throws Exception {
@Override
public void onSuccess() {
if (loadedTrack != null) {
- sendSuccess("Queued " + loadedTrack.getDisplayInterruptible());
+ sendSuccess("Queued " + loadedTrack.display());
}
if (loadedLocalList != null) {
sendSuccess("Queued playlist " + loadedLocalList.getName());
@@ -83,32 +86,45 @@ public void onSuccess() {
@Override
public void withUserResponse(Object chosenOption) throws Exception {
AudioManager audioManager = Botify.get().getAudioManager();
- PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild());
+ PlayableFactory playableFactory = audioManager.createPlayableFactory(getContext().getGuild(), getSpotifyService());
+ AudioQueue queue = audioManager.getQueue(getContext().getGuild());
+
+ List playables;
+ if (chosenOption instanceof Collection) {
+ playables = playableFactory.createPlayables(!argumentSet("preview"), chosenOption);
+ loadedAmount = playables.size();
+ } else {
+ playables = getPlayablesForOption(chosenOption, playableFactory);
+ }
+
+ queue.add(playables);
+ }
+
+ private List getPlayablesForOption(Object chosenOption, PlayableFactory playableFactory) throws Exception {
if (chosenOption instanceof Track || chosenOption instanceof YouTubeVideo) {
Playable track = playableFactory.createPlayable(!argumentSet("preview"), chosenOption);
- audioManager.getQueue(getContext().getGuild()).add(track);
loadedTrack = track;
+ return Collections.singletonList(track);
} else if (chosenOption instanceof PlaylistSimplified) {
PlaylistSimplified playlist = (PlaylistSimplified) chosenOption;
List tracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks(playlist));
- AudioPlayback playbackForGuild = audioManager.getPlaybackForGuild(getContext().getGuild());
List playables = playableFactory.createPlayables(!argumentSet("preview"), tracks, false);
- playbackForGuild.getAudioQueue().add(playables);
loadedSpotifyPlaylist = playlist;
+ return playables;
} else if (chosenOption instanceof YouTubePlaylist) {
- AudioPlayback playback = audioManager.getPlaybackForGuild(getContext().getGuild());
YouTubePlaylist youTubePlaylist = (YouTubePlaylist) chosenOption;
List playables = playableFactory.createPlayables(youTubePlaylist, false);
- playback.getAudioQueue().add(playables);
loadedYouTubePlaylist = youTubePlaylist;
+ return playables;
} else if (chosenOption instanceof AlbumSimplified) {
AlbumSimplified album = (AlbumSimplified) chosenOption;
- AudioPlayback playback = audioManager.getPlaybackForGuild(getContext().getGuild());
List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(album.getId()));
List playables = playableFactory.createPlayables(!argumentSet("preview"), tracks, false);
- playback.getAudioQueue().add(playables);
loadedAlbum = album;
+ return playables;
}
+
+ throw new UnsupportedOperationException("Unsupported chosen option type: " + chosenOption.getClass());
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/RemoveCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/RemoveCommand.java
index dc6f02b2..7bb91a03 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/RemoveCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/RemoveCommand.java
@@ -1,12 +1,10 @@
package net.robinfriedli.botify.command.commands;
+import java.util.Collection;
import java.util.List;
-import org.apache.commons.lang3.tuple.Pair;
-
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
-import net.robinfriedli.botify.command.ClientQuestionEvent;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.entities.Playlist;
@@ -19,6 +17,8 @@
import net.robinfriedli.stringlist.StringListImpl;
import org.hibernate.Session;
+import static java.lang.String.*;
+
public class RemoveCommand extends AbstractCommand {
public RemoveCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, String identifier, String description) {
@@ -28,18 +28,18 @@ public RemoveCommand(CommandContribution commandContribution, CommandContext con
@Override
public void doRun() {
- Pair pair = splitInlineArgument("from");
Session session = getContext().getSession();
- Playlist playlist = SearchEngine.searchLocalList(session, pair.getRight(), isPartitioned(), getContext().getGuild().getId());
+ String playlistName = getArgumentValue("from");
+ Playlist playlist = SearchEngine.searchLocalList(session, playlistName, isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + pair.getRight());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", playlistName));
} else if (playlist.isEmpty()) {
throw new InvalidCommandException("Playlist is empty");
}
if (argumentSet("index")) {
- StringList indices = StringListImpl.create(pair.getLeft(), "-");
+ StringList indices = StringListImpl.create(getCommandInput(), "-");
if (indices.size() == 1 || indices.size() == 2) {
indices.applyForEach(String::trim);
if (indices.size() == 1) {
@@ -61,19 +61,15 @@ public void doRun() {
throw new InvalidCommandException("Expected one or two indices but found " + indices.size());
}
} else {
- List playlistItems = SearchEngine.searchPlaylistItems(playlist, pair.getLeft());
+ List playlistItems = SearchEngine.searchPlaylistItems(playlist, getCommandInput());
if (playlistItems.size() == 1) {
invoke(() -> session.delete(playlistItems.get(0)));
} else if (playlistItems.isEmpty()) {
- throw new NoResultsFoundException("No tracks found for " + pair.getLeft() + " on list " + pair.getRight());
+ throw new NoResultsFoundException(String.format("No tracks found for '%s' on list '%s'", getCommandInput(), playlistName));
} else {
- ClientQuestionEvent question = new ClientQuestionEvent(this);
- for (int i = 0; i < playlistItems.size(); i++) {
- PlaylistItem item = playlistItems.get(i);
- question.mapOption(String.valueOf(i), item, item.display());
- }
- question.mapOption("all", playlistItems, "All of the above");
- askQuestion(question);
+ askQuestion(playlistItems,
+ PlaylistItem::display,
+ item -> valueOf(item.getIndex() + 1));
}
}
}
@@ -88,7 +84,7 @@ private int parse(String s) {
private void checkIndex(int index, Playlist playlist) {
if (!(index > 0 && index <= playlist.getSize())) {
- throw new InvalidCommandException(String.format("Invalid index '%s'. Needs to in range 1 - %s", index, playlist.getSize()));
+ throw new InvalidCommandException(format("Invalid index '%s'. Needs to in range 1 - %s", index, playlist.getSize()));
}
}
@@ -102,8 +98,8 @@ public void withUserResponse(Object option) {
Session session = getContext().getSession();
invoke(() -> {
- if (option instanceof List) {
- for (Object o : ((List) option)) {
+ if (option instanceof Collection) {
+ for (Object o : ((Collection) option)) {
session.delete(o);
}
} else {
@@ -116,6 +112,8 @@ public void withUserResponse(Object option) {
@Override
public ArgumentContribution setupArguments() {
ArgumentContribution argumentContribution = new ArgumentContribution(this);
+ argumentContribution.map("from").setRequiresValue(true)
+ .setDescription("Mandatory argument to specify the playlist from which to remove the items.");
argumentContribution.map("index").setDescription("Remove items by their index. You can also provide an index" +
"range like $botify remove 13-19 $from list. This includes starting and end index.");
return argumentContribution;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/RenameCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/RenameCommand.java
index 390a074d..bf0a6986 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/RenameCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/RenameCommand.java
@@ -20,11 +20,11 @@ public RenameCommand(CommandContribution commandContribution, CommandContext com
public void doRun() {
GuildManager guildManager = Botify.get().getGuildManager();
- if (getCommandBody().length() < 1 || getCommandBody().length() > 20) {
+ if (getCommandInput().length() < 1 || getCommandInput().length() > 20) {
throw new InvalidCommandException("Length should be 1 - 20 characters");
}
- couldChangeNickname = guildManager.setName(getContext().getGuild(), getCommandBody());
+ couldChangeNickname = getContext().getGuildContext().setBotName(getCommandInput());
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/ReverseCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/ReverseCommand.java
index 3bac95c3..ca76d57f 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/ReverseCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/ReverseCommand.java
@@ -29,12 +29,12 @@ public void doRun() {
long toReverseMs;
try {
if (argumentSet("minutes")) {
- toReverseMs = Integer.parseInt(getCommandBody()) * 60000;
+ toReverseMs = Integer.parseInt(getCommandInput()) * 60000;
} else {
- toReverseMs = Integer.parseInt(getCommandBody()) * 1000;
+ toReverseMs = Integer.parseInt(getCommandInput()) * 1000;
}
} catch (NumberFormatException e) {
- throw new InvalidCommandException("'" + getCommandBody() + "' is not convertible to type integer. " +
+ throw new InvalidCommandException("'" + getCommandInput() + "' is not convertible to type integer. " +
"Please enter a valid number.");
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/RewindCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/RewindCommand.java
index 8c347da5..d03553f9 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/RewindCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/RewindCommand.java
@@ -1,7 +1,7 @@
package net.robinfriedli.botify.command.commands;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.VoiceChannel;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.VoiceChannel;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
@@ -24,21 +24,21 @@ public void doRun() {
Guild guild = getContext().getGuild();
AudioPlayback playback = audioManager.getPlaybackForGuild(guild);
AudioQueue queue = playback.getAudioQueue();
- VoiceChannel channel = guild.getMember(getContext().getUser()).getVoiceState().getChannel();
+ VoiceChannel channel = getContext().getVoiceChannel();
if (!queue.hasPrevious()) {
throw new InvalidCommandException("No previous item in queue");
}
int queueSize = queue.getTracks().size();
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
queue.reverse();
} else {
int offset;
try {
- offset = Integer.parseInt(getCommandBody());
+ offset = Integer.parseInt(getCommandInput());
} catch (NumberFormatException e) {
- throw new InvalidCommandException(getCommandBody() + " is not an integer");
+ throw new InvalidCommandException(getCommandInput() + " is not an integer");
}
if (offset < 1) {
@@ -60,7 +60,7 @@ public void doRun() {
queue.setPosition(newIndex);
}
- audioManager.playTrack(guild, channel);
+ audioManager.startPlayback(guild, channel);
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/SearchCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/SearchCommand.java
index cde265b3..6c40c98a 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/SearchCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/SearchCommand.java
@@ -3,15 +3,16 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
-import com.wrapper.spotify.SpotifyApi;
import com.wrapper.spotify.model_objects.specification.AlbumSimplified;
import com.wrapper.spotify.model_objects.specification.ArtistSimplified;
import com.wrapper.spotify.model_objects.specification.PlaylistSimplified;
import com.wrapper.spotify.model_objects.specification.Track;
-import net.dv8tion.jda.core.EmbedBuilder;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.youtube.YouTubePlaylist;
import net.robinfriedli.botify.audio.youtube.YouTubeService;
@@ -25,6 +26,7 @@
import net.robinfriedli.botify.exceptions.InvalidCommandException;
import net.robinfriedli.botify.exceptions.NoResultsFoundException;
import net.robinfriedli.botify.exceptions.NoSpotifyResultsFoundException;
+import net.robinfriedli.botify.exceptions.UnavailableResourceException;
import net.robinfriedli.botify.util.PropertiesLoadingService;
import net.robinfriedli.botify.util.SearchEngine;
import net.robinfriedli.botify.util.Table2;
@@ -61,11 +63,11 @@ public void doRun() throws Exception {
}
private void searchSpotifyTrack() throws Exception {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
throw new InvalidCommandException("No search term entered");
}
- Callable> loadTrackCallable = () -> getSpotifyService().searchTrack(getCommandBody(), argumentSet("own"));
+ Callable> loadTrackCallable = () -> getSpotifyService().searchTrack(getCommandInput(), argumentSet("own"));
List found;
if (argumentSet("own")) {
found = runWithLogin(loadTrackCallable);
@@ -85,11 +87,11 @@ private void searchSpotifyTrack() throws Exception {
sendMessage(embedBuilder);
} else {
- throw new NoSpotifyResultsFoundException("No Spotify track found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify track found for '%s'", getCommandInput()));
}
}
- private void searchYouTubeVideo() throws InterruptedException {
+ private void searchYouTubeVideo() throws UnavailableResourceException {
YouTubeService youTubeService = Botify.get().getAudioManager().getYouTubeService();
if (argumentSet("limit")) {
int limit = getArgumentValue("limit", Integer.class);
@@ -97,27 +99,27 @@ private void searchYouTubeVideo() throws InterruptedException {
throw new InvalidCommandException("Limit must be between 1 and 10");
}
- List youTubeVideos = youTubeService.searchSeveralVideos(limit, getCommandBody());
+ List youTubeVideos = youTubeService.searchSeveralVideos(limit, getCommandInput());
if (youTubeVideos.size() == 1) {
listYouTubeVideo(youTubeVideos.get(0));
} else if (youTubeVideos.isEmpty()) {
- throw new NoResultsFoundException("No YouTube videos found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No YouTube videos found for '%s'", getCommandInput()));
} else {
askQuestion(youTubeVideos, youTubeVideo -> {
try {
return youTubeVideo.getTitle();
- } catch (InterruptedException e) {
- // Unreachable since only HollowYouTubeVideos might get interrupted
+ } catch (UnavailableResourceException e) {
+ // Unreachable since only HollowYouTubeVideos might get cancelled
throw new RuntimeException(e);
}
});
}
} else {
- listYouTubeVideo(youTubeService.searchVideo(getCommandBody()));
+ listYouTubeVideo(youTubeService.searchVideo(getCommandInput()));
}
}
- private void listYouTubeVideo(YouTubeVideo youTubeVideo) throws InterruptedException {
+ private void listYouTubeVideo(YouTubeVideo youTubeVideo) throws UnavailableResourceException {
StringBuilder responseBuilder = new StringBuilder();
responseBuilder.append("Title: ").append(youTubeVideo.getTitle()).append(System.lineSeparator());
responseBuilder.append("Id: ").append(youTubeVideo.getVideoId()).append(System.lineSeparator());
@@ -128,7 +130,7 @@ private void listYouTubeVideo(YouTubeVideo youTubeVideo) throws InterruptedExcep
}
private void listLocalList() throws IOException {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
Session session = getContext().getSession();
List playlists;
if (isPartitioned()) {
@@ -151,9 +153,9 @@ private void listLocalList() throws IOException {
sendMessage(embedBuilder);
} else {
- Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandBody(), isPartitioned(), getContext().getGuild().getId());
+ Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandInput(), isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new NoResultsFoundException("No local list found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No local list found for '%s'", getCommandInput()));
}
String createdUserId = playlist.getCreatedUserId();
@@ -161,7 +163,8 @@ private void listLocalList() throws IOException {
if (createdUserId.equals("system")) {
createdUser = playlist.getCreatedUser();
} else {
- createdUser = getContext().getJda().getUserById(createdUserId).getName();
+ User userById = getContext().getJda().getUserById(createdUserId);
+ createdUser = userById != null ? userById.getName() : playlist.getCreatedUser();
}
@@ -197,21 +200,21 @@ private void listYouTubePlaylists() {
throw new InvalidCommandException("Limit must be between 1 and 10");
}
- List playlists = youTubeService.searchSeveralPlaylists(limit, getCommandBody());
+ List playlists = youTubeService.searchSeveralPlaylists(limit, getCommandInput());
if (playlists.size() == 1) {
listYouTubePlaylist(playlists.get(0));
} else if (playlists.isEmpty()) {
- throw new NoResultsFoundException("No YouTube playlist found for " + getCommandBody());
+ throw new NoResultsFoundException(String.format("No YouTube playlist found for '%s'", getCommandInput()));
} else {
askQuestion(playlists, YouTubePlaylist::getTitle, YouTubePlaylist::getChannelTitle);
}
} else {
- listYouTubePlaylist(youTubeService.searchPlaylist(getCommandBody()));
+ listYouTubePlaylist(youTubeService.searchPlaylist(getCommandInput()));
}
}
private void listYouTubePlaylist(YouTubePlaylist youTubePlaylist) {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
throw new InvalidCommandException("Command body may not be empty when searching YouTube list");
}
@@ -225,7 +228,7 @@ private void listYouTubePlaylist(YouTubePlaylist youTubePlaylist) {
}
private void listSpotifyList() throws Exception {
- String commandBody = getCommandBody();
+ String commandBody = getCommandInput();
if (commandBody.isBlank()) {
throw new InvalidCommandException("Command may not be empty when searching spotify lists");
@@ -233,23 +236,23 @@ private void listSpotifyList() throws Exception {
List playlists;
if (argumentSet("own")) {
- playlists = runWithLogin(() -> getSpotifyService().searchOwnPlaylist(getCommandBody()));
+ playlists = runWithLogin(() -> getSpotifyService().searchOwnPlaylist(getCommandInput()));
} else {
- playlists = runWithCredentials(() -> getSpotifyService().searchPlaylist(getCommandBody()));
+ playlists = runWithCredentials(() -> getSpotifyService().searchPlaylist(getCommandInput()));
}
if (playlists.size() == 1) {
PlaylistSimplified playlist = playlists.get(0);
List tracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks(playlist));
listTracks(tracks, playlist.getName(), playlist.getOwner().getDisplayName(), null, "playlist/" + playlist.getId());
} else if (playlists.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No Spotify playlist found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No Spotify playlist found for '%s'", getCommandInput()));
} else {
askQuestion(playlists, PlaylistSimplified::getName, p -> p.getOwner().getDisplayName());
}
}
private void listSpotifyAlbum() throws Exception {
- Callable> loadAlbumsCallable = () -> getSpotifyService().searchAlbum(getCommandBody(), argumentSet("own"));
+ Callable> loadAlbumsCallable = () -> getSpotifyService().searchAlbum(getCommandInput(), argumentSet("own"));
List albums;
if (argumentSet("own")) {
albums = runWithLogin(loadAlbumsCallable);
@@ -268,7 +271,7 @@ private void listSpotifyAlbum() throws Exception {
"album/" + album.getId()
);
} else if (albums.isEmpty()) {
- throw new NoSpotifyResultsFoundException("No album found for " + getCommandBody());
+ throw new NoSpotifyResultsFoundException(String.format("No album found for '%s'", getCommandInput()));
} else {
askQuestion(albums, AlbumSimplified::getName, album -> StringListImpl.create(album.getArtists(), ArtistSimplified::getName).toSeparatedString(", "));
}
@@ -311,8 +314,11 @@ public void onSuccess() {
@Override
public void withUserResponse(Object chosenOption) throws Exception {
+ if (chosenOption instanceof Collection) {
+ throw new InvalidCommandException("Cannot select more than one result");
+ }
+
if (chosenOption instanceof PlaylistSimplified) {
- SpotifyApi spotifyApi = getContext().getSpotifyApi();
PlaylistSimplified playlist = (PlaylistSimplified) chosenOption;
List tracks = runWithCredentials(() -> getSpotifyService().getPlaylistTracks(playlist));
listTracks(tracks, playlist.getName(), playlist.getOwner().getDisplayName(), null, "playlist/" + playlist.getId());
@@ -321,7 +327,6 @@ public void withUserResponse(Object chosenOption) throws Exception {
} else if (chosenOption instanceof YouTubeVideo) {
listYouTubeVideo((YouTubeVideo) chosenOption);
} else if (chosenOption instanceof AlbumSimplified) {
- SpotifyApi spotifyApi = getContext().getSpotifyApi();
AlbumSimplified album = (AlbumSimplified) chosenOption;
List tracks = runWithCredentials(() -> getSpotifyService().getAlbumTracks(album.getId()));
listTracks(tracks,
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/ShuffleCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/ShuffleCommand.java
index 4301770f..741c91e0 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/ShuffleCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/ShuffleCommand.java
@@ -42,7 +42,7 @@ public void onSuccess() {
AudioQueue queue = playback.getAudioQueue();
if (queue.hasNext()) {
- messageBuilder.append(" New next track: ").append(queue.getNext().getDisplayInterruptible());
+ messageBuilder.append(" New next track: ").append(queue.getNext().display());
}
sendSuccess(messageBuilder.toString());
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/SkipCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/SkipCommand.java
index d8f14308..bb8bfb65 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/SkipCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/SkipCommand.java
@@ -1,7 +1,7 @@
package net.robinfriedli.botify.command.commands;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.VoiceChannel;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.VoiceChannel;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
@@ -24,20 +24,20 @@ public void doRun() {
Guild guild = getContext().getGuild();
AudioPlayback playback = audioManager.getPlaybackForGuild(guild);
AudioQueue queue = playback.getAudioQueue();
- VoiceChannel channel = guild.getMember(getContext().getUser()).getVoiceState().getChannel();
+ VoiceChannel channel = getContext().getVoiceChannel();
if (!queue.hasNext()) {
throw new InvalidCommandException("No next item in queue");
}
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
queue.iterate();
} else {
int offset;
try {
- offset = Integer.parseInt(getCommandBody());
+ offset = Integer.parseInt(getCommandInput());
} catch (NumberFormatException e) {
- throw new InvalidCommandException(getCommandBody() + " is not an integer");
+ throw new InvalidCommandException(getCommandInput() + " is not an integer");
}
if (offset < 1) {
@@ -61,7 +61,7 @@ public void doRun() {
queue.setPosition(newIndex);
}
- audioManager.playTrack(guild, channel);
+ audioManager.startPlayback(guild, channel);
}
@Override
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/StopCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/StopCommand.java
index 945af87a..99404934 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/StopCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/StopCommand.java
@@ -1,6 +1,6 @@
package net.robinfriedli.botify.command.commands;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/UploadCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/UploadCommand.java
index 99ccaf8f..767c3ecf 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/UploadCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/UploadCommand.java
@@ -25,10 +25,10 @@ public UploadCommand(CommandContribution commandContribution, CommandContext con
@Override
public void doRun() throws Exception {
SpotifyApi spotifyApi = getContext().getSpotifyApi();
- Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandBody(), isPartitioned(), getContext().getGuild().getId());
+ Playlist playlist = SearchEngine.searchLocalList(getContext().getSession(), getCommandInput(), isPartitioned(), getContext().getGuild().getId());
if (playlist == null) {
- throw new InvalidCommandException("No local list found for " + getCommandBody());
+ throw new InvalidCommandException(String.format("No local list found for '%s'", getCommandInput()));
}
runWithLogin(() -> {
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/VolumeCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/VolumeCommand.java
index 6bf2481a..ecc34b93 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/VolumeCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/VolumeCommand.java
@@ -20,9 +20,9 @@ public void doRun() {
int volume;
try {
- volume = Integer.parseInt(getCommandBody());
+ volume = Integer.parseInt(getCommandInput());
} catch (NumberFormatException e) {
- throw new InvalidCommandException("'" + getCommandBody() + "' is not an integer");
+ throw new InvalidCommandException("'" + getCommandInput() + "' is not an integer");
}
if (!(volume > 0 && volume <= 200)) {
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/CleanDbCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/CleanDbCommand.java
index 3dbcfd14..108da043 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/CleanDbCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/CleanDbCommand.java
@@ -8,9 +8,9 @@
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.ISnowflake;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.ISnowflake;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractAdminCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/LoadDocumentCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/LoadDocumentCommand.java
index 59d87c6c..c028ac84 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/LoadDocumentCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/LoadDocumentCommand.java
@@ -2,7 +2,7 @@
import java.util.List;
-import net.dv8tion.jda.core.EmbedBuilder;
+import net.dv8tion.jda.api.EmbedBuilder;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractAdminCommand;
import net.robinfriedli.botify.command.CommandContext;
@@ -28,17 +28,17 @@ public void runAdmin() {
JxpBackend jxpBackend = Botify.get().getJxpBackend();
EmbedBuilder embedBuilder;
try (Context context = jxpBackend.createLazyContext(PropertiesLoadingService.requireProperty("EMBED_DOCUMENTS_PATH"))) {
- if (getCommandBody().isBlank()) {
+ if (getCommandInput().isBlank()) {
List documents = context.getInstancesOf(EmbedDocumentContribution.class);
embedBuilder = new EmbedBuilder();
Util.appendEmbedList(embedBuilder, documents, EmbedDocumentContribution::getName, "Documents");
} else {
EmbedDocumentContribution document = context
- .query(attribute("name").fuzzyIs(getCommandBody()), EmbedDocumentContribution.class)
+ .query(attribute("name").fuzzyIs(getCommandInput()), EmbedDocumentContribution.class)
.getOnlyResult();
if (document == null) {
- throw new InvalidCommandException("No embed document found for " + getCommandBody());
+ throw new InvalidCommandException(String.format("No embed document found for '%s'", getCommandInput()));
}
embedBuilder = document.buildEmbed();
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/QuitCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/QuitCommand.java
index 7ca1199a..c72ac40a 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/QuitCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/QuitCommand.java
@@ -1,12 +1,15 @@
package net.robinfriedli.botify.command.commands.admin;
-import net.dv8tion.jda.core.EmbedBuilder;
+import java.util.Collection;
+
+import net.dv8tion.jda.api.EmbedBuilder;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractAdminCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.entities.xml.CommandContribution;
+import net.robinfriedli.botify.exceptions.InvalidCommandException;
public class QuitCommand extends AbstractAdminCommand {
@@ -17,8 +20,8 @@ public QuitCommand(CommandContribution commandContribution, CommandContext conte
@Override
public void runAdmin() {
StringBuilder confirmationMessageBuilder = new StringBuilder("Do you really want to stop the bot?");
- if (!getCommandBody().isBlank()) {
- confirmationMessageBuilder.append(" Reason: '").append(getCommandBody()).append("'");
+ if (!getCommandInput().isBlank()) {
+ confirmationMessageBuilder.append(" Reason: '").append(getCommandInput()).append("'");
}
askConfirmation(confirmationMessageBuilder.toString());
}
@@ -29,6 +32,10 @@ public void onSuccess() {
@Override
public void withUserResponse(Object chosenOption) {
+ if (chosenOption instanceof Collection) {
+ throw new InvalidCommandException("Expected a single selection");
+ }
+
if ((boolean) chosenOption) {
doQuit();
}
@@ -42,8 +49,8 @@ private void doQuit() {
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setTitle("Scheduled shutdown");
embedBuilder.setDescription("The bot is scheduled to shut down after completing queued actions. No commands will be accepted until then.");
- if (!getCommandBody().isBlank()) {
- embedBuilder.addField("Reason", getCommandBody(), false);
+ if (!getCommandInput().isBlank()) {
+ embedBuilder.addField("Reason", getCommandInput(), false);
}
sendToActiveGuilds(embedBuilder.build());
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/RebootCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/RebootCommand.java
index a20c3d55..3c6ded8b 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/RebootCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/RebootCommand.java
@@ -2,17 +2,19 @@
import java.io.IOException;
import java.io.PrintStream;
+import java.util.Collection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.dv8tion.jda.core.EmbedBuilder;
+import net.dv8tion.jda.api.EmbedBuilder;
import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.command.AbstractAdminCommand;
import net.robinfriedli.botify.command.ArgumentContribution;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
import net.robinfriedli.botify.entities.xml.CommandContribution;
+import net.robinfriedli.botify.exceptions.InvalidCommandException;
public class RebootCommand extends AbstractAdminCommand {
@@ -23,8 +25,8 @@ public RebootCommand(CommandContribution commandContribution, CommandContext con
@Override
public void runAdmin() {
StringBuilder confirmationMessageBuilder = new StringBuilder("Do you really want to restart the bot?");
- if (!getCommandBody().isBlank()) {
- confirmationMessageBuilder.append(" Reason: '").append(getCommandBody()).append("'");
+ if (!getCommandInput().isBlank()) {
+ confirmationMessageBuilder.append(" Reason: '").append(getCommandInput()).append("'");
}
askConfirmation(confirmationMessageBuilder.toString());
}
@@ -35,6 +37,10 @@ public void onSuccess() {
@Override
public void withUserResponse(Object chosenOption) {
+ if (chosenOption instanceof Collection) {
+ throw new InvalidCommandException("Expected a single selection");
+ }
+
if ((boolean) chosenOption) {
doRestart();
}
@@ -48,8 +54,8 @@ private void doRestart() {
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setTitle("Scheduled restart");
embedBuilder.setDescription("The bot is scheduled to restart after completing pending actions. No commands will be accepted until then.");
- if (!getCommandBody().isBlank()) {
- embedBuilder.addField("Reason", getCommandBody(), false);
+ if (!getCommandInput().isBlank()) {
+ embedBuilder.addField("Reason", getCommandInput(), false);
}
sendToActiveGuilds(embedBuilder.build());
}
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/ReloadContextCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/ReloadContextCommand.java
index b98f3768..b366bad6 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/ReloadContextCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/ReloadContextCommand.java
@@ -23,17 +23,17 @@ public void runAdmin() {
JxpBackend jxpBackend = Botify.get().getJxpBackend();
Context context;
if (argumentSet("property")) {
- String property = PropertiesLoadingService.loadProperty(getCommandBody());
+ String property = PropertiesLoadingService.loadProperty(getCommandInput());
if (Strings.isNullOrEmpty(property)) {
- throw new InvalidCommandException("Property " + getCommandBody() + " not set");
+ throw new InvalidCommandException("Property " + getCommandInput() + " not set");
}
context = jxpBackend.getExistingContext(property);
} else {
- context = jxpBackend.getExistingContext(getCommandBody());
+ context = jxpBackend.getExistingContext(getCommandInput());
}
if (context == null) {
- throw new InvalidCommandException("No context mapped to " + getCommandBody());
+ throw new InvalidCommandException("No context mapped to " + getCommandInput());
}
context.reload();
diff --git a/src/main/java/net/robinfriedli/botify/command/commands/admin/UpdateCommand.java b/src/main/java/net/robinfriedli/botify/command/commands/admin/UpdateCommand.java
index 9e88039b..2ba6842f 100644
--- a/src/main/java/net/robinfriedli/botify/command/commands/admin/UpdateCommand.java
+++ b/src/main/java/net/robinfriedli/botify/command/commands/admin/UpdateCommand.java
@@ -6,10 +6,11 @@
import java.io.InputStream;
import java.io.InputStreamReader;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.MessageBuilder;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageChannel;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.requests.restaction.MessageAction;
import net.robinfriedli.botify.command.AbstractAdminCommand;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.CommandManager;
@@ -47,7 +48,10 @@ private void sendOutput(Process process) throws IOException {
MessageChannel channel = getContext().getChannel();
Message message = new MessageBuilder().append("Output too long, attaching as file").build();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inputStreamString.getBytes());
- channel.sendFile(byteArrayInputStream, "output.txt", message).queue();
+ getMessageService().accept(channel, c -> {
+ MessageAction messageAction = c.sendMessage(message);
+ return messageAction.addFile(byteArrayInputStream, "output.txt");
+ });
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandExecutionInterceptor.java b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandExecutionInterceptor.java
index 6c41eac5..3fe96b19 100644
--- a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandExecutionInterceptor.java
+++ b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandExecutionInterceptor.java
@@ -3,10 +3,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.api.client.googleapis.json.GoogleJsonResponseException;
+import com.google.common.base.Strings;
import com.wrapper.spotify.exceptions.detailed.TooManyRequestsException;
import com.wrapper.spotify.exceptions.detailed.UnauthorizedException;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.interceptor.CommandInterceptor;
@@ -48,7 +50,7 @@ public void intercept(AbstractCommand command) {
errorMessage = message;
} catch (UserException e) {
MessageService messageService = new MessageService();
- messageService.sendError(e.getMessage(), command.getContext().getChannel());
+ messageService.send(e.buildEmbed().build(), command.getContext().getChannel());
errorMessage = e.getMessage();
} catch (UnauthorizedException e) {
MessageService messageService = new MessageService();
@@ -65,6 +67,17 @@ public void intercept(AbstractCommand command) {
logger.warn("Executing too many Spotify requests", e);
errorMessage = message;
unexpectedException = true;
+ } catch (GoogleJsonResponseException e) {
+ MessageService messageService = new MessageService();
+ String message = e.getDetails().getMessage();
+ StringBuilder responseBuilder = new StringBuilder("Error occurred when requesting data from YouTube.");
+ if (!Strings.isNullOrEmpty(message)) {
+ responseBuilder.append(" Error response: ").append(message);
+ }
+ messageService.sendException(responseBuilder.toString(), command.getContext().getChannel());
+ logger.error("Exception during YouTube request", e);
+ errorMessage = message;
+ unexpectedException = true;
} catch (CommandRuntimeException e) {
if (e.getCause() != null) {
errorMessage = e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage();
@@ -104,7 +117,7 @@ private void postCommand(AbstractCommand command,
Session session = context.getSession();
context.getGuildContext().getInvoker().invoke(session, () -> session.persist(history));
} else {
- logger.warn("Command " + command + " has not history");
+ logger.warn("Command " + command + " has no history");
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandParserInterceptor.java b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandParserInterceptor.java
new file mode 100644
index 00000000..d4e3e25d
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandParserInterceptor.java
@@ -0,0 +1,21 @@
+package net.robinfriedli.botify.command.interceptor.interceptors;
+
+import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.command.interceptor.AbstractChainableCommandInterceptor;
+import net.robinfriedli.botify.command.interceptor.CommandInterceptor;
+import net.robinfriedli.botify.command.parser.CommandParser;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
+import net.robinfriedli.botify.entities.xml.CommandInterceptorContribution;
+
+public class CommandParserInterceptor extends AbstractChainableCommandInterceptor {
+
+ public CommandParserInterceptor(CommandInterceptorContribution contribution, CommandInterceptor next) {
+ super(contribution, next);
+ }
+
+ @Override
+ public void performChained(AbstractCommand command) {
+ CommandParser commandParser = new CommandParser(command, ArgumentPrefixProperty.getForCurrentContext());
+ commandParser.parse();
+ }
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandVerificationInterceptor.java b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandVerificationInterceptor.java
index f4c79f8a..8687514c 100644
--- a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandVerificationInterceptor.java
+++ b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/CommandVerificationInterceptor.java
@@ -16,7 +16,7 @@ public CommandVerificationInterceptor(CommandInterceptorContribution contributio
public void performChained(AbstractCommand command) {
command.verify();
- if (command.getCommandBody().length() > 1000) {
+ if (command.getCommandInput().length() > 1000) {
throw new InvalidCommandException("Command input exceeds maximum length of 1000 characters.");
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/HistoryInterceptor.java b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/HistoryInterceptor.java
index 4f51144d..2e039749 100644
--- a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/HistoryInterceptor.java
+++ b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/HistoryInterceptor.java
@@ -26,7 +26,7 @@ public void performChained(AbstractCommand command) {
history.setTimestamp(LocalDateTime.ofInstant(Instant.ofEpochMilli(currentTimeMillis), ZoneId.systemDefault()));
history.setCommandContextId(command.getContext().getId());
history.setCommandIdentifier(command.getIdentifier());
- history.setCommandBody(command.getCommandBody());
+ history.setCommandBody(command.getCommandInput());
history.setInput(context.getMessage().getContentDisplay());
history.setGuild(context.getGuild().getName());
history.setGuildId(context.getGuild().getId());
diff --git a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/SecurityInterceptor.java b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/SecurityInterceptor.java
index 7dfbe62b..a2f3b85a 100644
--- a/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/SecurityInterceptor.java
+++ b/src/main/java/net/robinfriedli/botify/command/interceptor/interceptors/SecurityInterceptor.java
@@ -1,8 +1,8 @@
package net.robinfriedli.botify.command.interceptor.interceptors;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Member;
-import net.dv8tion.jda.core.entities.User;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.User;
import net.robinfriedli.botify.command.AbstractCommand;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.command.SecurityManager;
diff --git a/src/main/java/net/robinfriedli/botify/command/parser/ArgumentBuildingMode.java b/src/main/java/net/robinfriedli/botify/command/parser/ArgumentBuildingMode.java
new file mode 100644
index 00000000..ad313234
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/parser/ArgumentBuildingMode.java
@@ -0,0 +1,93 @@
+package net.robinfriedli.botify.command.parser;
+
+import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.command.ArgumentContribution;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
+
+/**
+ * Mode implementation that is used to build one argument per instance. This mode is typically switched to if the last
+ * character was an argument prefix. Records the entered argument and its assigned value, if available. Most arguments,
+ * such as '$list' or '$spotify' follow a-set-or-not-set principle and do not require a value and typically get terminated
+ * by whitespace or a following argument prefix. Arguments that are within the command head, meaning before the command
+ * input may be assigned a value using the '=' character (e.g. 'play $youtube $limit=4 foo' or 'play foo="ba ar" input').
+ * Inline arguments that are within the command input receive the command input that follows them up the next argument
+ * as value (e.g. when entering 'move track $on list $to index' the argument 'on' receives 'list' as value and the argument
+ * 'to' receives 'index' as value).
+ */
+public class ArgumentBuildingMode implements CommandParser.Mode {
+
+ private final AbstractCommand command;
+ private final CommandParser commandParser;
+ private final char argumentPrefix;
+ private final boolean isInline;
+
+ private StringBuilder argumentBuilder;
+ private StringBuilder argumentValueBuilder;
+
+ private boolean isRecodingValue;
+
+ public ArgumentBuildingMode(AbstractCommand command, CommandParser commandParser, char argumentPrefix) {
+ this(command, commandParser, argumentPrefix, false);
+ }
+
+ public ArgumentBuildingMode(AbstractCommand command, CommandParser commandParser, char argumentPrefix, boolean isInline) {
+ this.command = command;
+ this.commandParser = commandParser;
+ this.argumentPrefix = argumentPrefix;
+ this.isInline = isInline;
+ argumentBuilder = new StringBuilder();
+ argumentValueBuilder = new StringBuilder();
+ }
+
+ @Override
+ public CommandParser.Mode handle(char character) {
+ if (character == '=') {
+ isRecodingValue = true;
+ return this;
+ } else if (character == ' ') {
+ if (isInline) {
+ if (isRecodingValue) {
+ argumentValueBuilder.append(character);
+ return this;
+ } else {
+ isRecodingValue = true;
+ return this;
+ }
+ } else {
+ terminate();
+ return new ScanningMode(command, commandParser, argumentPrefix);
+ }
+ } else if (character == argumentPrefix || character == ArgumentPrefixProperty.DEFAULT) {
+ terminate();
+ return new ArgumentBuildingMode(command, commandParser, argumentPrefix, isInline);
+ } else {
+ if (isRecodingValue) {
+ argumentValueBuilder.append(character);
+ return this;
+ } else {
+ argumentBuilder.append(character);
+ return this;
+ }
+ }
+ }
+
+ @Override
+ public CommandParser.Mode handleLiteral(char character) {
+ if (isRecodingValue) {
+ argumentValueBuilder.append(character);
+ return this;
+ } else {
+ argumentBuilder.append(character);
+ return this;
+ }
+ }
+
+ @Override
+ public void terminate() {
+ ArgumentContribution argumentContribution = command.getArgumentContribution();
+ String argument = argumentBuilder.toString();
+ String argumentValue = argumentValueBuilder.toString();
+ argumentContribution.setArgument(argument, argumentValue);
+ commandParser.fireOnArgumentParsed(argument, argumentValue);
+ }
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/parser/CommandInputBuildingMode.java b/src/main/java/net/robinfriedli/botify/command/parser/CommandInputBuildingMode.java
new file mode 100644
index 00000000..73b412d5
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/parser/CommandInputBuildingMode.java
@@ -0,0 +1,48 @@
+package net.robinfriedli.botify.command.parser;
+
+import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
+
+/**
+ * Records the command input. E.g. if the used enters 'add $list 00s Rock Anthems $to rock' this mode will set
+ * '00s Rock Anthems' as command input.
+ */
+public class CommandInputBuildingMode implements CommandParser.Mode {
+
+ private final AbstractCommand command;
+ private final CommandParser commandParser;
+ private final char argumentPrefix;
+ private StringBuilder commandInputBuilder;
+ private char lastChar;
+
+ public CommandInputBuildingMode(AbstractCommand command, CommandParser commandParser, char argumentPrefix) {
+ this.command = command;
+ this.commandParser = commandParser;
+ this.argumentPrefix = argumentPrefix;
+ commandInputBuilder = new StringBuilder();
+ }
+
+ @Override
+ public CommandParser.Mode handle(char character) {
+ if ((lastChar == ' ' || lastChar == '"') && (character == argumentPrefix || character == ArgumentPrefixProperty.DEFAULT)) {
+ terminate();
+ return new ArgumentBuildingMode(command, commandParser, argumentPrefix, true);
+ }
+ commandInputBuilder.append(character);
+ lastChar = character;
+ return this;
+ }
+
+ @Override
+ public CommandParser.Mode handleLiteral(char character) {
+ commandInputBuilder.append(character);
+ return this;
+ }
+
+ @Override
+ public void terminate() {
+ String commandInput = commandInputBuilder.toString().trim();
+ command.setCommandInput(commandInput);
+ commandParser.fireOnCommandInputParsed(commandInput);
+ }
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/parser/CommandParseListener.java b/src/main/java/net/robinfriedli/botify/command/parser/CommandParseListener.java
new file mode 100644
index 00000000..a6fe2947
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/parser/CommandParseListener.java
@@ -0,0 +1,51 @@
+package net.robinfriedli.botify.command.parser;
+
+import net.robinfriedli.botify.command.AbstractCommand;
+
+/**
+ * Listener class tha listens to events fired when parsing a command using the {@link CommandParser}
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+public abstract class CommandParseListener {
+
+ /**
+ * Event fired when handling a character returns a different mode (e.g. if the current character is an argument prefix)
+ * than the mode that handled the character.
+ *
+ * @param previousMode the mode that handled the current character
+ * @param newMode the mode that was returned when handling the character, either a different mode or a different
+ * instance of the same mode
+ * @param index the index of the character that caused the mode switch
+ * @param character the character that caused the mode switch
+ */
+ public void onModeSwitch(CommandParser.Mode previousMode, CommandParser.Mode newMode, int index, char character) {
+ }
+
+ /**
+ * Fired when an argument has been fully parsed, including value. I.e. when {@link ArgumentBuildingMode#terminate()}
+ * is called.
+ *
+ * @param argument the argument that was parsed
+ * @param value the value the argument has been assigned
+ */
+ public void onArgumentParsed(String argument, String value) {
+ }
+
+ /**
+ * Fired when the command input has been fully parsed. I.e. when {@link CommandInputBuildingMode#terminate()}
+ * has been called
+ *
+ * @param commandInput the parsed command input
+ */
+ public void onCommandInputParsed(String commandInput) {
+ }
+
+ /**
+ * Fired when a command is done parsing
+ *
+ * @param command the command that has been parsed
+ */
+ public void onParseFinished(AbstractCommand command) {
+ }
+
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/parser/CommandParser.java b/src/main/java/net/robinfriedli/botify/command/parser/CommandParser.java
new file mode 100644
index 00000000..691cf6b2
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/parser/CommandParser.java
@@ -0,0 +1,166 @@
+package net.robinfriedli.botify.command.parser;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.command.interceptor.interceptors.CommandParserInterceptor;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
+import net.robinfriedli.botify.exceptions.CommandParseException;
+import net.robinfriedli.botify.exceptions.IllegalEscapeCharacterException;
+import net.robinfriedli.botify.exceptions.UnclosedQuotationsException;
+import net.robinfriedli.botify.exceptions.UserException;
+
+/**
+ * Parses the body of a command and builds the used arguments and the command input. The CommandParser handles each
+ * entered character individually with a certain {@link Mode}, starting with the {@link ScanningMode}. Each character
+ * returns the {@link Mode} with which to handle the next character.
+ *
+ * E.g. if the user enters '$botify add $list $youtube $limit=5 linkin park $to favs', the {@link CommandParserInterceptor}
+ * will call {@link #parse()} with '$list youtube $limit=5 linkin park $to favs'.
+ * The CommandParser will then recognise all used argument (list, youtube, limit and to) and assign the argument values
+ * (limit = 5 and to = favs) and set the command input to 'linkin park'
+ */
+public class CommandParser {
+
+ private static final Set META = ImmutableSet.of(ArgumentPrefixProperty.DEFAULT, '"', '\\', '=', ' ');
+
+ private final AbstractCommand command;
+ private final char argumentPrefix;
+ private final CommandParseListener[] listeners;
+ private final Logger logger;
+ private Mode currentMode;
+ private boolean isEscaped;
+ private boolean isOpenQuotation;
+
+ public CommandParser(AbstractCommand command, char argumentPrefix, CommandParseListener... listeners) {
+ this.command = command;
+ this.argumentPrefix = argumentPrefix;
+ this.listeners = listeners;
+ logger = LoggerFactory.getLogger(getClass());
+ currentMode = new ScanningMode(command, this, argumentPrefix);
+ }
+
+ public void parse() {
+ parse(command.getCommandBody());
+ }
+
+ public void parse(String input) {
+ char[] chars = input.toCharArray();
+ for (int i = 0; i < chars.length; i++) {
+ char character = chars[i];
+ Mode previousMode = currentMode;
+ try {
+ if (isEscaped) {
+ if (!checkLegalEscape(character)) {
+ throw new IllegalEscapeCharacterException("Illegal escape character at " + (i - 1));
+ }
+ currentMode = currentMode.handleLiteral(character);
+ isEscaped = false;
+ } else if (isOpenQuotation) {
+ if (character == '"') {
+ isOpenQuotation = false;
+ } else {
+ currentMode = currentMode.handleLiteral(character);
+ }
+ } else {
+ if (character == '\\') {
+ isEscaped = true;
+ } else if (character == '"') {
+ isOpenQuotation = true;
+ } else {
+ currentMode = currentMode.handle(character);
+ }
+ }
+
+ if (i == chars.length - 1) {
+ if (isOpenQuotation) {
+ throw new UnclosedQuotationsException("Unclosed double quotes");
+ }
+
+ currentMode.terminate();
+ }
+ } catch (UserException e) {
+ throw new CommandParseException(e.getMessage(), command.getCommandBody(), e, i);
+ } catch (Throwable e) {
+ logger.error("Unexpected exception while parsing command", e);
+ throw e;
+ }
+
+ // fire mode switch event even if it's a different instance of the same mode
+ if (previousMode != currentMode) {
+ fireOnModeSwitch(previousMode, currentMode, i, character);
+ }
+ }
+
+ fireOnParseFinished();
+ }
+
+ void fireOnParseFinished() {
+ emitEvent(listener -> listener.onParseFinished(command));
+ }
+
+ void fireOnModeSwitch(Mode previousMode, Mode newMode, int index, char character) {
+ emitEvent(listener -> listener.onModeSwitch(previousMode, newMode, index, character));
+ }
+
+ void fireOnArgumentParsed(String argument, String value) {
+ emitEvent(listener -> listener.onArgumentParsed(argument, value));
+ }
+
+ void fireOnCommandInputParsed(String commandInput) {
+ emitEvent(listener -> listener.onCommandInputParsed(commandInput));
+ }
+
+ private void emitEvent(Consumer event) {
+ for (CommandParseListener listener : listeners) {
+ try {
+ event.accept(listener);
+ } catch (Throwable e) {
+ logger.error("Error in CommandParseListener", e);
+ }
+ }
+ }
+
+ private boolean checkLegalEscape(char character) {
+ return character == argumentPrefix || META.contains(character);
+ }
+
+ /**
+ * Each parsed character is handled with a certain mode. A Mode defines what happens to an entered character,
+ * whether it is part of an argument declaration or part of the command input.
+ */
+ public interface Mode {
+
+ /**
+ * Handles the currently parsed character. This method is sensitive to meta characters, meaning certain characters
+ * might affect how the next character is parsed. For instance if the current character is '$' the next character
+ * will be part of an argument declaration, if the current character is '"' it might open a quotation etc.
+ *
+ * @param character the current character of the command body
+ * @return the mode to handle the next character with
+ */
+ Mode handle(char character);
+
+ /**
+ * Handles the current character as normal command input, ignoring meta characters. This method is called if the
+ * the last character was an escape character inside a quotation.
+ *
+ * @param character the current character
+ * @return the mode to handle the next character with
+ */
+ Mode handleLiteral(char character);
+
+ /**
+ * Method called before switching to a new mode or after parsing the last character of the command. Signals the
+ * mode to apply all characters that it recorded.
+ */
+ void terminate();
+
+ }
+
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/parser/ScanningMode.java b/src/main/java/net/robinfriedli/botify/command/parser/ScanningMode.java
new file mode 100644
index 00000000..e0921052
--- /dev/null
+++ b/src/main/java/net/robinfriedli/botify/command/parser/ScanningMode.java
@@ -0,0 +1,44 @@
+package net.robinfriedli.botify.command.parser;
+
+import net.robinfriedli.botify.command.AbstractCommand;
+import net.robinfriedli.botify.discord.properties.ArgumentPrefixProperty;
+
+/**
+ * Starting mode when parsing a command that scans for argument prefixes and switches into a {@link ArgumentBuildingMode}
+ * or {@link CommandInputBuildingMode}
+ */
+public class ScanningMode implements CommandParser.Mode {
+
+ private final AbstractCommand command;
+ private final CommandParser commandParser;
+ private final char argumentPrefix;
+
+ public ScanningMode(AbstractCommand command, CommandParser commandParser, char argumentPrefix) {
+ this.command = command;
+ this.commandParser = commandParser;
+ this.argumentPrefix = argumentPrefix;
+ }
+
+ @Override
+ public CommandParser.Mode handle(char character) {
+ if (' ' == character) {
+ return this;
+ } else {
+ if (argumentPrefix == character || ArgumentPrefixProperty.DEFAULT == character) {
+ return new ArgumentBuildingMode(command, commandParser, argumentPrefix);
+ } else {
+ return new CommandInputBuildingMode(command, commandParser, argumentPrefix).handle(character);
+ }
+ }
+ }
+
+ @Override
+ public CommandParser.Mode handleLiteral(char character) {
+ return new CommandInputBuildingMode(command, commandParser, argumentPrefix).handleLiteral(character);
+ }
+
+ @Override
+ public void terminate() {
+ // nothing to do
+ }
+}
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/AbstractWidgetAction.java b/src/main/java/net/robinfriedli/botify/command/widgets/AbstractWidgetAction.java
index a5aee7c3..66a28bae 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/AbstractWidgetAction.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/AbstractWidgetAction.java
@@ -2,7 +2,7 @@
import javax.annotation.Nullable;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import net.robinfriedli.botify.Botify;
/**
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/NowPlayingWidget.java b/src/main/java/net/robinfriedli/botify/command/widgets/NowPlayingWidget.java
index 94bf59ed..5532ed69 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/NowPlayingWidget.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/NowPlayingWidget.java
@@ -3,7 +3,7 @@
import java.util.List;
import com.google.common.collect.Lists;
-import net.dv8tion.jda.core.entities.Message;
+import net.dv8tion.jda.api.entities.Message;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.AbstractWidget;
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/QueueWidget.java b/src/main/java/net/robinfriedli/botify/command/widgets/QueueWidget.java
index 0bf941d9..bc699f21 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/QueueWidget.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/QueueWidget.java
@@ -4,10 +4,10 @@
import java.util.concurrent.CompletableFuture;
import com.google.common.collect.Lists;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
-import net.dv8tion.jda.core.exceptions.InsufficientPermissionException;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.AbstractWidget;
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/actions/PlayPauseAction.java b/src/main/java/net/robinfriedli/botify/command/widgets/actions/PlayPauseAction.java
index 4d050908..4f1960cb 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/actions/PlayPauseAction.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/actions/PlayPauseAction.java
@@ -1,8 +1,8 @@
package net.robinfriedli.botify.command.widgets.actions;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.GuildVoiceState;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.widgets.AbstractWidgetAction;
@@ -28,9 +28,9 @@ protected void handleReaction(GuildMessageReactionAddEvent event) {
if (audioPlayback.isPlaying()) {
audioPlayback.pause();
} else if (!audioPlayback.getAudioQueue().isEmpty() || audioPlayback.isPaused()) {
- User user = event.getUser();
Guild guild = event.getGuild();
- audioManager.playTrack(guild, guild.getMember(user).getVoiceState().getChannel());
+ GuildVoiceState voiceState = event.getMember().getVoiceState();
+ audioManager.startOrResumePlayback(guild, voiceState != null ? voiceState.getChannel() : null);
}
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/actions/RewindAction.java b/src/main/java/net/robinfriedli/botify/command/widgets/actions/RewindAction.java
index aa22346e..759aefbb 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/actions/RewindAction.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/actions/RewindAction.java
@@ -1,8 +1,8 @@
package net.robinfriedli.botify.command.widgets.actions;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.GuildVoiceState;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.audio.AudioQueue;
@@ -28,9 +28,9 @@ protected void handleReaction(GuildMessageReactionAddEvent event) {
queue.reverse();
}
- User user = event.getUser();
Guild guild = event.getGuild();
- audioManager.playTrack(guild, guild.getMember(user).getVoiceState().getChannel());
+ GuildVoiceState voiceState = event.getMember().getVoiceState();
+ audioManager.startPlayback(guild, voiceState != null ? voiceState.getChannel() : null);
}
}
}
diff --git a/src/main/java/net/robinfriedli/botify/command/widgets/actions/SkipAction.java b/src/main/java/net/robinfriedli/botify/command/widgets/actions/SkipAction.java
index 2c154502..56affe86 100644
--- a/src/main/java/net/robinfriedli/botify/command/widgets/actions/SkipAction.java
+++ b/src/main/java/net/robinfriedli/botify/command/widgets/actions/SkipAction.java
@@ -1,8 +1,8 @@
package net.robinfriedli.botify.command.widgets.actions;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.GuildVoiceState;
+import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.audio.AudioQueue;
@@ -24,11 +24,11 @@ public SkipAction(AudioPlayback audioPlayback, AudioManager audioManager) {
protected void handleReaction(GuildMessageReactionAddEvent event) {
AudioQueue queue = audioPlayback.getAudioQueue();
if (!queue.isEmpty()) {
- User user = event.getUser();
Guild guild = event.getGuild();
if (queue.hasNext()) {
queue.iterate();
- audioManager.playTrack(guild, guild.getMember(user).getVoiceState().getChannel());
+ GuildVoiceState voiceState = event.getMember().getVoiceState();
+ audioManager.startPlayback(guild, voiceState != null ? voiceState.getChannel() : null);
} else {
audioPlayback.stop();
}
diff --git a/src/main/java/net/robinfriedli/botify/concurrent/GuildTrackLoadingExecutor.java b/src/main/java/net/robinfriedli/botify/concurrent/GuildTrackLoadingExecutor.java
index ab339061..c56f463d 100644
--- a/src/main/java/net/robinfriedli/botify/concurrent/GuildTrackLoadingExecutor.java
+++ b/src/main/java/net/robinfriedli/botify/concurrent/GuildTrackLoadingExecutor.java
@@ -5,13 +5,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.dv8tion.jda.core.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.MessageChannel;
import net.robinfriedli.botify.audio.PlayableFactory;
import net.robinfriedli.botify.audio.youtube.HollowYouTubeVideo;
import net.robinfriedli.botify.audio.youtube.YouTubeService;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.discord.GuildContext;
-import net.robinfriedli.botify.exceptions.TrackLoadingExceptionHandler;
+import net.robinfriedli.botify.exceptions.handlers.TrackLoadingExceptionHandler;
/**
* Executes loading track information asynchronously, e.g. when populating a YouTube playlist or redirecting
@@ -37,7 +37,7 @@ public GuildTrackLoadingExecutor(GuildContext guildContext) {
* interruptible thread, to learn more about interruptible track loading see
* {@link PlayableFactory#createPlayables(boolean, Collection, boolean)}
*
- * @param r the action to run
+ * @param r the action to run
* @param singleThread if true creates a single thread that might get interrupted and replaced by the next action
*/
public void load(Runnable r, boolean singleThread) {
diff --git a/src/main/java/net/robinfriedli/botify/concurrent/Invoker.java b/src/main/java/net/robinfriedli/botify/concurrent/Invoker.java
index 66a26df9..e25fbdc0 100644
--- a/src/main/java/net/robinfriedli/botify/concurrent/Invoker.java
+++ b/src/main/java/net/robinfriedli/botify/concurrent/Invoker.java
@@ -27,9 +27,9 @@ public void invoke(Session session, CheckedRunnable runnable) {
* spamming of a command that uses this method, e.g. spamming the add command concurrently could evade the playlist
* size limit.
*
- * @param session the target hibernate session, individual for each command execution
+ * @param session the target hibernate session, individual for each command execution
* @param callable tho callable to run
- * @param the return type
+ * @param the return type
* @return the value the callable returns, often void
*/
public synchronized E invoke(Session session, Callable callable) {
diff --git a/src/main/java/net/robinfriedli/botify/concurrent/ThreadExecutionQueue.java b/src/main/java/net/robinfriedli/botify/concurrent/ThreadExecutionQueue.java
index 8730f661..731ac408 100644
--- a/src/main/java/net/robinfriedli/botify/concurrent/ThreadExecutionQueue.java
+++ b/src/main/java/net/robinfriedli/botify/concurrent/ThreadExecutionQueue.java
@@ -12,12 +12,14 @@ public class ThreadExecutionQueue {
private final ConcurrentLinkedQueue queue;
private final Set currentPool;
private final int size;
+ private final Object synchroniseLock;
private boolean closed;
public ThreadExecutionQueue(int size) {
this.queue = new ConcurrentLinkedQueue<>();
currentPool = new HashSet<>();
this.size = size;
+ this.synchroniseLock = new Object();
}
/**
@@ -25,16 +27,18 @@ public ThreadExecutionQueue(int size) {
* @return true if the currentPool has free space, false if the thread was queued instead
*/
public boolean add(QueuedThread thread) {
- if (!closed) {
- queue.add(thread);
- if (currentPool.size() < size) {
- runNext();
- return true;
- }
+ synchronized (synchroniseLock) {
+ if (!closed) {
+ queue.add(thread);
+ if (currentPool.size() < size) {
+ runNext();
+ return true;
+ }
- return false;
- } else {
- throw new IllegalStateException("This " + getClass().getSimpleName() + " has been closed");
+ return false;
+ } else {
+ throw new IllegalStateException("This " + getClass().getSimpleName() + " has been closed");
+ }
}
}
@@ -51,18 +55,20 @@ public void join() throws InterruptedException {
}
}
- synchronized void freeSlot(QueuedThread thread) {
+ void freeSlot(QueuedThread thread) {
currentPool.remove(thread);
if (!closed) {
runNext();
}
}
- private synchronized void runNext() {
- QueuedThread poll = queue.poll();
- if (poll != null) {
- currentPool.add(poll);
- poll.start();
+ private void runNext() {
+ synchronized (synchroniseLock) {
+ QueuedThread poll = queue.poll();
+ if (poll != null) {
+ currentPool.add(poll);
+ poll.start();
+ }
}
}
}
diff --git a/src/main/java/net/robinfriedli/botify/discord/CommandExecutionQueueManager.java b/src/main/java/net/robinfriedli/botify/discord/CommandExecutionQueueManager.java
index b6fb305d..82991185 100644
--- a/src/main/java/net/robinfriedli/botify/discord/CommandExecutionQueueManager.java
+++ b/src/main/java/net/robinfriedli/botify/discord/CommandExecutionQueueManager.java
@@ -1,6 +1,6 @@
package net.robinfriedli.botify.discord;
-import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.api.entities.Guild;
import net.robinfriedli.botify.concurrent.ThreadExecutionQueue;
import net.robinfriedli.botify.util.ISnowflakeMap;
diff --git a/src/main/java/net/robinfriedli/botify/discord/CompletablePlaceholderMessage.java b/src/main/java/net/robinfriedli/botify/discord/CompletablePlaceholderMessage.java
index caf0b864..b7eadf4b 100644
--- a/src/main/java/net/robinfriedli/botify/discord/CompletablePlaceholderMessage.java
+++ b/src/main/java/net/robinfriedli/botify/discord/CompletablePlaceholderMessage.java
@@ -3,36 +3,36 @@
import java.time.OffsetDateTime;
import java.util.Formatter;
import java.util.List;
-import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-import com.google.common.collect.Lists;
-import net.dv8tion.jda.client.entities.Group;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.entities.Category;
-import net.dv8tion.jda.core.entities.ChannelType;
-import net.dv8tion.jda.core.entities.Emote;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.IMentionable;
-import net.dv8tion.jda.core.entities.Member;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageActivity;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.MessageEmbed;
-import net.dv8tion.jda.core.entities.MessageReaction;
-import net.dv8tion.jda.core.entities.MessageType;
-import net.dv8tion.jda.core.entities.PrivateChannel;
-import net.dv8tion.jda.core.entities.Role;
-import net.dv8tion.jda.core.entities.TextChannel;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.requests.RestAction;
-import net.dv8tion.jda.core.requests.restaction.AuditableRestAction;
-import net.dv8tion.jda.core.requests.restaction.MessageAction;
+import org.apache.commons.collections4.Bag;
+
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Category;
+import net.dv8tion.jda.api.entities.ChannelType;
+import net.dv8tion.jda.api.entities.Emote;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.IMentionable;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageActivity;
+import net.dv8tion.jda.api.entities.MessageChannel;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.MessageReaction;
+import net.dv8tion.jda.api.entities.MessageType;
+import net.dv8tion.jda.api.entities.PrivateChannel;
+import net.dv8tion.jda.api.entities.Role;
+import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
+import net.dv8tion.jda.api.requests.restaction.MessageAction;
/**
* Mocks a discord message that has not been loaded yet.
@@ -53,444 +53,308 @@ public void completeExceptionally(Throwable e) {
futureMessage.completeExceptionally(e);
}
- @Override
- public List getMentionedUsers() {
+ public Message unwrap() {
try {
- return getChecked().getMentionedUsers();
- } catch (CancellationException e) {
- return Lists.newArrayList();
+ return futureMessage.get(1, TimeUnit.MINUTES);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException("Wrapped message failed to load", e);
}
}
+ @Nonnull
+ @Override
+ public List getMentionedUsers() {
+ return unwrap().getMentionedUsers();
+ }
+
+ @Nonnull
+ @Override
+ public Bag getMentionedUsersBag() {
+ return unwrap().getMentionedUsersBag();
+ }
+
+ @Nonnull
@Override
public List getMentionedChannels() {
- try {
- return getChecked().getMentionedChannels();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getMentionedChannels();
+ }
+
+ @Nonnull
+ @Override
+ public Bag getMentionedChannelsBag() {
+ return unwrap().getMentionedChannelsBag();
}
+ @Nonnull
@Override
public List getMentionedRoles() {
- try {
- return getChecked().getMentionedRoles();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getMentionedRoles();
}
+ @Nonnull
@Override
- public List getMentionedMembers(Guild guild) {
- try {
- return getChecked().getMentionedMembers(guild);
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ public Bag getMentionedRolesBag() {
+ return unwrap().getMentionedRolesBag();
}
+ @Nonnull
+ @Override
+ public List getMentionedMembers(@Nonnull Guild guild) {
+ return unwrap().getMentionedMembers(guild);
+ }
+
+ @Nonnull
@Override
public List getMentionedMembers() {
- try {
- return getChecked().getMentionedMembers();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getMentionedMembers();
}
+ @Nonnull
@Override
- public List getMentions(MentionType... mentionTypes) {
- try {
- return getChecked().getMentions(mentionTypes);
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ public List getMentions(@Nonnull MentionType... mentionTypes) {
+ return unwrap().getMentions(mentionTypes);
}
@Override
- public boolean isMentioned(IMentionable iMentionable, MentionType... mentionTypes) {
- try {
- return getChecked().isMentioned(iMentionable, mentionTypes);
- } catch (CancellationException e) {
- return false;
- }
+ public boolean isMentioned(@Nonnull IMentionable iMentionable, @Nonnull MentionType... mentionTypes) {
+ return unwrap().isMentioned(iMentionable, mentionTypes);
}
@Override
public boolean mentionsEveryone() {
- try {
- return getChecked().mentionsEveryone();
- } catch (CancellationException e) {
- return false;
- }
+ return unwrap().mentionsEveryone();
}
@Override
public boolean isEdited() {
- try {
- return getChecked().isEdited();
- } catch (CancellationException e) {
- return false;
- }
+ return unwrap().isEdited();
}
+ @Nullable
@Override
- public OffsetDateTime getEditedTime() {
- try {
- return getChecked().getEditedTime();
- } catch (CancellationException e) {
- return null;
- }
+ public OffsetDateTime getTimeEdited() {
+ return unwrap().getTimeEdited();
}
+ @Nonnull
@Override
public User getAuthor() {
- try {
- return getChecked().getAuthor();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getAuthor();
}
+ @Nullable
@Override
public Member getMember() {
- try {
- return getChecked().getMember();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getMember();
}
+ @Nonnull
@Override
public String getJumpUrl() {
- try {
- return getChecked().getJumpUrl();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getJumpUrl();
}
+ @Nonnull
@Override
public String getContentDisplay() {
- try {
- return getChecked().getContentDisplay();
- } catch (CancellationException e) {
- return "";
- }
+ return unwrap().getContentDisplay();
}
+ @Nonnull
@Override
public String getContentRaw() {
- try {
- return getChecked().getContentRaw();
- } catch (CancellationException e) {
- return "";
- }
+ return unwrap().getContentRaw();
}
+ @Nonnull
@Override
public String getContentStripped() {
- try {
- return getChecked().getContentStripped();
- } catch (CancellationException e) {
- return "";
- }
+ return unwrap().getContentStripped();
}
+ @Nonnull
@Override
public List getInvites() {
- try {
- return getChecked().getInvites();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getInvites();
}
+ @Nullable
@Override
public String getNonce() {
- try {
- return getChecked().getNonce();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getNonce();
}
@Override
- public boolean isFromType(ChannelType channelType) {
- try {
- return getChecked().isFromType(channelType);
- } catch (CancellationException e) {
- return false;
- }
+ public boolean isFromType(@Nonnull ChannelType channelType) {
+ return unwrap().isFromType(channelType);
}
+ @Nonnull
@Override
public ChannelType getChannelType() {
- try {
- return getChecked().getChannelType();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getChannelType();
}
@Override
public boolean isWebhookMessage() {
- try {
- return getChecked().isWebhookMessage();
- } catch (CancellationException e) {
- return false;
- }
+ return unwrap().isWebhookMessage();
}
+ @Nonnull
@Override
public MessageChannel getChannel() {
- try {
- return getChecked().getChannel();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getChannel();
}
+ @Nonnull
@Override
public PrivateChannel getPrivateChannel() {
- try {
- return getChecked().getPrivateChannel();
- } catch (CancellationException e) {
- return null;
- }
- }
-
- @Override
- public Group getGroup() {
- try {
- return getChecked().getGroup();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getPrivateChannel();
}
+ @Nonnull
@Override
public TextChannel getTextChannel() {
- try {
- return getChecked().getTextChannel();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getTextChannel();
}
+ @Nullable
@Override
public Category getCategory() {
- try {
- return getChecked().getCategory();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getCategory();
}
+ @Nonnull
@Override
public Guild getGuild() {
- try {
- return getChecked().getGuild();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getGuild();
}
+ @Nonnull
@Override
public List getAttachments() {
- try {
- return getChecked().getAttachments();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getAttachments();
}
+ @Nonnull
@Override
public List getEmbeds() {
- try {
- return getChecked().getEmbeds();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getEmbeds();
}
+ @Nonnull
@Override
public List getEmotes() {
- try {
- return getChecked().getEmotes();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getEmotes();
+ }
+
+ @Nonnull
+ @Override
+ public Bag getEmotesBag() {
+ return unwrap().getEmotesBag();
}
+ @Nonnull
@Override
public List getReactions() {
- try {
- return getChecked().getReactions();
- } catch (CancellationException e) {
- return Lists.newArrayList();
- }
+ return unwrap().getReactions();
}
@Override
public boolean isTTS() {
- try {
- return getChecked().isTTS();
- } catch (CancellationException e) {
- return false;
- }
+ return unwrap().isTTS();
}
@Nullable
@Override
public MessageActivity getActivity() {
- try {
- return getChecked().getActivity();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getActivity();
}
+ @Nonnull
@Override
- public MessageAction editMessage(CharSequence charSequence) {
- try {
- return getChecked().editMessage(charSequence);
- } catch (CancellationException e) {
- return null;
- }
+ public MessageAction editMessage(@Nonnull CharSequence charSequence) {
+ return unwrap().editMessage(charSequence);
}
+ @Nonnull
@Override
- public MessageAction editMessage(MessageEmbed messageEmbed) {
- try {
- return getChecked().editMessage(messageEmbed);
- } catch (CancellationException e) {
- return null;
- }
+ public MessageAction editMessage(@Nonnull MessageEmbed messageEmbed) {
+ return unwrap().editMessage(messageEmbed);
}
+ @Nonnull
@Override
- public MessageAction editMessageFormat(String s, Object... objects) {
- try {
- return getChecked().editMessageFormat(s, objects);
- } catch (CancellationException e) {
- return null;
- }
+ public MessageAction editMessageFormat(@Nonnull String s, @Nonnull Object... objects) {
+ return unwrap().editMessageFormat(s, objects);
}
+ @Nonnull
@Override
- public MessageAction editMessage(Message message) {
- try {
- return getChecked().editMessage(message);
- } catch (CancellationException e) {
- return null;
- }
+ public MessageAction editMessage(@Nonnull Message message) {
+ return unwrap().editMessage(message);
}
+ @Nonnull
@Override
public AuditableRestAction delete() {
- try {
- return getChecked().delete();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().delete();
}
+ @Nonnull
@Override
public JDA getJDA() {
- try {
- return getChecked().getJDA();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getJDA();
}
@Override
public boolean isPinned() {
- try {
- return getChecked().isPinned();
- } catch (CancellationException e) {
- return false;
- }
+ return unwrap().isPinned();
}
+ @Nonnull
@Override
public RestAction pin() {
- try {
- return getChecked().pin();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().pin();
}
+ @Nonnull
@Override
public RestAction unpin() {
- try {
- return getChecked().unpin();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().unpin();
}
+ @Nonnull
@Override
- public RestAction addReaction(Emote emote) {
- try {
- return getChecked().addReaction(emote);
- } catch (CancellationException e) {
- return null;
- }
+ public RestAction addReaction(@Nonnull Emote emote) {
+ return unwrap().addReaction(emote);
}
+ @Nonnull
@Override
- public RestAction addReaction(String s) {
- try {
- return getChecked().addReaction(s);
- } catch (CancellationException e) {
- return null;
- }
+ public RestAction addReaction(@Nonnull String s) {
+ return unwrap().addReaction(s);
}
+ @Nonnull
@Override
public RestAction clearReactions() {
- try {
- return getChecked().clearReactions();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().clearReactions();
}
+ @Nonnull
@Override
public MessageType getType() {
- try {
- return getChecked().getType();
- } catch (CancellationException e) {
- return null;
- }
+ return unwrap().getType();
}
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
- try {
- getChecked().formatTo(formatter, flags, width, precision);
- } catch (CancellationException ignored) {
- }
+ unwrap().formatTo(formatter, flags, width, precision);
}
@Override
public long getIdLong() {
- try {
- return getChecked().getIdLong();
- } catch (CancellationException e) {
- return 0;
- }
- }
-
- private Message getChecked() {
- try {
- return futureMessage.get(1, TimeUnit.MINUTES);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- throw new CancellationException();
- }
+ return unwrap().getIdLong();
}
-
}
diff --git a/src/main/java/net/robinfriedli/botify/discord/GuildContext.java b/src/main/java/net/robinfriedli/botify/discord/GuildContext.java
index 114058d2..a8339123 100644
--- a/src/main/java/net/robinfriedli/botify/discord/GuildContext.java
+++ b/src/main/java/net/robinfriedli/botify/discord/GuildContext.java
@@ -6,12 +6,18 @@
import javax.annotation.Nullable;
import com.google.api.client.util.Lists;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
+import net.robinfriedli.botify.Botify;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.ClientQuestionEvent;
import net.robinfriedli.botify.command.CommandContext;
import net.robinfriedli.botify.concurrent.GuildTrackLoadingExecutor;
import net.robinfriedli.botify.concurrent.Invoker;
+import net.robinfriedli.botify.discord.properties.AbstractGuildProperty;
+import net.robinfriedli.botify.discord.properties.GuildPropertyManager;
import net.robinfriedli.botify.entities.GuildSpecification;
+import net.robinfriedli.botify.util.StaticSessionProvider;
import org.hibernate.Session;
/**
@@ -21,6 +27,7 @@
*/
public class GuildContext {
+ private final Guild guild;
private final AudioPlayback playback;
private final Invoker invoker;
private final long specificationPk;
@@ -31,7 +38,8 @@ public class GuildContext {
*/
private final List pendingQuestions;
- public GuildContext(AudioPlayback playback, long specificationPk, @Nullable Invoker sharedInvoker) {
+ public GuildContext(Guild guild, AudioPlayback playback, long specificationPk, @Nullable Invoker sharedInvoker) {
+ this.guild = guild;
this.playback = playback;
this.specificationPk = specificationPk;
invoker = sharedInvoker == null ? new Invoker() : sharedInvoker;
@@ -39,6 +47,10 @@ public GuildContext(AudioPlayback playback, long specificationPk, @Nullable Invo
pendingQuestions = Lists.newArrayList();
}
+ public Guild getGuild() {
+ return guild;
+ }
+
public AudioPlayback getPlayback() {
return playback;
}
@@ -61,6 +73,56 @@ public GuildSpecification getSpecification() {
return getSpecification(commandContext.getSession());
}
+ public String getBotName() {
+ return StaticSessionProvider.invokeWithSession(session -> {
+ GuildPropertyManager guildPropertyManager = Botify.get().getGuildPropertyManager();
+ GuildSpecification specification = getSpecification(session);
+ AbstractGuildProperty botName = guildPropertyManager.getProperty("botName");
+
+ if (botName != null) {
+ return (String) botName.get(specification);
+ }
+
+ return specification.getBotName();
+ });
+ }
+
+ public boolean setBotName(String name) {
+ StaticSessionProvider.invokeWithSession(session -> {
+ GuildSpecification guildSpecification = getSpecification(session);
+
+ getInvoker().invoke(session, () -> guildSpecification.setBotName(name));
+ });
+ try {
+ guild.getSelfMember().modifyNickname(name).queue();
+ return true;
+ } catch (InsufficientPermissionException ignored) {
+ return false;
+ }
+ }
+
+ public String getPrefix() {
+ return StaticSessionProvider.invokeWithSession(session -> {
+ GuildPropertyManager guildPropertyManager = Botify.get().getGuildPropertyManager();
+ GuildSpecification specification = getSpecification(session);
+ AbstractGuildProperty prefix = guildPropertyManager.getProperty("prefix");
+
+ if (prefix != null) {
+ return (String) prefix.get(specification);
+ }
+
+ return specification.getPrefix();
+ });
+ }
+
+ public void setPrefix(String prefix) {
+ StaticSessionProvider.invokeWithSession(session -> {
+ GuildSpecification guildSpecification = getSpecification(session);
+
+ getInvoker().invoke(session, () -> guildSpecification.setPrefix(prefix));
+ });
+ }
+
public void addQuestion(ClientQuestionEvent question) {
Optional existingQuestion = getQuestion(question.getCommandContext());
existingQuestion.ifPresent(ClientQuestionEvent::destroy);
diff --git a/src/main/java/net/robinfriedli/botify/discord/GuildManager.java b/src/main/java/net/robinfriedli/botify/discord/GuildManager.java
index e77afde0..0a4ee6a1 100644
--- a/src/main/java/net/robinfriedli/botify/discord/GuildManager.java
+++ b/src/main/java/net/robinfriedli/botify/discord/GuildManager.java
@@ -6,13 +6,11 @@
import javax.annotation.Nullable;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Member;
-import net.dv8tion.jda.core.exceptions.InsufficientPermissionException;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.concurrent.Invoker;
-import net.robinfriedli.botify.discord.properties.AbstractGuildProperty;
import net.robinfriedli.botify.discord.properties.GuildPropertyManager;
import net.robinfriedli.botify.entities.AccessConfiguration;
import net.robinfriedli.botify.entities.GuildSpecification;
@@ -49,54 +47,12 @@ public void addGuild(Guild guild) {
}
public String getNameForGuild(Guild guild) {
- return StaticSessionProvider.invokeWithSession(session -> {
- GuildSpecification specification = getContextForGuild(guild).getSpecification(session);
- AbstractGuildProperty botName = guildPropertyManager.getProperty("botName");
-
- if (botName != null) {
- return (String) botName.get(specification);
- }
-
- return specification.getBotName();
- });
+ return getContextForGuild(guild).getBotName();
}
@Nullable
public String getPrefixForGuild(Guild guild) {
- return StaticSessionProvider.invokeWithSession(session -> {
- GuildSpecification specification = getContextForGuild(guild).getSpecification(session);
- AbstractGuildProperty prefix = guildPropertyManager.getProperty("prefix");
-
- if (prefix != null) {
- return (String) prefix.get(specification);
- }
-
- return specification.getPrefix();
- });
- }
-
- public boolean setName(Guild guild, String name) {
- StaticSessionProvider.invokeWithSession(session -> {
- GuildContext guildContext = getContextForGuild(guild);
- GuildSpecification guildSpecification = guildContext.getSpecification(session);
-
- guildContext.getInvoker().invoke(session, () -> guildSpecification.setBotName(name));
- });
- try {
- guild.getController().setNickname(guild.getSelfMember(), name).queue();
- return true;
- } catch (InsufficientPermissionException ignored) {
- return false;
- }
- }
-
- public void setPrefix(Guild guild, String prefix) {
- StaticSessionProvider.invokeWithSession(session -> {
- GuildContext guildContext = getContextForGuild(guild);
- GuildSpecification guildSpecification = guildContext.getSpecification(session);
-
- guildContext.getInvoker().invoke(session, () -> guildSpecification.setPrefix(prefix));
- });
+ return getContextForGuild(guild).getPrefix();
}
public boolean checkAccess(String commandIdentifier, Member member) {
@@ -151,7 +107,7 @@ private GuildContext initializeGuild(Guild guild) {
+ " where guildId = '" + guild.getId() + "'", Long.class).uniqueResultOptional();
if (existingSpecification.isPresent()) {
- GuildContext guildContext = new GuildContext(new AudioPlayback(player, guild), existingSpecification.get(), sharedInvoker);
+ GuildContext guildContext = new GuildContext(guild, new AudioPlayback(player, guild), existingSpecification.get(), sharedInvoker);
guildContexts.put(guild, guildContext);
return guildContext;
} else {
@@ -166,7 +122,7 @@ private GuildContext initializeGuild(Guild guild) {
});
session.getTransaction().commit();
- GuildContext guildContext = new GuildContext(new AudioPlayback(player, guild), newSpecification.getPk(), sharedInvoker);
+ GuildContext guildContext = new GuildContext(guild, new AudioPlayback(player, guild), newSpecification.getPk(), sharedInvoker);
guildContexts.put(guild, guildContext);
return guildContext;
diff --git a/src/main/java/net/robinfriedli/botify/discord/MessageService.java b/src/main/java/net/robinfriedli/botify/discord/MessageService.java
index ef54fa04..e86a94dc 100644
--- a/src/main/java/net/robinfriedli/botify/discord/MessageService.java
+++ b/src/main/java/net/robinfriedli/botify/discord/MessageService.java
@@ -23,19 +23,19 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-import net.dv8tion.jda.core.EmbedBuilder;
-import net.dv8tion.jda.core.JDA;
-import net.dv8tion.jda.core.MessageBuilder;
-import net.dv8tion.jda.core.Permission;
-import net.dv8tion.jda.core.entities.Guild;
-import net.dv8tion.jda.core.entities.Message;
-import net.dv8tion.jda.core.entities.MessageChannel;
-import net.dv8tion.jda.core.entities.MessageEmbed;
-import net.dv8tion.jda.core.entities.TextChannel;
-import net.dv8tion.jda.core.entities.User;
-import net.dv8tion.jda.core.exceptions.ErrorResponseException;
-import net.dv8tion.jda.core.exceptions.InsufficientPermissionException;
-import net.dv8tion.jda.core.requests.restaction.MessageAction;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.JDA;
+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.Message;
+import net.dv8tion.jda.api.entities.MessageChannel;
+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.ErrorResponseException;
+import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
+import net.dv8tion.jda.api.requests.restaction.MessageAction;
import net.robinfriedli.botify.audio.AudioManager;
import net.robinfriedli.botify.audio.AudioPlayback;
import net.robinfriedli.botify.command.CommandContext;
@@ -127,22 +127,28 @@ public CompletableFuture sendWithLogo(EmbedBuilder embedBuilder, Messag
return send(messageBuilder, file, "logo.png", channel);
}
- public void sendWithLogo(EmbedBuilder embedBuilder, Guild guild) throws IOException {
+ public CompletableFuture sendWithLogo(EmbedBuilder embedBuilder, Guild guild) throws IOException {
MessageBuilder messageBuilder = new MessageBuilder();
String baseUri = PropertiesLoadingService.requireProperty("BASE_URI");
InputStream file = new URL(baseUri + "/resources-public/img/botify-logo.png").openStream();
embedBuilder.setThumbnail("attachment://logo.png");
embedBuilder.setColor(ColorSchemeProperty.getColor());
messageBuilder.setEmbed(embedBuilder.build());
- send(messageBuilder, file, "logo.png", guild);
+ return send(messageBuilder, file, "logo.png", guild);
}
public CompletableFuture send(MessageBuilder messageBuilder, InputStream file, String fileName, MessageChannel messageChannel) {
- return accept(messageChannel, c -> c.sendFile(file, fileName, messageBuilder.build()));
+ return accept(messageChannel, c -> {
+ MessageAction messageAction = c.sendMessage(messageBuilder.build());
+ return messageAction.addFile(file, fileName);
+ });
}
public CompletableFuture send(MessageBuilder messageBuilder, InputStream file, String fileName, Guild guild) {
- return acceptForGuild(guild, c -> c.sendFile(file, fileName, messageBuilder.build()));
+ return acceptForGuild(guild, c -> {
+ MessageAction messageAction = c.sendMessage(messageBuilder.build());
+ return messageAction.addFile(file, fileName);
+ });
}
public CompletableFuture sendSuccess(String message, MessageChannel channel) {
@@ -231,15 +237,7 @@ public void sendToActiveGuilds(MessageEmbed message, JDA jda, AudioManager audio
}
}
- private CompletableFuture sendInternal(MessageChannel channel, String text) {
- return accept(channel, c -> c.sendMessage(text));
- }
-
- private CompletableFuture sendInternal(MessageChannel channel, MessageEmbed messageEmbed) {
- return accept(channel, c -> c.sendMessage(messageEmbed));
- }
-
- private CompletableFuture accept(MessageChannel channel, Function function) {
+ public CompletableFuture accept(MessageChannel channel, Function function) {
CompletableFuture futureMessage = new CompletableFuture<>();
try {
MessageAction messageAction = function.apply(channel);
@@ -253,11 +251,7 @@ private CompletableFuture accept(MessageChannel channel, Function {
- if (futureMessage.isDone()) {
- futureMessage.complete(message);
- }
- });
+ acceptForGuild(guild, function).thenAccept(futureMessage::complete);
} else if (channel instanceof TextChannel) {
logger.warn("Unable to send messages to guild " + ((TextChannel) channel).getGuild());
futureMessage.completeExceptionally(e);
@@ -281,7 +275,7 @@ private CompletableFuture accept(MessageChannel channel, Function acceptForGuild(Guild guild, Function function) {
+ public CompletableFuture