From 1fca679a79e97d2727b5cded903f52e1354def39 Mon Sep 17 00:00:00 2001 From: Isaac Parenteau Date: Fri, 11 Oct 2019 20:55:22 -0500 Subject: [PATCH] Added ability to delete playlists --- .../handlers/DiscordEventHandler.java | 29 +++- .../handlers/GuildMusicHandler.java | 126 +++++++++++++++- .../eighttrack/handlers/ReactionHandler.java | 133 +++++++++++++++++ .../listeners/ReactionListener.java | 139 ++++++++++++++++++ .../discord/eighttrack/utils/Reactions.java | 37 +++++ 5 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/locusworks/discord/eighttrack/handlers/ReactionHandler.java create mode 100644 src/main/java/net/locusworks/discord/eighttrack/listeners/ReactionListener.java create mode 100644 src/main/java/net/locusworks/discord/eighttrack/utils/Reactions.java 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 51f09ff..8e64e3d 100644 --- a/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java +++ b/src/main/java/net/locusworks/discord/eighttrack/handlers/GuildMusicHandler.java @@ -40,6 +40,7 @@ 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; @@ -55,19 +56,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; @@ -86,9 +92,10 @@ 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 { + 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; @@ -96,6 +103,7 @@ public class GuildMusicHandler { this.uploadHandler = uploadHandler; this.guildSongRepoService = guildSongRepoService; this.guildId = guildId; + this.reactionHandler = reactionHandler; this.apm = new DefaultAudioPlayerManager(); AudioSourceManagers.registerLocalSource(apm); @@ -438,6 +446,9 @@ public class GuildMusicHandler { case "add": addPlayList(event, commands); return; + case "delete": + deletePlayList(event, commands); + return; case "list": listPlayList(event, commands); return; @@ -512,6 +523,103 @@ public class GuildMusicHandler { ); } + + 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) { if (commands == null || commands.isEmpty()) { @@ -558,18 +666,15 @@ public class GuildMusicHandler { List gplsList = new ArrayList<>(); StringBuilder sb = new StringBuilder(); - for(Iterator iter = commands.iterator(); iter.hasNext();) { - String id = iter.next(); + 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())); - iter.remove(); continue; } if (gs == null) { sb.append("Song with id `" + id + "` not found. Please check id\n"); - iter.remove(); continue; } @@ -580,7 +685,6 @@ public class GuildMusicHandler { gplsList.add(gpls); sb.append(String.format("**%s** by __%s__ added to playlist.%n", gs.getSong().getTitle(), gs.getSong().getArtist())); - iter.remove(); } if (!gplsList.isEmpty()) { @@ -597,6 +701,14 @@ public class GuildMusicHandler { .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/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"; + +}