From 168c2910712ed5244cf14a2a8e047068eaa01139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 12:05:34 +0200 Subject: [PATCH 01/30] First pass on poll handling --- .../net/dv8tion/jda/api/entities/Message.java | 4 + .../api/entities/messages/MessagePoll.java | 142 ++++++++++++++++++ .../entities/messages/MessagePollImpl.java | 81 ++++++++++ .../jda/api/requests/ErrorResponse.java | 7 + .../net/dv8tion/jda/api/requests/Route.java | 3 + .../jda/internal/entities/EntityBuilder.java | 46 +++++- .../internal/entities/ReceivedMessage.java | 12 +- 7 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java create mode 100644 src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index d10b6cdd8d..7599be8bd2 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -31,6 +31,7 @@ import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; import net.dv8tion.jda.api.entities.sticker.GuildSticker; import net.dv8tion.jda.api.entities.sticker.Sticker; import net.dv8tion.jda.api.entities.sticker.StickerItem; @@ -681,6 +682,9 @@ default String getGuildId() @Nonnull List getComponents(); + @Nullable + MessagePoll getPoll(); + /** * Rows of interactive components such as {@link Button Buttons}. *
You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java new file mode 100644 index 0000000000..0a533feb99 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.entities.messages; + +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.OffsetDateTime; +import java.util.List; + +public interface MessagePoll +{ + @Nonnull + LayoutType getLayout(); + + @Nonnull + Question getQuestion(); + + @Nonnull + List getAnswers(); + + @Nonnull + OffsetDateTime getTimeExpiresAt(); + + boolean isMultiAnswer(); + + boolean isFinalizedVotes(); + + + class Question + { + private final String text; + private final EmojiUnion emoji; + + public Question(String text, Emoji emoji) + { + this.text = text; + this.emoji = (EmojiUnion) emoji; + } + + @Nonnull + public String getText() + { + return text; + } + + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + } + + 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; + } + + public long getId() + { + return id; + } + + @Nonnull + public String getText() + { + return text; + } + + @Nullable + public EmojiUnion getEmoji() + { + return emoji; + } + + public int getVotes() + { + return votes; + } + + public boolean isSelfVoted() + { + return selfVoted; + } + } + + enum LayoutType + { + DEFAULT(1), + UNKNOWN(-1); + + private final int key; + + LayoutType(int key) + { + this.key = key; + } + + public int getKey() + { + return key; + } + + 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/requests/ErrorResponse.java b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java index 450f4f9844..01bb681807 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -185,6 +185,13 @@ 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( 520004, "Cannot expire a non-poll message"), + POLL_ALREADY_EXPIRED( 520007, "Poll is already expired"), SERVER_ERROR( 0, "Discord encountered an internal server error! Not good!"); 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..7de5db51f5 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 EXPIRE_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/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 97d7258a77..eb3f16cfab 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,45 @@ 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.getOffsetDateTime("expiry"); + boolean isMultiAnswer = data.getBoolean("allow_multiselect"); + + DataArray answersData = data.getArray("answers"); + DataObject questionData = data.getObject("question"); + + DataObject resultsData = data.getObject("results"); + 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/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index 7b598e5788..fe08850777 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -33,6 +33,7 @@ import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; import net.dv8tion.jda.api.entities.sticker.StickerItem; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; import net.dv8tion.jda.api.exceptions.PermissionException; @@ -97,6 +98,7 @@ public class ReceivedMessage implements Message protected final String content; protected final String nonce; protected final MessageActivity activity; + protected final MessagePoll poll; protected final OffsetDateTime editedTime; protected final Mentions mentions; protected final Message.Interaction interaction; @@ -118,7 +120,7 @@ public class ReceivedMessage implements Message public ReceivedMessage( long id, long channelId, long guildId, JDA jda, Guild guild, MessageChannel channel, MessageType type, MessageReference messageReference, boolean fromWebhook, long applicationId, boolean tts, boolean pinned, - String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, + String content, String nonce, User author, Member member, MessageActivity activity, MessagePoll poll, OffsetDateTime editTime, Mentions mentions, List 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,13 @@ public List getComponents() return components; } + @Override + public MessagePoll getPoll() + { + checkIntent(); + return poll; + } + @Nonnull @Override public Mentions getMentions() From dc7afa1de94f318e6a3227b6a31b693d68a2995c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 12:18:12 +0200 Subject: [PATCH 02/30] Add poll expire endpoint --- .../net/dv8tion/jda/api/entities/Message.java | 4 ++++ .../channel/middleman/MessageChannel.java | 20 +++++++++++++++++++ .../internal/entities/ReceivedMessage.java | 14 +++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 7599be8bd2..564e8cc9bb 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -685,6 +685,10 @@ default String getGuildId() @Nullable MessagePoll getPoll(); + @Nonnull + @CheckReturnValue + AuditableRestAction expirePoll(); + /** * Rows of interactive components such as {@link Button Buttons}. *
You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index c325f09390..35674153b2 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -972,6 +972,26 @@ default AuditableRestAction deleteMessageById(long messageId) return deleteMessageById(Long.toUnsignedString(messageId)); } + // FIXME Note: can't expire polls from other users + + @Nonnull + @CheckReturnValue + default AuditableRestAction expirePollById(@Nonnull String messageId) + { + Checks.isSnowflake(messageId, "Message ID"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_POLL.compile(getId(), messageId), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + return jda.getEntityBuilder().createMessageWithChannel(response.getObject(), MessageChannel.this, false); + }); + } + + @Nonnull + @CheckReturnValue + default AuditableRestAction expirePollById(long messageId) + { + return expirePollById(Long.toUnsignedString(messageId)); + } + /** * Creates a new {@link net.dv8tion.jda.api.entities.MessageHistory MessageHistory} object for each call of this method. *
MessageHistory is NOT an internal message cache, but rather it queries the Discord servers for previously sent messages. diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index fe08850777..c19afa1e63 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -623,6 +623,20 @@ public MessagePoll getPoll() return poll; } + @Nonnull + @Override + public AuditableRestAction expirePoll() + { + checkUser(); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_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() From ce4dfc9b1f1724e1e9ba5b0673e6a7c2355efcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 12:43:06 +0200 Subject: [PATCH 03/30] Add poll intents --- src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java | 3 +++ 1 file changed, 3 insertions(+) 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..38c796ea79 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,9 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), + GUILD_MESSAGE_POLLS(24), + DIRECT_MESSAGE_POLLS(25), + ; /** From 99d760ffefc244b5942e8a64b67893bf87f6fc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 12:43:20 +0200 Subject: [PATCH 04/30] Add poll request data --- .../utils/messages/MessageCreateBuilder.java | 20 +++++- .../api/utils/messages/MessageCreateData.java | 12 +++- .../utils/messages/MessageCreateRequest.java | 7 ++ .../utils/messages/MessagePollCreateData.java | 69 +++++++++++++++++++ .../message/MessageCreateBuilderMixin.java | 16 +++++ 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java 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..697fd2e47e 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 MessagePollCreateData poll; private boolean tts; public MessageCreateBuilder() {} @@ -191,6 +192,21 @@ public List getAttachments() return Collections.unmodifiableList(files); } + @Nullable + @Override + public MessagePollCreateData getPoll() + { + return poll; + } + + @Nonnull + @Override + public MessageCreateBuilder setPoll(@Nullable MessagePollCreateData poll) + { + this.poll = poll; + return this; + } + @Nonnull @Override public MessageCreateBuilder addFiles(@Nonnull Collection files) @@ -222,7 +238,7 @@ public MessageCreateBuilder setSuppressedNotifications(boolean suppressed) @Override public boolean isEmpty() { - return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty(); + return Helpers.isBlank(content) && embeds.isEmpty() && files.isEmpty() && components.isEmpty() && poll == null; } @Override @@ -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..f74fbc6cd0 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 MessagePollCreateData 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, MessagePollCreateData 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,12 @@ public List getAttachments() return getFiles(); } + @Nullable + public MessagePollCreateData getPoll() + { + return poll; + } + @Override public boolean isSuppressEmbeds() { @@ -316,6 +325,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..528929ad53 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,12 @@ default R addFiles(@Nonnull FileUpload... files) @Override List getAttachments(); + @Nullable + MessagePollCreateData getPoll(); + + @Nonnull + R setPoll(@Nullable MessagePollCreateData poll); + /** * Whether the message should use Text-to-Speech (TTS). * diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java new file mode 100644 index 0000000000..17065db72e --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java @@ -0,0 +1,69 @@ +/* + * 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 java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class MessagePollCreateData 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 MessagePollCreateData(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; + } + + @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/utils/message/MessageCreateBuilderMixin.java b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java index af8f154de3..d4ab9b55ae 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.MessagePollCreateData; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -62,6 +63,21 @@ default R addFiles(@Nonnull Collection files) return (R) this; } + @Nullable + @Override + default MessagePollCreateData getPoll() + { + return getBuilder().getPoll(); + } + + @Nonnull + @Override + default R setPoll(@Nullable MessagePollCreateData poll) + { + getBuilder().setPoll(poll); + return (R) this; + } + @Nonnull @Override default R setTTS(boolean tts) From 0a6cc8ab7cace7d4863ca143c0b5abc21a71f9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 12:54:26 +0200 Subject: [PATCH 05/30] Fix temporary issues --- .../java/net/dv8tion/jda/api/requests/GatewayIntent.java | 5 +++-- .../dv8tion/jda/api/utils/messages/MessageCreateBuilder.java | 4 ++-- .../net/dv8tion/jda/internal/entities/EntityBuilder.java | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) 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 38c796ea79..45469bd390 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java +++ b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java @@ -194,8 +194,9 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), - GUILD_MESSAGE_POLLS(24), - DIRECT_MESSAGE_POLLS(25), + // FIXME: Add these once they work +// GUILD_MESSAGE_POLLS(24), +// DIRECT_MESSAGE_POLLS(25), ; 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 697fd2e47e..0a0f22e8d7 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 @@ -259,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) 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 eb3f16cfab..65ccf3fa07 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1911,7 +1911,9 @@ public static MessagePollImpl createMessagePoll(DataObject data) DataArray answersData = data.getArray("answers"); DataObject questionData = data.getObject("question"); - DataObject resultsData = data.getObject("results"); + 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"); From 451dcdd807abd91d9ca9dcbd756769a6c6b7eed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 13:35:28 +0200 Subject: [PATCH 06/30] Add poll builder --- .../api/entities/messages/MessagePoll.java | 5 + .../utils/messages/MessageCreateBuilder.java | 6 +- .../api/utils/messages/MessageCreateData.java | 6 +- .../utils/messages/MessageCreateRequest.java | 4 +- .../utils/messages/MessagePollBuilder.java | 133 ++++++++++++++++++ ...llCreateData.java => MessagePollData.java} | 11 +- .../message/MessageCreateBuilderMixin.java | 6 +- 7 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java rename src/main/java/net/dv8tion/jda/api/utils/messages/{MessagePollCreateData.java => MessagePollData.java} (84%) diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java index 0a533feb99..c49f09cae0 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -26,6 +26,11 @@ public interface MessagePoll { + int MAX_QUESTION_TEXT_LENGTH = 300; + int MAX_ANSWER_TEXT_LENGTH = 55; + int MAX_ANSWERS = 10; + long MAX_DURATION_HOURS = 7 * 24; + @Nonnull LayoutType getLayout(); 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 0a0f22e8d7..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,7 +59,7 @@ public class MessageCreateBuilder extends AbstractMessageBuilder implements MessageCreateRequest { private final List files = new ArrayList<>(10); - private MessagePollCreateData poll; + private MessagePollData poll; private boolean tts; public MessageCreateBuilder() {} @@ -194,14 +194,14 @@ public List getAttachments() @Nullable @Override - public MessagePollCreateData getPoll() + public MessagePollData getPoll() { return poll; } @Nonnull @Override - public MessageCreateBuilder setPoll(@Nullable MessagePollCreateData poll) + public MessageCreateBuilder setPoll(@Nullable MessagePollData poll) { this.poll = poll; return this; 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 f74fbc6cd0..c9a928dfda 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 @@ -45,14 +45,14 @@ public class MessageCreateData implements MessageData, AutoCloseable, Serializab private final List files; private final List components; private final AllowedMentionsData mentions; - private final MessagePollCreateData poll; + private final MessagePollData poll; private final boolean tts; private final int flags; protected MessageCreateData( String content, List embeds, List files, List components, - AllowedMentionsData mentions, MessagePollCreateData poll, boolean tts, int flags) + AllowedMentionsData mentions, MessagePollData poll, boolean tts, int flags) { this.content = content; this.embeds = Collections.unmodifiableList(embeds); @@ -241,7 +241,7 @@ public List getAttachments() } @Nullable - public MessagePollCreateData getPoll() + public MessagePollData getPoll() { return poll; } 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 528929ad53..0da5736d20 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 @@ -299,10 +299,10 @@ default R addFiles(@Nonnull FileUpload... files) List getAttachments(); @Nullable - MessagePollCreateData getPoll(); + MessagePollData getPoll(); @Nonnull - R setPoll(@Nullable MessagePollCreateData poll); + 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..aa021d22cd --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -0,0 +1,133 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class MessagePollBuilder +{ + private MessagePoll.LayoutType layout = MessagePoll.LayoutType.DEFAULT; + private String title; + private Map answers = new LinkedHashMap<>(); + private Duration duration = Duration.ofHours(24); + private boolean isMultiAnswer; + + public MessagePollBuilder(@Nonnull String title) + { + this.setTitle(title); + } + + @Nonnull + public MessagePollBuilder setLayout(@Nonnull MessagePoll.LayoutType layout) + { + Checks.notNull(layout, "Layout"); + + this.layout = layout; + return this; + } + + @Nonnull + public MessagePollBuilder setTitle(@Nonnull String title) + { + Checks.notBlank(title, "Title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_QUESTION_TEXT_LENGTH, "Poll question title"); + + this.title = title; + return this; + } + + @Nonnull + public MessagePollBuilder setDuration(@Nonnull Duration duration) + { + Checks.notNull(duration, "Duration"); + Checks.positive(duration.toHours(), "Duration"); + Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Poll duration may not be longer than 7 days. Provided: %d hours", duration.toHours()); + + this.duration = duration; + return this; + } + + @Nonnull + public MessagePollBuilder setDuration(long duration, @Nonnull TimeUnit unit) + { + Checks.notNull(unit, "TimeUnit"); + this.duration = Duration.ofHours(unit.toHours(duration)); + return this; + } + + @Nonnull + public MessagePollBuilder setMultiAnswer(boolean multiAnswer) + { + isMultiAnswer = multiAnswer; + return this; + } + + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title) + { + return addAnswer(this.answers.size(), title, null); + } + + @Nonnull + public MessagePollBuilder addAnswer(@Nonnull String title, @Nullable Emoji emoji) + { + return addAnswer(this.answers.size(), title, emoji); + } + + @Nonnull + public MessagePollBuilder addAnswer(long id, @Nonnull String title) + { + return addAnswer(id, title, null); + } + + @Nonnull + public MessagePollBuilder addAnswer(long id, @Nonnull String title, @Nullable Emoji emoji) + { + Checks.notBlank(title, "Answer title"); + title = title.trim(); + Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Poll answer title"); + if (!this.answers.containsKey(id)) + Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); + + this.answers.put(id, new MessagePoll.Answer(id, title, (EmojiUnion) emoji, 0, false)); + return this; + } + + @Nonnull + public MessagePollData build() + { + return new MessagePollData( + layout, + new MessagePoll.Question(title, null), + new ArrayList<>(answers.values()), + duration, + isMultiAnswer + ); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java similarity index 84% rename from src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java rename to src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java index 17065db72e..b83994d11a 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollCreateData.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java @@ -22,11 +22,12 @@ 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; -public class MessagePollCreateData implements SerializableData +public class MessagePollData implements SerializableData { private final MessagePoll.LayoutType layout; private final MessagePoll.Question question; @@ -34,7 +35,7 @@ public class MessagePollCreateData implements SerializableData private final Duration duration; private final boolean isMultiAnswer; - public MessagePollCreateData(MessagePoll.LayoutType layout, MessagePoll.Question question, List answers, Duration duration, boolean isMultiAnswer) + public MessagePollData(MessagePoll.LayoutType layout, MessagePoll.Question question, List answers, Duration duration, boolean isMultiAnswer) { this.layout = layout; this.question = question; @@ -43,6 +44,12 @@ public MessagePollCreateData(MessagePoll.LayoutType layout, MessagePoll.Question this.isMultiAnswer = isMultiAnswer; } + @Nonnull + public static MessagePollBuilder builder(@Nonnull String title) + { + return new MessagePollBuilder(title); + } + @NotNull @Override public DataObject toData() 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 d4ab9b55ae..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,7 +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.MessagePollCreateData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -65,14 +65,14 @@ default R addFiles(@Nonnull Collection files) @Nullable @Override - default MessagePollCreateData getPoll() + default MessagePollData getPoll() { return getBuilder().getPoll(); } @Nonnull @Override - default R setPoll(@Nullable MessagePollCreateData poll) + default R setPoll(@Nullable MessagePollData poll) { getBuilder().setPoll(poll); return (R) this; From c51ae8b820b590b97ffa992664e4113607c7e28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 13:40:54 +0200 Subject: [PATCH 07/30] Adjust tests --- .../net/dv8tion/jda/test/restaction/MessageCreateActionTest.java | 1 + 1 file changed, 1 insertion(+) 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..2d7b09d518 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -54,6 +54,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) From 7ae834f1078cf4cfe1d6d6c258607c0a3c926cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 15:45:05 +0200 Subject: [PATCH 08/30] Add some documentation --- .../net/dv8tion/jda/api/entities/Message.java | 15 +++ .../channel/middleman/MessageChannel.java | 55 +++++++- .../api/entities/messages/MessagePoll.java | 126 ++++++++++++++++++ .../api/utils/messages/MessagePollData.java | 30 +++++ .../internal/entities/ReceivedMessage.java | 2 + 5 files changed, 226 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 564e8cc9bb..2daad552c9 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -682,9 +682,24 @@ default String getGuildId() @Nonnull List getComponents(); + /** + * The {@link MessagePoll} attached to this message. + * + * @return Possibly-null poll instance for this message + * + * @see #expirePoll() + */ @Nullable MessagePoll getPoll(); + /** + * Expire the poll attached to this message. + * + * @throws IllegalStateException + * If this poll was not sent by the currently logged in account or no poll was attached to this message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ @Nonnull @CheckReturnValue AuditableRestAction expirePoll(); diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index 35674153b2..bf0a61ea94 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -972,8 +972,34 @@ default AuditableRestAction deleteMessageById(long messageId) return deleteMessageById(Long.toUnsignedString(messageId)); } - // FIXME Note: can't expire polls from other users - + /** + * Expire the poll attached to this message. + * + *

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

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

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_ALREADY_EXPIRED POLL_ALREADY_EXPIRED} + *
    If the poll has already expired
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @throws IllegalArgumentException + * If the provided messageId is not a valid snowflake + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ @Nonnull @CheckReturnValue default AuditableRestAction expirePollById(@Nonnull String messageId) @@ -985,6 +1011,31 @@ default AuditableRestAction expirePollById(@Nonnull String messageId) }); } + /** + * Expire the poll attached to this message. + * + *

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

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

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_ALREADY_EXPIRED POLL_ALREADY_EXPIRED} + *
    If the poll has already expired
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} + *
    If the poll was sent by another user
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_EXPIRE_MISSING_POLL CANNOT_EXPIRE_MISSING_POLL} + *
    The message did not have a poll attached
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + *
    The message no longer exists
  • + *
+ * + * @param messageId + * The ID for the poll message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ @Nonnull @CheckReturnValue default AuditableRestAction expirePollById(long messageId) diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java index c49f09cae0..407770430f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -16,38 +16,102 @@ package net.dv8tion.jda.api.entities.messages; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.Duration; import java.time.OffsetDateTime; import java.util.List; +/** + * Poll sent with messages. + * + * @see Message#getPoll() + * @see Message#expirePoll() + */ public interface MessagePoll { + /** Maximum length of a {@link MessagePollBuilder#setTitle(String) poll question title} ({@value}) */ int MAX_QUESTION_TEXT_LENGTH = 300; + /** Maximum length of a {@link MessagePollBuilder#addAnswer(String)} poll answer title} ({@value}) */ int MAX_ANSWER_TEXT_LENGTH = 55; + /** Maximum amount of {@link MessagePollBuilder#addAnswer(String) poll answers} ({@value}) */ int MAX_ANSWERS = 10; + /** Maximum {@link MessagePollBuilder#setDuration(Duration) duration} of poll ({@value}) */ long MAX_DURATION_HOURS = 7 * 24; + /** + * The layout of the poll. + * + * @return The poll layout, or {@link LayoutType#UNKNOWN} if unknown + */ @Nonnull LayoutType getLayout(); + /** + * The poll question, representing the title. + * + * @return {@link Question} + */ @Nonnull Question getQuestion(); + /** + * The poll answers. + * + *

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

The author of the poll can always expire the poll manually, using {@link Message#expirePoll()}. + * + * @return {@link OffsetDateTime} representing the time when the poll expires automatically + */ @Nonnull 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; @@ -59,12 +123,24 @@ public Question(String text, Emoji emoji) 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() { @@ -72,6 +148,11 @@ public EmojiUnion getEmoji() } } + /** + * 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; @@ -89,34 +170,66 @@ public Answer(long id, String text, EmojiUnion emoji, int votes, boolean selfVot 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), @@ -129,11 +242,24 @@ enum LayoutType 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()) 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 index b83994d11a..4d71a28c2f 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java @@ -27,6 +27,23 @@ 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; @@ -44,6 +61,19 @@ public MessagePollData(MessagePoll.LayoutType layout, MessagePoll.Question quest 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) { diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index c19afa1e63..1bd00d4722 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -628,6 +628,8 @@ public MessagePoll getPoll() public AuditableRestAction expirePoll() { checkUser(); + if (poll == null) + throw new IllegalStateException("This message does not contain a poll"); return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_POLL.compile(getChannelId(), getId()), (response, request) -> { JDAImpl jda = (JDAImpl) getJDA(); EntityBuilder entityBuilder = jda.getEntityBuilder(); From 4c98757a0645db0b9849a08afd1cf98927391f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 17:04:22 +0200 Subject: [PATCH 09/30] More docs --- .../api/utils/messages/MessageCreateData.java | 5 + .../utils/messages/MessageCreateRequest.java | 15 ++ .../utils/messages/MessagePollBuilder.java | 138 ++++++++++++++++++ 3 files changed, 158 insertions(+) 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 c9a928dfda..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 @@ -240,6 +240,11 @@ public List getAttachments() return getFiles(); } + /** + * The poll to send with the message + * + * @return The poll, or null if no poll is sent + */ @Nullable public MessagePollData getPoll() { 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 0da5736d20..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 @@ -298,9 +298,24 @@ 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); 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 index aa021d22cd..e5e4b77aa4 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -29,6 +29,11 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +/** + * Builder for {@link MessagePollData} + * + * @see MessageCreateBuilder#setPoll(MessagePollData) + */ public class MessagePollBuilder { private MessagePoll.LayoutType layout = MessagePoll.LayoutType.DEFAULT; @@ -37,20 +42,52 @@ public class MessagePollBuilder 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) { @@ -62,6 +99,20 @@ public MessagePollBuilder setTitle(@Nonnull String 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) { @@ -73,6 +124,22 @@ public MessagePollBuilder setDuration(@Nonnull 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) { @@ -81,6 +148,15 @@ public MessagePollBuilder setDuration(long duration, @Nonnull TimeUnit unit) return this; } + /** + * 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) { @@ -88,24 +164,76 @@ public MessagePollBuilder setMultiAnswer(boolean 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(this.answers.size(), 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) { return addAnswer(this.answers.size(), title, emoji); } + /** + * Add an answer to this poll. + * + * @param id + * The unique identifier for this answer + * @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(long id, @Nonnull String title) { return addAnswer(id, title, null); } + /** + * Add an answer to this poll. + * + * @param id + * The unique identifier for this answer + * @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(long id, @Nonnull String title, @Nullable Emoji emoji) { @@ -119,9 +247,19 @@ public MessagePollBuilder addAnswer(long id, @Nonnull String title, @Nullable Em 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), From 452cd9e58553a09c83ccd8df2b886160a9414513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 17:07:05 +0200 Subject: [PATCH 10/30] Update error message --- .../internal/requests/restaction/MessageCreateActionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()) From cdf047cd814690ad0995dc74ce970c69496a9e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 17:16:40 +0200 Subject: [PATCH 11/30] Add send shortcuts for polls --- .../net/dv8tion/jda/api/entities/Message.java | 43 ++++++++++++++++ .../jda/api/entities/WebhookClient.java | 39 +++++++++++++++ .../channel/middleman/MessageChannel.java | 47 ++++++++++++++++++ .../callbacks/IReplyCallback.java | 49 +++++++++++++++++++ .../entities/AbstractWebhookClient.java | 9 ++++ .../mixin/middleman/MessageChannelMixin.java | 29 +++++++++++ 6 files changed, 216 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 2daad552c9..4679a4624b 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -56,6 +56,7 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.ReceivedMessage; @@ -1382,6 +1383,48 @@ default MessageCreateAction reply(@Nonnull MessageCreateData msg) return getChannel().sendMessage(msg).setMessageReference(this); } + /** + * Shortcut for {@code getChannel().sendMessagePoll(data).setMessageReference(this)}. + * + *

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

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    if this channel was deleted
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_SEND_TO_USER CANNOT_SEND_TO_USER} + *
    If this is a {@link PrivateChannel} and the currently logged in account + * does not share any Guilds with the recipient User
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The poll to send + * + * @throws InsufficientPermissionException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * @throws IllegalArgumentException + * If {@link MessageChannel#sendMessage(MessageCreateData)} throws + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction replyPoll(@Nonnull MessagePollData poll) + { + return getChannel().sendMessagePoll(poll).setMessageReference(this); + } + /** * Shortcut for {@code getChannel().sendMessageEmbeds(embed, other).setMessageReference(this)}. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java index 2956d91d39..5a81a73cd9 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java +++ b/src/main/java/net/dv8tion/jda/api/entities/WebhookClient.java @@ -32,6 +32,8 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.IncomingWebhookClientImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -128,6 +130,43 @@ public interface WebhookClient extends ISnowflake @CheckReturnValue WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData message); + /** + * Send a message poll to this webhook. + * + *

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

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

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_WEBHOOK UNKNOWN_WEBHOOK} + *
    The webhook is no longer available, either it was deleted or in case of interactions it expired.
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The {@link MessagePollData} to send + * + * @throws IllegalArgumentException + * If null is provided + * + * @return {@link net.dv8tion.jda.api.requests.restaction.WebhookMessageCreateAction} + * + * @see MessagePollBuilder + */ + @Nonnull + @CheckReturnValue + WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll); + /** * Send a message to this webhook. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index bf0a61ea94..84b1aedf07 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -42,6 +42,7 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.RestActionImpl; @@ -644,6 +645,52 @@ default MessageCreateAction sendMessageComponents(@Nonnull CollectionPossible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *
    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    if this channel was deleted
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#CANNOT_SEND_TO_USER CANNOT_SEND_TO_USER} + *
    If this is a {@link PrivateChannel} and the currently logged in account + * does not share any Guilds with the recipient User
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_AUTOMOD MESSAGE_BLOCKED_BY_AUTOMOD} + *
    If this message was blocked by an {@link net.dv8tion.jda.api.entities.automod.AutoModRule AutoModRule}
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER MESSAGE_BLOCKED_BY_HARMFUL_LINK_FILTER} + *
    If this message was blocked by the harmful link filter
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_INVALID_CHANNEL_TYPE POLL_INVALID_CHANNEL_TYPE} + *
    This channel does not allow polls
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_WITH_UNUSABLE_EMOJI POLL_WITH_UNUSABLE_EMOJI} + *
    This poll uses an external emoji that the bot is not allowed to use
  • + *
+ * + * @param poll + * The poll to send + * + * @throws UnsupportedOperationException + * If this is a {@link PrivateChannel} and the recipient is a bot + * @throws IllegalArgumentException + * If the poll is null + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If this is a {@link GuildMessageChannel} and this account does not have + * {@link net.dv8tion.jda.api.Permission#VIEW_CHANNEL Permission.VIEW_CHANNEL} or {@link net.dv8tion.jda.api.Permission#MESSAGE_SEND Permission.MESSAGE_SEND} + * + * @return {@link MessageCreateAction} + */ + @Nonnull + @CheckReturnValue + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Poll"); + return new MessageCreateActionImpl(this).setPoll(poll); + } + /** * Send a message to this channel. * 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/internal/entities/AbstractWebhookClient.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java index 1e636eb5e0..386e43c337 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java @@ -29,6 +29,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageDeleteActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; @@ -102,6 +103,14 @@ public WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData mess return sendRequest().applyData(message); } + @Nonnull + @Override + public WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return sendRequest().setPoll(poll); + } + @Nonnull @Override public WebhookMessageCreateAction sendFiles(@Nonnull Collection files) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java index 8f33c5990d..e2992ae842 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java @@ -40,7 +40,9 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.RestActionImpl; +import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -155,6 +157,33 @@ default MessageCreateAction sendMessageEmbeds(@Nonnull Collection components) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessageComponents(components); + } + + @Nonnull + @Override + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessagePoll(poll); + } + @Nonnull @CheckReturnValue default MessageCreateAction sendMessage(@Nonnull MessageCreateData msg) From a9f85eec3837c15d8e1977211bff7d31d9dc70bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 17:20:00 +0200 Subject: [PATCH 12/30] Use correct setter for duration overload --- .../net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index e5e4b77aa4..a35518a97a 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -144,8 +144,7 @@ public MessagePollBuilder setDuration(@Nonnull Duration duration) public MessagePollBuilder setDuration(long duration, @Nonnull TimeUnit unit) { Checks.notNull(unit, "TimeUnit"); - this.duration = Duration.ofHours(unit.toHours(duration)); - return this; + return setDuration(Duration.ofHours(unit.toHours(duration))); } /** From a81987cc54b031dcf4349f9420d682570de2da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 17:21:56 +0200 Subject: [PATCH 13/30] Update test case --- .../dv8tion/jda/test/restaction/MessageCreateActionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2d7b09d518..52bd929b65 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -74,7 +74,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 From 6c7241c83562e92676ce8047097c7980c1be22d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 18:26:05 +0200 Subject: [PATCH 14/30] Add new test case --- .../restaction/MessageCreateActionTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 52bd929b65..0bb90f796f 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,6 +30,7 @@ import org.mockito.Mock; import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; import static net.dv8tion.jda.api.requests.Method.POST; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -107,6 +110,51 @@ 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(DataObject.empty() + .put("answer_id", 0) + .put("poll_media", DataObject.empty() + .put("text", "Test answer 1") + .put("emoji", null))) + .add(DataObject.empty() + .put("answer_id", 1) + .put("poll_media", DataObject.empty() + .put("text", "Test answer 2") + .put("emoji", DataObject.empty() + .put("name", "🤔")))) + .add(DataObject.empty() + .put("answer_id", 2) + .put("poll_media", DataObject.empty() + .put("text", "Test answer 3") + .put("emoji", DataObject.empty() + .put("name", "minn") + .put("id", 821355005788684298L) + .put("animated", true))))))) + .whenQueueCalled(); + } + @Nonnull protected DataObject normalizeRequestBody(@Nonnull DataObject body) { From cc2aac55fc1f8b7a49254b46132fcb7776695cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 18:58:25 +0200 Subject: [PATCH 15/30] Add poll checks tests --- .../utils/messages/MessagePollBuilder.java | 4 +- .../net/dv8tion/jda/test/ChecksHelper.java | 53 ++++++++++++ .../net/dv8tion/jda/test/IntegrationTest.java | 1 + .../net/dv8tion/jda/test/TestHelpers.java | 28 +++++++ .../checks/AbstractChecksAssertions.java | 43 ++++++++++ .../checks/StringChecksAssertions.java | 65 +++++++++++++++ .../restaction}/RestActionAssertions.java | 2 +- .../entities/MessageSerializationTest.java | 1 - .../entities/message/MessagePollDataTest.java | 83 +++++++++++++++++++ 9 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/test/ChecksHelper.java create mode 100644 src/test/java/net/dv8tion/jda/test/TestHelpers.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java rename src/test/java/net/dv8tion/jda/test/{ => assertions/restaction}/RestActionAssertions.java (99%) create mode 100644 src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java 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 index a35518a97a..8724f96d3a 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -93,7 +93,7 @@ public MessagePollBuilder setTitle(@Nonnull String title) { Checks.notBlank(title, "Title"); title = title.trim(); - Checks.notLonger(title, MessagePoll.MAX_QUESTION_TEXT_LENGTH, "Poll question title"); + Checks.notLonger(title, MessagePoll.MAX_QUESTION_TEXT_LENGTH, "Title"); this.title = title; return this; @@ -238,7 +238,7 @@ public MessagePollBuilder addAnswer(long id, @Nonnull String title, @Nullable Em { Checks.notBlank(title, "Answer title"); title = title.trim(); - Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Poll answer title"); + Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Answer title"); if (!this.answers.containsKey(id)) Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); 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..ef35643051 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java @@ -0,0 +1,53 @@ +/* + * 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.StringChecksAssertions; +import org.junit.jupiter.api.function.ThrowingConsumer; + +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 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 notPositiveError(String name) + { + return name + " may not be negative or zero"; + } + + public static StringChecksAssertions assertStringChecks(String name, ThrowingConsumer callable) + { + return new StringChecksAssertions(name, callable); + } +} 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/TestHelpers.java b/src/test/java/net/dv8tion/jda/test/TestHelpers.java new file mode 100644 index 0000000000..f0bc3fb100 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/TestHelpers.java @@ -0,0 +1,28 @@ +/* + * 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; + +public class TestHelpers +{ + public static String repeat(String str, int count) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) + sb.append(str); + return sb.toString(); + } +} 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..c914bb954d --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.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.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; + } + + @SuppressWarnings("unchecked") + public S checksNotNull() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(null)) + .withMessage(isNullError(name)); + return (S) this; + } +} 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..01253410c1 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -0,0 +1,65 @@ +/* + * 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.*; +import static net.dv8tion.jda.test.TestHelpers.repeat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class StringChecksAssertions extends AbstractChecksAssertions +{ + public StringChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public StringChecksAssertions checksNotEmpty() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(null)) + .withMessage(isNullError(name)); + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept("")) + .withMessage(isEmptyError(name)); + return this; + } + + public StringChecksAssertions checksNotBlank() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(null)) + .withMessage(isNullError(name)); + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept("")) + .withMessage(isBlankError(name)); + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(" ")) + .withMessage(isBlankError(name)); + return this; + } + + public StringChecksAssertions checksNotLonger(int maxLength) + { + String invalidInput = repeat("s", maxLength + 1); + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(invalidInput)) + .withMessage(tooLongError(name, maxLength, invalidInput)); + return this; + } +} 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/message/MessagePollDataTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java new file mode 100644 index 0000000000..668b2ce648 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.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.entities.message; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +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) + .checksNotBlank() + .checksNotLonger(300); + + MessagePollBuilder builder = new MessagePollBuilder("test title"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setLayout(null)) + .withMessage(isNullError("Layout")); + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setLayout(MessagePoll.LayoutType.UNKNOWN)) + .withMessage("Layout cannot be UNKNOWN"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(null)) + .withMessage(isNullError("Duration")); + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(Duration.ZERO)) + .withMessage(notPositiveError("Duration")); + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(Duration.ofHours(500))) + .withMessage("Poll duration may not be longer than 7 days. Provided: 500 hours"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(10, null)) + .withMessage(isNullError("TimeUnit")); + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(-1, TimeUnit.HOURS)) + .withMessage(notPositiveError("Duration")); + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.setDuration(10, TimeUnit.DAYS)) + .withMessage("Poll duration may not be longer than 7 days. Provided: 240 hours"); + + assertStringChecks("Answer title", builder::addAnswer) + .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"); + } +} From 1af13658759671622e0668d8bd650ac3e76a33b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 19:24:00 +0200 Subject: [PATCH 16/30] Adjust max duration error --- .../dv8tion/jda/api/utils/messages/MessagePollBuilder.java | 2 +- .../jda/test/entities/message/MessagePollDataTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 8724f96d3a..6c4432d227 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -118,7 +118,7 @@ public MessagePollBuilder setDuration(@Nonnull Duration duration) { Checks.notNull(duration, "Duration"); Checks.positive(duration.toHours(), "Duration"); - Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Poll duration may not be longer than 7 days. Provided: %d hours", duration.toHours()); + Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Poll duration may not be longer than 168 hours (= 7 days). Provided: %d hours", duration.toHours()); this.duration = duration; return this; 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 index 668b2ce648..a662bbfd58 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -53,7 +53,7 @@ void testInvalidInputs() .withMessage(notPositiveError("Duration")); assertThatIllegalArgumentException() .isThrownBy(() -> builder.setDuration(Duration.ofHours(500))) - .withMessage("Poll duration may not be longer than 7 days. Provided: 500 hours"); + .withMessage("Poll duration may not be longer than 168 hours (= 7 days). Provided: 500 hours"); assertThatIllegalArgumentException() .isThrownBy(() -> builder.setDuration(10, null)) @@ -62,8 +62,8 @@ void testInvalidInputs() .isThrownBy(() -> builder.setDuration(-1, TimeUnit.HOURS)) .withMessage(notPositiveError("Duration")); assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(10, TimeUnit.DAYS)) - .withMessage("Poll duration may not be longer than 7 days. Provided: 240 hours"); + .isThrownBy(() -> builder.setDuration(8, TimeUnit.DAYS)) + .withMessage("Poll duration may not be longer than 168 hours (= 7 days). Provided: 192 hours"); assertStringChecks("Answer title", builder::addAnswer) .checksNotBlank() From 540c018dcd710fd1a327907c2fee629bb32d3028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 19:51:55 +0200 Subject: [PATCH 17/30] Refactor assertions --- .../utils/messages/MessagePollBuilder.java | 2 +- .../net/dv8tion/jda/test/ChecksHelper.java | 25 +++++++++++- .../checks/AbstractChecksAssertions.java | 11 ++++-- .../checks/DurationChecksAssertions.java | 38 ++++++++++++++++++ .../checks/EnumChecksAssertions.java | 33 ++++++++++++++++ .../checks/LongChecksAssertions.java | 36 +++++++++++++++++ .../checks/SimpleChecksAssertions.java | 27 +++++++++++++ .../checks/StringChecksAssertions.java | 25 +++--------- .../entities/message/MessagePollDataTest.java | 39 +++++++------------ 9 files changed, 188 insertions(+), 48 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/LongChecksAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java 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 index 6c4432d227..68a9774348 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -118,7 +118,7 @@ public MessagePollBuilder setDuration(@Nonnull Duration duration) { Checks.notNull(duration, "Duration"); Checks.positive(duration.toHours(), "Duration"); - Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Poll duration may not be longer than 168 hours (= 7 days). Provided: %d hours", duration.toHours()); + Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Duration may not be longer than 168 hours (= 7 days). Provided: %d hours", duration.toHours()); this.duration = duration; return this; diff --git a/src/test/java/net/dv8tion/jda/test/ChecksHelper.java b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java index ef35643051..c02edfa12d 100644 --- a/src/test/java/net/dv8tion/jda/test/ChecksHelper.java +++ b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java @@ -16,9 +16,12 @@ package net.dv8tion.jda.test; -import net.dv8tion.jda.test.assertions.checks.StringChecksAssertions; +import net.dv8tion.jda.test.assertions.checks.*; import org.junit.jupiter.api.function.ThrowingConsumer; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + public class ChecksHelper { public static String tooLongError(String name, int maxLength, String value) @@ -50,4 +53,24 @@ public static StringChecksAssertions assertStringChecks(String name, ThrowingCon { 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 SimpleChecksAssertions assertTimeUnitChecks(String name, ThrowingConsumer callable) + { + return new SimpleChecksAssertions<>(name, callable); + } + + public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) + { + return new LongChecksAssertions(name, callable); + } } 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 index c914bb954d..f8f964a7c4 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java @@ -32,12 +32,17 @@ public AbstractChecksAssertions(String name, ThrowingConsumer callable) this.callable = callable; } - @SuppressWarnings("unchecked") public S checksNotNull() + { + return throwsFor(null, isNullError(name)); + } + + @SuppressWarnings("unchecked") + public S throwsFor(T input, String expectedError) { assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept(null)) - .withMessage(isNullError(name)); + .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..27c4a4bb27 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -0,0 +1,38 @@ +/* + * 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 static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class DurationChecksAssertions extends AbstractChecksAssertions +{ + public DurationChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public DurationChecksAssertions checksPositive() + { + throwsFor(Duration.ofSeconds(-1), notPositiveError(name)); + throwsFor(Duration.ZERO, notPositiveError(name)); + 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 index 01253410c1..33adedeef3 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -20,7 +20,6 @@ import static net.dv8tion.jda.test.ChecksHelper.*; import static net.dv8tion.jda.test.TestHelpers.repeat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; public class StringChecksAssertions extends AbstractChecksAssertions { @@ -31,35 +30,23 @@ public StringChecksAssertions(String name, ThrowingConsumer callable) public StringChecksAssertions checksNotEmpty() { - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept(null)) - .withMessage(isNullError(name)); - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept("")) - .withMessage(isEmptyError(name)); + throwsFor(null, isNullError(name)); + throwsFor("", isEmptyError(name)); return this; } public StringChecksAssertions checksNotBlank() { - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept(null)) - .withMessage(isNullError(name)); - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept("")) - .withMessage(isBlankError(name)); - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept(" ")) - .withMessage(isBlankError(name)); + throwsFor(null, isNullError(name)); + throwsFor("", isBlankError(name)); + throwsFor(" ", isBlankError(name)); return this; } public StringChecksAssertions checksNotLonger(int maxLength) { String invalidInput = repeat("s", maxLength + 1); - assertThatIllegalArgumentException() - .isThrownBy(() -> callable.accept(invalidInput)) - .withMessage(tooLongError(name, maxLength, invalidInput)); + throwsFor(invalidInput, tooLongError(name, maxLength, invalidInput)); return this; } } 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 index a662bbfd58..743661d299 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -33,39 +33,30 @@ public class MessagePollDataTest void testInvalidInputs() { assertStringChecks("Title", MessagePollBuilder::new) + .checksNotNull() .checksNotBlank() .checksNotLonger(300); MessagePollBuilder builder = new MessagePollBuilder("test title"); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setLayout(null)) - .withMessage(isNullError("Layout")); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setLayout(MessagePoll.LayoutType.UNKNOWN)) - .withMessage("Layout cannot be UNKNOWN"); + assertEnumChecks("Layout", builder::setLayout) + .checksNotNull() + .checkIsNot(MessagePoll.LayoutType.UNKNOWN); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(null)) - .withMessage(isNullError("Duration")); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(Duration.ZERO)) - .withMessage(notPositiveError("Duration")); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(Duration.ofHours(500))) - .withMessage("Poll duration may not be longer than 168 hours (= 7 days). Provided: 500 hours"); + assertDurationChecks("Duration", builder::setDuration) + .checksNotNull() + .checksPositive() + .throwsFor(Duration.ofHours(500), "Duration may not be longer than 168 hours (= 7 days). Provided: 500 hours"); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(10, null)) - .withMessage(isNullError("TimeUnit")); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(-1, TimeUnit.HOURS)) - .withMessage(notPositiveError("Duration")); - assertThatIllegalArgumentException() - .isThrownBy(() -> builder.setDuration(8, TimeUnit.DAYS)) - .withMessage("Poll duration may not be longer than 168 hours (= 7 days). Provided: 192 hours"); + assertTimeUnitChecks("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"); assertStringChecks("Answer title", builder::addAnswer) + .checksNotNull() .checksNotBlank() .checksNotLonger(55); From b1b86b957c4f03bf832eaa435dc6a04da593eca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 20:37:47 +0200 Subject: [PATCH 18/30] Refactor checks assertions --- .../commands/build/SubcommandData.java | 5 +- .../commands/build/SubcommandGroupData.java | 8 +-- .../jda/internal/entities/GuildImpl.java | 3 +- .../interactions/CommandDataImpl.java | 3 +- .../net/dv8tion/jda/test/ChecksHelper.java | 30 +++++++-- .../checks/DurationChecksAssertions.java | 7 ++ .../checks/StringChecksAssertions.java | 23 +++++++ .../jda/test/entities/guild/BulkBanTest.java | 12 ++-- .../entities/message/MessagePollDataTest.java | 3 +- .../test/interactions/CommandDataTest.java | 67 ++++++++++--------- 10 files changed, 105 insertions(+), 56 deletions(-) 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/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 assertTimeUnitChecks(String name, ThrowingConsumer callable) + public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) { - return new SimpleChecksAssertions<>(name, callable); + return new LongChecksAssertions(name, callable); } - public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) + public static SimpleChecksAssertions assertChecks(String name, ThrowingConsumer callable) { - return new LongChecksAssertions(name, callable); + return new SimpleChecksAssertions<>(name, callable); } } 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 index 27c4a4bb27..4f94b4216d 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -20,6 +20,7 @@ import java.time.Duration; +import static net.dv8tion.jda.test.ChecksHelper.isNegativeError; import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; public class DurationChecksAssertions extends AbstractChecksAssertions @@ -29,6 +30,12 @@ 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)); 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 index 33adedeef3..29ba09db98 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.function.ThrowingConsumer; +import java.util.regex.Pattern; + import static net.dv8tion.jda.test.ChecksHelper.*; import static net.dv8tion.jda.test.TestHelpers.repeat; @@ -49,4 +51,25 @@ public StringChecksAssertions checksNotLonger(int maxLength) 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 = repeat("s", maxLength + 1); + String tooShort = 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/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 index 743661d299..9ff9f31472 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -18,6 +18,7 @@ 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; @@ -48,7 +49,7 @@ void testInvalidInputs() .checksPositive() .throwsFor(Duration.ofHours(500), "Duration may not be longer than 168 hours (= 7 days). Provided: 500 hours"); - assertTimeUnitChecks("TimeUnit", (unit) -> builder.setDuration(1, unit)) + ChecksHelper.assertChecks("TimeUnit", (unit) -> builder.setDuration(1, unit)) .checksNotNull(); assertLongChecks("Duration", (duration) -> builder.setDuration(duration, TimeUnit.SECONDS)) 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<>(); From 827de6cf7ac3f25a341864e4adc622730c9968c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 21:00:07 +0200 Subject: [PATCH 19/30] Refactor duration checks --- .../utils/messages/MessagePollBuilder.java | 2 +- .../dv8tion/jda/internal/utils/Checks.java | 14 +++++++++++ .../dv8tion/jda/internal/utils/Helpers.java | 24 +++++++++++++++++++ .../checks/DurationChecksAssertions.java | 12 ++++++++++ .../entities/message/MessagePollDataTest.java | 4 ++-- 5 files changed, 53 insertions(+), 3 deletions(-) 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 index 68a9774348..228b2b94e7 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -118,7 +118,7 @@ public MessagePollBuilder setDuration(@Nonnull Duration duration) { Checks.notNull(duration, "Duration"); Checks.positive(duration.toHours(), "Duration"); - Checks.check(duration.toHours() <= MessagePoll.MAX_DURATION_HOURS, "Duration may not be longer than 168 hours (= 7 days). Provided: %d hours", duration.toHours()); + Checks.notLonger(duration, Duration.ofHours(MessagePoll.MAX_DURATION_HOURS), TimeUnit.HOURS, "Duration"); this.duration = duration; return this; diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java index 5e913595b8..edd1fe10fd 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java @@ -28,7 +28,9 @@ import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.Contract; +import java.time.Duration; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -212,6 +214,18 @@ public static void notNegative(final long n, final String name) throw new IllegalArgumentException(name + " may not be negative"); } + public static void notLonger(final Duration duration, final Duration maxDuration, final TimeUnit resolutionUnit, final String name) + { + notNull(duration, name); + check( + duration.compareTo(maxDuration) <= 0, + "%s may not be longer than %s. Provided: %s", + name, + JDALogger.getLazyString(() -> Helpers.durationToString(maxDuration, resolutionUnit)), + JDALogger.getLazyString(() -> Helpers.durationToString(duration, resolutionUnit)) + ); + } + // Unique streams checks public static void checkUnique(Stream stream, String format, BiFunction getArgs) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index b8a7051213..703f7d1e84 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -26,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.ToLongFunction; import java.util.stream.Collector; @@ -316,4 +317,27 @@ public static boolean hasCause(Throwable throwable, Class c { return Collector.of(DataArray::empty, DataArray::add, DataArray::addAll); } + + public static String durationToString(Duration duration, TimeUnit resolutionUnit) + { + long actual = resolutionUnit.convert(duration.getSeconds(), TimeUnit.SECONDS); + String raw = actual + " " + resolutionUnit.toString().toLowerCase(Locale.ROOT); + + long days = duration.toDays(); + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() - TimeUnit.DAYS.toSeconds(days) - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); + + StringJoiner joiner = new StringJoiner(" "); + if (days > 0) + joiner.add(days + " days"); + if (hours > 0) + joiner.add(hours + " hours"); + if (minutes > 0) + joiner.add(minutes + " minutes"); + if (seconds > 0) + joiner.add(seconds + " seconds"); + + return raw + " (" + joiner + ")"; + } } diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java index 4f94b4216d..ba50121459 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -19,7 +19,10 @@ 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; @@ -42,4 +45,13 @@ public DurationChecksAssertions checksPositive() 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/entities/message/MessagePollDataTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java index 9ff9f31472..6cd1f67b5b 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -47,14 +47,14 @@ void testInvalidInputs() assertDurationChecks("Duration", builder::setDuration) .checksNotNull() .checksPositive() - .throwsFor(Duration.ofHours(500), "Duration may not be longer than 168 hours (= 7 days). Provided: 500 hours"); + .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"); + .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() From b336ec2bfb978c9c0e00525cd66d5e82748daa97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 22:26:21 +0200 Subject: [PATCH 20/30] Add poll voters pagination --- .../net/dv8tion/jda/api/entities/Message.java | 17 ++++ .../channel/middleman/MessageChannel.java | 39 +++++++++ .../PollVotersPaginationAction.java | 31 +++++++ .../PollVotersPaginationActionImpl.java | 85 +++++++++++++++++++ .../message/PollVotersPaginationTest.java | 59 +++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/PollVotersPaginationAction.java create mode 100644 src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java create mode 100644 src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 4679a4624b..5bd0b770d0 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -49,6 +49,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.AttachmentProxy; @@ -61,6 +62,7 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.requests.FunctionalCallback; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.IOUtil; @@ -705,6 +707,21 @@ default String getGuildId() @CheckReturnValue AuditableRestAction expirePoll(); + /** + * Paginate the users who voted for a poll answer. + * + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVoters(long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getChannelId(), getId(), answerId); + } + /** * Rows of interactive components such as {@link Button Buttons}. *
You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index 84b1aedf07..074a671ad8 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -35,6 +35,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.pagination.MessagePaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.FileUpload; @@ -50,6 +51,7 @@ import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.MessageEditActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.MessagePaginationActionImpl; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.ReactionPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -1090,6 +1092,43 @@ default AuditableRestAction expirePollById(long messageId) return expirePollById(Long.toUnsignedString(messageId)); } + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @throws IllegalArgumentException + * If the message id is not a valid snowflake + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(@Nonnull String messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), messageId, answerId); + } + + /** + * Paginate the users who voted for a poll answer. + * + * @param messageId + * The message id for the poll + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVotersById(long messageId, long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getId(), Long.toUnsignedString(messageId), answerId); + } + /** * Creates a new {@link net.dv8tion.jda.api.entities.MessageHistory MessageHistory} object for each call of this method. *
MessageHistory is NOT an internal message cache, but rather it queries the Discord servers for previously sent messages. diff --git a/src/main/java/net/dv8tion/jda/api/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/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/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."); + } +} From f6724ebace27a3fa114e3877aa5ed3ad36bb5947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 22:30:00 +0200 Subject: [PATCH 21/30] Make answer id read-only --- .../utils/messages/MessagePollBuilder.java | 54 +++---------------- .../restaction/MessageCreateActionTest.java | 6 +-- 2 files changed, 9 insertions(+), 51 deletions(-) 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 index 228b2b94e7..14b4d3641c 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java @@ -25,8 +25,7 @@ import javax.annotation.Nullable; import java.time.Duration; import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -36,9 +35,9 @@ */ public class MessagePollBuilder { + private final List answers = new ArrayList<>(MessagePoll.MAX_ANSWERS); private MessagePoll.LayoutType layout = MessagePoll.LayoutType.DEFAULT; private String title; - private Map answers = new LinkedHashMap<>(); private Duration duration = Duration.ofHours(24); private boolean isMultiAnswer; @@ -177,7 +176,7 @@ public MessagePollBuilder setMultiAnswer(boolean multiAnswer) @Nonnull public MessagePollBuilder addAnswer(@Nonnull String title) { - return addAnswer(this.answers.size(), title, null); + return addAnswer(title, null); } /** @@ -195,54 +194,13 @@ public MessagePollBuilder addAnswer(@Nonnull String title) */ @Nonnull public MessagePollBuilder addAnswer(@Nonnull String title, @Nullable Emoji emoji) - { - return addAnswer(this.answers.size(), title, emoji); - } - - /** - * Add an answer to this poll. - * - * @param id - * The unique identifier for this answer - * @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(long id, @Nonnull String title) - { - return addAnswer(id, title, null); - } - - /** - * Add an answer to this poll. - * - * @param id - * The unique identifier for this answer - * @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(long id, @Nonnull String title, @Nullable Emoji emoji) { Checks.notBlank(title, "Answer title"); title = title.trim(); Checks.notLonger(title, MessagePoll.MAX_ANSWER_TEXT_LENGTH, "Answer title"); - if (!this.answers.containsKey(id)) - Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); + Checks.check(this.answers.size() < MessagePoll.MAX_ANSWERS, "Poll cannot have more than %d answers", MessagePoll.MAX_ANSWERS); - this.answers.put(id, new MessagePoll.Answer(id, title, (EmojiUnion) emoji, 0, false)); + this.answers.add(new MessagePoll.Answer(this.answers.size() + 1, title, (EmojiUnion) emoji, 0, false)); return this; } @@ -262,7 +220,7 @@ public MessagePollData build() return new MessagePollData( layout, new MessagePoll.Question(title, null), - new ArrayList<>(answers.values()), + new ArrayList<>(answers), duration, isMultiAnswer ); 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 0bb90f796f..5a65ee2773 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -134,18 +134,18 @@ void testPollOnly() .put("text", "Test poll")) .put("answers", DataArray.empty() .add(DataObject.empty() - .put("answer_id", 0) + .put("answer_id", 1) .put("poll_media", DataObject.empty() .put("text", "Test answer 1") .put("emoji", null))) .add(DataObject.empty() - .put("answer_id", 1) + .put("answer_id", 2) .put("poll_media", DataObject.empty() .put("text", "Test answer 2") .put("emoji", DataObject.empty() .put("name", "🤔")))) .add(DataObject.empty() - .put("answer_id", 2) + .put("answer_id", 3) .put("poll_media", DataObject.empty() .put("text", "Test answer 3") .put("emoji", DataObject.empty() From 1732d7485bb755b73ca351e88b5319bbaddc0d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Apr 2024 22:46:21 +0200 Subject: [PATCH 22/30] Cleanup MessageCreateActionTest#testPollOnly --- .../restaction/MessageCreateActionTest.java | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) 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 5a65ee2773..8b9ee1c397 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -33,6 +33,8 @@ 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; @@ -133,25 +135,9 @@ void testPollOnly() .put("question", DataObject.empty() .put("text", "Test poll")) .put("answers", DataArray.empty() - .add(DataObject.empty() - .put("answer_id", 1) - .put("poll_media", DataObject.empty() - .put("text", "Test answer 1") - .put("emoji", null))) - .add(DataObject.empty() - .put("answer_id", 2) - .put("poll_media", DataObject.empty() - .put("text", "Test answer 2") - .put("emoji", DataObject.empty() - .put("name", "🤔")))) - .add(DataObject.empty() - .put("answer_id", 3) - .put("poll_media", DataObject.empty() - .put("text", "Test answer 3") - .put("emoji", DataObject.empty() - .put("name", "minn") - .put("id", 821355005788684298L) - .put("animated", true))))))) + .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(); } @@ -160,4 +146,29 @@ 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); + } + } } From a737645ca5eac2f7c3a58d5ab3e5dda5f82f7d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 14 Apr 2024 14:04:02 +0200 Subject: [PATCH 23/30] Add commons lang3 as test dependency --- build.gradle.kts | 1 + .../net/dv8tion/jda/test/TestHelpers.java | 28 ------------------- .../checks/StringChecksAssertions.java | 8 +++--- 3 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 src/test/java/net/dv8tion/jda/test/TestHelpers.java diff --git a/build.gradle.kts b/build.gradle.kts index f7b22d2bbd..e28658df62 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -136,6 +136,7 @@ dependencies { testImplementation("org.reflections:reflections:0.10.2") testImplementation("org.mockito:mockito-core:5.8.0") testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.apache.commons:commons-lang3:3.14.0") } val compileJava: JavaCompile by tasks diff --git a/src/test/java/net/dv8tion/jda/test/TestHelpers.java b/src/test/java/net/dv8tion/jda/test/TestHelpers.java deleted file mode 100644 index f0bc3fb100..0000000000 --- a/src/test/java/net/dv8tion/jda/test/TestHelpers.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; - -public class TestHelpers -{ - public static String repeat(String str, int count) - { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < count; i++) - sb.append(str); - return sb.toString(); - } -} 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 index 29ba09db98..91e89207fd 100644 --- a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -16,12 +16,12 @@ 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.*; -import static net.dv8tion.jda.test.TestHelpers.repeat; public class StringChecksAssertions extends AbstractChecksAssertions { @@ -47,7 +47,7 @@ public StringChecksAssertions checksNotBlank() public StringChecksAssertions checksNotLonger(int maxLength) { - String invalidInput = repeat("s", maxLength + 1); + String invalidInput = StringUtils.repeat("s", maxLength + 1); throwsFor(invalidInput, tooLongError(name, maxLength, invalidInput)); return this; } @@ -60,8 +60,8 @@ public StringChecksAssertions checksLowercaseOnly() public StringChecksAssertions checksRange(int minLength, int maxLength) { - String tooLong = repeat("s", maxLength + 1); - String tooShort = repeat("s", minLength - 1); + 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; From 24ffde17320add11137797a85a2fc1fd82b141ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Mon, 15 Apr 2024 17:26:45 +0200 Subject: [PATCH 24/30] Add docs for intents --- .../java/net/dv8tion/jda/api/requests/GatewayIntent.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 45469bd390..cb7e207372 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java +++ b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java @@ -194,8 +194,15 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), - // FIXME: Add these once they work +// FIXME: Add these once they work +// /** +// * 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), ; From 5012349c7e77428c7a7b6dc80bfdb59a8b13f91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Tue, 16 Apr 2024 00:13:53 +0200 Subject: [PATCH 25/30] Apply api changes --- .../java/net/dv8tion/jda/api/entities/Message.java | 6 +++--- .../api/entities/channel/middleman/MessageChannel.java | 10 +++++----- .../dv8tion/jda/api/entities/messages/MessagePoll.java | 8 ++++---- .../dv8tion/jda/internal/entities/EntityBuilder.java | 2 +- .../dv8tion/jda/internal/entities/ReceivedMessage.java | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index 5bd0b770d0..15c4d4c07a 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -690,13 +690,13 @@ default String getGuildId() * * @return Possibly-null poll instance for this message * - * @see #expirePoll() + * @see #endPoll() */ @Nullable MessagePoll getPoll(); /** - * Expire the poll attached to this message. + * End the poll attached to this message. * * @throws IllegalStateException * If this poll was not sent by the currently logged in account or no poll was attached to this message @@ -705,7 +705,7 @@ default String getGuildId() */ @Nonnull @CheckReturnValue - AuditableRestAction expirePoll(); + AuditableRestAction endPoll(); /** * Paginate the users who voted for a poll answer. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index 074a671ad8..77ff3ef446 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -1022,7 +1022,7 @@ default AuditableRestAction deleteMessageById(long messageId) } /** - * Expire the poll attached to this message. + * End the poll attached to this message. * *

A bot cannot expire the polls of other users. * @@ -1051,7 +1051,7 @@ default AuditableRestAction deleteMessageById(long messageId) */ @Nonnull @CheckReturnValue - default AuditableRestAction expirePollById(@Nonnull String messageId) + default AuditableRestAction endPollById(@Nonnull String messageId) { Checks.isSnowflake(messageId, "Message ID"); return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_POLL.compile(getId(), messageId), (response, request) -> { @@ -1061,7 +1061,7 @@ default AuditableRestAction expirePollById(@Nonnull String messageId) } /** - * Expire the poll attached to this message. + * End the poll attached to this message. * *

A bot cannot expire the polls of other users. * @@ -1087,9 +1087,9 @@ default AuditableRestAction expirePollById(@Nonnull String messageId) */ @Nonnull @CheckReturnValue - default AuditableRestAction expirePollById(long messageId) + default AuditableRestAction endPollById(long messageId) { - return expirePollById(Long.toUnsignedString(messageId)); + return endPollById(Long.toUnsignedString(messageId)); } /** diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java index 407770430f..f7bcef204d 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePoll.java @@ -31,7 +31,7 @@ * Poll sent with messages. * * @see Message#getPoll() - * @see Message#expirePoll() + * @see Message#endPoll() */ public interface MessagePoll { @@ -75,11 +75,11 @@ public interface MessagePoll /** * The time when this poll will automatically expire. * - *

The author of the poll can always expire the poll manually, using {@link Message#expirePoll()}. + *

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 + * @return {@link OffsetDateTime} representing the time when the poll expires automatically, or null if it never expires */ - @Nonnull + @Nullable OffsetDateTime getTimeExpiresAt(); /** 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 65ccf3fa07..3c69ee7ecb 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1905,7 +1905,7 @@ private static MessageActivity createMessageActivity(DataObject jsonObject) public static MessagePollImpl createMessagePoll(DataObject data) { MessagePoll.LayoutType layout = MessagePoll.LayoutType.fromKey(data.getInt("layout_type")); - OffsetDateTime expiresAt = data.getOffsetDateTime("expiry"); + OffsetDateTime expiresAt = data.isNull("expiry") ? null : data.getOffsetDateTime("expiry"); boolean isMultiAnswer = data.getBoolean("allow_multiselect"); DataArray answersData = data.getArray("answers"); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index 1bd00d4722..73e0610297 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -625,7 +625,7 @@ public MessagePoll getPoll() @Nonnull @Override - public AuditableRestAction expirePoll() + public AuditableRestAction endPoll() { checkUser(); if (poll == null) From e757b7430cdfb8012c458f7b05654852e33ca743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Thu, 18 Apr 2024 20:17:49 +0200 Subject: [PATCH 26/30] Add new permission and uncomment intents --- .../java/net/dv8tion/jda/api/Permission.java | 1 + .../jda/api/requests/GatewayIntent.java | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 22e99f903d..cd0f45eb2f 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -68,6 +68,7 @@ public enum Permission USE_APPLICATION_COMMANDS( 31, true, true, "Use Application Commands"), MESSAGE_EXT_STICKER( 37, true, true, "Use External Stickers"), MESSAGE_ATTACH_VOICE_MESSAGE(46, true, true, "Send Voice Messages"), + MESSAGE_SEND_POLLS( 49, true, true, "Create Polls"), // Thread Permissions MANAGE_THREADS( 34, true, true, "Manage Threads"), diff --git a/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java b/src/main/java/net/dv8tion/jda/api/requests/GatewayIntent.java index cb7e207372..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,16 +194,15 @@ public enum GatewayIntent */ AUTO_MODERATION_EXECUTION(21), -// FIXME: Add these once they work -// /** -// * 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), + /** + * 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), ; From c7e7f9e622721b4ed6387ad3fffd0a75909a8ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 20 Apr 2024 13:39:10 +0200 Subject: [PATCH 27/30] Add new events --- .../poll/GenericMessagePollVoteEvent.java | 137 ++++++++++++++++++ .../message/poll/MessagePollVoteAddEvent.java | 43 ++++++ .../poll/MessagePollVoteRemoveEvent.java | 43 ++++++ .../jda/api/hooks/ListenerAdapter.java | 6 + .../handle/MessagePollVoteHandler.java | 84 +++++++++++ .../internal/requests/WebSocketClient.java | 2 + 6 files changed, 315 insertions(+) create mode 100644 src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteAddEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/message/poll/MessagePollVoteRemoveEvent.java create mode 100644 src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java 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/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/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)); From 6bfcaaadfa7da58b64597180daf5483e535ad160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 20 Apr 2024 14:05:45 +0200 Subject: [PATCH 28/30] Add event handler test --- .../java/net/dv8tion/jda/test/Constants.java | 1 + .../events/EventFiredAssertions.java | 61 ++++++++++++++ .../events/AbstractSocketHandlerTest.java | 60 ++++++++++++++ .../test/events/MessagePollHandlerTests.java | 83 +++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java 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/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/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))); + }); + } +} From dfe5af97f83b50780227cd6005a0dacfd17f15ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 20 Apr 2024 14:16:34 +0200 Subject: [PATCH 29/30] Rename route to END_POLL --- .../jda/api/entities/channel/middleman/MessageChannel.java | 2 +- src/main/java/net/dv8tion/jda/api/requests/Route.java | 2 +- .../java/net/dv8tion/jda/internal/entities/ReceivedMessage.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index 77ff3ef446..6774bc222f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -1054,7 +1054,7 @@ default AuditableRestAction deleteMessageById(long messageId) default AuditableRestAction endPollById(@Nonnull String messageId) { Checks.isSnowflake(messageId, "Message ID"); - return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_POLL.compile(getId(), messageId), (response, request) -> { + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getId(), messageId), (response, request) -> { JDAImpl jda = (JDAImpl) getJDA(); return jda.getEntityBuilder().createMessageWithChannel(response.getObject(), MessageChannel.this, false); }); 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 7de5db51f5..6c1adb4904 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Route.java @@ -263,7 +263,7 @@ 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 EXPIRE_POLL = new Route(POST, "channels/{channel_id}/polls/{message_id}/expire"); + 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}"); } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index 73e0610297..da9816ec95 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -630,7 +630,7 @@ public AuditableRestAction endPoll() checkUser(); if (poll == null) throw new IllegalStateException("This message does not contain a poll"); - return new AuditableRestActionImpl<>(getJDA(), Route.Messages.EXPIRE_POLL.compile(getChannelId(), getId()), (response, request) -> { + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getChannelId(), getId()), (response, request) -> { JDAImpl jda = (JDAImpl) getJDA(); EntityBuilder entityBuilder = jda.getEntityBuilder(); if (hasChannel()) From 04c23366b9c3b7afb839a8bfac948288bebe4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:17:13 +0200 Subject: [PATCH 30/30] Update ErrorResponse enum --- .../jda/api/entities/channel/middleman/MessageChannel.java | 6 ------ .../java/net/dv8tion/jda/api/requests/ErrorResponse.java | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java index 6774bc222f..4d4fc7314f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/middleman/MessageChannel.java @@ -1028,9 +1028,6 @@ default AuditableRestAction deleteMessageById(long messageId) * *

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

    - *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_ALREADY_EXPIRED POLL_ALREADY_EXPIRED} - *
    If the poll has already expired
  • - * *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} *
    If the poll was sent by another user
  • * @@ -1067,9 +1064,6 @@ default AuditableRestAction endPollById(@Nonnull String messageId) * *

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

      - *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#POLL_ALREADY_EXPIRED POLL_ALREADY_EXPIRED} - *
      If the poll has already expired
    • - * *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#INVALID_AUTHOR_EDIT INVALID_AUTHOR_EDIT} *
      If the poll was sent by another user
    • * 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 b944c24f14..49de2c3448 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -191,8 +191,7 @@ public enum ErrorResponse 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( 520004, "Cannot expire a non-poll message"), - POLL_ALREADY_EXPIRED( 520007, "Poll is already expired"), + CANNOT_EXPIRE_MISSING_POLL( 520006, "Cannot expire a non-poll message"), SERVER_ERROR( 0, "Discord encountered an internal server error! Not good!");