diff --git a/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistRepository.java b/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistRepository.java new file mode 100644 index 0000000..bf02e1b --- /dev/null +++ b/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistRepository.java @@ -0,0 +1,56 @@ +/** + * + * Project: Eight Track, File: GuildSongRepository.java + * + * Copyright 2019-2019 Locusworks LLC. + * All rights reserved. Federal copyright law prohibits unauthorized reproduction by + * any means and imposes fines up to $25,000 for violation. No part of this material + * may be reproduced, transmitted, transcribed, stored in a retrieval system, copied, + * modified, duplicated, adapted or translated into another program language in any + * form or by any means, electronic, mechanical, photocopying, recording, or + * otherwise, without the prior written permission from Locusworks. Locusworks + * affirms that Eight-Track(R) software and data is subject to United States + * Government Purpose Rights. Contact Locusworks, 1313 Lawnview Drive + * Forney TX 75126, (802) 488-0438, for commercial licensing opportunities. + * + * IN NO EVENT SHALL LOCUSWORKS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, + * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF LOCUSWORKS HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NO RESPONSIBILITY IS ASSUMED BY + * LOCUSWORKS FOR ITS USE, OR FOR ANY INFRINGEMENTS OF PATENTS OR OTHER RIGHTS OF + * THIRD PARTIES RESULTING FROM ITS USE. LOCUSWORKS SPECIFICALLY DISCLAIMS ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND + * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS + * IS". LOCUSWORKS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + */ +package net.locusworks.discord.eighttrack.database.repos; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import net.locusworks.discord.eighttrack.database.entities.DiscordGuild; +import net.locusworks.discord.eighttrack.database.entities.GuildPlaylist; + +public interface GuildPlaylistRepository extends CrudRepository { + + GuildPlaylist findByGuildAndUserIdAndPlaylist(DiscordGuild guild, Long userId, String playlist); + + @Query("SELECT gpl FROM GuildPlaylist gpl WHERE gpl.guild.guildId = ?1 AND gpl.userId = ?2 AND gpl.playlist = ?3") + GuildPlaylist findByGuildAndUserIdAndPlaylist(Long guild, Long userId, String playlist); + + List findByGuildAndUserId(DiscordGuild guild, Long userId); + + @Query("SELECT gpl FROM GuildPlaylist gpl WHERE gpl.guild.guildId = ?1 AND gpl.userId = ?2") + List findByGuildAndUserId(Long guild, Long userId); + + @Query("SELECT DISTINCT gpl FROM GuildPlaylist gpl LEFT JOIN FETCH gpl.guildPlaylistSongList WHERE gpl.guild.guildId = ?1 AND gpl.userId = ?2") + List findByGuildAndUserIdFetchSongs(Long guild, Long userId); + + @Query("SELECT DISTINCT gpl FROM GuildPlaylist gpl LEFT JOIN FETCH gpl.guildPlaylistSongList WHERE gpl.guild.guildId = ?1 AND gpl.userId = ?2 AND gpl.playlist = ?3") + GuildPlaylist findGuildUserPlaylistFetchSongs(Long guild, Long userId, String playlist); + +} diff --git a/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistSongRepository.java b/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistSongRepository.java new file mode 100644 index 0000000..6138877 --- /dev/null +++ b/src/main/java/net/locusworks/discord/eighttrack/database/repos/GuildPlaylistSongRepository.java @@ -0,0 +1,46 @@ +/** + * + * Project: Eight Track, File: GuildSongRepository.java + * + * Copyright 2019-2019 Locusworks LLC. + * All rights reserved. Federal copyright law prohibits unauthorized reproduction by + * any means and imposes fines up to $25,000 for violation. No part of this material + * may be reproduced, transmitted, transcribed, stored in a retrieval system, copied, + * modified, duplicated, adapted or translated into another program language in any + * form or by any means, electronic, mechanical, photocopying, recording, or + * otherwise, without the prior written permission from Locusworks. Locusworks + * affirms that Eight-Track(R) software and data is subject to United States + * Government Purpose Rights. Contact Locusworks, 1313 Lawnview Drive + * Forney TX 75126, (802) 488-0438, for commercial licensing opportunities. + * + * IN NO EVENT SHALL LOCUSWORKS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, + * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF LOCUSWORKS HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NO RESPONSIBILITY IS ASSUMED BY + * LOCUSWORKS FOR ITS USE, OR FOR ANY INFRINGEMENTS OF PATENTS OR OTHER RIGHTS OF + * THIRD PARTIES RESULTING FROM ITS USE. LOCUSWORKS SPECIFICALLY DISCLAIMS ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND + * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS + * IS". LOCUSWORKS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + */ +package net.locusworks.discord.eighttrack.database.repos; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import net.locusworks.discord.eighttrack.database.entities.GuildPlaylist; +import net.locusworks.discord.eighttrack.database.entities.GuildPlaylistSong; + +public interface GuildPlaylistSongRepository extends CrudRepository { + + List findByGuildPlaylist(GuildPlaylist gpl); + + @Query("SELECT gpls FROM GuildPlaylistSong gpls WHERE gpls.guildPlaylist = ?1 AND gpls.guildSong.uuid IN ?2") + List findByGuildPlaylistAndSongIds(GuildPlaylist gpl, List songIds); + + +} diff --git a/src/main/java/net/locusworks/discord/eighttrack/handlers/DiscordEventHandler.java b/src/main/java/net/locusworks/discord/eighttrack/handlers/DiscordEventHandler.java index a255546..462b0b4 100644 --- a/src/main/java/net/locusworks/discord/eighttrack/handlers/DiscordEventHandler.java +++ b/src/main/java/net/locusworks/discord/eighttrack/handlers/DiscordEventHandler.java @@ -38,10 +38,14 @@ import java.util.List; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.events.guild.GuildJoinEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.locusworks.discord.eighttrack.database.entities.DiscordGuild; import net.locusworks.discord.eighttrack.services.ConfigurationService; @@ -66,6 +70,9 @@ public class DiscordEventHandler extends ListenerAdapter { @Autowired private GuildSongRepoService guildSongRepoService; + @Autowired + private ReactionHandler reactionHandler; + @PostConstruct private void init() throws IOException { this.musicDir = confService.getMusicDirectory(); @@ -74,6 +81,26 @@ public class DiscordEventHandler extends ListenerAdapter { } } + @Scheduled(fixedRate = 300000L, initialDelay=300000L) + private void clearHandlerCache() { + logger.debug("Clearing reaction handler cache of dead reactions"); + reactionHandler.cleanCache(); + } + + @Override + public void onMessageReactionAdd(MessageReactionAddEvent e) { + if (e.getUser().isBot()) { + return; + } + if (!e.getChannel().getType().equals(ChannelType.TEXT)) { + return; + } + TextChannel channel = (TextChannel) e.getChannel(); + if (reactionHandler.canHandle(channel.getGuild().getIdLong(), e.getMessageIdLong())) { + reactionHandler.handle(channel, e.getMessageIdLong(), e.getUser().getIdLong(), e.getReaction()); + } + } + @Override public void onGuildJoin(GuildJoinEvent event) { try { @@ -108,7 +135,7 @@ public class DiscordEventHandler extends ListenerAdapter { if (!GuildMusicService.getMap().containsKey(event.getGuild().getIdLong())) { GuildMusicService.getMap().put(event.getGuild().getIdLong(), - new GuildMusicHandler(musicDir, event.getGuild().getIdLong(), uploadHandler, guildSongRepoService)); + new GuildMusicHandler(musicDir, event.getGuild().getIdLong(), uploadHandler, reactionHandler, guildSongRepoService)); } GuildMusicHandler gmh = GuildMusicService.getMap().get(event.getGuild().getIdLong()); diff --git a/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java b/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java index 3712755..8bf32f5 100644 --- a/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java +++ b/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java @@ -33,12 +33,16 @@ import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -51,17 +55,24 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.ChannelType; import net.dv8tion.jda.api.entities.GuildChannel; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message.Attachment; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.VoiceChannel; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.managers.AudioManager; +import net.dv8tion.jda.api.requests.ErrorResponse; import net.locusworks.crypto.utils.HashUtils; import net.locusworks.discord.eighttrack.database.entities.DiscordGuild; +import net.locusworks.discord.eighttrack.database.entities.GuildPlaylist; +import net.locusworks.discord.eighttrack.database.entities.GuildPlaylistSong; import net.locusworks.discord.eighttrack.database.entities.GuildSong; import net.locusworks.discord.eighttrack.database.entities.Song; +import net.locusworks.discord.eighttrack.listeners.ReactionListener; import net.locusworks.discord.eighttrack.scheduler.TrackScheduler; import net.locusworks.discord.eighttrack.services.GuildSongRepoService; +import net.locusworks.discord.eighttrack.utils.Reactions; import net.locusworks.logger.ApplicationLogger; import net.locusworks.logger.ApplicationLoggerFactory; @@ -80,9 +91,12 @@ public class GuildMusicHandler { private Consumer callback; private Mp3UploadHandler uploadHandler; private GuildSongRepoService guildSongRepoService; + private ReactionHandler reactionHandler; private long guildId; - public GuildMusicHandler(Path musicDir, long guildId, Mp3UploadHandler uploadHandler, GuildSongRepoService guildSongRepoService) throws IOException { + private GuildPlaylist currentPlaylist; + + public GuildMusicHandler(Path musicDir, long guildId, Mp3UploadHandler uploadHandler, ReactionHandler reactionHandler, GuildSongRepoService guildSongRepoService) throws IOException { this.logger = ApplicationLoggerFactory.getLogger(GuildMusicHandler.class); this.playing = new AtomicBoolean(false); this.musicDir = musicDir; @@ -90,6 +104,7 @@ public class GuildMusicHandler { this.uploadHandler = uploadHandler; this.guildSongRepoService = guildSongRepoService; this.guildId = guildId; + this.reactionHandler = reactionHandler; this.apm = new DefaultAudioPlayerManager(); AudioSourceManagers.registerLocalSource(apm); @@ -98,11 +113,31 @@ public class GuildMusicHandler { this.ts = new TrackScheduler(); player.addListener(ts); } - + + public void playlist(GuildMessageReceivedEvent event, List commands) throws Exception { + + String command = commands.remove(0); + + switch(command) { + case "add": + addPlayList(event, commands); + return; + case "delete": + deletePlayList(event, commands); + return; + case "list": + listPlayList(event, commands); + return; + case "play": + playPlayList(event, commands); + return; + } + } + public Long getCurrentVoiceChannelId() { return voiceChannelId; } - + public OffsetDateTime getLastPlayed() { return lastPlayed; } @@ -114,7 +149,7 @@ public class GuildMusicHandler { public void isPlaying(boolean playing) { this.playing.set(playing); } - + public void upNext(GuildMessageReceivedEvent event) throws Exception { upNext(event, null); } @@ -152,7 +187,7 @@ public class GuildMusicHandler { } }); } - + public void next(GuildMessageReceivedEvent event) throws Exception { next(event, null); } @@ -172,23 +207,30 @@ public class GuildMusicHandler { } playing.set(false); - stop(event); + stop(event, true); play(event, commands); playing.set(true); } public void stop(GuildMessageReceivedEvent event) { + stop(event, false); + } + + public void stop(GuildMessageReceivedEvent event, boolean stoppedFromRepeat) { player.stopTrack(); voiceChannelId = null; + if (!stoppedFromRepeat) { + currentPlaylist = null; + } } - + public void play(GuildMessageReceivedEvent event) throws Exception { play(event, null); } public void play(GuildMessageReceivedEvent event, List commands) throws Exception { if (playing.get()) return; - + VoiceChannel vc = event.getMember().getVoiceState().getChannel(); if (vc == null) { event.getChannel().sendMessage(String.format("<@%s> you are not in a voice channel to play music", event.getMember().getId())).queue(); @@ -200,7 +242,7 @@ public class GuildMusicHandler { event.getMember().getId(), vc.getName())).queue(); return; } - + Long tmpId = vc.getIdLong(); if (voiceChannelId != null && voiceChannelId != tmpId) { String channelName = event.getGuild().getVoiceChannelById(voiceChannelId).getName(); @@ -208,14 +250,14 @@ public class GuildMusicHandler { event.getMember().getId(), channelName)).queue(); return; } - + voiceChannelId = tmpId; AudioManager manager = event.getGuild().getAudioManager(); manager.openAudioConnection(vc); manager.setSendingHandler(new EightTrackAudioSendHandler(player)); - + if (commands != null && !commands.isEmpty()) { String uuid = commands.remove(0); boolean foundSong = findSong(uuid); @@ -223,9 +265,9 @@ public class GuildMusicHandler { event.getChannel().sendMessageFormat("Unable to find song with identifier: %s", uuid).queue(); return; } - stop(event); + stop(event, true); } - + if (!ts.hasTracks()) { loadRandomSong(); } @@ -285,7 +327,31 @@ public class GuildMusicHandler { }); } } - + + private void playPlayList(GuildMessageReceivedEvent event, List commands) throws Exception { + if (commands == null || commands.isEmpty()) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + " please provide the name of the playlist to play").queue(); + return; + } + String playlist = commands.remove(0); + currentPlaylist = guildSongRepoService.getGuildPlaylistRepo().findByGuildAndUserIdAndPlaylist(guildId, event.getMember().getIdLong(), playlist); + if (currentPlaylist == null) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + ", no playlist with the name of " + playlist + " exists").queue(); + return; + } + + ts.clearTracks(); + + for(GuildPlaylistSong gpls : guildSongRepoService.getGuildPlaylistSongRepo().findByGuildPlaylist(currentPlaylist)) { + Song s = gpls.getGuildSong().getSong(); + apm.loadItem(s.getFilePath(), ts).get(); + } + + playing.set(false); + play(event); + playing.set(true); + } + private MessageEmbed error(GuildMessageReceivedEvent event, Throwable ex, String fileName) { return new EmbedBuilder() .setTitle("Unable to upload file: " + fileName) @@ -348,9 +414,9 @@ public class GuildMusicHandler { gs.setDateAdded(new Date()); gs.setGuild(guild); gs.setSong(song); - + String[] uuidArray = UUID.randomUUID().toString().split("-"); - + gs.setUuid(uuidArray[uuidArray.length - 1]); guildSongRepoService.getGuildSongRepo().save(gs); @@ -373,71 +439,316 @@ public class GuildMusicHandler { return embed; } - + public void list(GuildMessageReceivedEvent event) { List gsList = guildSongRepoService.getGuildSongRepo().findByGuild(guildId); - + if (gsList == null || gsList.isEmpty()) { event.getChannel().sendMessage("There is no music for this guild.").queue(); return; } - + int longestSong = 0; int longestArtist = 0; for(GuildSong gs : gsList) { if (gs.getSong().getTitle().length() > longestSong) longestSong = gs.getSong().getTitle().length(); if (gs.getSong().getArtist().length() > longestArtist) longestArtist = gs.getSong().getArtist().length(); } - + String fmt = "%6s | %-" + longestSong +"s | %-" + longestArtist +"s | %s%n"; int count = 0; StringBuilder sb = new StringBuilder(); sb.append("**" + "Currently available songs for " + event.getGuild().getName() + "**\n\n```"); sb.append(String.format(fmt, "Track", "Title", "Artist", "id")); sb.append(StringUtils.repeat("-", 27 + longestSong + longestArtist)).append("\n"); - + for(GuildSong gs : gsList) { sb.append(String.format(fmt, ++count, gs.getSong().getTitle(), gs.getSong().getArtist(), gs.getUuid())); } sb.append("```"); event.getChannel().sendMessage(sb.toString()).queue(); } - + private void loadRandomSong() throws Exception { - List gsList = guildSongRepoService.getGuildSongRepo().findByGuild(guildId); + List gsList = null; + + if (currentPlaylist == null) { + gsList = guildSongRepoService.getGuildSongRepo().findByGuild(guildId); + } else { + gsList = guildSongRepoService.getGuildPlaylistSongRepo().findByGuildPlaylist(currentPlaylist).stream().map(gpl -> gpl.getGuildSong()).collect(Collectors.toList()); + } if (gsList == null || gsList.isEmpty()) return; - + Random random = new Random(System.currentTimeMillis()); int item = random.nextInt(gsList.size()); - + GuildSong song = gsList.get(item); - + apm.loadItem(song.getSong().getFilePath(), ts).get(); } - + private boolean findSong(String uuid) throws Exception { GuildSong gs = guildSongRepoService.getGuildSongRepo().findByUuid(uuid); if (gs == null) return false; - + apm.loadItem(gs.getSong().getFilePath(), ts).get(); - + return true; } - public void playlist(GuildMessageReceivedEvent event, List commands) { - - String command = commands.remove(0); - - switch(command) { - case "add": - addPlayList(event, commands); + private void listPlayList(GuildMessageReceivedEvent event, List commands) { + if (commands == null || commands.isEmpty()) { + List userPlaylist = guildSongRepoService.getGuildPlaylistRepo().findByGuildAndUserIdFetchSongs(event.getGuild().getIdLong(), event.getMember().getIdLong()); + if (userPlaylist.isEmpty()) { + event.getChannel().sendMessage(event.getMember().getAsMention() + " you have no defined playlists on this server").queue(); return; + } + + int longestPlaylist = 0; + for(GuildPlaylist gpl : userPlaylist) { + if (gpl.getPlaylist().length() > longestPlaylist) longestPlaylist = gpl.getPlaylist().length(); + } + + String fmt = "%6s | %-" + longestPlaylist +"s | %s%n"; + + StringBuilder sb = new StringBuilder("```"); + sb.append(String.format(fmt, "", "Playlist", "Song Count")); + sb.append(StringUtils.repeat("-", 24 + longestPlaylist)).append("\n"); + + int count = 0; + for(GuildPlaylist gpl : userPlaylist) { + sb.append(String.format(fmt, ++count, gpl.getPlaylist(), gpl.getGuildPlaylistSongList().size())); + } + sb.append("```"); + event.getChannel().sendMessage("**" + "Currently available playlists for " + event.getMember().getAsMention() + "**").queue((succcess)-> + event.getChannel().sendMessage(sb.toString()).queue() + ); + return; } - + + String playlist = commands.remove(0); + + GuildPlaylist gpl = guildSongRepoService.getGuildPlaylistRepo().findGuildUserPlaylistFetchSongs(event.getGuild().getIdLong(), event.getMember().getIdLong(), playlist); + if (gpl == null) { + event.getChannel().sendMessage(event.getMember().getAsMention() + " you have no defined playlists on this server by the name of " + playlist).queue(); + return; + } + + if (gpl.getGuildPlaylistSongList().isEmpty()) { + event.getChannel().sendMessage(event.getMember().getAsMention() + " you have no defined songs for playlist " + playlist).queue(); + return; + } + + int longestSong = 0; + int longestArtist = 0; + for(GuildPlaylistSong gpls : gpl.getGuildPlaylistSongList()) { + GuildSong gs = gpls.getGuildSong(); + if (gs.getSong().getTitle().length() > longestSong) longestSong = gs.getSong().getTitle().length(); + if (gs.getSong().getArtist().length() > longestArtist) longestArtist = gs.getSong().getArtist().length(); + } + + String fmt = "%6s | %-" + longestSong +"s | %-" + longestArtist +"s | %s%n"; + int count = 0; + StringBuilder sb = new StringBuilder("```"); + sb.append(String.format(fmt, "Track", "Title", "Artist", "id")); + sb.append(StringUtils.repeat("-", 27 + longestSong + longestArtist)).append("\n"); + + for(GuildPlaylistSong gpls : gpl.getGuildPlaylistSongList()) { + GuildSong gs = gpls.getGuildSong(); + sb.append(String.format(fmt, ++count, gs.getSong().getTitle(), gs.getSong().getArtist(), gs.getUuid())); + } + sb.append("```"); + event.getChannel().sendMessage("**" + "Current songs in playlists " + playlist + " for " + event.getMember().getAsMention() + "**").queue((succcess)-> + event.getChannel().sendMessage(sb.toString()).queue() + ); + + } + + private void deletePlayList(GuildMessageReceivedEvent event, List commands) { + if (commands == null || commands.isEmpty()) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + " you have to provide the playlist to create a new playlist (no spaces in name) and optionally a list of uuids for songs to add").queue(); + return; + } + + Long guildId = event.getGuild().getIdLong(); + + DiscordGuild guild = guildSongRepoService.getGuildRepo().findByGuildId(guildId); + if (guild == null) { + event.getChannel().sendMessage("Unable to find guild in local database. Please contact administrator").queue(); + return; + } + + String playlist = commands.remove(0); + + long userId = event.getMember().getIdLong(); + + GuildPlaylist gpl = guildSongRepoService.getGuildPlaylistRepo().findByGuildAndUserIdAndPlaylist(guild, userId, playlist); + + if (commands.isEmpty()) { + + MessageEmbed embed = new EmbedBuilder() + .setColor(Color.RED) + .setTitle("Delete Playlist " + playlist) + .setDescription("Are you sure you want to delete the playlist " + playlist) + .setTimestamp(OffsetDateTime.now()) + .setFooter(event.getGuild().getSelfMember().getEffectiveName(), event.getGuild().getSelfMember().getUser().getAvatarUrl()) + .build(); + + event.getChannel().sendMessage(embed).queue((msg) -> { + + ReactionListener handler = new ReactionListener<>(userId, msg.getId()); + handler.setExpiresIn(TimeUnit.MINUTES, 1); + handler.registerReaction(Reactions.CHECK_MARK_BUTTON, (ret) -> deletePlayListConfirm(gpl, event.getMember().getAsMention(), msg)); + handler.registerReaction(Reactions.CROSS_MARK_BUTTON, (ret) -> deleteMessage(msg)); + + reactionHandler.addReactionListener(guildId, msg, handler); + }); + return; + } + + List songs = guildSongRepoService.getGuildPlaylistSongRepo().findByGuildPlaylistAndSongIds(gpl, commands); + + StringBuilder sb = new StringBuilder("Are you sure you want to delete the following songs from playlist " + playlist + "?\n"); + for (GuildPlaylistSong gpls : songs) { + GuildSong gs = gpls.getGuildSong(); + sb.append(String.format("**%s** by __%s__%n", gs.getSong().getTitle(), gs.getSong().getArtist())); + } + + MessageEmbed embed = new EmbedBuilder() + .setColor(Color.RED) + .setTitle("Delete Playlist Songs") + .setDescription(sb.toString()) + .setTimestamp(OffsetDateTime.now()) + .setFooter(event.getGuild().getSelfMember().getEffectiveName(), event.getGuild().getSelfMember().getUser().getAvatarUrl()) + .build(); + + event.getChannel().sendMessage(embed).queue((msg) -> { + + ReactionListener handler = new ReactionListener<>(userId, msg.getId()); + handler.setExpiresIn(TimeUnit.MINUTES, 1); + handler.registerReaction(Reactions.CHECK_MARK_BUTTON, (ret) -> deletePlayListSongConfirm(songs, event.getMember().getAsMention(), msg)); + handler.registerReaction(Reactions.CROSS_MARK_BUTTON, (ret) -> deleteMessage(msg)); + + reactionHandler.addReactionListener(guildId, msg, handler); + }); + } + + private void deletePlayListSongConfirm(List songs, String user, Message msg) { + try { + guildSongRepoService.getGuildPlaylistSongRepo().deleteAll(songs); + msg.getChannel().sendMessage(user + ", songs removed successfully fom playlist").complete(); + } catch (Exception ex) { + msg.getChannel().sendMessage("Sorry " + user + " I was unable to remove songs from the playlist. Reason: " + ex.getMessage()).complete(); + logger.error("Unable to delete songs from playlist : " + ex.getMessage()); + logger.error(ex); + } finally { + deleteMessage(msg); + } + } + + private void deletePlayListConfirm(GuildPlaylist gpl, String user, Message msg) { + String playlist = gpl.getPlaylist(); + try { + guildSongRepoService.getGuildPlaylistRepo().delete(gpl); + msg.getChannel().sendMessage(user + " " + playlist + " deleted successfully").complete(); + } catch (Exception ex) { + msg.getChannel().sendMessage("Sorry " + user + " I was unable to remove playlist " + playlist + ". Reason: " + ex.getMessage()).complete(); + logger.error("Unable to delete playlist " + playlist + ": " + ex.getMessage()); + logger.error(ex); + } finally { + deleteMessage(msg); + } + } private void addPlayList(GuildMessageReceivedEvent event, List commands) { - // TODO Auto-generated method stub - + if (commands == null || commands.isEmpty()) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + " you have to provide the playlist to create a new playlist (no spaces in name) and optionally a list of uuids for songs to add").queue(); + return; + } + + Long guildId = event.getGuild().getIdLong(); + + DiscordGuild guild = guildSongRepoService.getGuildRepo().findByGuildId(guildId); + if (guild == null) { + event.getChannel().sendMessage("Unable to find guild in local database. Please contact administrator").queue(); + return; + } + + String playlist = commands.remove(0); + + long userId = event.getMember().getIdLong(); + + GuildPlaylist gpl = guildSongRepoService.getGuildPlaylistRepo().findByGuildAndUserIdAndPlaylist(guild, userId, playlist); + boolean newList = false; + if (gpl != null && commands.isEmpty()) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + " a playlist with the name " + playlist + " already exist for you.").queue(); + return; + } else if (gpl == null) { + newList = true; + gpl = new GuildPlaylist(); + gpl.setDateAdded(new Date()); + gpl.setGuild(guild); + gpl.setPlaylist(playlist); + gpl.setUserId(userId); + guildSongRepoService.getGuildPlaylistRepo().save(gpl); + } + + if (commands.isEmpty() && newList) { + event.getChannel().sendMessage(event.getAuthor().getAsMention() + " playlist " + playlist + " successfully created.").queue(); + return; + } + + Set songIds = guildSongRepoService.getGuildPlaylistSongRepo() + .findByGuildPlaylistAndSongIds(gpl, commands).stream() + .map(g -> g.getGuildSong().getUuid()) + .collect(Collectors.toSet()); + + List gplsList = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for(String id : commands.stream().collect(Collectors.toSet())) { + GuildSong gs = guildSongRepoService.getGuildSongRepo().findByUuid(id); + if (songIds.contains(id)) { + sb.append(String.format("**%s** by __%s__ already exists in this playlist.%n", gs.getSong().getTitle(), gs.getSong().getArtist())); + continue; + } + + if (gs == null) { + sb.append("Song with id `" + id + "` not found. Please check id\n"); + continue; + } + + GuildPlaylistSong gpls = new GuildPlaylistSong(); + gpls.setDateAdded(new Date()); + gpls.setGuildPlaylist(gpl); + gpls.setGuildSong(gs); + gplsList.add(gpls); + + sb.append(String.format("**%s** by __%s__ added to playlist.%n", gs.getSong().getTitle(), gs.getSong().getArtist())); + } + + if (!gplsList.isEmpty()) { + guildSongRepoService.getGuildPlaylistSongRepo().saveAll(gplsList); + } + + MessageEmbed embed = new EmbedBuilder() + .setTitle("Results for adding playlist " + playlist) + .setAuthor(event.getMember().getEffectiveName(), null, event.getAuthor().getAvatarUrl()) + .setColor(Color.GREEN) + .setDescription(sb.toString()) + .setFooter("", event.getGuild().getSelfMember().getUser().getAvatarUrl()) + .setTimestamp(OffsetDateTime.now()) + .build(); + + event.getChannel().sendMessage(embed).queue(); + } + + private void deleteMessage(Message msg) { + try { + msg.delete().complete(); + reactionHandler.removeReactionListener(guildId, msg.getIdLong()); + } catch (ErrorResponseException ex) { + if (ex.getErrorResponse() != ErrorResponse.UNKNOWN_MESSAGE) throw ex; + } } } diff --git a/src/main/java/net/locusworks/discord/eighttrack/handlers/ReactionHandler.java b/src/main/java/net/locusworks/discord/eighttrack/handlers/ReactionHandler.java new file mode 100644 index 0000000..cca35de --- /dev/null +++ b/src/main/java/net/locusworks/discord/eighttrack/handlers/ReactionHandler.java @@ -0,0 +1,133 @@ +/** + * + * Project: Pseudo-Bot, File: ReactionHandler.java + * + * Copyright 2019 Locusworks LLC. + * All rights reserved. Federal copyright law prohibits unauthorized reproduction by + * any means and imposes fines up to $25,000 for violation. No part of this material + * may be reproduced, transmitted, transcribed, stored in a retrieval system, copied, + * modified, duplicated, adapted or translated into another program language in any + * form or by any means, electronic, mechanical, photocopying, recording, or + * otherwise, without the prior written permission from Locusworks. Locusworks + * affirms that PSEUDO-BOT(R) software and data is subject to United States + * Government Purpose Rights. Contact Locusworks, 1313 Lawnview Drive + * Forney TX 75126, (802) 488-0438, for commercial licensing opportunities. + * + * IN NO EVENT SHALL LOCUSWORKS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, + * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF LOCUSWORKS HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NO RESPONSIBILITY IS ASSUMED BY + * LOCUSWORKS FOR ITS USE, OR FOR ANY INFRINGEMENTS OF PATENTS OR OTHER RIGHTS OF + * THIRD PARTIES RESULTING FROM ITS USE. LOCUSWORKS SPECIFICALLY DISCLAIMS ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND + * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS + * IS". LOCUSWORKS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + */ +package net.locusworks.discord.eighttrack.handlers; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageReaction; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.internal.utils.PermissionUtil; +import net.locusworks.discord.eighttrack.listeners.ReactionListener; + +@Component +public class ReactionHandler { + + private final ConcurrentHashMap>> reactions; + + private ReactionHandler() { + reactions = new ConcurrentHashMap<>(); + } + + public synchronized void addReactionListener(long guildId, Message message, ReactionListener handler) { + addReactionListener(guildId, message, handler, true); + } + + public synchronized void addReactionListener(long guildId, Message message, ReactionListener handler, boolean queue) { + if (handler == null) { + return; + } + if (message.getChannelType().equals(ChannelType.TEXT)) { + if (!PermissionUtil.checkPermission(message.getTextChannel(), message.getGuild().getSelfMember(), Permission.MESSAGE_ADD_REACTION)) { + return; + } + } + if (!reactions.containsKey(guildId)) { + reactions.put(guildId, new ConcurrentHashMap<>()); + } + if (!reactions.get(guildId).containsKey(message.getIdLong())) { + for (String emote : handler.getEmotes()) { + RestAction action = message.addReaction(emote); + if (queue) action.queue(); else action.complete(); + } + } + reactions.get(guildId).put(message.getIdLong(), handler); + } + + public synchronized void removeReactionListener(long guildId, long messageId) { + if (!reactions.containsKey(guildId)) return; + reactions.get(guildId).remove(messageId); + } + + /** + * Handles the reaction + * + * @param channel TextChannel of the message + * @param messageId id of the message + * @param userId id of the user reacting + * @param reaction the reaction + */ + public void handle(TextChannel channel, long messageId, long userId, MessageReaction reaction) { + ReactionListener listener = reactions.get(channel.getGuild().getIdLong()).get(messageId); + if (!listener.isActive() || listener.getExpiresInTimestamp() < System.currentTimeMillis()) { + reactions.get(channel.getGuild().getIdLong()).remove(messageId); + } else if ((listener.hasReaction(reaction.getReactionEmote().getName())) && listener.getUserId() == userId) { + reactions.get(channel.getGuild().getIdLong()).get(messageId).updateLastAction(); + Message message = channel.retrieveMessageById(messageId).complete(); + listener.react(reaction.getReactionEmote().getName(), message); + } + } + + /** + * Do we have an event for a message? + * + * @param guildId discord guild-id of the message + * @param messageId id of the message + * @return do we have an handler? + */ + public boolean canHandle(long guildId, long messageId) { + return reactions.containsKey(guildId) && reactions.get(guildId).containsKey(messageId); + } + + public synchronized void removeGuild(long guildId) { + reactions.remove(guildId); + } + + /** + * Delete expired handlers + */ + public synchronized void cleanCache() { + long now = System.currentTimeMillis(); + for (Iterator>>> iterator = reactions.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry>> mapEntry = iterator.next(); + mapEntry.getValue().values().removeIf(listener -> !listener.isActive() || listener.getExpiresInTimestamp() < now); + if (mapEntry.getValue().values().isEmpty()) { + reactions.remove(mapEntry.getKey()); + } + } + } + + +} diff --git a/src/main/java/net/locusworks/discord/eighttrack/listeners/ReactionListener.java b/src/main/java/net/locusworks/discord/eighttrack/listeners/ReactionListener.java new file mode 100644 index 0000000..7194929 --- /dev/null +++ b/src/main/java/net/locusworks/discord/eighttrack/listeners/ReactionListener.java @@ -0,0 +1,139 @@ +/** + * + * Project: Pseudo-Bot, File: ReactionListener.java + * + * Copyright 2019 Locusworks LLC. + * All rights reserved. Federal copyright law prohibits unauthorized reproduction by + * any means and imposes fines up to $25,000 for violation. No part of this material + * may be reproduced, transmitted, transcribed, stored in a retrieval system, copied, + * modified, duplicated, adapted or translated into another program language in any + * form or by any means, electronic, mechanical, photocopying, recording, or + * otherwise, without the prior written permission from Locusworks. Locusworks + * affirms that PSEUDO-BOT(R) software and data is subject to United States + * Government Purpose Rights. Contact Locusworks, 1313 Lawnview Drive + * Forney TX 75126, (802) 488-0438, for commercial licensing opportunities. + * + * IN NO EVENT SHALL LOCUSWORKS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, + * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF LOCUSWORKS HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NO RESPONSIBILITY IS ASSUMED BY + * LOCUSWORKS FOR ITS USE, OR FOR ANY INFRINGEMENTS OF PATENTS OR OTHER RIGHTS OF + * THIRD PARTIES RESULTING FROM ITS USE. LOCUSWORKS SPECIFICALLY DISCLAIMS ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND + * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS + * IS". LOCUSWORKS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + */ +package net.locusworks.discord.eighttrack.listeners; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import net.dv8tion.jda.api.entities.Message; + +public class ReactionListener { + + private final Map> reactions; + private final long userId; + private volatile T data; + private Long expiresIn, lastAction; + private boolean active; + + public ReactionListener(long userId, T data) { + this.data = data; + this.userId = userId; + reactions = new LinkedHashMap<>(); + active = true; + lastAction = System.currentTimeMillis(); + expiresIn = TimeUnit.MINUTES.toMillis(5); + } + + public boolean isActive() { + return active; + } + + public void disable() { + this.active = false; + } + + /** + * The time after which this listener expires which is now + specified time + * Defaults to now+5min + * + * @param timeUnit time units + * @param time amount of time units + */ + public void setExpiresIn(TimeUnit timeUnit, long time) { + expiresIn = timeUnit.toMillis(time); + } + + /** + * Check if this listener has specified emote + * + * @param emote the emote to check for + * @return does this listener do anything with this emote? + */ + public boolean hasReaction(String emote) { + return reactions.containsKey(emote); + } + + /** + * React to the reaction :') + * + * @param emote the emote used + * @param message the message bound to the reaction + */ + public void react(String emote, Message message) { + if (hasReaction(emote)) reactions.get(emote).accept(message); + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + /** + * Register a consumer for a specified emote + * Multiple emote's will result in overriding the old one + * + * @param emote the emote to respond to + * @param consumer the behaviour when emote is used + */ + public void registerReaction(String emote, Consumer consumer) { + reactions.put(emote, consumer); + } + + /** + * @return list of all emotes used in this reaction listener + */ + public Set getEmotes() { + return reactions.keySet(); + } + + /** + * updates the timestamp when the reaction was last accessed + */ + public void updateLastAction() { + lastAction = System.currentTimeMillis(); + } + + /** + * When does this reaction listener expire? + * + * @return timestamp in millis + */ + public Long getExpiresInTimestamp() { + return lastAction + expiresIn; + } + + public long getUserId() { + return userId; + } +} diff --git a/src/main/java/net/locusworks/discord/eighttrack/scheduler/TrackScheduler.java b/src/main/java/net/locusworks/discord/eighttrack/scheduler/TrackScheduler.java index f18e043..e57965f 100644 --- a/src/main/java/net/locusworks/discord/eighttrack/scheduler/TrackScheduler.java +++ b/src/main/java/net/locusworks/discord/eighttrack/scheduler/TrackScheduler.java @@ -78,6 +78,10 @@ public class TrackScheduler extends AudioEventAdapter implements AudioEventListe trackQueue.add(at); } } + + public void clearTracks() { + trackQueue.clear(); + } public void noMatches() { // TODO Auto-generated method stub @@ -106,6 +110,5 @@ public class TrackScheduler extends AudioEventAdapter implements AudioEventListe public boolean playing() { return started; } - } diff --git a/src/main/java/net/locusworks/discord/eighttrack/services/GuildSongRepoService.java b/src/main/java/net/locusworks/discord/eighttrack/services/GuildSongRepoService.java index 33ee8cc..e38be4a 100644 --- a/src/main/java/net/locusworks/discord/eighttrack/services/GuildSongRepoService.java +++ b/src/main/java/net/locusworks/discord/eighttrack/services/GuildSongRepoService.java @@ -30,6 +30,8 @@ package net.locusworks.discord.eighttrack.services; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import net.locusworks.discord.eighttrack.database.repos.GuildPlaylistRepository; +import net.locusworks.discord.eighttrack.database.repos.GuildPlaylistSongRepository; import net.locusworks.discord.eighttrack.database.repos.GuildRepository; import net.locusworks.discord.eighttrack.database.repos.GuildSongRepository; import net.locusworks.discord.eighttrack.database.repos.SongRepository; @@ -43,6 +45,12 @@ public class GuildSongRepoService { @Autowired private GuildSongRepository guildSongRepo; + @Autowired + private GuildPlaylistRepository guildPlaylistRepo; + + @Autowired + private GuildPlaylistSongRepository guildPlaylistSongRepo; + @Autowired private SongRepository songRepo; @@ -60,6 +68,20 @@ public class GuildSongRepoService { return guildSongRepo; } + /** + * @return the guildPlaylistRepo + */ + public final GuildPlaylistRepository getGuildPlaylistRepo() { + return guildPlaylistRepo; + } + + /** + * @return the guildPlaylistSongRepo + */ + public final GuildPlaylistSongRepository getGuildPlaylistSongRepo() { + return guildPlaylistSongRepo; + } + /** * @return the songRepo */ diff --git a/src/main/java/net/locusworks/discord/eighttrack/utils/Reactions.java b/src/main/java/net/locusworks/discord/eighttrack/utils/Reactions.java new file mode 100644 index 0000000..5fa7b6c --- /dev/null +++ b/src/main/java/net/locusworks/discord/eighttrack/utils/Reactions.java @@ -0,0 +1,37 @@ +/** + * + * Project: Pseudo-Bot, File: Reactions.java + * + * Copyright 2019 Locusworks LLC. + * All rights reserved. Federal copyright law prohibits unauthorized reproduction by + * any means and imposes fines up to $25,000 for violation. No part of this material + * may be reproduced, transmitted, transcribed, stored in a retrieval system, copied, + * modified, duplicated, adapted or translated into another program language in any + * form or by any means, electronic, mechanical, photocopying, recording, or + * otherwise, without the prior written permission from Locusworks. Locusworks + * affirms that PSEUDO-BOT(R) software and data is subject to United States + * Government Purpose Rights. Contact Locusworks, 1313 Lawnview Drive + * Forney TX 75126, (802) 488-0438, for commercial licensing opportunities. + * + * IN NO EVENT SHALL LOCUSWORKS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, + * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF LOCUSWORKS HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NO RESPONSIBILITY IS ASSUMED BY + * LOCUSWORKS FOR ITS USE, OR FOR ANY INFRINGEMENTS OF PATENTS OR OTHER RIGHTS OF + * THIRD PARTIES RESULTING FROM ITS USE. LOCUSWORKS SPECIFICALLY DISCLAIMS ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND + * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS + * IS". LOCUSWORKS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + */ +package net.locusworks.discord.eighttrack.utils; + +public class Reactions { + + public static final String CHECK_MARK_BUTTON = "\u2705"; + public static final String CHECK_MARK = "\u2714"; + public static final String CROSS_MARK_BUTTON = "\u274e"; + public static final String CROSS_MARK = "\u274c"; + +}