diff --git a/build.gradle.kts b/build.gradle.kts index b9a13115be..cd79b01c47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { testImplementation(libs.reflections) testImplementation(libs.mockito) testImplementation(libs.assertj) + testImplementation(libs.commons.lang3) } val compileJava: JavaCompile by tasks diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 798ffba081..1b15040d78 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -64,6 +64,7 @@ public enum Permission USE_APPLICATION_COMMANDS( 31, true, true, "Use Application Commands"), MESSAGE_EXT_STICKER( 37, true, true, "Use External Stickers"), MESSAGE_ATTACH_VOICE_MESSAGE(46, true, true, "Send Voice Messages"), + MESSAGE_SEND_POLLS( 49, true, true, "Create Polls"), // Thread Permissions MANAGE_THREADS( 34, true, true, "Manage Threads"), 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 d10b6cdd8d..15c4d4c07a 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -31,6 +31,7 @@ import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; import net.dv8tion.jda.api.entities.sticker.GuildSticker; import net.dv8tion.jda.api.entities.sticker.Sticker; import net.dv8tion.jda.api.entities.sticker.StickerItem; @@ -48,6 +49,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.AttachmentProxy; @@ -55,10 +57,12 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.requests.FunctionalCallback; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.IOUtil; @@ -681,6 +685,43 @@ default String getGuildId() @Nonnull List getComponents(); + /** + * The {@link MessagePoll} attached to this message. + * + * @return Possibly-null poll instance for this message + * + * @see #endPoll() + */ + @Nullable + MessagePoll getPoll(); + + /** + * End the poll attached to this message. + * + * @throws IllegalStateException + * If this poll was not sent by the currently logged in account or no poll was attached to this message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + AuditableRestAction endPoll(); + + /** + * Paginate the users who voted for a poll answer. + * + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVoters(long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getChannelId(), getId(), answerId); + } + /** * Rows of interactive components such as {@link Button Buttons}. *
You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. @@ -1359,6 +1400,48 @@ default MessageCreateAction reply(@Nonnull MessageCreateData msg) return getChannel().sendMessage(msg).setMessageReference(this); } + /** + * Shortcut for {@code getChannel().sendMessagePoll(data).setMessageReference(this)}. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

+ * + * @param poll + * The poll to send + * + * @throws InsufficientPermissionException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * @throws IllegalArgumentException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction replyPoll(@Nonnull MessagePollData poll) + { + return getChannel().sendMessagePoll(poll).setMessageReference(this); + } + /** * Shortcut for {@code getChannel().sendMessageEmbeds(embed, other).setMessageReference(this)}. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java index 2956d91d39..5a81a73cd9 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java +++ b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java @@ -32,6 +32,8 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.IncomingWebhookClientImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -128,6 +130,43 @@ public interface WebhookClient extends ISnowflake @CheckReturnValue WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData message); + /** + * Send a message poll to this webhook. + * + *

If this is an {@link InteractionHook InteractionHook} this method will be delayed until the interaction is acknowledged. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_WEBHOOK UNKNOWN_WEBHOOK} + *
    The webhook is no longer available, either it was deleted or in case of interactions it expired.
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The {@link MessagePollData} to send + * + * @throws IllegalArgumentException + * If null is provided + * + * @return {@link net.dv8tion.jda.api.requests.restaction.WebhookMessageCreateAction} + * + * @see MessagePollBuilder + */ + @Nonnull + @CheckReturnValue + WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll); + /** * Send a message to this webhook. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index c325f09390..4d4fc7314f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -35,6 +35,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.pagination.MessagePaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.FileUpload; @@ -42,6 +43,7 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.RestActionImpl; @@ -49,6 +51,7 @@ import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.MessageEditActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.MessagePaginationActionImpl; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.ReactionPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -644,6 +647,52 @@ default MessageCreateAction sendMessageComponents(@Nonnull CollectionPossible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *
    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    if this channel was deleted
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_SEND_TO_USER CANNOT_SEND_TO_USER} + *
    If this is a {@link PrivateChannel} and the currently logged in account + * does not share any Guilds with the recipient User
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The poll to send + * + * @throws UnsupportedOperationException + * If this is a {@link PrivateChannel} and the recipient is a bot + * @throws IllegalArgumentException + * If the poll is null + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If this is a {@link GuildMessageChannel} and this account does not have + * {@link net.dv8tion.jda.api.Permission#VIEW_CHANNEL Permission.VIEW_CHANNEL} or {@link net.dv8tion.jda.api.Permission#MESSAGE_SEND Permission.MESSAGE_SEND} + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Poll"); + return new MessageCreateActionImpl(this).setPoll(poll); + } + /** * Send a message to this channel. * @@ -972,6 +1021,108 @@ default AuditableRestAction deleteMessageById(long messageId) return deleteMessageById(Long.toUnsignedString(messageId)); } + /** + * End the poll attached to this message. + * + *

A bot cannot expire the polls of other users. + * + *

The following {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} are possible: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @throws IllegalArgumentException + * If the provided messageId is not a valid snowflake + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction endPollById(@Nonnull String messageId) + { + Checks.isSnowflake(messageId, "Message ID"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getId(), messageId), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + return jda.getEntityBuilder().createMessageWithChannel(response.getObject(), MessageChannel.this, false); + }); + } + + /** + * End the poll attached to this message. + * + *

A bot cannot expire the polls of other users. + * + *

The following {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} are possible: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction endPollById(long messageId) + { + return endPollById(Long.toUnsignedString(messageId)); + } + + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @throws IllegalArgumentException + * If the message id is not a valid snowflake + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(@Nonnull String messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), messageId, answerId); + } + + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(long messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), Long.toUnsignedString(messageId), answerId); + } + /** * Creates a new {@link net.dv8tion.jda.api.entities.MessageHistory MessageHistory} object for each call of this method. *
MessageHistory is NOT an internal message cache, but rather it queries the Discord servers for previously sent messages. diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java new file mode 100644 index 0000000000..f7bcef204d --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -0,0 +1,273 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.entities.messages; + +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; + +/** + * Poll sent with messages. + * + * @see Message#getPoll() + * @see Message#endPoll() + */ +public interface MessagePoll +{ + /** Maximum length of a {@link MessagePollBuilder#setTitle(String) poll question title} ({@value}) */ + int MAX_QUESTION_TEXT_LENGTH = 300; + /** Maximum length of a {@link MessagePollBuilder#addAnswer(String)} poll answer title} ({@value}) */ + int MAX_ANSWER_TEXT_LENGTH = 55; + /** Maximum amount of {@link MessagePollBuilder#addAnswer(String) poll answers} ({@value}) */ + int MAX_ANSWERS = 10; + /** Maximum {@link MessagePollBuilder#setDuration(Duration) duration} of poll ({@value}) */ + long MAX_DURATION_HOURS = 7 * 24; + + /** + * The layout of the poll. + * + * @return The poll layout, or {@link LayoutType#UNKNOWN} if unknown + */ + @Nonnull + LayoutType getLayout(); + + /** + * The poll question, representing the title. + * + * @return {@link Question} + */ + @Nonnull + Question getQuestion(); + + /** + * The poll answers. + * + *

Each answer also has the current {@link Answer#getVotes() votes}. + * The votes might not be finalized and might be incorrect before the poll has expired, + * see {@link #isFinalizedVotes()}. + * + * @return Immutable {@link List} of {@link Answer} + */ + @Nonnull + List getAnswers(); + + /** + * The time when this poll will automatically expire. + * + *

The author of the poll can always expire the poll manually, using {@link Message#endPoll()}. + * + * @return {@link OffsetDateTime} representing the time when the poll expires automatically, or null if it never expires + */ + @Nullable + OffsetDateTime getTimeExpiresAt(); + + /** + * Whether this poll allows multiple answers to be selected. + * + * @return True, if this poll allows multi selection + */ + boolean isMultiAnswer(); + + /** + * Whether this poll is finalized and recounted. + * + *

The votes for answers might be inaccurate due to eventual consistency, until this is true. + * Finalization does not mean the votes cannot change anymore, use {@link #isExpired()} to check if a poll has ended. + * + * @return True, if the votes have been precisely counted + */ + boolean isFinalizedVotes(); + + /** + * Whether this poll has passed its {@link #getTimeExpiresAt() expiration time}. + * + * @return True, if this poll is expired. + */ + default boolean isExpired() + { + return getTimeExpiresAt().isBefore(OffsetDateTime.now()); + } + + /** + * The question for a poll. + */ + class Question + { + private final String text; + private final EmojiUnion emoji; + + public Question(String text, Emoji emoji) + { + this.text = text; + this.emoji = (EmojiUnion) emoji; + } + + /** + * The poll question title. + * + *

Shown above all answers. + * + * @return The question title + */ + @Nonnull + public String getText() + { + return text; + } + + /** + * Possible emoji related to the poll question. + * + * @return Possibly-null emoji + */ + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + } + + /** + * One of the answers for a poll. + * + *

Provides the current {@link #getVotes()} and whether you have voted for it. + */ + class Answer + { + private final long id; + private final String text; + private final EmojiUnion emoji; + private final int votes; + private final boolean selfVoted; + + public Answer(long id, String text, EmojiUnion emoji, int votes, boolean selfVoted) + { + this.id = id; + this.text = text; + this.emoji = emoji; + this.votes = votes; + this.selfVoted = selfVoted; + } + + /** + * The id of this answer. + * + * @return The answer id. + */ + public long getId() + { + return id; + } + + /** + * The text content of the answer. + * + * @return The answer label. + */ + @Nonnull + public String getText() + { + return text; + } + + /** + * The emoji assigned to this answer. + * + * @return {@link EmojiUnion} + */ + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + + /** + * The number of votes this answer has received so far. + * + *

This might not be {@link #isFinalizedVotes() finalized}. + * + * @return The current number of votes + */ + public int getVotes() + { + return votes; + } + + /** + * Whether the answer was voted for by the currently logged in account. + * + * @return True, if the bot has voted for this. + */ + public boolean isSelfVoted() + { + return selfVoted; + } + } + + /** + * The poll layout. + * + *

Currently always {@link #DEFAULT}. + */ + enum LayoutType + { + DEFAULT(1), + UNKNOWN(-1); + + private final int key; + + LayoutType(int key) + { + this.key = key; + } + + /** + * The raw API key used to identify this layout. + * + * @return The API key + */ + public int getKey() + { + return key; + } + + /** + * Resolves the provided raw API key to the layout enum constant. + * + * @param key + * The API key + * + * @return The layout type or {@link #UNKNOWN} + */ + public static LayoutType fromKey(int key) + { + for (LayoutType type : values()) + { + if (type.key == key) + return type; + } + return UNKNOWN; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java new file mode 100644 index 0000000000..f5e6dbbb1d --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.entities.messages; + +import javax.annotation.Nonnull; +import java.time.OffsetDateTime; +import java.util.List; + +public class MessagePollImpl implements MessagePoll +{ + private final LayoutType layout; + private final Question question; + private final List answers; + private final OffsetDateTime expiresAt; + private final boolean isMultiAnswer; + private final boolean isFinalizedVotes; + + public MessagePollImpl(LayoutType layout, Question question, List answers, OffsetDateTime expiresAt, boolean isMultiAnswer, boolean isFinalizedVotes) + { + this.layout = layout; + this.question = question; + this.answers = answers; + this.expiresAt = expiresAt; + this.isMultiAnswer = isMultiAnswer; + this.isFinalizedVotes = isFinalizedVotes; + } + + @Nonnull + @Override + public LayoutType getLayout() + { + return layout; + } + + @Nonnull + @Override + public Question getQuestion() + { + return question; + } + + @Nonnull + @Override + public List getAnswers() + { + return answers; + } + + @Nonnull + @Override + public OffsetDateTime getTimeExpiresAt() + { + return expiresAt; + } + + @Override + public boolean isMultiAnswer() + { + return isMultiAnswer; + } + + @Override + public boolean isFinalizedVotes() + { + return isFinalizedVotes; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java new file mode 100644 index 0000000000..f8ada658eb --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.GenericMessageEvent; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; + +/** + * Indicates that a poll vote was added/removed. + *
Every MessagePollVoteEvent is derived from this event and can be casted. + * + *

Can be used to detect both remove and add events. + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class GenericMessagePollVoteEvent extends GenericMessageEvent +{ + protected final long userId; + protected final long messageId; + protected final long answerId; + + public GenericMessagePollVoteEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel.getJDA(), responseNumber, messageId, channel); + this.userId = userId; + this.messageId = messageId; + this.answerId = answerId; + } + + /** + * The id of the voting user. + * + * @return The user id + */ + @Nonnull + public String getUserId() + { + return Long.toUnsignedString(userId); + } + + /** + * The id for the voting user. + * + * @return The user id + */ + public long getUserIdLong() + { + return userId; + } + + /** + * The id of the answer, usually the ordinal position. + *
The first answer options is usually 1. + * + * @return The answer id + */ + public long getAnswerId() + { + return answerId; + } + + /** + * Retrieves the voting {@link User}. + * + * @return {@link RestAction} - Type: {@link User} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveUser() + { + return getJDA().retrieveUserById(getUserIdLong()); + } + + /** + * Retrieves the voting {@link Member}. + * + *

Note that banning a member will also fire {@link MessagePollVoteRemoveEvent} and no member will be available + * in those cases. An {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} error response + * should be the failure result. + * + * @throws IllegalStateException + * If this event is not from a guild + * + * @return {@link RestAction} - Type: {@link Member} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveMember() + { + if (!getChannel().getType().isGuild()) + throw new IllegalStateException("Cannot retrieve member for a vote that happened outside of a guild"); + return getGuild().retrieveMemberById(getUserIdLong()); + } + + /** + * Retrieves the message for this event. + *
Simple shortcut for {@code getChannel().retrieveMessageById(getMessageId())}. + * + *

The {@link Message#getMember() Message.getMember()} method will always return null for the resulting message. + * To retrieve the member you can use {@code getGuild().retrieveMember(message.getAuthor())}. + * + * @return {@link RestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + public RestAction retrieveMessage() + { + return getChannel().retrieveMessageById(getMessageId()); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java new file mode 100644 index 0000000000..24fa90eefb --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import javax.annotation.Nonnull; + +/** + * Indicates that a user voted for a poll answer. + *
If the poll allows selecting multiple answers, one event per vote is sent. + * + *

Can be used to track when a user votes for a poll answer + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class MessagePollVoteAddEvent extends GenericMessagePollVoteEvent +{ + public MessagePollVoteAddEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel, responseNumber, messageId, userId, answerId); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java new file mode 100644 index 0000000000..5c261f88fe --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.events.message.poll; + +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; + +import javax.annotation.Nonnull; + +/** + * Indicates that a user removed a vote for a poll answer. + *
If the poll allows selecting multiple answers, one event per vote is sent. + * + *

Can be used to track when a user removes a vote for a poll answer + * + *

Requirements
+ * + *

These events require at least one of the following intents (Will not fire at all if neither is enabled): + *

    + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MESSAGE_POLLS GUILD_MESSAGE_POLLS} to work in guild text channels
  • + *
  • {@link net.dv8tion.jda.api.requests.GatewayIntent#DIRECT_MESSAGE_POLLS DIRECT_MESSAGE_POLLS} to work in private channels
  • + *
+ */ +public class MessagePollVoteRemoveEvent extends GenericMessagePollVoteEvent +{ + public MessagePollVoteRemoveEvent(@Nonnull MessageChannel channel, long responseNumber, long messageId, long userId, long answerId) + { + super(channel, responseNumber, messageId, userId, answerId); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java index fa764c405e..0ee9eb6160 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -61,6 +61,9 @@ import net.dv8tion.jda.api.events.interaction.command.*; import net.dv8tion.jda.api.events.interaction.component.*; import net.dv8tion.jda.api.events.message.*; +import net.dv8tion.jda.api.events.message.poll.GenericMessagePollVoteEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; import net.dv8tion.jda.api.events.message.react.*; import net.dv8tion.jda.api.events.role.GenericRoleEvent; import net.dv8tion.jda.api.events.role.RoleCreateEvent; @@ -186,6 +189,8 @@ public void onMessageReactionAdd(@Nonnull MessageReactionAddEvent event) {} public void onMessageReactionRemove(@Nonnull MessageReactionRemoveEvent event) {} public void onMessageReactionRemoveAll(@Nonnull MessageReactionRemoveAllEvent event) {} public void onMessageReactionRemoveEmoji(@Nonnull MessageReactionRemoveEmojiEvent event) {} + public void onMessagePollVoteAdd(@Nonnull MessagePollVoteAddEvent event) {} + public void onMessagePollVoteRemove(@Nonnull MessagePollVoteRemoveEvent event) {} //PermissionOverride Events public void onPermissionOverrideDelete(@Nonnull PermissionOverrideDeleteEvent event) {} @@ -389,6 +394,7 @@ public void onGenericContextInteraction(@Nonnull GenericContextInteractionEvent< public void onGenericSelectMenuInteraction(@Nonnull GenericSelectMenuInteractionEvent event) {} public void onGenericMessage(@Nonnull GenericMessageEvent event) {} public void onGenericMessageReaction(@Nonnull GenericMessageReactionEvent event) {} + public void onGenericMessagePollVote(@Nonnull GenericMessagePollVoteEvent event) {} public void onGenericUser(@Nonnull GenericUserEvent event) {} public void onGenericUserPresence(@Nonnull GenericUserPresenceEvent event) {} public void onGenericUserUpdate(@Nonnull GenericUserUpdateEvent event) {} diff --git a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java index 2988de9137..dbaca131ea 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java @@ -26,6 +26,8 @@ import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -146,6 +148,53 @@ default ReplyCallbackAction reply(@Nonnull MessageCreateData message) return action.applyData(message); } + /** + * Reply to this interaction and acknowledge it. + *
This will send a reply message for this interaction. + * You can use {@link ReplyCallbackAction#setEphemeral(boolean) setEphemeral(true)} to only let the target user see the message. + * Replies are non-ephemeral by default. + * + *

You only have 3 seconds to acknowledge an interaction! + *
When the acknowledgement is sent after the interaction expired, you will receive {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_INTERACTION ErrorResponse.UNKNOWN_INTERACTION}. + *

If your handling can take longer than 3 seconds, due to various rate limits or other conditions, you should use {@link #deferReply()} instead. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_INTERACTION UNKNOWN_INTERACTION} + *
    If the interaction has already been acknowledged or timed out
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The {@link MessagePollData} to send + * + * @throws IllegalArgumentException + * If null is provided + * + * @return {@link ReplyCallbackAction} + * + * @see net.dv8tion.jda.api.utils.messages.MessageCreateBuilder MessageCreateBuilder + * @see MessagePollBuilder + */ + @Nonnull + @CheckReturnValue + default ReplyCallbackAction replyPoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return deferReply().setPoll(poll); + } + /** * Reply to this interaction and acknowledge it. *
This will send a reply message for this interaction. diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java index 60cfc3a3d2..4fee17fb19 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandData.java @@ -67,14 +67,13 @@ public SubcommandData(@Nonnull String name, @Nonnull String description) protected void checkName(@Nonnull String name) { Checks.inRange(name, 1, 32, "Name"); - Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); Checks.isLowercase(name, "Name"); + Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); } protected void checkDescription(@Nonnull String description) { - Checks.notEmpty(description, "Description"); - Checks.notLonger(description, 100, "Description"); + Checks.inRange(description, 1, 100, "Description"); } /** diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java index 36d519f964..7635b92906 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/build/SubcommandGroupData.java @@ -58,12 +58,10 @@ public class SubcommandGroupData implements SerializableData */ public SubcommandGroupData(@Nonnull String name, @Nonnull String description) { - Checks.notEmpty(name, "Name"); - Checks.notEmpty(description, "Description"); - Checks.notLonger(name, 32, "Name"); - Checks.notLonger(description, 100, "Description"); - Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); + Checks.inRange(name, 1, 32, "Name"); Checks.isLowercase(name, "Name"); + Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name"); + Checks.inRange(description, 1, 100, "Description"); this.name = name; this.description = description; } diff --git a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java index d2de7b32ba..49de2c3448 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -186,6 +186,12 @@ public enum ErrorResponse TITLE_BLOCKED_BY_AUTOMOD( 200001, "Title was blocked by automatic moderation"), MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER( 240000, "Message blocked by harmful links filter"), FAILED_TO_BAN_USERS( 500000, "Failed to ban users"), + POLL_VOTING_BLOCKED( 520000, "Poll voting blocked"), + POLL_EXPIRED( 520001, "Poll expired"), + POLL_INVALID_CHANNEL_TYPE( 520002, "Invalid channel type for poll creation"), + CANNOT_UPDATE_POLL_MESSAGE( 520003, "Cannot edit a poll message"), + POLL_WITH_UNUSABLE_EMOJI( 520004, "Cannot use an emoji included with the poll"), + CANNOT_EXPIRE_MISSING_POLL( 520006, "Cannot expire a non-poll message"), SERVER_ERROR( 0, "Discord encountered an internal server error! Not good!"); diff --git a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java index c7eb6f6a9f..89814da397 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java +++ b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java @@ -194,6 +194,16 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), + /** + * Events for poll votes in {@link net.dv8tion.jda.api.entities.Guild Guilds}. + */ + GUILD_MESSAGE_POLLS(24), + + /** + * Events for poll votes in {@link net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel PrivateChannels}. + */ + DIRECT_MESSAGE_POLLS(25), + ; /** diff --git a/src/main/java/net/dv8tion/jda/api/requests/Route.java b/src/main/java/net/dv8tion/jda/api/requests/Route.java index a522a1943e..6c1adb4904 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Route.java @@ -262,6 +262,9 @@ public static class Messages public static final Route GET_MESSAGE = new Route(GET, "channels/{channel_id}/messages/{message_id}"); public static final Route DELETE_MESSAGES = new Route(POST, "channels/{channel_id}/messages/bulk-delete"); + + public static final Route END_POLL = new Route(POST, "channels/{channel_id}/polls/{message_id}/expire"); + public static final Route GET_POLL_ANSWER_VOTERS = new Route(GET, "channels/{channel_id}/polls/{message_id}/answers/{answer_id}"); } public static class Invites diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java new file mode 100644 index 0000000000..79d0aa28ab --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.requests.restaction.pagination; + +import net.dv8tion.jda.api.entities.User; + +/** + * {@link PaginationAction PaginationAction} that paginates the votes for a poll answer. + * + *

Limits
+ * Minimum - 1
+ * Maximum - 1000 + *
Default - 1000 + */ +public interface PollVotersPaginationAction extends PaginationAction +{ +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java index 44690c659b..afa82c5858 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateBuilder.java @@ -59,6 +59,7 @@ public class MessageCreateBuilder extends AbstractMessageBuilder implements MessageCreateRequest { private final List files = new ArrayList<>(10); + private MessagePollData poll; private boolean tts; public MessageCreateBuilder() {} @@ -191,6 +192,21 @@ public List getAttachments() return Collections.unmodifiableList(files); } + @Nullable + @Override + public MessagePollData getPoll() + { + return poll; + } + + @Nonnull + @Override + public MessageCreateBuilder setPoll(@Nullable MessagePollData poll) + { + this.poll = poll; + return this; + } + @Nonnull @Override public MessageCreateBuilder addFiles(@Nonnull Collection files) @@ -222,7 +238,7 @@ public MessageCreateBuilder setSuppressedNotifications(boolean suppressed) @Override public boolean isEmpty() { - return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty(); + return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty() && poll == null; } @Override @@ -243,8 +259,8 @@ public MessageCreateData build() List components = new ArrayList<>(this.components); AllowedMentionsData mentions = this.mentions.copy(); - if (content.isEmpty() && embeds.isEmpty() && files.isEmpty() && components.isEmpty()) - throw new IllegalStateException("Cannot build an empty message. You need at least one of content, embeds, components, or files"); + if (content.isEmpty() && embeds.isEmpty() && files.isEmpty() && components.isEmpty() && poll == null) + throw new IllegalStateException("Cannot build an empty message. You need at least one of content, embeds, components, poll, or files"); int length = Helpers.codePointLength(content); if (length > Message.MAX_CONTENT_LENGTH) @@ -255,7 +271,7 @@ public MessageCreateData build() if (components.size() > Message.MAX_COMPONENT_COUNT) throw new IllegalStateException("Cannot build message with over " + Message.MAX_COMPONENT_COUNT + " component layouts, provided " + components.size()); - return new MessageCreateData(content, embeds, files, components, mentions, tts, messageFlags); + return new MessageCreateData(content, embeds, files, components, mentions, poll, tts, messageFlags); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java index 77e5690ef0..43110c8c27 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateData.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.internal.utils.IOUtil; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; /** @@ -44,19 +45,21 @@ public class MessageCreateData implements MessageData, AutoCloseable, Serializab private final List files; private final List components; private final AllowedMentionsData mentions; + private final MessagePollData poll; private final boolean tts; private final int flags; protected MessageCreateData( String content, List embeds, List files, List components, - AllowedMentionsData mentions, boolean tts, int flags) + AllowedMentionsData mentions, MessagePollData poll, boolean tts, int flags) { this.content = content; this.embeds = Collections.unmodifiableList(embeds); this.files = Collections.unmodifiableList(files); this.components = Collections.unmodifiableList(components); this.mentions = mentions; + this.poll = poll; this.tts = tts; this.flags = flags; } @@ -237,6 +240,17 @@ public List getAttachments() return getFiles(); } + /** + * The poll to send with the message + * + * @return The poll, or null if no poll is sent + */ + @Nullable + public MessagePollData getPoll() + { + return poll; + } + @Override public boolean isSuppressEmbeds() { @@ -316,6 +330,7 @@ public DataObject toData() { DataObject json = DataObject.empty(); json.put("content", content); + json.put("poll", poll); json.put("embeds", DataArray.fromCollection(embeds)); json.put("components", DataArray.fromCollection(components)); json.put("tts", tts); diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java index 5f5d54f7d7..19c8ee9044 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessageCreateRequest.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.File; import java.util.Arrays; import java.util.Collection; @@ -297,6 +298,27 @@ default R addFiles(@Nonnull FileUpload... files) @Override List getAttachments(); + /** + * The poll attached to this message + * + * @return The attached poll, or null if no poll is present + */ + @Nullable + MessagePollData getPoll(); + + /** + * Add a poll to this message. + * + * @param poll + * The poll to send + * + * @return The same instance for chaining + * + * @see MessagePollBuilder + */ + @Nonnull + R setPoll(@Nullable MessagePollData poll); + /** * Whether the message should use Text-to-Speech (TTS). * diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java new file mode 100644 index 0000000000..14b4d3641c --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -0,0 +1,228 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.utils.messages; + +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Builder for {@link MessagePollData} + * + * @see MessageCreateBuilder#setPoll(MessagePollData) + */ +public class MessagePollBuilder +{ + private final List answers = new ArrayList<>(MessagePoll.MAX_ANSWERS); + private MessagePoll.LayoutType layout = MessagePoll.LayoutType.DEFAULT; + private String title; + private Duration duration = Duration.ofHours(24); + private boolean isMultiAnswer; + + /** + * Create a new builder instance + * + * @param title + * The poll title (up to {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + */ + public MessagePollBuilder(@Nonnull String title) + { + this.setTitle(title); + } + + /** + * They poll layout. + * + * @param layout + * The layout + * + * @throws IllegalArgumentException + * If null or {@link net.dv8tion.jda.api.entities.messages.MessagePoll.LayoutType#UNKNOWN UNKNOWN} is provided + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setLayout(@Nonnull MessagePoll.LayoutType layout) + { + Checks.notNull(layout, "Layout"); + Checks.check(layout != MessagePoll.LayoutType.UNKNOWN, "Layout cannot be UNKNOWN"); + + this.layout = layout; + return this; + } + + /** + * Change the title for this poll. + * + * @param title + * The poll title (up to {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@link MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setTitle(@Nonnull String title) + { + Checks.notBlank(title, "Title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_QUESTION_TEXT_LENGTH, "Title"); + + this.title = title; + return this; + } + + /** + * Change the duration for this poll. + *
Default: {@code 1} day + * + *

The poll will automatically expire after this duration. + * + * @param duration + * The duration of this poll (in hours resolution) + * + * @throws IllegalArgumentException + * If the duration is null, less than 1 hour, or longer than {@value MessagePoll#MAX_DURATION_HOURS} hours (7 days) + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setDuration(@Nonnull Duration duration) + { + Checks.notNull(duration, "Duration"); + Checks.positive(duration.toHours(), "Duration"); + Checks.notLonger(duration, Duration.ofHours(MessagePoll.MAX_DURATION_HOURS), TimeUnit.HOURS, "Duration"); + + this.duration = duration; + return this; + } + + /** + * Change the duration for this poll. + *
Default: {@code 1} day + * + *

The poll will automatically expire after this duration. + * + * @param duration + * The duration of this poll (in hours resolution) + * @param unit + * The time unit for the duration + * + * @throws IllegalArgumentException + * If the time unit is null or the duration is not between 1 and {@value MessagePoll#MAX_DURATION_HOURS} hours (7 days) long + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setDuration(long duration, @Nonnull TimeUnit unit) + { + Checks.notNull(unit, "TimeUnit"); + return setDuration(Duration.ofHours(unit.toHours(duration))); + } + + /** + * Whether this poll allows selecting multiple answers. + *
Default: {@code false} + * + * @param multiAnswer + * True, if this poll should allow multiple answers + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder setMultiAnswer(boolean multiAnswer) + { + isMultiAnswer = multiAnswer; + return this; + } + + /** + * Add an answer to this poll. + * + * @param title + * The answer title + * + * @throws IllegalArgumentException + * If the title is null, blank, or longer than {@value MessagePoll#MAX_ANSWER_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title) + { + return addAnswer(title, null); + } + + /** + * Add an answer to this poll. + * + * @param title + * The answer title + * @param emoji + * Optional emoji to show next to the answer text + * + * @throws IllegalArgumentException + * If the title is null, blank, or longer than {@value MessagePoll#MAX_ANSWER_TEXT_LENGTH} characters + * + * @return The updated builder + */ + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title, @Nullable Emoji emoji) + { + Checks.notBlank(title, "Answer title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Answer title"); + Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); + + this.answers.add(new MessagePoll.Answer(this.answers.size() + 1, title, (EmojiUnion) emoji, 0, false)); + return this; + } + + /** + * Build the poll data. + * + * @throws IllegalStateException + * If no answers have been added to the builder + * + * @return {@link MessagePollData} + */ + @Nonnull + public MessagePollData build() + { + if (answers.isEmpty()) + throw new IllegalStateException("Cannot build a poll without answers"); + return new MessagePollData( + layout, + new MessagePoll.Question(title, null), + new ArrayList<>(answers), + duration, + isMultiAnswer + ); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java new file mode 100644 index 0000000000..4d71a28c2f --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.utils.messages; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.data.SerializableData; +import net.dv8tion.jda.internal.utils.Helpers; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A poll that can be attached to a {@link MessageCreateRequest}. + * + *

Example
+ *

{@code
+ * channel.sendMessage("Hello guys! Check my poll:")
+ *   .setPoll(
+ *     MessagePollData.builder("Which programming language is better?")
+ *       .addAnswer("Java", Emoji.fromFormatted("<:java:1006323566314274856>"))
+ *       .addAnswer("Kotlin", Emoji.fromFormatted("<:kotlin:295940257797636096>"))
+ *       .build())
+ *   .queue()
+ * }
+ * + * @see #builder(String) + * @see MessageCreateBuilder#setPoll(MessagePollData) + */ +public class MessagePollData implements SerializableData +{ + private final MessagePoll.LayoutType layout; + private final MessagePoll.Question question; + private final List answers; + private final Duration duration; + private final boolean isMultiAnswer; + + public MessagePollData(MessagePoll.LayoutType layout, MessagePoll.Question question, List answers, Duration duration, boolean isMultiAnswer) + { + this.layout = layout; + this.question = question; + this.answers = answers; + this.duration = duration; + this.isMultiAnswer = isMultiAnswer; + } + + /** + * Creates a new {@link MessagePollBuilder}. + * + *

A poll must have at least one answer. + * + * @param title + * The poll title (up to {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + * + * @return {@link MessagePollBuilder} + */ + @Nonnull + public static MessagePollBuilder builder(@Nonnull String title) + { + return new MessagePollBuilder(title); + } + + @NotNull + @Override + public DataObject toData() + { + DataObject data = DataObject.empty(); + + data.put("duration", TimeUnit.SECONDS.toHours(duration.getSeconds())); + data.put("allow_multiselect", isMultiAnswer); + data.put("layout_type", layout.getKey()); + + data.put("question", DataObject.empty() + .put("text", question.getText())); + + data.put("answers", answers.stream() + .map(answer -> DataObject.empty() + .put("answer_id", answer.getId()) + .put("poll_media", DataObject.empty() + .put("text", answer.getText()) + .put("emoji", answer.getEmoji()))) + .collect(Helpers.toDataArray())); + + return data; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java index 1e636eb5e0..386e43c337 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java @@ -29,6 +29,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageDeleteActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; @@ -102,6 +103,14 @@ public WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData mess return sendRequest().applyData(message); } + @Nonnull + @Override + public WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return sendRequest().setPoll(poll); + } + @Nonnull @Override public WebhookMessageCreateAction sendFiles(@Nonnull Collection files) 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 97d7258a77..3c69ee7ecb 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -39,8 +39,11 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.EmojiUnion; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.entities.messages.MessagePollImpl; import net.dv8tion.jda.api.entities.sticker.*; import net.dv8tion.jda.api.entities.templates.Template; import net.dv8tion.jda.api.entities.templates.TemplateChannel; @@ -1836,6 +1839,8 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) ); } + MessagePoll poll = jsonObject.optObject("poll").map(EntityBuilder::createMessagePoll).orElse(null); + // Message Components List components = Collections.emptyList(); Optional componentsArrayOpt = jsonObject.optArray("components"); @@ -1866,7 +1871,7 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) int position = jsonObject.getInt("position", -1); return new ReceivedMessage(id, channelId, guildId, api, guild, channel, type, messageReference, fromWebhook, applicationId, tts, pinned, - content, nonce, user, member, activity, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, + content, nonce, user, member, activity, poll, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, messageInteraction, startedThread, position); } @@ -1897,6 +1902,47 @@ private static MessageActivity createMessageActivity(DataObject jsonObject) return new MessageActivity(activityType, partyId, application); } + public static MessagePollImpl createMessagePoll(DataObject data) + { + MessagePoll.LayoutType layout = MessagePoll.LayoutType.fromKey(data.getInt("layout_type")); + OffsetDateTime expiresAt = data.isNull("expiry") ? null : data.getOffsetDateTime("expiry"); + boolean isMultiAnswer = data.getBoolean("allow_multiselect"); + + DataArray answersData = data.getArray("answers"); + DataObject questionData = data.getObject("question"); + + DataObject resultsData = data.optObject("results").orElseGet( + () -> DataObject.empty().put("answer_counts", DataArray.empty()) // FIXME: Discord bug + ); + boolean isFinalized = resultsData.getBoolean("is_finalized"); + + DataArray resultVotes = resultsData.getArray("answer_counts"); + TLongObjectMap voteMapping = new TLongObjectHashMap<>(); + resultVotes.stream(DataArray::getObject) + .forEach(votes -> voteMapping.put(votes.getLong("id"), votes)); + + MessagePoll.Question question = new MessagePoll.Question( + questionData.getString("text"), + questionData.optObject("emoji").map(Emoji::fromData).orElse(null)); + + List answers = answersData.stream(DataArray::getObject) + .map(answer -> { + long answerId = answer.getLong("answer_id"); + DataObject media = answer.getObject("poll_media"); + DataObject votes = voteMapping.get(answerId); + return new MessagePoll.Answer( + answerId, + media.getString("text"), + media.optObject("emoji").map(Emoji::fromData).orElse(null), + votes != null ? votes.getInt("count") : 0, + votes != null && votes.getBoolean("me_voted") + ); + }) + .collect(Helpers.toUnmodifiableList()); + + return new MessagePollImpl(layout, question, answers, expiresAt, isMultiAnswer, isFinalized); + } + public MessageReaction createMessageReaction(MessageChannel chan, long channelId, long messageId, DataObject obj) { DataObject emoji = obj.getObject("emoji"); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index e0dedbf78b..9965195cd2 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -82,7 +82,6 @@ import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; import okhttp3.MediaType; import okhttp3.MultipartBody; -import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -1553,7 +1552,7 @@ public AuditableRestAction ban(@Nonnull Collection reactions, List attachments, List embeds, List stickers, List components, int flags, Message.Interaction interaction, ThreadChannel startedThread, int position) @@ -151,6 +153,7 @@ public ReceivedMessage( this.interaction = interaction; this.startedThread = startedThread; this.position = position; + this.poll = poll; } private void checkSystem(String comment) @@ -613,6 +616,29 @@ public List getComponents() return components; } + @Override + public MessagePoll getPoll() + { + checkIntent(); + return poll; + } + + @Nonnull + @Override + public AuditableRestAction endPoll() + { + checkUser(); + if (poll == null) + throw new IllegalStateException("This message does not contain a poll"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getChannelId(), getId()), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + EntityBuilder entityBuilder = jda.getEntityBuilder(); + if (hasChannel()) + return entityBuilder.createMessageWithChannel(response.getObject(), channel, false); + return entityBuilder.createMessageFromWebhook(response.getObject(), hasGuild() ? getGuild() : null); + }); + } + @Nonnull @Override public Mentions getMentions() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java index 8f33c5990d..e2992ae842 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java @@ -40,7 +40,9 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.RestActionImpl; +import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -155,6 +157,33 @@ default MessageCreateAction sendMessageEmbeds(@Nonnull Collection components) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessageComponents(components); + } + + @Nonnull + @Override + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessagePoll(poll); + } + @Nonnull @CheckReturnValue default MessageCreateAction sendMessage(@Nonnull MessageCreateData msg) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java new file mode 100644 index 0000000000..1e38887ce9 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.requests.WebSocketClient; + +public class MessagePollVoteHandler extends SocketHandler +{ + private final boolean add; + + public MessagePollVoteHandler(JDAImpl api, boolean add) + { + super(api); + this.add = add; + } + + @Override + protected Long handleInternally(DataObject content) + { + long answerId = content.getLong("answer_id"); + long userId = content.getUnsignedLong("user_id"); + long messageId = content.getUnsignedLong("message_id"); + long channelId = content.getUnsignedLong("channel_id"); + long guildId = content.getUnsignedLong("guild_id", 0); + + if (api.getGuildSetupController().isLocked(guildId)) + return guildId; + + Guild guild = api.getGuildById(guildId); + MessageChannel channel = api.getChannelById(MessageChannel.class, channelId); + if (channel == null) + { + if (guild != null) + { + GuildChannel actual = guild.getGuildChannelById(channelId); + if (actual != null) + { + WebSocketClient.LOG.debug("Dropping message poll vote event for unexpected channel of type {}", actual.getType()); + return null; + } + } + + if (guildId != 0) + { + api.getEventCache().cache(EventCache.Type.CHANNEL, channelId, responseNumber, allContent, this::handle); + EventCache.LOG.debug("Received a vote for a channel that JDA does not currently have cached"); + return null; + } + + channel = getJDA().getEntityBuilder().createPrivateChannel( + DataObject.empty() + .put("id", channelId) + ); + } + + if (add) + api.handleEvent(new MessagePollVoteAddEvent(channel, responseNumber, messageId, userId, answerId)); + else + api.handleEvent(new MessagePollVoteRemoveEvent(channel, responseNumber, messageId, userId, answerId)); + + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java index c3bf85f68b..88bc202595 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java @@ -87,8 +87,7 @@ public void checkName(@Nonnull String name) public void checkDescription(@Nonnull String description) { checkType(Command.Type.SLASH, "set description"); - Checks.notEmpty(description, "Description"); - Checks.notLonger(description, MAX_DESCRIPTION_LENGTH, "Description"); + Checks.inRange(description, 1, MAX_DESCRIPTION_LENGTH, "Description"); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index 08f24d1ad5..924c45724a 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -1395,6 +1395,8 @@ protected void setupHandlers() handlers.put("MESSAGE_REACTION_REMOVE", new MessageReactionHandler(api, false)); handlers.put("MESSAGE_REACTION_REMOVE_ALL", new MessageReactionBulkRemoveHandler(api)); handlers.put("MESSAGE_REACTION_REMOVE_EMOJI", new MessageReactionClearEmojiHandler(api)); + handlers.put("MESSAGE_POLL_VOTE_ADD", new MessagePollVoteHandler(api, true)); + handlers.put("MESSAGE_POLL_VOTE_REMOVE", new MessagePollVoteHandler(api, false)); handlers.put("MESSAGE_UPDATE", new MessageUpdateHandler(api)); handlers.put("PRESENCE_UPDATE", new PresenceUpdateHandler(api)); handlers.put("READY", new ReadyHandler(api)); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java index 6f80cd8167..6e76e56a5b 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java @@ -79,7 +79,7 @@ protected RequestBody finalizeData() { if (!stickers.isEmpty()) return getRequestBody(DataObject.empty().put("sticker_ids", stickers)); - throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } try (MessageCreateData data = builder.build()) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java new file mode 100644 index 0000000000..5ac181ee7b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.requests.restaction.pagination; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.exceptions.ParsingException; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; +import net.dv8tion.jda.api.requests.Route; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.EntityBuilder; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class PollVotersPaginationActionImpl extends PaginationActionImpl implements PollVotersPaginationAction +{ + public PollVotersPaginationActionImpl(JDA jda, String channelId, String messageId, long answerId) + { + super(jda, Route.Messages.GET_POLL_ANSWER_VOTERS.compile(channelId, messageId, Long.toString(answerId)), 1, 1000, 1000); + this.order = PaginationOrder.FORWARD; + } + + @NotNull + @Override + public EnumSet getSupportedOrders() + { + return EnumSet.of(PaginationOrder.FORWARD); + } + + @Override + protected long getKey(User it) + { + return it.getIdLong(); + } + + @Override + protected void handleSuccess(Response response, Request> request) + { + DataArray array = response.getObject().getArray("users"); + List users = new ArrayList<>(array.length()); + EntityBuilder builder = api.getEntityBuilder(); + for (int i = 0; i < array.length(); i++) + { + try + { + DataObject object = array.getObject(i); + users.add(builder.createUser(object)); + } + catch(ParsingException | NullPointerException e) + { + LOG.warn("Encountered an exception in PollVotersPaginationAction", e); + } + } + + if (!users.isEmpty()) + { + if (useCache) + cached.addAll(users); + last = users.get(users.size() - 1); + lastKey = last.getIdLong(); + } + + request.onSuccess(users); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java index 5e913595b8..edd1fe10fd 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java @@ -28,7 +28,9 @@ import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.Contract; +import java.time.Duration; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -212,6 +214,18 @@ public static void notNegative(final long n, final String name) throw new IllegalArgumentException(name + " may not be negative"); } + public static void notLonger(final Duration duration, final Duration maxDuration, final TimeUnit resolutionUnit, final String name) + { + notNull(duration, name); + check( + duration.compareTo(maxDuration) <= 0, + "%s may not be longer than %s. Provided: %s", + name, + JDALogger.getLazyString(() -> Helpers.durationToString(maxDuration, resolutionUnit)), + JDALogger.getLazyString(() -> Helpers.durationToString(duration, resolutionUnit)) + ); + } + // Unique streams checks public static void checkUnique(Stream stream, String format, BiFunction getArgs) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index b8a7051213..703f7d1e84 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -26,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.ToLongFunction; import java.util.stream.Collector; @@ -316,4 +317,27 @@ public static boolean hasCause(Throwable throwable, Class c { return Collector.of(DataArray::empty, DataArray::add, DataArray::addAll); } + + public static String durationToString(Duration duration, TimeUnit resolutionUnit) + { + long actual = resolutionUnit.convert(duration.getSeconds(), TimeUnit.SECONDS); + String raw = actual + " " + resolutionUnit.toString().toLowerCase(Locale.ROOT); + + long days = duration.toDays(); + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() - TimeUnit.DAYS.toSeconds(days) - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); + + StringJoiner joiner = new StringJoiner(" "); + if (days > 0) + joiner.add(days + " days"); + if (hours > 0) + joiner.add(hours + " hours"); + if (minutes > 0) + joiner.add(minutes + " minutes"); + if (seconds > 0) + joiner.add(seconds + " seconds"); + + return raw + " (" + joiner + ")"; + } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java index af8f154de3..7cf498d989 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; import net.dv8tion.jda.api.utils.messages.MessageCreateRequest; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -62,6 +63,21 @@ default R addFiles(@Nonnull Collection files) return (R) this; } + @Nullable + @Override + default MessagePollData getPoll() + { + return getBuilder().getPoll(); + } + + @Nonnull + @Override + default R setPoll(@Nullable MessagePollData poll) + { + getBuilder().setPoll(poll); + return (R) this; + } + @Nonnull @Override default R setTTS(boolean tts) diff --git a/src/test/java/net/dv8tion/jda/test/ChecksHelper.java b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java new file mode 100644 index 0000000000..142f2792d9 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test; + +import net.dv8tion.jda.test.assertions.checks.*; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.regex.Pattern; + +public class ChecksHelper +{ + public static String tooLongError(String name, int maxLength, String value) + { + return name + " may not be longer than " + maxLength + " characters! Provided: \"" + value + "\""; + } + + public static String notInRangeError(String name, int minLength, int maxLength, String value) + { + return name + " must be between " + minLength + " and " + maxLength + " characters long! Provided: \"" + value + "\""; + } + + public static String isNullError(String name) + { + return name + " may not be null"; + } + + public static String isEmptyError(String name) + { + return name + " may not be empty"; + } + + public static String isBlankError(String name) + { + return name + " may not be blank"; + } + + public static String isNotLowercase(String name, String value) + { + return name + " must be lowercase only! Provided: \"" + value + "\""; + } + + public static String notRegexMatch(String name, Pattern pattern, String value) + { + return name + " must match regex ^" + pattern + "$. Provided: \"" + value + "\""; + } + + public static String isNegativeError(String name) + { + return name + " may not be negative"; + } + + public static String notPositiveError(String name) + { + return name + " may not be negative or zero"; + } + + public static StringChecksAssertions assertStringChecks(String name, ThrowingConsumer callable) + { + return new StringChecksAssertions(name, callable); + } + + public static > EnumChecksAssertions assertEnumChecks(String name, ThrowingConsumer callable) + { + return new EnumChecksAssertions<>(name, callable); + } + + public static DurationChecksAssertions assertDurationChecks(String name, ThrowingConsumer callable) + { + return new DurationChecksAssertions(name, callable); + } + + public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) + { + return new LongChecksAssertions(name, callable); + } + + public static SimpleChecksAssertions assertChecks(String name, ThrowingConsumer callable) + { + return new SimpleChecksAssertions<>(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/Constants.java b/src/test/java/net/dv8tion/jda/test/Constants.java index 0e1c4c9ef4..4dc3e449bf 100644 --- a/src/test/java/net/dv8tion/jda/test/Constants.java +++ b/src/test/java/net/dv8tion/jda/test/Constants.java @@ -19,6 +19,7 @@ public interface Constants { long GUILD_ID = 125227483518861312L; + long CHANNEL_ID = 125227483518861312L; long MINN_USER_ID = 86699011792191488L; long BUTLER_USER_ID = 150203841827045376L; } diff --git a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java index a884a14a06..ebb0952457 100644 --- a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java +++ b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.Requester; import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.test.assertions.restaction.RestActionAssertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java new file mode 100644 index 0000000000..f8f964a7c4 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static net.dv8tion.jda.test.ChecksHelper.isNullError; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class AbstractChecksAssertions> +{ + protected final String name; + protected final ThrowingConsumer callable; + + public AbstractChecksAssertions(String name, ThrowingConsumer callable) + { + this.name = name; + this.callable = callable; + } + + public S checksNotNull() + { + return throwsFor(null, isNullError(name)); + } + + @SuppressWarnings("unchecked") + public S throwsFor(T input, String expectedError) + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(input)) + .withMessage(expectedError); + return (S) this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java new file mode 100644 index 0000000000..ba50121459 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.internal.utils.Helpers.durationToString; +import static net.dv8tion.jda.test.ChecksHelper.isNegativeError; +import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class DurationChecksAssertions extends AbstractChecksAssertions +{ + public DurationChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public DurationChecksAssertions checksNotNegative() + { + throwsFor(Duration.ofSeconds(-1), isNegativeError(name)); + return this; + } + + public DurationChecksAssertions checksPositive() + { + throwsFor(Duration.ofSeconds(-1), notPositiveError(name)); + throwsFor(Duration.ZERO, notPositiveError(name)); + return this; + } + + public DurationChecksAssertions checksNotLonger(Duration maxDuration, TimeUnit resolution) + { + Duration input = maxDuration.plusSeconds(resolution.toSeconds(1)); + throwsFor(input, + String.format(Locale.ROOT, "%s may not be longer than %s. Provided: %s", + name, durationToString(maxDuration, resolution), durationToString(input, resolution))); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java new file mode 100644 index 0000000000..1ca7712fdf --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +public class EnumChecksAssertions> extends AbstractChecksAssertions> +{ + public EnumChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public EnumChecksAssertions checkIsNot(E variant) + { + throwsFor(variant, name + " cannot be " + variant); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java new file mode 100644 index 0000000000..6ba7b8bafc --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class LongChecksAssertions extends AbstractChecksAssertions +{ + public LongChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public LongChecksAssertions checksPositive() + { + throwsFor( 0L, notPositiveError(name)); + throwsFor( -1L, notPositiveError(name)); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java new file mode 100644 index 0000000000..91441c2660 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +public class SimpleChecksAssertions extends AbstractChecksAssertions> +{ + public SimpleChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java new file mode 100644 index 0000000000..91e89207fd --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.regex.Pattern; + +import static net.dv8tion.jda.test.ChecksHelper.*; + +public class StringChecksAssertions extends AbstractChecksAssertions +{ + public StringChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public StringChecksAssertions checksNotEmpty() + { + throwsFor(null, isNullError(name)); + throwsFor("", isEmptyError(name)); + return this; + } + + public StringChecksAssertions checksNotBlank() + { + throwsFor(null, isNullError(name)); + throwsFor("", isBlankError(name)); + throwsFor(" ", isBlankError(name)); + return this; + } + + public StringChecksAssertions checksNotLonger(int maxLength) + { + String invalidInput = StringUtils.repeat("s", maxLength + 1); + throwsFor(invalidInput, tooLongError(name, maxLength, invalidInput)); + return this; + } + + public StringChecksAssertions checksLowercaseOnly() + { + throwsFor("InvalidCasing", isNotLowercase(name, "InvalidCasing")); + return this; + } + + public StringChecksAssertions checksRange(int minLength, int maxLength) + { + String tooLong = StringUtils.repeat("s", maxLength + 1); + String tooShort = StringUtils.repeat("s", minLength - 1); + throwsFor(tooShort, notInRangeError(name, minLength, maxLength, tooShort)); + throwsFor(tooLong, notInRangeError(name, minLength, maxLength, tooLong)); + return this; + } + + public StringChecksAssertions checksRegex(String input, Pattern regex) + { + throwsFor(input, notRegexMatch(name, regex, input)); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java new file mode 100644 index 0000000000..3cb518678c --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.events; + +import net.dv8tion.jda.internal.JDAImpl; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.*; + +public class EventFiredAssertions +{ + private final Class eventType; + private final JDAImpl jda; + private final List> assertions = new ArrayList<>(); + + public EventFiredAssertions(Class eventType, JDAImpl jda) + { + this.eventType = eventType; + this.jda = jda; + } + + public EventFiredAssertions hasGetterWithValueEqualTo(Function getter, V value) + { + assertions.add(event -> assertThat(getter.apply(event)).isEqualTo(value)); + return this; + } + + public void isFiredBy(Runnable runnable) + { + doNothing().when(jda).handleEvent(assertArg(arg -> { + assertThat(arg).isInstanceOf(eventType); + T casted = eventType.cast(arg); + for (ThrowingConsumer assertion : assertions) + assertion.accept(casted); + })); + + runnable.run(); + + verify(jda, times(1)).handleEvent(any()); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java similarity index 99% rename from src/test/java/net/dv8tion/jda/test/RestActionAssertions.java rename to src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java index 37f9f24518..3e12458291 100644 --- a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.test; +package net.dv8tion.jda.test.assertions.restaction; import net.dv8tion.jda.api.requests.Method; import net.dv8tion.jda.api.requests.Request; diff --git a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java index 8d4bb8ec33..87e28a2072 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java @@ -28,7 +28,6 @@ public class MessageSerializationTest { - private static final String DESCRIPTION_TEXT = "Description Text"; private static final String TITLE_TEXT = "Title Text"; private static final String TITLE_URL = "https://example.com/title"; diff --git a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java index 8192df657b..d5d126d338 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import java.util.stream.LongStream; +import static net.dv8tion.jda.test.ChecksHelper.assertDurationChecks; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -70,6 +71,10 @@ void testInvalidInputs() { hasPermission(true); + assertDurationChecks("Deletion timeframe", duration -> guild.ban(Collections.emptyList(), duration)) + .checksNotNegative() + .throwsFor(Duration.ofDays(100), "Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + Set users = Collections.singleton(null); assertThatIllegalArgumentException() @@ -78,12 +83,7 @@ void testInvalidInputs() assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban(null, null).queue()) .withMessage("Users may not be null"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofSeconds(-1)).queue()) - .withMessage("Deletion time cannot be negative"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofDays(100)).queue()) - .withMessage("Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban( diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java new file mode 100644 index 0000000000..6cd1f67b5b --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.test.ChecksHelper; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.test.ChecksHelper.*; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +public class MessagePollDataTest +{ + @Test + void testInvalidInputs() + { + assertStringChecks("Title", MessagePollBuilder::new) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(300); + + MessagePollBuilder builder = new MessagePollBuilder("test title"); + + assertEnumChecks("Layout", builder::setLayout) + .checksNotNull() + .checkIsNot(MessagePoll.LayoutType.UNKNOWN); + + assertDurationChecks("Duration", builder::setDuration) + .checksNotNull() + .checksPositive() + .checksNotLonger(Duration.ofHours(7 * 24), TimeUnit.HOURS); + + ChecksHelper.assertChecks("TimeUnit", (unit) -> builder.setDuration(1, unit)) + .checksNotNull(); + + assertLongChecks("Duration", (duration) -> builder.setDuration(duration, TimeUnit.SECONDS)) + .checksPositive() + .throwsFor(TimeUnit.DAYS.toSeconds(8), "Duration may not be longer than 168 hours (7 days). Provided: 192 hours (8 days)"); + + assertStringChecks("Answer title", builder::addAnswer) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(55); + + assertThatIllegalStateException() + .isThrownBy(builder::build) + .withMessage("Cannot build a poll without answers"); + + for (int i = 0; i < 10; i++) + builder.addAnswer("Answer " + i); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.addAnswer("Answer " + 10)) + .withMessage("Poll cannot have more than 10 answers"); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java new file mode 100644 index 0000000000..57010d81c6 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.requests.Method; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class PollVotersPaginationTest extends IntegrationTest +{ + private PollVotersPaginationActionImpl newAction() + { + return new PollVotersPaginationActionImpl(jda, "381886978205155338", "1228092239079804968", 5); + } + + @Test + void testDefaults() + { + assertThatRequestFrom(newAction()) + .hasMethod(Method.GET) + .hasCompiledRoute("channels/381886978205155338/polls/1228092239079804968/answers/5?limit=1000&after=0") + .whenQueueCalled(); + } + + @Test + void testSkipTo() + { + long randomId = random.nextLong(); + assertThatRequestFrom(newAction().skipTo(randomId)) + .hasMethod(Method.GET) + .hasQueryParams("limit", "1000", "after", Long.toUnsignedString(randomId)) + .whenQueueCalled(); + } + + @Test + void testOrder() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> newAction().reverse()) + .withMessage("Cannot use PaginationOrder.BACKWARD for this pagination endpoint."); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java b/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java new file mode 100644 index 0000000000..686a00df44 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.events; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.handle.GuildSetupController; +import net.dv8tion.jda.test.Constants; +import net.dv8tion.jda.test.IntegrationTest; +import net.dv8tion.jda.test.assertions.events.EventFiredAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class AbstractSocketHandlerTest extends IntegrationTest +{ + @Mock + protected GuildSetupController setupController; + @Mock + protected Guild guild; + + @BeforeEach + final void setupHandlerContext() + { + when(jda.getGuildSetupController()).thenReturn(setupController); + when(setupController.isLocked(anyLong())).thenReturn(false); + when(jda.getGuildById(eq(Constants.GUILD_ID))).thenReturn(guild); + } + + protected DataObject event(String type, DataObject data) + { + return DataObject.empty() + .put("s", 1) + .put("op", 0) + .put("t", type) + .put("d", data); + } + + protected EventFiredAssertions assertThatEvent(Class eventType) + { + return new EventFiredAssertions<>(eventType, jda); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java b/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java new file mode 100644 index 0000000000..e3acc34a37 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.events; + +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.handle.MessagePollVoteHandler; +import net.dv8tion.jda.test.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class MessagePollHandlerTests extends AbstractSocketHandlerTest +{ + @Mock + protected GuildMessageChannel channel; + + @BeforeEach + final void setupMessageContext() + { + when(jda.getChannelById(eq(MessageChannel.class), eq(Constants.CHANNEL_ID))).thenReturn(channel); + } + + @Test + void testMinimalVoteAdd() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, true); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteAddEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_ADD", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } + + @Test + void testMinimalVoteRemove() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, false); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteRemoveEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_REMOVE", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java index 1dae4f186c..e568089b5d 100644 --- a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java @@ -27,12 +27,14 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.interactions.CommandDataImpl; +import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.test.PrettyRepresentation; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import static net.dv8tion.jda.test.ChecksHelper.assertStringChecks; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -175,50 +177,51 @@ void testRequiredThrows() @Test void testNameChecks() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("valid_name", "")) - .withMessage("Description may not be empty"); + assertStringChecks("Name", input -> new CommandDataImpl(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandGroupData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Description", input -> new CommandDataImpl("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandGroupData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); } @Test void testChoices() { OptionData stringOption = new OptionData(OptionType.STRING, "choice", "Option with choices!"); + + assertStringChecks("Value", value -> stringOption.addChoice("valid_name", value)) + .checksNotEmpty(); + assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalid name", 0)) .withMessage("Cannot add long choice for OptionType.STRING"); assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalidName", 0.0)) .withMessage("Cannot add double choice for OptionType.STRING"); - assertThatIllegalArgumentException() - .isThrownBy(() -> stringOption.addChoice("valid_name", "")) - .withMessage("Value may not be empty"); OptionData intOption = new OptionData(OptionType.INTEGER, "choice", "Option with choices!"); List choices = new ArrayList<>(); diff --git a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java index 57456b018d..8b9ee1c397 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -18,9 +18,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.test.IntegrationTest; import org.junit.jupiter.api.BeforeEach; @@ -28,8 +30,11 @@ import org.mockito.Mock; import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; import static net.dv8tion.jda.api.requests.Method.POST; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.emoji; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.pollAnswer; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.when; @@ -54,6 +59,7 @@ private static DataObject defaultMessageRequest() .put("components", DataArray.empty()) .put("content", "") .put("embeds", DataArray.empty()) + .put("poll", null) .put("enforce_nonce", true) .put("flags", 0) .put("nonce", FIXED_NONCE) @@ -73,7 +79,7 @@ void testEmpty() assertThatIllegalStateException().isThrownBy(() -> new MessageCreateActionImpl(channel) .queue() - ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } @Test @@ -106,9 +112,63 @@ void testEmbedOnly() .whenQueueCalled(); } + @Test + void testPollOnly() + { + MessageCreateAction action = new MessageCreateActionImpl(channel) + .setPoll(new MessagePollBuilder("Test poll") + .setDuration(3, TimeUnit.DAYS) + .setMultiAnswer(true) + .addAnswer("Test answer 1") + .addAnswer("Test answer 2", Emoji.fromUnicode("🤔")) + .addAnswer("Test answer 3", Emoji.fromCustom("minn", 821355005788684298L, true)) + .build()); + + assertThatRequestFrom(action) + .hasMethod(POST) + .hasCompiledRoute(ENDPOINT_URL) + .hasBodyEqualTo(defaultMessageRequest() + .put("poll", DataObject.empty() + .put("duration", 72) + .put("allow_multiselect", true) + .put("layout_type", 1) + .put("question", DataObject.empty() + .put("text", "Test poll")) + .put("answers", DataArray.empty() + .add(pollAnswer(1, "Test answer 1", null)) + .add(pollAnswer(2, "Test answer 2", emoji("🤔"))) + .add(pollAnswer(3, "Test answer 3", emoji("minn", 821355005788684298L, true)))))) + .whenQueueCalled(); + } + @Nonnull protected DataObject normalizeRequestBody(@Nonnull DataObject body) { return body.put("nonce", FIXED_NONCE); } + + static class Data + { + static DataObject pollAnswer(long id, String title, DataObject emoji) + { + return DataObject.empty() + .put("answer_id", id) + .put("poll_media", DataObject.empty() + .put("text", title) + .put("emoji", emoji)); + } + + static DataObject emoji(String name) + { + return DataObject.empty().put("name", name); + } + + static DataObject emoji(String name, long id, boolean animated) + { + return DataObject.empty() + .put("name", name) + .put("id", id) + .put("animated", animated); + } + } }