Java tutorial
/* * * MIT License * * Copyright (c) 2017 Frederik Ar. Mikkelsen * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package fredboat.messaging; import fredboat.feature.I18n; import fredboat.feature.metrics.Metrics; import fredboat.shared.constant.BotConstants; import net.dv8tion.jda.core.EmbedBuilder; import net.dv8tion.jda.core.MessageBuilder; import net.dv8tion.jda.core.Permission; import net.dv8tion.jda.core.entities.Member; 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.exceptions.ErrorResponseException; import net.dv8tion.jda.core.exceptions.InsufficientPermissionException; import net.dv8tion.jda.core.requests.ErrorResponse; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.ResourceBundle; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; /** * Created by napster on 10.09.17. * <p> * Everything related to sending things out from FredBoat */ public class CentralMessaging { private static final Logger log = LoggerFactory.getLogger(CentralMessaging.class); //this is needed for when we absolutely don't care about a rest action failing (use this only after good consideration!) // because if we pass null for a failure handler to JDA it uses a default handler that results in a warning/error level log public static final Consumer<Throwable> NOOP_EXCEPTION_HANDLER = __ -> { }; //use this to schedule rest actions whenever queueAfter() or similar JDA methods would be used // this makes it way easier to track stats + handle failures of such delayed RestActions // instead of implementing a ton of overloaded methods in this class public static final ScheduledExecutorService restService = Executors.newScheduledThreadPool(10, runnable -> new Thread(runnable, "central-messaging-scheduler")); // ******************************************************************************** // Thread local handling and providing of Messages and Embeds builders // ******************************************************************************** //instead of creating millions of MessageBuilder and EmbedBuilder objects we're going to reuse the existing ones, on // a per-thread scope // this makes sense since the vast majority of message processing in FredBoat is happening in the main JDA threads private static ThreadLocal<MessageBuilder> threadLocalMessageBuilder = ThreadLocal .withInitial(MessageBuilder::new); private static ThreadLocal<EmbedBuilder> threadLocalEmbedBuilder = ThreadLocal.withInitial(EmbedBuilder::new); public static MessageBuilder getClearThreadLocalMessageBuilder() { return threadLocalMessageBuilder.get().clear(); } //presets fredboat color on a clear embed public static EmbedBuilder getColoredEmbedBuilder() { return getClearThreadLocalEmbedBuilder().setColor(BotConstants.FREDBOAT_COLOR); } public static EmbedBuilder getClearThreadLocalEmbedBuilder() { return threadLocalEmbedBuilder.get().clearFields().setTitle(null).setDescription(null).setTimestamp(null) .setColor(null).setThumbnail(null).setAuthor(null, null, null).setFooter(null, null).setImage(null); } //May not be an empty string, as MessageBuilder#build() will throw an exception public static Message from(String string) { return getClearThreadLocalMessageBuilder().append(string).build(); } public static Message from(MessageEmbed embed) { return getClearThreadLocalMessageBuilder().setEmbed(embed).build(); } // ******************************************************************************** // Convenience methods that convert input to Message objects and send it // ******************************************************************************** /** * Combines the benefits of JDAs RestAction#queue and returning a Future that can be get() if necessary * * @param channel The channel that should be messaged * @param message Message to be sent * @param onSuccess Optional success handler * @param onFail Optional exception handler * @return Future that can be waited on in case the code requires completion. Similar to JDA's RestAction#complete, * avoid usage where not absolutely needed. */ public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull Message message, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return sendMessage0(channel, message, onSuccess, onFail); } // Message public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull Message message, @Nullable Consumer<Message> onSuccess) { return sendMessage0(channel, message, onSuccess, null); } // Message public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull Message message) { return sendMessage0(channel, message, null, null); } // Embed public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull MessageEmbed embed, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return sendMessage0(channel, from(embed), onSuccess, onFail); } // Embed public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull MessageEmbed embed, @Nullable Consumer<Message> onSuccess) { return sendMessage0(channel, from(embed), onSuccess, null); } // Embed public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull MessageEmbed embed) { return sendMessage0(channel, from(embed), null, null); } // String public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull String content, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return sendMessage0(channel, from(content), onSuccess, onFail); } // String public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull String content, @Nullable Consumer<Message> onSuccess) { return sendMessage0(channel, from(content), onSuccess, null); } // String public static MessageFuture sendMessage(@Nonnull MessageChannel channel, @Nonnull String content) { return sendMessage0(channel, from(content), null, null); } // ******************************************************************************** // File sending methods // ******************************************************************************** /** * Combines the benefits of JDAs RestAction#queue and returning a Future that can be get() if necessary * * @param channel The channel that should be messaged * @param file File to be sent * @param message Optional message * @param onSuccess Optional success handler * @param onFail Optional exception handler * @return Future that can be waited on in case the code requires completion. Similar to JDA's RestAction#complete, * avoid usage where not absolutely needed. */ public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Message message, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return sendFile0(channel, file, message, onSuccess, onFail); } public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Message message, @Nullable Consumer<Message> onSuccess) { return sendFile0(channel, file, message, onSuccess, null); } public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Message message) { return sendFile0(channel, file, message, null, null); } public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return sendFile0(channel, file, null, onSuccess, onFail); } public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Consumer<Message> onSuccess) { return sendFile0(channel, file, null, onSuccess, null); } public static MessageFuture sendFile(@Nonnull MessageChannel channel, @Nonnull File file) { return sendFile0(channel, file, null, null, null); } // ******************************************************************************** // Message editing methods // ******************************************************************************** /** * Combines the benefits of JDAs RestAction#queue and returning a Future that can be get() if necessary * * @param oldMessage The message to be edited * @param newMessage The message to be set * @param onSuccess Optional success handler * @param onFail Optional exception handler * @return Future that can be waited on in case the code requires completion. Similar to JDA's RestAction#complete, * avoid usage where not absolutely needed. */ public static MessageFuture editMessage(@Nonnull Message oldMessage, @Nonnull Message newMessage, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return editMessage0(oldMessage.getChannel(), oldMessage.getIdLong(), newMessage, onSuccess, onFail); } public static MessageFuture editMessage(@Nonnull Message oldMessage, @Nonnull Message newMessage) { return editMessage0(oldMessage.getChannel(), oldMessage.getIdLong(), newMessage, null, null); } public static MessageFuture editMessage(@Nonnull Message oldMessage, @Nonnull String newContent) { return editMessage0(oldMessage.getChannel(), oldMessage.getIdLong(), from(newContent), null, null); } public static MessageFuture editMessage(@Nonnull MessageChannel channel, long oldMessageId, @Nonnull Message newMessage, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { return editMessage0(channel, oldMessageId, newMessage, onSuccess, onFail); } public static MessageFuture editMessage(@Nonnull MessageChannel channel, long oldMessageId, @Nonnull Message newMessage) { return editMessage0(channel, oldMessageId, newMessage, null, null); } public static MessageFuture editMessage(@Nonnull MessageChannel channel, long oldMessageId, @Nonnull String newContent) { return editMessage0(channel, oldMessageId, from(newContent), null, null); } // ******************************************************************************** // Miscellaneous messaging related methods // ******************************************************************************** public static void sendTyping(MessageChannel channel) { try { channel.sendTyping().queue(__ -> Metrics.successfulRestActions.labels("sendTyping").inc(), getJdaRestActionFailureHandler("Could not send typing event in channel " + channel.getId())); } catch (InsufficientPermissionException e) { handleInsufficientPermissionsException(channel, e); } } //make sure that all the messages are from the channel you provide public static void deleteMessages(@Nonnull TextChannel channel, @Nonnull Collection<Message> messages) { if (!messages.isEmpty()) { try { channel.deleteMessages(messages).queue( __ -> Metrics.successfulRestActions.labels("bulkDeleteMessages").inc(), getJdaRestActionFailureHandler( String.format("Could not bulk delete %s messages in channel %s", messages.size(), channel.getId()))); } catch (InsufficientPermissionException e) { handleInsufficientPermissionsException(channel, e); } } } public static void deleteMessageById(@Nonnull MessageChannel channel, long messageId) { try { channel.getMessageById(messageId).queue(message -> { Metrics.successfulRestActions.labels("getMessageById").inc(); CentralMessaging.deleteMessage(message); }, NOOP_EXCEPTION_HANDLER //prevent logging an error if that message could not be found in the first place ); } catch (InsufficientPermissionException e) { handleInsufficientPermissionsException(channel, e); } } //make sure that the message passed in here is actually existing in Discord // e.g. dont pass messages in here that were created with a MessageBuilder in our code public static void deleteMessage(@Nonnull Message message) { try { message.delete().queue(__ -> Metrics.successfulRestActions.labels("deleteMessage").inc(), getJdaRestActionFailureHandler( String.format("Could not delete message %s in channel %s with content\n%s", message.getId(), message.getChannel().getId(), message.getRawContent()))); } catch (InsufficientPermissionException e) { handleInsufficientPermissionsException(message.getChannel(), e); } } @Nonnull public static EmbedBuilder addFooter(@Nonnull EmbedBuilder eb, @Nonnull Member author) { return eb.setFooter(author.getEffectiveName(), author.getUser().getAvatarUrl()); } @Nonnull public static EmbedBuilder addNpFooter(@Nonnull EmbedBuilder eb, @Nonnull Member requester) { return eb.setFooter("Requested by " + requester.getEffectiveName(), requester.getUser().getAvatarUrl()); } // ******************************************************************************** // Class internal methods // ******************************************************************************** //class internal message sending method private static MessageFuture sendMessage0(@Nonnull MessageChannel channel, @Nonnull Message message, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { if (channel == null) { throw new IllegalArgumentException("Channel is null"); } if (message == null) { throw new IllegalArgumentException("Message is null"); } MessageFuture result = new MessageFuture(); Consumer<Message> successWrapper = m -> { result.complete(m); Metrics.successfulRestActions.labels("sendMessage").inc(); if (onSuccess != null) { onSuccess.accept(m); } }; Consumer<Throwable> failureWrapper = t -> { result.completeExceptionally(t); if (onFail != null) { onFail.accept(t); } else { String info = String.format("Could not sent message\n%s\nto channel %s in guild %s", message.getRawContent(), channel.getId(), (channel instanceof TextChannel) ? ((TextChannel) channel).getGuild().getIdLong() : "null"); getJdaRestActionFailureHandler(info).accept(t); } }; try { channel.sendMessage(message).queue(successWrapper, failureWrapper); } catch (InsufficientPermissionException e) { failureWrapper.accept(e); if (e.getPermission() == Permission.MESSAGE_EMBED_LINKS) { handleInsufficientPermissionsException(channel, e); } else { //do not call CentralMessaging#handleInsufficientPermissionsException() from here as that will result in a loop log.warn("Could not send message to channel {} due to missing permission {}", channel.getIdLong(), e.getPermission().getName(), e); } } return result; } //class internal file sending method private static MessageFuture sendFile0(@Nonnull MessageChannel channel, @Nonnull File file, @Nullable Message message, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { if (channel == null) { throw new IllegalArgumentException("Channel is null"); } if (file == null) { throw new IllegalArgumentException("File is null"); } MessageFuture result = new MessageFuture(); Consumer<Message> successWrapper = m -> { result.complete(m); Metrics.successfulRestActions.labels("sendFile").inc(); if (onSuccess != null) { onSuccess.accept(m); } }; Consumer<Throwable> failureWrapper = t -> { result.completeExceptionally(t); if (onFail != null) { onFail.accept(t); } else { String info = String.format("Could not send file %s to channel %s in guild %s", file.getAbsolutePath(), channel.getId(), (channel instanceof TextChannel) ? ((TextChannel) channel).getGuild().getIdLong() : "null"); getJdaRestActionFailureHandler(info).accept(t); } }; try { // ATTENTION: Do not use JDA's MessageChannel#sendFile(File file, Message message) // as it will skip permission checks, since TextChannel does not override that method // this is scheduled to be fixed through JDA's message-rw branch channel.sendFile(FileUtils.readFileToByteArray(file), file.getName(), message).queue(successWrapper, failureWrapper); } catch (InsufficientPermissionException e) { failureWrapper.accept(e); handleInsufficientPermissionsException(channel, e); } catch (IOException e) { log.error("Could not send file {}, it appears to be borked", file.getAbsolutePath(), e); } return result; } //class internal editing method private static MessageFuture editMessage0(@Nonnull MessageChannel channel, long oldMessageId, @Nonnull Message newMessage, @Nullable Consumer<Message> onSuccess, @Nullable Consumer<Throwable> onFail) { if (channel == null) { throw new IllegalArgumentException("Channel is null"); } if (newMessage == null) { throw new IllegalArgumentException("New message is null"); } MessageFuture result = new MessageFuture(); Consumer<Message> successWrapper = m -> { result.complete(m); Metrics.successfulRestActions.labels("editMessage").inc(); if (onSuccess != null) { onSuccess.accept(m); } }; Consumer<Throwable> failureWrapper = t -> { result.completeExceptionally(t); if (onFail != null) { onFail.accept(t); } else { String info = String.format( "Could not edit message %s in channel %s in guild %s with new content %s", oldMessageId, channel.getId(), (channel instanceof TextChannel) ? ((TextChannel) channel).getGuild().getIdLong() : "null", newMessage.getRawContent()); getJdaRestActionFailureHandler(info).accept(t); } }; try { channel.editMessageById(oldMessageId, newMessage).queue(successWrapper, failureWrapper); } catch (InsufficientPermissionException e) { failureWrapper.accept(e); handleInsufficientPermissionsException(channel, e); } return result; } private static void handleInsufficientPermissionsException(@Nonnull MessageChannel channel, @Nonnull InsufficientPermissionException e) { final ResourceBundle i18n; if (channel instanceof TextChannel) { i18n = I18n.get(((TextChannel) channel).getGuild()); } else { i18n = I18n.DEFAULT.getProps(); } //only ever try sending a simple string from here so we don't end up handling a loop of insufficient permissions sendMessage(channel, i18n.getString("permissionMissingBot") + " **" + e.getPermission().getName() + "**"); } //handles failed JDA rest actions by logging them with an informational string and optionally ignoring some error response codes public static Consumer<Throwable> getJdaRestActionFailureHandler(String info, ErrorResponse... ignored) { return t -> { if (t instanceof ErrorResponseException) { ErrorResponseException e = (ErrorResponseException) t; Metrics.failedRestActions.labels(Integer.toString(e.getErrorCode())).inc(); if (Arrays.asList(ignored).contains(e.getErrorResponse()) || e.getErrorCode() == -1) { //socket timeout, fuck those return; } } log.warn(info, t); }; } }