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/CommandInteractionPayload.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java
index 36103c8c72..35b31f8454 100644
--- a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java
+++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java
@@ -149,6 +149,20 @@ default String getCommandString()
for (OptionMapping o : getOptions())
{
builder.append(" ").append(o.getName()).append(": ");
+ // Discord doesn't send the resolved entities on autocomplete interactions
+ if (this instanceof CommandAutoCompleteInteraction)
+ {
+ switch (o.getType())
+ {
+ case CHANNEL:
+ case USER:
+ case ROLE:
+ case MENTIONABLE:
+ builder.append(o.getAsLong());
+ continue;
+ }
+ }
+
switch (o.getType())
{
case CHANNEL:
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 450f4f9844..49de2c3448 100644
--- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java
+++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java
@@ -164,6 +164,7 @@ public enum ErrorResponse
SERVER_NOT_ENOUGH_BOOSTS( 50101, "This server needs more boosts to perform this action"),
MIXED_PREMIUM_ROLES_FOR_EMOJI( 50144, "Cannot mix subscription and non subscription roles for an emoji"),
ILLEGAL_EMOJI_CONVERSION( 50145, "Cannot convert between premium emoji and normal emoji"),
+ USER_MUST_BE_VERIFIED( 50178, "The user account must first be verified"),
MFA_NOT_ENABLED( 60003, "MFA auth required but not enabled"),
NO_USER_WITH_TAG_EXISTS( 80004, "No users with DiscordTag exist"),
REACTION_BLOCKED( 90001, "Reaction Blocked"),
@@ -185,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 extends FileUpload> 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 extends FileUpload> 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 extends FileUpload> 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 extends Mess
return MessageChannelUnion.super.sendMessageEmbeds(embeds);
}
+ @NotNull
+ @Override
+ default MessageCreateAction sendMessageComponents(@NotNull LayoutComponent component, @NotNull LayoutComponent... other)
+ {
+ checkCanAccessChannel();
+ checkCanSendMessage();
+ return MessageChannelUnion.super.sendMessageComponents(component, other);
+ }
+
+ @Nonnull
+ @Override
+ default MessageCreateAction sendMessageComponents(@Nonnull Collection extends LayoutComponent> 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 extends Throwable> 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 extends FileUpload> 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);
+ }
+ }
}