diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 1a4d3d0ced..6349f369a1 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -23,6 +23,7 @@ import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; +import net.dv8tion.jda.api.utils.AttachmentOption; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.FunctionalCallback; import net.dv8tion.jda.internal.requests.Requester; @@ -199,6 +200,18 @@ public interface Message extends ISnowflake, Formattable "(?:\\?\\S*)?(?:#\\S*)?", // Useless query or URN appendix Pattern.CASE_INSENSITIVE); + /** + * Referenced message. + * + *

This will have different meaning depending on the {@link #getType() type} of message. + * Usually, this is a {@link MessageType#INLINE_REPLY INLINE_REPLY} reference. + * This can be null even if the type is {@link MessageType#INLINE_REPLY INLINE_REPLY}, when the message it references doesn't exist or discord wasn't able to resolve it in time. + * + * @return The referenced message, or null + */ + @Nullable + Message getReferencedMessage(); + /** * An immutable list of all mentioned {@link net.dv8tion.jda.api.entities.User Users}. *
If no user was mentioned, this list is empty. Elements are sorted in order of appearance. This only @@ -948,6 +961,174 @@ default boolean isFromGuild() @CheckReturnValue MessageAction editMessage(@Nonnull Message newContent); + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendMessage(content).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendMessage(CharSequence)} and {@link MessageAction#reference(Message)}. + * + * @param content + * The content of the reply message + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull CharSequence content) + { + return getChannel().sendMessage(content).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendMessage(content).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendMessage(MessageEmbed)} and {@link MessageAction#reference(Message)}. + * + * @param content + * The content of the reply message + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull MessageEmbed content) + { + return getChannel().sendMessage(content).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendMessage(content).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendMessage(Message)} and {@link MessageAction#reference(Message)}. + * + * @param content + * The content of the reply message + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull Message content) + { + return getChannel().sendMessage(content).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendMessageFormat(content, args).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendMessageFormat(String, Object...)} and {@link MessageAction#reference(Message)}. + * + * @param format + * The string that should be formatted, if this is null or empty the content of the Message would be empty and cause a builder exception. + * @param args + * The arguments for your format + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction replyFormat(@Nonnull String format, @Nonnull Object... args) + { + return getChannel().sendMessageFormat(format, args).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendFile(file, options).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendFile(File, net.dv8tion.jda.api.utils.AttachmentOption...)} and {@link MessageAction#reference(Message)}. + * + * @param file + * The file to upload to the channel in the reply + * @param options + * Possible options to apply to this attachment, such as marking it as spoiler image + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull File file, @Nonnull AttachmentOption... options) + { + return getChannel().sendFile(file, options).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendFile(data, name, options).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendFile(File, String, net.dv8tion.jda.api.utils.AttachmentOption...)} and {@link MessageAction#reference(Message)}. + * + * @param data + * The data to upload to the channel in the reply + * @param name + * The name that should be sent to discord + * @param options + * Possible options to apply to this attachment, such as marking it as spoiler image + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull File data, @Nonnull String name, @Nonnull AttachmentOption... options) + { + return getChannel().sendFile(data, name, options).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendFile(data, name, options).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendFile(InputStream, String, net.dv8tion.jda.api.utils.AttachmentOption...)} and {@link MessageAction#reference(Message)}. + * + * @param data + * The data to upload to the channel in the reply + * @param name + * The name that should be sent to discord + * @param options + * Possible options to apply to this attachment, such as marking it as spoiler image + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull InputStream data, @Nonnull String name, @Nonnull AttachmentOption... options) + { + return getChannel().sendFile(data, name, options).reference(this); + } + + /** + * Replies and references this message. + *
This is identical to {@code message.getChannel().sendFile(data, name, options).reference(message)}. + * You can use {@link MessageAction#mentionRepliedUser(boolean) mentionRepliedUser(false)} to not mention the author of the message. + * + *

For further info, see {@link MessageChannel#sendFile(byte[], String, net.dv8tion.jda.api.utils.AttachmentOption...)} and {@link MessageAction#reference(Message)}. + * + * @param data + * The data to upload to the channel in the reply + * @param name + * The name that should be sent to discord + * @param options + * Possible options to apply to this attachment, such as marking it as spoiler image + * + * @return {@link MessageAction} Providing the {@link Message} created from this upload. + */ + @Nonnull + @CheckReturnValue + default MessageAction reply(@Nonnull byte[] data, @Nonnull String name, @Nonnull AttachmentOption... options) + { + return getChannel().sendFile(data, name, options).reference(this); + } + /** * Deletes this Message from Discord. *
If this Message was not sent by the currently logged in account, then this will fail unless the Message is from diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageType.java b/src/main/java/net/dv8tion/jda/api/entities/MessageType.java index ca94ba98ee..6030574ddf 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageType.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageType.java @@ -88,6 +88,11 @@ public enum MessageType */ CHANNEL_FOLLOW_ADD(12), + /** + * Reply to another message. This usually comes with a {@link Message#getReferencedMessage() referenced message}. + */ + INLINE_REPLY(19), + /** * Unknown MessageType. */ diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageAction.java index f533bc916c..db38314a6c 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageAction.java +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageAction.java @@ -16,12 +16,14 @@ package net.dv8tion.jda.api.requests.restaction; +import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.IMentionable; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.AttachmentOption; +import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.internal.requests.restaction.MessageActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -123,6 +125,33 @@ static EnumSet getDefaultMentions() return MessageActionImpl.getDefaultMentions(); } + /** + * Sets the default value for {@link #mentionRepliedUser(boolean)} + * + *

Default: true + * + * @param mention + * True, if replies should mention by default + */ + static void setDefaultMentionRepliedUser(boolean mention) + { + MessageActionImpl.setDefaultMentionRepliedUser(mention); + } + + /** + * Returns the default mention behavior for replies. + *
If this is {@code true} then all replies will mention the author of the target message by default. + * You can specify this individually with {@link #mentionRepliedUser(boolean)} for each message. + * + *

Default: true + * + * @return True, if replies mention by default + */ + static boolean isDefaultMentionRepliedUser() + { + return MessageActionImpl.isDefaultMentionRepliedUser(); + } + @Nonnull @Override MessageAction setCheck(@Nullable BooleanSupplier checks); @@ -181,6 +210,85 @@ static EnumSet getDefaultMentions() @CheckReturnValue MessageAction apply(@Nullable final Message message); + /** + * Make the message a reply to the referenced message. + *
You can only reply to messages from the same channel! + *
This will mention the author of the target message. You can disable this through {@link #mentionRepliedUser(boolean)}. + * + *

This requires {@link net.dv8tion.jda.api.Permission#MESSAGE_HISTORY Permission.MESSAGE_HISTORY} in the channel! + * + * @param messageId + * The target message + * + * @return Updated MessageAction for chaining convenience + */ + @Nonnull + @CheckReturnValue + MessageAction referenceById(long messageId); + + /** + * Make the message a reply to the referenced message. + *
You can only reply to messages from the same channel! + *
This will mention the author of the target message. You can disable this through {@link #mentionRepliedUser(boolean)}. + * + *

This requires {@link net.dv8tion.jda.api.Permission#MESSAGE_HISTORY Permission.MESSAGE_HISTORY} in the channel! + * You cannot reply to system messages (such as {@link net.dv8tion.jda.api.entities.MessageType#CHANNEL_PINNED_ADD CHANNEL_PINNED_ADD} and similar). + * + * @param messageId + * The target message + * + * @throws IllegalArgumentException + * If the provided ID is null or not a valid snowflake + * + * @return Updated MessageAction for chaining convenience + */ + @Nonnull + @CheckReturnValue + default MessageAction referenceById(@Nonnull String messageId) + { + return referenceById(MiscUtil.parseSnowflake(messageId)); + } + + /** + * Make the message a reply to the referenced message. + *
You can only reply to messages from the same channel! + *
This will mention the author of the target message. You can disable this through {@link #mentionRepliedUser(boolean)}. + * + *

This requires {@link net.dv8tion.jda.api.Permission#MESSAGE_HISTORY Permission.MESSAGE_HISTORY} in the channel! + * + * @param message + * The target message + * + * @throws IllegalArgumentException + * If the provided message is null + * @throws UnsupportedOperationException + * If the provided message is from a {@link MessageBuilder} + * + * @return Updated MessageAction for chaining convenience + */ + @Nonnull + @CheckReturnValue + default MessageAction reference(@Nonnull Message message) + { + Checks.notNull(message, "Message"); + return referenceById(message.getIdLong()); + } + + /** + * Whether to mention the used, when replying to a message. + *
This only matters in combination with {@link #reference(Message)} and {@link #referenceById(long)}! + * + *

This is true by default but can be configured using {@link #setDefaultMentionRepliedUser(boolean)}! + * + * @param mention + * True, to mention the author if the referenced message + * + * @return Updated MessageAction for chaining convenience + */ + @Nonnull + @CheckReturnValue + MessageAction mentionRepliedUser(boolean mention); + /** * Enable/Disable Text-To-Speech for the resulting message. *
This is only relevant to MessageActions that are not {@code isEdit() == true}! diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java index 0db4a965ee..b8ae9466f7 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java @@ -106,6 +106,12 @@ protected void appendFormat(Formatter formatter, int width, int precision, boole } } + @Override + public Message getReferencedMessage() + { + return null; + } + @Nonnull @Override public Bag getMentionedUsersBag() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index e3ea42044d..35a8af4ab8 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1139,10 +1139,18 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean MessageType type = MessageType.fromId(jsonObject.getInt("type")); ReceivedMessage message; + Message referencedMessage = null; + if (!jsonObject.isNull("referenced_message")) + { + referencedMessage = createMessage(jsonObject.getObject("referenced_message"), chan, false); + if (type == MessageType.DEFAULT) + type = MessageType.INLINE_REPLY; + } switch (type) { + case INLINE_REPLY: case DEFAULT: - message = new ReceivedMessage(id, chan, type, fromWebhook, + message = new ReceivedMessage(id, chan, type, referencedMessage, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, content, nonce, user, member, activity, editTime, reactions, attachments, embeds, flags); break; @@ -1155,14 +1163,7 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean break; } - if (!message.isFromGuild()) - return message; - - GuildImpl guild = (GuildImpl) message.getGuild(); - - // Don't do more computations when members are loaded already - if (guild.isLoaded()) - return message; + GuildImpl guild = message.isFromGuild() ? (GuildImpl) message.getGuild() : null; // Load users/members from message object through mentions List mentionedUsersList = new ArrayList<>(); @@ -1172,14 +1173,17 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean for (int i = 0; i < userMentions.length(); i++) { DataObject mentionJson = userMentions.getObject(i); - if (mentionJson.isNull("member")) + if (guild == null || mentionJson.isNull("member")) { // Can't load user without member context so fake them if possible User mentionedUser = createUser(mentionJson); mentionedUsersList.add(mentionedUser); - Member mentionedMember = guild.getMember(mentionedUser); - if (mentionedMember != null) - mentionedMembersList.add(mentionedMember); + if (guild != null) + { + Member mentionedMember = guild.getMember(mentionedUser); + if (mentionedMember != null) + mentionedMembersList.add(mentionedMember); + } continue; } @@ -1192,8 +1196,7 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean mentionedUsersList.add(mentionedMember.getUser()); } - if (!mentionedUsersList.isEmpty()) - message.setMentions(mentionedUsersList, mentionedMembersList); + message.setMentions(mentionedUsersList, mentionedMembersList); return message; } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index d67e56e1c6..8ffb3ffc6f 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -55,6 +55,7 @@ public class ReceivedMessage extends AbstractMessage protected final long id; protected final MessageType type; protected final MessageChannel channel; + protected final Message referencedMessage; protected final boolean fromWebhook; protected final boolean mentionsEveryone; protected final boolean pinned; @@ -81,7 +82,7 @@ public class ReceivedMessage extends AbstractMessage protected List invites = null; public ReceivedMessage( - long id, MessageChannel channel, MessageType type, + long id, MessageChannel channel, MessageType type, Message referencedMessage, boolean fromWebhook, boolean mentionsEveryone, TLongSet mentionedUsers, TLongSet mentionedRoles, boolean tts, boolean pinned, String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, List reactions, List attachments, List embeds, int flags) @@ -89,6 +90,7 @@ public ReceivedMessage( super(content, nonce, tts); this.id = id; this.channel = channel; + this.referencedMessage = referencedMessage; this.type = type; this.api = (channel != null) ? (JDAImpl) channel.getJDA() : null; this.fromWebhook = fromWebhook; @@ -113,6 +115,12 @@ public JDA getJDA() return api; } + @Override + public Message getReferencedMessage() + { + return referencedMessage; + } + @Override public boolean isPinned() { diff --git a/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java index db57654561..6fe514c563 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java @@ -34,7 +34,7 @@ public SystemMessage( String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, List reactions, List attachments, List embeds, int flags) { - super(id, channel, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, + super(id, channel, type, null, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, content, nonce, author, member, activity, editTime, reactions, attachments, embeds, flags); } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java index 11e206df40..0b6a070cf1 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java @@ -223,12 +223,14 @@ public T complete(boolean shouldQueue) throws RateLimitedException if (e.getCause() != null) { Throwable cause = e.getCause(); + if (cause instanceof ErrorResponseException) + throw (ErrorResponseException) cause.fillInStackTrace(); // this method will update the stacktrace to the current thread stack + if (cause instanceof RateLimitedException) + throw (RateLimitedException) cause.fillInStackTrace(); if (cause instanceof RuntimeException) throw (RuntimeException) cause; - else if (cause instanceof Error) + if (cause instanceof Error) throw (Error) cause; - else if (cause instanceof RateLimitedException) - throw (RateLimitedException) cause; } throw e; } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java index a0ac3df566..42e97f968c 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java @@ -52,6 +52,7 @@ public class MessageActionImpl extends RestActionImpl implements Messag { private static final String CONTENT_TOO_BIG = String.format("A message may not exceed %d characters. Please limit your input!", Message.MAX_CONTENT_LENGTH); protected static EnumSet defaultMentions = EnumSet.allOf(Message.MentionType.class); + protected static boolean defaultMentionRepliedUser = true; protected final Map files = new HashMap<>(); protected final Set ownedResources = new HashSet<>(); protected final StringBuilder content; @@ -59,9 +60,11 @@ public class MessageActionImpl extends RestActionImpl implements Messag protected MessageEmbed embed = null; protected String nonce = null; protected boolean tts = false, override = false; + protected boolean mentionRepliedUser = defaultMentionRepliedUser; protected EnumSet allowedMentions; protected Set mentionableUsers = new HashSet<>(); protected Set mentionableRoles = new HashSet<>(); + protected long messageReference; public static void setDefaultMentions(@Nullable Collection allowedMentions) { @@ -76,6 +79,16 @@ public static EnumSet getDefaultMentions() return defaultMentions.clone(); } + public static void setDefaultMentionRepliedUser(boolean mention) + { + defaultMentionRepliedUser = mention; + } + + public static boolean isDefaultMentionRepliedUser() + { + return defaultMentionRepliedUser; + } + public MessageActionImpl(JDA api, Route.CompiledRoute route, MessageChannel channel) { super(api, route); @@ -185,6 +198,22 @@ public MessageActionImpl apply(final Message message) return content(content).tts(message.isTTS()); } + @Nonnull + @Override + public MessageActionImpl referenceById(long messageId) + { + messageReference = messageId; + return this; + } + + @Nonnull + @Override + public MessageAction mentionRepliedUser(boolean mention) + { + mentionRepliedUser = mention; + return this; + } + @Nonnull @Override @CheckReturnValue @@ -480,11 +509,15 @@ protected DataObject getJSON() if (nonce != null) obj.put("nonce", nonce); } - obj.put("tts", tts); - if (allowedMentions != null || !mentionableUsers.isEmpty() || !mentionableRoles.isEmpty()) + if (messageReference != 0) { - obj.put("allowed_mentions", getAllowedMentionsObj()); + obj.put("message_reference", DataObject.empty() + .put("message_id", messageReference) + .put("channel_id", channel.getId())); } + obj.put("tts", tts); + if ((messageReference != 0L && !mentionRepliedUser) || allowedMentions != null || !mentionableUsers.isEmpty() || !mentionableRoles.isEmpty()) + obj.put("allowed_mentions", getAllowedMentionsObj()); return obj; } @@ -513,6 +546,8 @@ protected DataObject getAllowedMentionsObj() parsable.remove(Message.MentionType.ROLE.getParseKey()); allowedMentionsObj.put("roles", DataArray.fromCollection(mentionableRoles)); } + if (messageReference != 0L) + allowedMentionsObj.put("replied_user", mentionRepliedUser); return allowedMentionsObj.put("parse", parsable); }