diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9beb865 --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + net.agspace.handiebot + handiebot + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + jar + + + + jcenter + http://jcenter.bintray.com + + + jitpack.io + https://jitpack.io + + + + + + com.github.austinv11 + Discord4J + 2.8.2 + + + com.sedmelluq + lavaplayer + 1.2.39 + + + + \ No newline at end of file diff --git a/src/main/java/handiebot/HandieBot.java b/src/main/java/handiebot/HandieBot.java new file mode 100644 index 0000000..274ccce --- /dev/null +++ b/src/main/java/handiebot/HandieBot.java @@ -0,0 +1,149 @@ +package handiebot; + +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import handiebot.command.CommandHandler; +import handiebot.lavaplayer.GuildMusicManager; +import sx.blah.discord.api.ClientBuilder; +import sx.blah.discord.api.IDiscordClient; +import sx.blah.discord.api.events.EventSubscriber; +import sx.blah.discord.handle.audio.IAudioManager; +import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IGuild; +import sx.blah.discord.handle.obj.IVoiceChannel; +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.MissingPermissionsException; +import sx.blah.discord.util.RateLimitException; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Andrew Lalis + * Main Class for the discord bot. Contains client loading information and general event processing. + */ +public class HandieBot { + + private static final String TOKEN = "MjgzNjUyOTg5MjEyNjg4Mzg0.C45A_Q.506b0G6my1FEFa7_YY39lxLBHUY"; + + private static IDiscordClient client; + + private CommandHandler commandHandler; + + public static void main(String[] args) throws DiscordException, RateLimitException { + System.out.println("Logging bot in..."); + client = new ClientBuilder().withToken(TOKEN).build(); + client.getDispatcher().registerListener(new HandieBot()); + client.login(); + } + + private final AudioPlayerManager playerManager; + private final Map musicManagers; + + private HandieBot() { + this.musicManagers = new HashMap<>(); + this.playerManager = new DefaultAudioPlayerManager(); + AudioSourceManagers.registerRemoteSources(playerManager); + AudioSourceManagers.registerLocalSource(playerManager); + + this.commandHandler = new CommandHandler(this); + } + + private synchronized GuildMusicManager getGuildAudioPlayer(IGuild guild) { + long guildId = Long.parseLong(guild.getID()); + GuildMusicManager musicManager = musicManagers.get(guildId); + + if (musicManager == null) { + musicManager = new GuildMusicManager(playerManager); + musicManagers.put(guildId, musicManager); + } + + guild.getAudioManager().setAudioProvider(musicManager.getAudioProvider()); + + return musicManager; + } + + @EventSubscriber + public void onMessageReceived(MessageReceivedEvent event) { + this.commandHandler.handleCommand(event); + } + + public void loadAndPlay(final IChannel channel, final String trackUrl) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + + playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { + @Override + public void trackLoaded(AudioTrack track) { + sendMessageToChannel(channel, "Adding to queue " + track.getInfo().title); + + play(channel.getGuild(), musicManager, track); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) { + AudioTrack firstTrack = playlist.getSelectedTrack(); + + if (firstTrack == null) { + firstTrack = playlist.getTracks().get(0); + } + + sendMessageToChannel(channel, "Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")"); + + play(channel.getGuild(), musicManager, firstTrack); + } + + @Override + public void noMatches() { + sendMessageToChannel(channel, "Nothing found by " + trackUrl); + } + + @Override + public void loadFailed(FriendlyException exception) { + sendMessageToChannel(channel, "Could not play: " + exception.getMessage()); + } + }); + } + + private void play(IGuild guild, GuildMusicManager musicManager, AudioTrack track) { + connectToFirstVoiceChannel(guild.getAudioManager()); + + musicManager.scheduler.queue(track); + } + + public void skipTrack(IChannel channel) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + musicManager.scheduler.nextTrack(); + + sendMessageToChannel(channel, "Skipped to next track."); + } + + private void sendMessageToChannel(IChannel channel, String message) { + try { + channel.sendMessage(message); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void connectToFirstVoiceChannel(IAudioManager audioManager) { + for (IVoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) { + if (voiceChannel.isConnected()) { + return; + } + } + + for (IVoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) { + try { + voiceChannel.join(); + } catch (MissingPermissionsException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/handiebot/command/CommandHandler.java b/src/main/java/handiebot/command/CommandHandler.java new file mode 100644 index 0000000..2567e3f --- /dev/null +++ b/src/main/java/handiebot/command/CommandHandler.java @@ -0,0 +1,88 @@ +package handiebot.command; + +import com.sun.istack.internal.NotNull; +import handiebot.HandieBot; +import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; +import sx.blah.discord.handle.obj.*; +import sx.blah.discord.util.EmbedBuilder; + +/** + * @author Andrew Lalis + * Class to process commands. + */ +public class CommandHandler { + + private static String PREFIX = "!"; + + private final HandieBot bot; + + public CommandHandler(HandieBot bot){ + this.bot = bot; + } + + /** + * Main method to handle user messages. + * @param event The event generated by the message. + */ + public void handleCommand(MessageReceivedEvent event){ + IMessage message = event.getMessage(); + IUser user = event.getAuthor(); + IChannel channel = event.getChannel(); + IGuild guild = event.getGuild(); + String command = extractCommand(message); + String[] args = extractArgs(message); + if (guild != null && command != null){ + if (command.equals("play") && args.length == 1){ + this.bot.loadAndPlay(channel, args[0]); + } else if (command.equals("help")){ + + } + } + } + + /** + * Returns a command word, if one exists, from a given message. + * @param message The message to get a command from. + * @return The command word, minus the prefix, or null. + */ + private String extractCommand(IMessage message){ + String[] words = message.getContent().split(" "); + if (words[0].startsWith(PREFIX)){ + return words[0].replaceFirst(PREFIX, "").toLowerCase(); + } + return null; + } + + /** + * Extracts a list of arguments from a message, assuming a command exists. + * @param message The message to parse. + * @return A list of strings representing args. + */ + @NotNull + private String[] extractArgs(IMessage message){ + String[] words = message.getContent().split(" "); + if (words[0].startsWith(PREFIX)){ + String[] args = new String[words.length-1]; + for (int i = 0; i < words.length-1; i++){ + args[i] = words[i+1]; + } + return args; + } + return new String[0]; + } + + private void sendHelpInfo(IUser user){ + IPrivateChannel pm = user.getOrCreatePMChannel(); + EmbedBuilder builder = new EmbedBuilder(); + + } + + /** + * Sets the prefix used to identify commands. + * @param prefix The prefix appended to the beginning of commands. + */ + public void setPrefix(String prefix){ + PREFIX = prefix; + } + +} diff --git a/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java b/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java new file mode 100644 index 0000000..a07d21d --- /dev/null +++ b/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java @@ -0,0 +1,40 @@ +package handiebot.lavaplayer; + +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +/** + * @author Andrew Lalis + */ +public class AudioLoadResultHandler implements com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler { + + private TrackScheduler scheduler; + + public AudioLoadResultHandler(TrackScheduler scheduler){ + this.scheduler = scheduler; + } + + @Override + public void trackLoaded(AudioTrack audioTrack) { + System.out.println("Adding to queue "+ audioTrack.getInfo().title); + scheduler.queue(audioTrack); + } + + @Override + public void playlistLoaded(AudioPlaylist audioPlaylist) { + System.out.println("Adding playlist to queue."); + audioPlaylist.getTracks().forEach(track -> this.scheduler.queue(track)); + } + + @Override + public void noMatches() { + System.out.println("No matches!"); + } + + @Override + public void loadFailed(FriendlyException e) { + System.out.println("Load failed."); + e.printStackTrace(); + } +} diff --git a/src/main/java/handiebot/lavaplayer/AudioProvider.java b/src/main/java/handiebot/lavaplayer/AudioProvider.java new file mode 100644 index 0000000..0155f4e --- /dev/null +++ b/src/main/java/handiebot/lavaplayer/AudioProvider.java @@ -0,0 +1,52 @@ +package handiebot.lavaplayer; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; +import sx.blah.discord.handle.audio.AudioEncodingType; +import sx.blah.discord.handle.audio.IAudioProvider; + +/** + * Created by Andrew's Computer on 18-Jun-17. + */ +public class AudioProvider implements IAudioProvider { + private final AudioPlayer audioPlayer; + private AudioFrame lastFrame; + + /** + * @param audioPlayer Audio player to wrap. + */ + public AudioProvider(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + } + + @Override + public boolean isReady() { + if (lastFrame == null) { + lastFrame = audioPlayer.provide(); + } + + return lastFrame != null; + } + + @Override + public byte[] provide() { + if (lastFrame == null) { + lastFrame = audioPlayer.provide(); + } + + byte[] data = lastFrame != null ? lastFrame.data : null; + lastFrame = null; + + return data; + } + + @Override + public int getChannels() { + return 2; + } + + @Override + public AudioEncodingType getAudioEncodingType() { + return AudioEncodingType.OPUS; + } +} diff --git a/src/main/java/handiebot/lavaplayer/GuildMusicManager.java b/src/main/java/handiebot/lavaplayer/GuildMusicManager.java new file mode 100644 index 0000000..06af150 --- /dev/null +++ b/src/main/java/handiebot/lavaplayer/GuildMusicManager.java @@ -0,0 +1,26 @@ +package handiebot.lavaplayer; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; + +/** + * @author Andrew Lalis + * Holds the player and track scheduler for a guild. + */ +public class GuildMusicManager { + + public final AudioPlayer player; + + public final TrackScheduler scheduler; + + public GuildMusicManager(AudioPlayerManager manager){ + this.player = manager.createPlayer(); + this.scheduler = new TrackScheduler(this.player); + this.player.addListener(this.scheduler); + } + + public AudioProvider getAudioProvider(){ + return new AudioProvider(this.player); + } + +} diff --git a/src/main/java/handiebot/lavaplayer/TrackScheduler.java b/src/main/java/handiebot/lavaplayer/TrackScheduler.java new file mode 100644 index 0000000..555a6f4 --- /dev/null +++ b/src/main/java/handiebot/lavaplayer/TrackScheduler.java @@ -0,0 +1,65 @@ +package handiebot.lavaplayer; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * @author Andrew Lalis + */ +public class TrackScheduler extends AudioEventAdapter { + + private final AudioPlayer player; + private final BlockingQueue queue; + + /** + * Constructs a new track scheduler with the given player. + * @param player The audio player this scheduler uses. + */ + public TrackScheduler(AudioPlayer player){ + this.player = player; + this.queue = new LinkedBlockingQueue<>(); + } + + /** + * Add the next track to the queue or play right away if nothing is in the queue. + * @param track The track to play or add to the queue. + */ + public void queue(AudioTrack track){ + if (!player.startTrack(track, true)){ + System.out.println("Unable to start track immediately, adding to queue."); + queue.offer(track); + } + } + + /** + * Starts the next track, stopping the current one if it's playing. + */ + public void nextTrack(){ + player.startTrack(queue.poll(), false); + } + + @Override + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + if (endReason.mayStartNext){ + nextTrack(); + } else { + System.out.println(endReason.toString()); + } + } + + @Override + public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception){ + exception.printStackTrace(); + } + + @Override + public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { + super.onTrackStuck(player, track, thresholdMs); + } +} diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png new file mode 100644 index 0000000..f11fbb9 Binary files /dev/null and b/src/main/resources/icon.png differ diff --git a/src/main/resources/icon.svg b/src/main/resources/icon.svg new file mode 100644 index 0000000..5ab890d --- /dev/null +++ b/src/main/resources/icon.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + +