From 833563d1188c148178d2ad542ad50f709a0b5d71 Mon Sep 17 00:00:00 2001 From: Duncan Sterken Date: Sat, 17 Feb 2024 11:10:51 +0000 Subject: [PATCH 01/25] Add missing generic type to shardmanager (#2612) --- src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java index d14eb62dd67..0f1bce911af 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java @@ -60,7 +60,7 @@ * @since 3.4 * @author Aljoscha Grebe */ -public interface ShardManager extends IGuildChannelContainer +public interface ShardManager extends IGuildChannelContainer { /** * Adds all provided listeners to the event-listeners that will be used to handle events. @@ -969,7 +969,7 @@ default List getUsers() /** * Restarts all shards, shutting old ones down first. - * + * *

As all shards need to connect to discord again this will take equally long as the startup of a new ShardManager * (using the 5000ms + backoff as delay between starting new JDA instances). * From 621fb7bf897293b2ce71c26cdca1bcf4cf17765e Mon Sep 17 00:00:00 2001 From: shaksternano <54268387+shaksternano@users.noreply.github.com> Date: Sat, 17 Feb 2024 11:16:16 +0000 Subject: [PATCH 02/25] Add missing proxy url field to the MessageEmbed.VideoInfo class (#2618) --- .../jda/api/entities/MessageEmbed.java | 31 ++++++++++++++++++- .../jda/internal/entities/EntityBuilder.java | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageEmbed.java b/src/main/java/net/dv8tion/jda/api/entities/MessageEmbed.java index d6771470591..13efabaf0be 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageEmbed.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageEmbed.java @@ -18,6 +18,7 @@ import net.dv8tion.jda.annotations.ForRemoval; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.utils.AttachmentProxy; +import net.dv8tion.jda.api.utils.FileProxy; import net.dv8tion.jda.api.utils.ImageProxy; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; @@ -655,12 +656,14 @@ public boolean equals(Object obj) public static class VideoInfo { protected final String url; + protected final String proxyUrl; protected final int width; protected final int height; - public VideoInfo(String url, int width, int height) + public VideoInfo(String url, String proxyUrl, int width, int height) { this.url = url; + this.proxyUrl = proxyUrl; this.width = width; this.height = height; } @@ -676,6 +679,32 @@ public String getUrl() return url; } + /** + * The url of the video, proxied by Discord + *
This url is used to access the video through Discord instead of directly to prevent ip scraping. + * + * @return Possibly-null String containing the proxied video url. + */ + @Nullable + public String getProxyUrl() + { + return proxyUrl; + } + + /** + * Returns a {@link FileProxy} for this embed video. + * + * @return Possibly-null {@link FileProxy} of this embed video + * + * @see #getProxyUrl() + */ + @Nullable + public FileProxy getProxy() + { + final String proxyUrl = getProxyUrl(); + return proxyUrl == null ? null : new FileProxy(proxyUrl); + } + /** * The width of the video. *
This usually isn't the actual video width, but instead the starting embed window size. 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 6b30eab45b2..00440cd5349 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1991,6 +1991,7 @@ public MessageEmbed createMessageEmbed(DataObject content) { DataObject obj = content.getObject("video"); video = new VideoInfo(obj.getString("url", null), + obj.getString("proxy_url", null), obj.getInt("width", -1), obj.getInt("height", -1)); } From ae90da36a0c095d3c3db4dcaa48497a82af4351c Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:14:29 +0100 Subject: [PATCH 03/25] Fix suppressing embeds on messages with webhooks (#2620) --- .../java/net/dv8tion/jda/internal/entities/ReceivedMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d5dab6962c..69a4f8ef3d7 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -808,7 +808,7 @@ public AuditableRestAction suppressEmbeds(boolean suppressed) Route.CompiledRoute route; if (isWebhookRequest()) { - route = Route.Webhooks.EXECUTE_WEBHOOK_DELETE.compile(webhook.getId(), webhook.getToken(), getId()); + route = Route.Webhooks.EXECUTE_WEBHOOK_EDIT.compile(webhook.getId(), webhook.getToken(), getId()); } else { From 184e8bfadb72cfc40b8ec4642c30740d3b6f2738 Mon Sep 17 00:00:00 2001 From: Ivan Rizkyanto Date: Sun, 3 Mar 2024 04:30:21 +0700 Subject: [PATCH 04/25] Add DiscordLocale values for these locales: Indonesian and Latin America (Spanish LATAM) (#2627) --- .../java/net/dv8tion/jda/api/interactions/DiscordLocale.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/interactions/DiscordLocale.java b/src/main/java/net/dv8tion/jda/api/interactions/DiscordLocale.java index e03e7afa1d8..1c3426a83e3 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/DiscordLocale.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/DiscordLocale.java @@ -43,6 +43,7 @@ public enum DiscordLocale GREEK ("el", "Greek", "Ελληνικά"), HINDI ("hi", "Hindi", "हिन्दी"), HUNGARIAN ("hu", "Hungarian", "Magyar"), + INDONESIAN ("id", "Indonesian", "Bahasa Indonesia"), ITALIAN ("it", "Italian", "Italiano"), JAPANESE ("ja", "Japanese", "日本語"), KOREAN ("ko", "Korean", "한국어"), @@ -53,6 +54,7 @@ public enum DiscordLocale ROMANIAN_ROMANIA ("ro", "Romanian, Romania", "Română"), RUSSIAN ("ru", "Russian", "Pусский"), SPANISH ("es-ES", "Spanish", "Español"), + SPANISH_LATAM ("es-419", "Spanish, LATAM", "Español, LATAM"), SWEDISH ("sv-SE", "Swedish", "Svenska"), THAI ("th", "Thai", "ไทย"), TURKISH ("tr", "Turkish", "Türkçe"), From f8bdadbf85f56bc54b7e60c34f60e80b771f74a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 10 Mar 2024 18:31:36 +0100 Subject: [PATCH 05/25] Add support for enforce_nonce (#2614) --- .../api/requests/restaction/MessageCreateAction.java | 11 ++++++----- .../requests/restaction/MessageCreateActionImpl.java | 7 ++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageCreateAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageCreateAction.java index b15e92444ed..b60895186df 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageCreateAction.java +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/MessageCreateAction.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.entities.sticker.GuildSticker; import net.dv8tion.jda.api.entities.sticker.Sticker; import net.dv8tion.jda.api.entities.sticker.StickerSnowflake; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.requests.FluentRestAction; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageCreateRequest; @@ -54,14 +55,14 @@ static void setDefaultFailOnInvalidReply(boolean fail) } /** - * Unique string/number used to identify messages using {@link Message#getNonce()} in events. - *
This can be useful to handle round-trip messages. + * Unique string/number used to identify messages using {@link Message#getNonce()} in {@link MessageReceivedEvent}. * - *

Discord also uses the nonce to dedupe messages for users, but this is not currently supported for bots. - * However, for future proofing, it is highly recommended to use a unique nonce for each message. + *

The nonce can be used for deduping messages and marking them for use with {@link MessageReceivedEvent}. + * JDA will automatically generate a unique nonce per message, it is not necessary to do this manually. * * @param nonce - * The nonce string to use + * The nonce string to use, must be unique per message. + * A unique nonce will be generated automatically if this is null. * * @throws IllegalArgumentException * If the provided nonce is longer than {@value Message#MAX_NONCE_LENGTH} characters 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 395366df567..6f80cd8167c 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 @@ -35,6 +35,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -43,6 +44,7 @@ public class MessageCreateActionImpl extends RestActionImpl implements MessageCreateAction, MessageCreateBuilderMixin { + protected static final SecureRandom nonceGenerator = new SecureRandom(); protected static boolean defaultFailOnInvalidReply = false; private final MessageChannel channel; @@ -83,8 +85,11 @@ protected RequestBody finalizeData() try (MessageCreateData data = builder.build()) { DataObject json = data.toData(); - if (nonce != null) + json.put("enforce_nonce", true); + if (nonce != null && !nonce.isEmpty()) json.put("nonce", nonce); + else + json.put("nonce", Long.toUnsignedString(nonceGenerator.nextLong())); if (stickers != null && !stickers.isEmpty()) json.put("sticker_ids", stickers); if (messageReferenceId != null) From 711e4e758b2d53ff85961f9f428a3b169e0f34a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 10 Mar 2024 18:37:55 +0100 Subject: [PATCH 06/25] Bump version to 5.0.0-beta.21 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f7745dfc224..863d28d0980 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,7 @@ plugins { } val javaVersion = JavaVersion.current() -val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.20") +val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.21") val isCI = System.getProperty("BUILD_NUMBER") != null // jenkins || System.getenv("BUILD_NUMBER") != null || System.getProperty("GIT_COMMIT") != null // jitpack From 33ecfe97695dfdb8ef773f469f8822833c81568c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 30 Mar 2024 19:54:04 +0100 Subject: [PATCH 07/25] Add support for bulk banning users (#2630) --- .../jda/api/entities/BulkBanResponse.java | 62 +++++++++++++ .../net/dv8tion/jda/api/entities/Guild.java | 88 ++++++++++++++++++- .../jda/api/requests/ErrorResponse.java | 1 + .../net/dv8tion/jda/api/requests/Route.java | 1 + .../jda/internal/entities/GuildImpl.java | 40 +++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/entities/BulkBanResponse.java diff --git a/src/main/java/net/dv8tion/jda/api/entities/BulkBanResponse.java b/src/main/java/net/dv8tion/jda/api/entities/BulkBanResponse.java new file mode 100644 index 00000000000..d165bace9bc --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/BulkBanResponse.java @@ -0,0 +1,62 @@ +/* + * 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; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +/** + * Response to {@link Guild#ban(java.util.Collection, Duration)} + * + *

This response includes a list of successfully banned users and users which could not be banned. + * Discord might fail to ban a user due to permission issues or an internal server error. + */ +public class BulkBanResponse +{ + private final List bannedUsers; + private final List failedUsers; + + public BulkBanResponse(@Nonnull List bannedUsers, @Nonnull List failedUsers) + { + this.bannedUsers = Collections.unmodifiableList(bannedUsers); + this.failedUsers = Collections.unmodifiableList(failedUsers); + } + + /** + * List of successfully banned users. + * + * @return {@link List} of {@link UserSnowflake} + */ + @Nonnull + public List getBannedUsers() + { + return bannedUsers; + } + + /** + * List of users which could not be banned. + * + * @return {@link List} of {@link UserSnowflake} + */ + @Nonnull + public List getFailedUsers() + { + return failedUsers; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index c409d0ab6ee..2881b502950 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -3629,8 +3629,8 @@ default AuditableRestAction kick(@Nonnull UserSnowflake user, @Nullable St *

  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} *
    The target Member cannot be banned due to a permission discrepancy
  • * - *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} - *
    The specified Member was removed from the Guild before finishing the task
  • + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_USER UNKNOWN_USER} + *
    The user does not exist
  • * * * @param user @@ -3661,6 +3661,90 @@ default AuditableRestAction kick(@Nonnull UserSnowflake user, @Nullable St @CheckReturnValue AuditableRestAction ban(@Nonnull UserSnowflake user, int deletionTimeframe, @Nonnull TimeUnit unit); + /** + * Bans up to 200 of the provided users. + *
    To set a ban reason, use {@link AuditableRestAction#reason(String)}. + * + *

    The {@link BulkBanResponse} includes a list of {@link BulkBanResponse#getFailedUsers() failed users}, + * which is populated with users that could not be banned, for instance due to some internal server error or permission issues. + * This list of failed users also includes all users that were already banned. + * + *

    Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link RestAction RestAction} include the following: + *

      + *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
      The target Member cannot be banned due to a permission discrepancy
    • + * + *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#FAILED_TO_BAN_USERS FAILED_TO_BAN_USERS} + *
      None of the users could be banned
    • + *
    + * + * @param users + * The users to ban + * @param deletionTime + * Delete recent messages of the given timeframe (for instance the last hour with {@code Duration.ofHours(1)}) + * + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If any of the provided users is the guild owner or has a higher or equal role position + * @throws InsufficientPermissionException + * If the bot does not have {@link Permission#BAN_MEMBERS} or {@link Permission#MANAGE_SERVER} + * @throws IllegalArgumentException + *
      + *
    • If the users collection is null or contains null
    • + *
    • If the deletionTime is negative
    • + *
    + * + * @return {@link AuditableRestAction} - Type: {@link BulkBanResponse} + */ + @Nonnull + @CheckReturnValue + AuditableRestAction ban(@Nonnull Collection users, @Nullable Duration deletionTime); + + /** + * Bans up to 200 of the provided users. + *
    To set a ban reason, use {@link AuditableRestAction#reason(String)}. + * + *

    The {@link BulkBanResponse} includes a list of {@link BulkBanResponse#getFailedUsers() failed users}, + * which is populated with users that could not be banned, for instance due to some internal server error or permission issues. + * This list of failed users also includes all users that were already banned. + * + *

    Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link RestAction RestAction} include the following: + *

      + *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
      The target Member cannot be banned due to a permission discrepancy
    • + * + *
    • {@link net.dv8tion.jda.api.requests.ErrorResponse#FAILED_TO_BAN_USERS FAILED_TO_BAN_USERS} + *
      None of the users could be banned
    • + *
    + * + * @param users + * The users to ban + * @param deletionTimeframe + * The timeframe for the history of messages that will be deleted. (seconds precision) + * @param unit + * Timeframe unit as a {@link TimeUnit} (for example {@code ban(user, 7, TimeUnit.DAYS)}). + * + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If any of the provided users is the guild owner or has a higher or equal role position + * @throws InsufficientPermissionException + * If the bot does not have {@link Permission#BAN_MEMBERS} or {@link Permission#MANAGE_SERVER} + * @throws IllegalArgumentException + *
      + *
    • If null is provided
    • + *
    • If the deletionTimeframe is negative
    • + *
    + * + * @return {@link AuditableRestAction} - Type: {@link BulkBanResponse} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction ban(@Nonnull Collection users, int deletionTimeframe, @Nonnull TimeUnit unit) + { + Checks.notNull(unit, "TimeUnit"); + return ban(users, Duration.ofSeconds(unit.toSeconds(deletionTimeframe))); + } + /** * Unbans the specified {@link UserSnowflake} from this Guild. * 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 f59c3a5c65e..450f4f98447 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -184,6 +184,7 @@ public enum ErrorResponse MESSAGE_BLOCKED_BY_AUTOMOD( 200000, "Message was blocked by automatic moderation"), 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"), 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 f15462924ca..d4542550485 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Route.java @@ -103,6 +103,7 @@ public static class Guilds public static final Route GET_BAN = new Route(GET, "guilds/{guild_id}/bans/{user_id}"); public static final Route UNBAN = new Route(DELETE, "guilds/{guild_id}/bans/{user_id}"); public static final Route BAN = new Route(PUT, "guilds/{guild_id}/bans/{user_id}"); + public static final Route BULK_BAN = new Route(POST, "guilds/{guild_id}/bulk-ban"); public static final Route KICK_MEMBER = new Route(DELETE, "guilds/{guild_id}/members/{user_id}"); public static final Route MODIFY_MEMBER = new Route(PATCH, "guilds/{guild_id}/members/{user_id}"); public static final Route ADD_MEMBER = new Route(PUT, "guilds/{guild_id}/members/{user_id}"); 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 c13416bf144..e0dedbf78b3 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -82,10 +82,12 @@ 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; import javax.annotation.Nullable; +import java.time.Duration; import java.time.OffsetDateTime; import java.time.temporal.TemporalAccessor; import java.util.*; @@ -1545,6 +1547,44 @@ public AuditableRestAction ban(@Nonnull UserSnowflake user, int duration, return new AuditableRestActionImpl<>(getJDA(), route, params); } + @Nonnull + @Override + public AuditableRestAction ban(@Nonnull Collection users, @Nullable Duration deletionTime) + { + deletionTime = deletionTime == null ? Duration.ZERO : deletionTime; + Checks.noneNull(users, "Users"); + Checks.check(!deletionTime.isNegative(), "Deletion time cannot be negative"); + Checks.check(deletionTime.getSeconds() <= TimeUnit.DAYS.toSeconds(7), "Deletion timeframe must not be larger than 7 days. Provided: %d seconds", deletionTime.getSeconds()); + Checks.check(users.size() <= 200, "Cannot ban more than 200 users at once"); + checkPermission(Permission.BAN_MEMBERS); + checkPermission(Permission.MANAGE_SERVER); + + for (UserSnowflake user : users) + { + checkOwner(user.getIdLong(), "ban"); + checkPosition(user); + } + + Set userIds = users.stream().map(UserSnowflake::getIdLong).collect(Collectors.toSet()); + DataObject body = DataObject.empty() + .put("user_ids", userIds) + .put("delete_message_seconds", deletionTime.getSeconds()); + Route.CompiledRoute route = Route.Guilds.BULK_BAN.compile(getId()); + + return new AuditableRestActionImpl<>(getJDA(), route, body, (res, req) -> { + DataObject responseBody = res.getObject(); + List bannedUsers = responseBody.getArray("banned_users") + .stream(DataArray::getLong) + .map(UserSnowflake::fromId) + .collect(Collectors.toList()); + List failedUsers = responseBody.getArray("failed_users") + .stream(DataArray::getLong) + .map(UserSnowflake::fromId) + .collect(Collectors.toList()); + return new BulkBanResponse(bannedUsers, failedUsers); + }); + } + @Nonnull @Override public AuditableRestAction unban(@Nonnull UserSnowflake user) From 2b82a62075219d58f3d07134aba71738e964de55 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Mar 2024 19:55:44 +0100 Subject: [PATCH 08/25] Add AccountManager#setBanner (#2629) --- .../dv8tion/jda/api/managers/AccountManager.java | 15 +++++++++++++++ .../jda/internal/managers/AccountManagerImpl.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/managers/AccountManager.java b/src/main/java/net/dv8tion/jda/api/managers/AccountManager.java index 079f11ab6ce..6c51dd078ec 100644 --- a/src/main/java/net/dv8tion/jda/api/managers/AccountManager.java +++ b/src/main/java/net/dv8tion/jda/api/managers/AccountManager.java @@ -50,6 +50,8 @@ public interface AccountManager extends Manager long NAME = 1; /** Used to reset the avatar field */ long AVATAR = 1 << 1; + /** Used to reset the banner field */ + long BANNER = 1 << 2; /** * The {@link net.dv8tion.jda.api.entities.SelfUser SelfUser} that will be @@ -137,4 +139,17 @@ public interface AccountManager extends Manager @Nonnull @CheckReturnValue AccountManager setAvatar(@Nullable Icon avatar); + + /** + * Sets the banner for the currently logged in account + * + * @param banner + * An {@link net.dv8tion.jda.api.entities.Icon Icon} instance representing + * the new banner for the current account, {@code null} to reset the banner to the default banner. + * + * @return AccountManager for chaining convenience + */ + @Nonnull + @CheckReturnValue + AccountManager setBanner(@Nullable Icon banner); } diff --git a/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java index 6e62bc1df14..4da6e543d67 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java @@ -35,6 +35,7 @@ public class AccountManagerImpl extends ManagerBase implements A protected String name; protected Icon avatar; + protected Icon banner; /** * Creates a new AccountManager instance @@ -63,6 +64,8 @@ public AccountManagerImpl reset(long fields) super.reset(fields); if ((fields & AVATAR) == AVATAR) avatar = null; + if ((fields & BANNER) == BANNER) + banner = null; return this; } @@ -110,6 +113,16 @@ public AccountManagerImpl setAvatar(Icon avatar) return this; } + @Nonnull + @Override + @CheckReturnValue + public AccountManager setBanner(Icon banner) + { + this.banner = banner; + set |= BANNER; + return this; + } + @Override protected RequestBody finalizeData() { @@ -123,6 +136,8 @@ protected RequestBody finalizeData() body.put("username", name); if (shouldUpdate(AVATAR)) body.put("avatar", avatar == null ? null : avatar.getEncoding()); + if (shouldUpdate(BANNER)) + body.put("banner", banner == null ? null : banner.getEncoding()); reset(); return getRequestBody(body); From ce6e2e2bfdb884eef9ae2ec73284b344744e964c Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 30 Mar 2024 19:56:04 +0100 Subject: [PATCH 09/25] Fix format specifiers when adding invalid choices (#2628) --- .../interactions/AutoCompleteCallbackActionImpl.java | 8 ++++---- src/main/java/net/dv8tion/jda/internal/utils/Checks.java | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/AutoCompleteCallbackActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/AutoCompleteCallbackActionImpl.java index d7a4fd255f5..755cf65d8dc 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/AutoCompleteCallbackActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/AutoCompleteCallbackActionImpl.java @@ -69,10 +69,10 @@ public AutoCompleteCallbackAction addChoices(@Nonnull Collection "Choice of type %s cannot be converted to INTEGER", choice.getType()); long valueLong = choice.getAsLong(); Checks.check(valueLong <= OptionData.MAX_POSITIVE_NUMBER, - "Choice value cannot be larger than %d Provided: %d", + "Choice value cannot be larger than %f Provided: %d", OptionData.MAX_POSITIVE_NUMBER, valueLong); Checks.check(valueLong >= OptionData.MIN_NEGATIVE_NUMBER, - "Choice value cannot be smaller than %d. Provided: %d", + "Choice value cannot be smaller than %f. Provided: %d", OptionData.MIN_NEGATIVE_NUMBER, valueLong); break; case NUMBER: @@ -80,10 +80,10 @@ public AutoCompleteCallbackAction addChoices(@Nonnull Collection "Choice of type %s cannot be converted to NUMBER", choice.getType()); double valueDouble = choice.getAsDouble(); Checks.check(valueDouble <= OptionData.MAX_POSITIVE_NUMBER, - "Choice value cannot be larger than %d Provided: %d", + "Choice value cannot be larger than %f Provided: %f", OptionData.MAX_POSITIVE_NUMBER, valueDouble); Checks.check(valueDouble >= OptionData.MIN_NEGATIVE_NUMBER, - "Choice value cannot be smaller than %d. Provided: %d", + "Choice value cannot be smaller than %f. Provided: %f", OptionData.MIN_NEGATIVE_NUMBER, valueDouble); break; case STRING: 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 4ce2bee3d88..5e913595b8e 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.api.interactions.components.ActionComponent; import net.dv8tion.jda.api.interactions.components.Component; import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.Contract; import java.util.*; @@ -63,14 +64,14 @@ public static void check(final boolean expression, final String message) } @Contract("false, _, _ -> fail") - public static void check(final boolean expression, final String message, final Object... args) + public static void check(final boolean expression, @PrintFormat final String message, final Object... args) { if (!expression) throw new IllegalArgumentException(String.format(message, args)); } @Contract("false, _, _ -> fail") - public static void check(final boolean expression, final String message, final Object arg) + public static void check(final boolean expression, @PrintFormat final String message, final Object arg) { if (!expression) throw new IllegalArgumentException(String.format(message, arg)); From e7bafe96f93dd56b244620580ec1c43d6ff25623 Mon Sep 17 00:00:00 2001 From: Whizyyy <47752694+Whizyyy@users.noreply.github.com> Date: Sat, 30 Mar 2024 21:09:05 +0100 Subject: [PATCH 10/25] Fix FlatMapRestAction predicate with complete or submit (#2636) --- .../operator/FlatMapRestAction.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java index 11f49098066..84db460f730 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java @@ -21,6 +21,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; @@ -61,7 +62,13 @@ public void queue(@Nullable Consumer success, @Nullable Consumer submit(boolean shouldQueue) { return action.submit(shouldQueue) - .thenCompose((result) -> supply(result).submit(shouldQueue)); + .thenCompose((result) -> + { + if (condition != null && !condition.test(result)) + { + CompletableFuture future = new CompletableFuture<>(); + future.cancel(true); + + return future; + } + return supply(result).submit(shouldQueue); + }); } } From e2ef2dab0306168f4c9240061e20de8a56dfce20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Thu, 4 Apr 2024 22:20:33 +0200 Subject: [PATCH 11/25] Handle numeric keys for ETF maps (#2642) --- .../java/net/dv8tion/jda/api/utils/data/etf/ExTermDecoder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermDecoder.java b/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermDecoder.java index de0a7469ff8..4500b8cdb26 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermDecoder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermDecoder.java @@ -278,7 +278,8 @@ private static Map unpackMap0(ByteBuffer buffer) int arity = buffer.getInt(); while (arity-- > 0) { - String key = (String) unpack0(buffer); + Object rawKey = unpack0(buffer); + String key = String.valueOf(rawKey); Object value = unpack0(buffer); map.put(key, value); } From 13abdcb95b59994c758b09318401c497793793c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Thu, 4 Apr 2024 22:33:14 +0200 Subject: [PATCH 12/25] Expand test framework (#2638) * Use assertj typed assertions * Add getTestEmbed * Remove public visibility from tests * Expand JsonTest and fix bugs in DataArray * Add more tests for ser/de * Refactor LocalizationTest * Improve test package layout * Add basic RestAction tests --- build.gradle.kts | 1 + .../dv8tion/jda/api/utils/data/DataArray.java | 33 +- .../jda/api/utils/data/DataObject.java | 12 +- .../jda/api/utils/data/etf/ExTermEncoder.java | 5 +- src/test/java/CommandDataTest.java | 205 -------- src/test/java/DataPathTest.java | 101 ---- src/test/java/HelpersTest.java | 121 ----- src/test/java/JsonTest.java | 43 -- src/test/java/LocalizationTest.java | 140 ------ src/test/java/MarkdownTest.java | 468 ----------------- src/test/java/MarkdownUtilTest.java | 106 ---- .../entities/MessageSerializationTest.java | 80 --- .../jda/entitystring/EntityStringTest.java | 188 ------- .../net/dv8tion/jda/test/IntegrationTest.java | 82 +++ .../jda/test/PrettyRepresentation.java | 34 ++ .../ChannelConsistencyComplianceTest.java} | 36 +- .../EventConsistencyComplianceTest.java} | 16 +- .../dv8tion/jda/test/data/DataPathTest.java | 117 +++++ .../net/dv8tion/jda/test/data/JsonTest.java | 353 +++++++++++++ .../jda/{ => test}/entities/ActivityTest.java | 77 +-- .../entities/MessageSerializationTest.java | 106 ++++ .../channel/ChannelCacheViewTest.java | 62 ++- .../{ => test}/entitystring/ASnowflake.java | 2 +- .../jda/{ => test}/entitystring/AnEntity.java | 2 +- .../test/entitystring/EntityStringTest.java | 206 ++++++++ .../test/interactions/CommandDataTest.java | 238 +++++++++ .../test/interactions/LocalizationTest.java | 209 ++++++++ .../interactions/SelectMenuTests.java | 56 +-- .../restaction/MessageCreateActionTest.java | 113 +++++ .../jda/test/restaction/RestActionTest.java | 115 +++++ .../dv8tion/jda/test/util/HelpersTest.java | 124 +++++ .../dv8tion/jda/test/util/MarkdownTest.java | 471 ++++++++++++++++++ .../jda/test/util/MarkdownUtilTest.java | 108 ++++ 33 files changed, 2453 insertions(+), 1577 deletions(-) delete mode 100644 src/test/java/CommandDataTest.java delete mode 100644 src/test/java/DataPathTest.java delete mode 100644 src/test/java/HelpersTest.java delete mode 100644 src/test/java/JsonTest.java delete mode 100644 src/test/java/LocalizationTest.java delete mode 100644 src/test/java/MarkdownTest.java delete mode 100644 src/test/java/MarkdownUtilTest.java delete mode 100644 src/test/java/net/dv8tion/jda/entities/MessageSerializationTest.java delete mode 100644 src/test/java/net/dv8tion/jda/entitystring/EntityStringTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/IntegrationTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/PrettyRepresentation.java rename src/test/java/net/dv8tion/jda/{ChannelConsistencyTest.java => test/compliance/ChannelConsistencyComplianceTest.java} (72%) rename src/test/java/net/dv8tion/jda/{EventConsistencyTest.java => test/compliance/EventConsistencyComplianceTest.java} (80%) create mode 100644 src/test/java/net/dv8tion/jda/test/data/DataPathTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/data/JsonTest.java rename src/test/java/net/dv8tion/jda/{ => test}/entities/ActivityTest.java (69%) create mode 100644 src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java rename src/test/java/net/dv8tion/jda/{ => test}/entities/channel/ChannelCacheViewTest.java (83%) rename src/test/java/net/dv8tion/jda/{ => test}/entitystring/ASnowflake.java (95%) rename src/test/java/net/dv8tion/jda/{ => test}/entitystring/AnEntity.java (94%) create mode 100644 src/test/java/net/dv8tion/jda/test/entitystring/EntityStringTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/interactions/LocalizationTest.java rename src/test/java/net/dv8tion/jda/{ => test}/interactions/SelectMenuTests.java (56%) create mode 100644 src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/restaction/RestActionTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/util/HelpersTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/util/MarkdownTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/util/MarkdownUtilTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 863d28d0980..cab68f5e665 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") testImplementation("org.reflections:reflections:0.10.2") testImplementation("org.mockito:mockito-core:5.8.0") + testImplementation("org.assertj:assertj-core:3.25.3") } val compileJava: JavaCompile by tasks diff --git a/src/main/java/net/dv8tion/jda/api/utils/data/DataArray.java b/src/main/java/net/dv8tion/jda/api/utils/data/DataArray.java index 6c2bb1a0adc..bd8ab473e52 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/data/DataArray.java +++ b/src/main/java/net/dv8tion/jda/api/utils/data/DataArray.java @@ -17,9 +17,9 @@ package net.dv8tion.jda.api.utils.data; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.type.CollectionType; import net.dv8tion.jda.api.exceptions.ParsingException; @@ -218,7 +218,7 @@ public static DataArray fromETF(@Nonnull byte[] data) */ public boolean isNull(int index) { - return data.get(index) == null; + return index >= length() || data.get(index) == null; } /** @@ -782,12 +782,12 @@ public String toString() @Nonnull public String toPrettyString() { - DefaultPrettyPrinter.Indenter indent = new DefaultIndenter(" ", DefaultIndenter.SYS_LF); - DefaultPrettyPrinter printer = new DefaultPrettyPrinter(); - printer.withObjectIndenter(indent).withArrayIndenter(indent); try { - return mapper.writer(printer).writeValueAsString(data); + return mapper.writer(new DefaultPrettyPrinter()) + .with(SerializationFeature.INDENT_OUTPUT) + .with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .writeValueAsString(data); } catch (JsonProcessingException e) { @@ -820,7 +820,9 @@ private T get(@Nonnull Class type, int index) @Nullable private T get(@Nonnull Class type, int index, @Nullable Function stringMapper, @Nullable Function numberMapper) { - Object value = data.get(index); + if (index < 0) + throw new IndexOutOfBoundsException("Index out of range: " + index); + Object value = index < data.size() ? data.get(index) : null; if (value == null) return null; if (type.isInstance(value)) @@ -857,4 +859,21 @@ public DataArray toDataArray() { return this; } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (!(o instanceof DataArray)) + return false; + DataArray objects = (DataArray) o; + return Objects.equals(data, objects.data); + } + + @Override + public int hashCode() + { + return Objects.hash(data); + } } diff --git a/src/main/java/net/dv8tion/jda/api/utils/data/DataObject.java b/src/main/java/net/dv8tion/jda/api/utils/data/DataObject.java index fed87b68d9b..a935ee64b40 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/data/DataObject.java +++ b/src/main/java/net/dv8tion/jda/api/utils/data/DataObject.java @@ -16,10 +16,14 @@ package net.dv8tion.jda.api.utils.data; +import com.fasterxml.jackson.core.FormatFeature; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.type.MapType; import net.dv8tion.jda.api.exceptions.ParsingException; @@ -850,12 +854,12 @@ public String toString() @Nonnull public String toPrettyString() { - DefaultPrettyPrinter.Indenter indent = new DefaultIndenter(" ", DefaultIndenter.SYS_LF); - DefaultPrettyPrinter printer = new DefaultPrettyPrinter(); - printer.withObjectIndenter(indent).withArrayIndenter(indent); try { - return mapper.writer(printer).writeValueAsString(data); + return mapper.writer(new DefaultPrettyPrinter()) + .with(SerializationFeature.INDENT_OUTPUT) + .with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .writeValueAsString(data); } catch (JsonProcessingException e) { diff --git a/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermEncoder.java b/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermEncoder.java index cfb550804be..ceb8d10645b 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermEncoder.java +++ b/src/main/java/net/dv8tion/jda/api/utils/data/etf/ExTermEncoder.java @@ -88,16 +88,15 @@ private static ByteBuffer pack(ByteBuffer buffer, Object value) if (value instanceof Byte) return packSmallInt(buffer, (byte) value); if (value instanceof Integer || value instanceof Short) - return packInt(buffer, (int) value); + return packInt(buffer, ((Number) value).intValue()); if (value instanceof Long) return packLong(buffer, (long) value); if (value instanceof Float || value instanceof Double) - return packFloat(buffer, (double) value); + return packFloat(buffer, ((Number) value).doubleValue()); if (value instanceof Boolean) return packAtom(buffer, String.valueOf(value)); if (value == null) return packAtom(buffer, "nil"); - // imagine we had templates :O if (value instanceof long[]) return packArray(buffer, (long[]) value); if (value instanceof int[]) diff --git a/src/test/java/CommandDataTest.java b/src/test/java/CommandDataTest.java deleted file mode 100644 index 5352c720470..00000000000 --- a/src/test/java/CommandDataTest.java +++ /dev/null @@ -1,205 +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. - */ - -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.interactions.commands.Command; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; -import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.CommandData; -import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; -import net.dv8tion.jda.api.utils.data.DataArray; -import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.internal.interactions.CommandDataImpl; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -public class CommandDataTest -{ - @Test - public void testNormal() - { - CommandData command = new CommandDataImpl("ban", "Ban a user from this server") - .setGuildOnly(true) - .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS)) - .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required - .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false - .addOption(OptionType.INTEGER, "days", "The duration of the ban", false); // test with explicit false - - DataObject data = command.toData(); - Assertions.assertEquals("ban", data.getString("name")); - Assertions.assertEquals("Ban a user from this server", data.getString("description")); - Assertions.assertFalse(data.getBoolean("dm_permission")); - Assertions.assertEquals(Permission.BAN_MEMBERS.getRawValue(), data.getUnsignedLong("default_member_permissions")); - - DataArray options = data.getArray("options"); - - DataObject option = options.getObject(0); - Assertions.assertTrue(option.getBoolean("required")); - Assertions.assertEquals("user", option.getString("name")); - Assertions.assertEquals("The user to ban", option.getString("description")); - - option = options.getObject(1); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("reason", option.getString("name")); - Assertions.assertEquals("The ban reason", option.getString("description")); - - option = options.getObject(2); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("days", option.getString("name")); - Assertions.assertEquals("The duration of the ban", option.getString("description")); - } - - @Test - public void testDefaultMemberPermissions() - { - CommandData command = new CommandDataImpl("ban", "Ban a user from this server") - .setDefaultPermissions(DefaultMemberPermissions.DISABLED); - DataObject data = command.toData(); - - Assertions.assertEquals(0, data.getUnsignedLong("default_member_permissions")); - - command.setDefaultPermissions(DefaultMemberPermissions.ENABLED); - data = command.toData(); - Assertions.assertTrue(data.isNull("default_member_permissions")); - } - - @Test - public void testSubcommand() - { - CommandDataImpl command = new CommandDataImpl("mod", "Moderation commands") - .addSubcommands(new SubcommandData("ban", "Ban a user from this server") - .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required - .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false - .addOption(OptionType.INTEGER, "days", "The duration of the ban", false)); // test with explicit false - - DataObject data = command.toData(); - Assertions.assertEquals("mod", data.getString("name")); - Assertions.assertEquals("Moderation commands", data.getString("description")); - - DataObject subdata = data.getArray("options").getObject(0); - Assertions.assertEquals("ban", subdata.getString("name")); - Assertions.assertEquals("Ban a user from this server", subdata.getString("description")); - - DataArray options = subdata.getArray("options"); - - DataObject option = options.getObject(0); - Assertions.assertTrue(option.getBoolean("required")); - Assertions.assertEquals("user", option.getString("name")); - Assertions.assertEquals("The user to ban", option.getString("description")); - - option = options.getObject(1); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("reason", option.getString("name")); - Assertions.assertEquals("The ban reason", option.getString("description")); - - option = options.getObject(2); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("days", option.getString("name")); - Assertions.assertEquals("The duration of the ban", option.getString("description")); - } - - @Test - public void testSubcommandGroup() - { - CommandDataImpl command = new CommandDataImpl("mod", "Moderation commands") - .addSubcommandGroups(new SubcommandGroupData("ban", "Ban or unban a user from this server") - .addSubcommands(new SubcommandData("add", "Ban a user from this server") - .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required - .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false - .addOption(OptionType.INTEGER, "days", "The duration of the ban", false))); // test with explicit false - - DataObject data = command.toData(); - Assertions.assertEquals("mod", data.getString("name")); - Assertions.assertEquals("Moderation commands", data.getString("description")); - - DataObject group = data.getArray("options").getObject(0); - Assertions.assertEquals("ban", group.getString("name")); - Assertions.assertEquals("Ban or unban a user from this server", group.getString("description")); - - DataObject subdata = group.getArray("options").getObject(0); - Assertions.assertEquals("add", subdata.getString("name")); - Assertions.assertEquals("Ban a user from this server", subdata.getString("description")); - DataArray options = subdata.getArray("options"); - - DataObject option = options.getObject(0); - Assertions.assertTrue(option.getBoolean("required")); - Assertions.assertEquals("user", option.getString("name")); - Assertions.assertEquals("The user to ban", option.getString("description")); - - option = options.getObject(1); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("reason", option.getString("name")); - Assertions.assertEquals("The ban reason", option.getString("description")); - - option = options.getObject(2); - Assertions.assertFalse(option.getBoolean("required")); - Assertions.assertEquals("days", option.getString("name")); - Assertions.assertEquals("The duration of the ban", option.getString("description")); - } - - @Test - public void testRequiredThrows() - { - CommandDataImpl command = new CommandDataImpl("ban", "Simple ban command"); - command.addOption(OptionType.STRING, "opt", "desc"); - - Assertions.assertThrows(IllegalArgumentException.class, () -> command.addOption(OptionType.STRING, "other", "desc", true)); - - SubcommandData subcommand = new SubcommandData("sub", "Simple subcommand"); - subcommand.addOption(OptionType.STRING, "opt", "desc"); - Assertions.assertThrows(IllegalArgumentException.class, () -> subcommand.addOption(OptionType.STRING, "other", "desc", true)); - } - - @Test - public void testNameChecks() - { - Assertions.assertThrows(IllegalArgumentException.class, () -> new CommandDataImpl("invalid name", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new CommandDataImpl("invalidName", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new CommandDataImpl("valid_name", "")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandData("invalid name", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandData("invalidName", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandData("valid_name", "")); - - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandGroupData("invalid name", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandGroupData("invalidName", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> new SubcommandGroupData("valid_name", "")); - } - - @Test - public void testChoices() - { - OptionData option = new OptionData(OptionType.INTEGER, "choice", "Option with choices!"); - Assertions.assertThrows(IllegalArgumentException.class, () -> option.addChoice("invalid name", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> option.addChoice("invalidName", "Valid description")); - Assertions.assertThrows(IllegalArgumentException.class, () -> option.addChoice("valid_name", "")); - - List choices = new ArrayList<>(); - for (int i = 0; i < 25; i++) - { - option.addChoice("choice_" + i, i); - choices.add(new Command.Choice("choice_" + i, i)); - } - Assertions.assertThrows(IllegalArgumentException.class, () -> option.addChoice("name", 100)); - Assertions.assertEquals(25, option.getChoices().size()); - Assertions.assertEquals(choices, option.getChoices()); - } -} diff --git a/src/test/java/DataPathTest.java b/src/test/java/DataPathTest.java deleted file mode 100644 index 9f79294722f..00000000000 --- a/src/test/java/DataPathTest.java +++ /dev/null @@ -1,101 +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. - */ - -import net.dv8tion.jda.api.exceptions.ParsingException; -import net.dv8tion.jda.api.utils.data.DataArray; -import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.api.utils.data.DataPath; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class DataPathTest -{ - @Test - void testSimple() - { - DataObject object = DataObject.empty() - .put("foo", "10"); // string to also test parsing - - Assertions.assertEquals(10, DataPath.getInt(object, "foo")); - - DataArray array = DataArray.empty().add("20"); - Assertions.assertEquals(20, DataPath.getInt(array, "[0]")); - } - - @Test - void testSimpleMissing() - { - DataObject object = DataObject.empty(); - - Assertions.assertEquals(0L, DataPath.getLong(object, "foo?", 0)); - Assertions.assertThrows(ParsingException.class, () -> DataPath.getLong(object, "foo")); - - DataArray array = DataArray.empty(); - - Assertions.assertTrue(DataPath.getBoolean(array, "[0]?", true)); - Assertions.assertThrows(ParsingException.class, () -> DataPath.getObject(array, "[0]")); - } - - @Test - void testObjectInArray() - { - DataObject object = DataObject.empty().put("foo", 10.0); - DataArray array = DataArray.empty().add(object); - - Assertions.assertEquals(10.0, DataPath.getDouble(array, "[0].foo")); - Assertions.assertEquals(20.0, DataPath.getDouble(array, "[1]?.foo", 20.0)); - Assertions.assertThrows(IndexOutOfBoundsException.class, () -> DataPath.getDouble(array, "[1].foo")); - } - - @Test - void testArrayInObject() - { - DataArray array = DataArray.empty().add("hello"); - DataObject object = DataObject.empty().put("foo", array); - - Assertions.assertEquals("hello", DataPath.getString(object, "foo[0]")); - Assertions.assertEquals("world", DataPath.getString(object, "foo[1]?", "world")); - Assertions.assertThrows(IndexOutOfBoundsException.class, () -> DataPath.getString(object, "foo[1]")); - } - - @Test - void testArrayInArray() - { - DataArray array = DataArray.empty().add(DataArray.empty().add("10")); - - Assertions.assertEquals(10, DataPath.getUnsignedInt(array, "[0][0]")); - Assertions.assertEquals(20, DataPath.getUnsignedInt(array, "[0][1]?", 20)); - Assertions.assertEquals(20, DataPath.getUnsignedInt(array, "[1]?[0]", 20)); - Assertions.assertThrows(IndexOutOfBoundsException.class, () -> DataPath.getUnsignedInt(array, "[0][1]")); - Assertions.assertThrows(IndexOutOfBoundsException.class, () -> DataPath.getUnsignedInt(array, "[1][0]")); - Assertions.assertThrows(ParsingException.class, () -> DataPath.getUnsignedInt(array, "[0][1]?")); - Assertions.assertThrows(ParsingException.class, () -> DataPath.getUnsignedInt(array, "[1]?[0]")); - } - - @Test - void testComplex() - { - DataObject object = DataObject.empty() - .put("array", DataArray.empty() - .add(DataObject.empty() - .put("foo", DataObject.empty() - .put("bar", "hello")))); - - Assertions.assertEquals("hello", DataPath.getString(object, "array[0].foo.bar")); - Assertions.assertEquals("world", DataPath.getString(object, "array[0].wrong?.bar", "world")); - Assertions.assertThrows(ParsingException.class, () -> DataPath.getString(object, "array[0].wrong?.bar")); - } -} diff --git a/src/test/java/HelpersTest.java b/src/test/java/HelpersTest.java deleted file mode 100644 index f5fed4bca51..00000000000 --- a/src/test/java/HelpersTest.java +++ /dev/null @@ -1,121 +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. - */ - -import net.dv8tion.jda.internal.utils.Helpers; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - -public class HelpersTest -{ - @Test - public void testIsEmpty() - { - Assertions.assertTrue(Helpers.isEmpty(null)); - Assertions.assertTrue(Helpers.isEmpty("")); - Assertions.assertFalse(Helpers.isEmpty("null")); - Assertions.assertFalse(Helpers.isEmpty("testing with spaces")); - } - - @Test - public void testContainsWhitespace() - { - Assertions.assertTrue(Helpers.containsWhitespace(" ")); - Assertions.assertTrue(Helpers.containsWhitespace("testing with spaces")); - Assertions.assertFalse(Helpers.containsWhitespace(null)); - Assertions.assertFalse(Helpers.containsWhitespace("")); - Assertions.assertFalse(Helpers.containsWhitespace("null")); - } - - @Test - public void testIsBlank() - { - Assertions.assertTrue(Helpers.isBlank(" ")); - Assertions.assertTrue(Helpers.isBlank(null)); - Assertions.assertTrue(Helpers.isBlank("")); - Assertions.assertFalse(Helpers.isBlank("testing with spaces")); - Assertions.assertFalse(Helpers.isBlank("null")); - } - - @Test - public void testCountMatches() - { - Assertions.assertEquals(3, Helpers.countMatches("Hello World", 'l')); - Assertions.assertEquals(1, Helpers.countMatches("Hello World", ' ')); - Assertions.assertEquals(0, Helpers.countMatches("Hello World", '_')); - Assertions.assertEquals(0, Helpers.countMatches("", '!')); - Assertions.assertEquals(0, Helpers.countMatches(null, '?')); - } - - @Test - public void testTruncate() - { - Assertions.assertEquals("Hello", Helpers.truncate("Hello World", 5)); - Assertions.assertEquals("Hello", Helpers.truncate("Hello", 5)); - Assertions.assertEquals("Hello", Helpers.truncate("Hello", 10)); - Assertions.assertEquals("", Helpers.truncate("", 10)); - Assertions.assertEquals("", Helpers.truncate("Test", 0)); - Assertions.assertNull(Helpers.truncate(null, 10)); - } - - @Test - public void testRightPad() - { - Assertions.assertEquals("Hello ", Helpers.rightPad("Hello", 9)); - Assertions.assertEquals("Hello World", Helpers.rightPad("Hello World", 9)); - Assertions.assertEquals("Hello", Helpers.rightPad("Hello", 5)); - } - - @Test - public void testLeftPad() - { - Assertions.assertEquals(" Hello", Helpers.leftPad("Hello", 9)); - Assertions.assertEquals("Hello World", Helpers.leftPad("Hello World", 9)); - Assertions.assertEquals("Hello", Helpers.leftPad("Hello", 5)); - } - - @Test - public void testIsNumeric() - { - Assertions.assertTrue(Helpers.isNumeric("10")); - Assertions.assertTrue(Helpers.isNumeric("1")); - Assertions.assertTrue(Helpers.isNumeric("0")); - Assertions.assertTrue(Helpers.isNumeric(String.valueOf(Long.MAX_VALUE))); - Assertions.assertFalse(Helpers.isNumeric(null)); - Assertions.assertFalse(Helpers.isNumeric("")); - Assertions.assertFalse(Helpers.isNumeric("Test")); - Assertions.assertFalse(Helpers.isNumeric("1.0")); - Assertions.assertFalse(Helpers.isNumeric("1e10")); - } - - @Test - public void testDeepEquals() - { - List a = Arrays.asList("A", "B", "C"); - List b = Arrays.asList("B", "A", "C"); - List c = Arrays.asList("A", "B"); - List d = Arrays.asList("A", "B", "C"); - - Assertions.assertTrue(Helpers.deepEquals(a, a)); - Assertions.assertTrue(Helpers.deepEquals(a, d)); - Assertions.assertTrue(Helpers.deepEqualsUnordered(a, b)); - Assertions.assertFalse(Helpers.deepEquals(a, b)); - Assertions.assertFalse(Helpers.deepEquals(a, c)); - Assertions.assertFalse(Helpers.deepEqualsUnordered(b, c)); - } -} diff --git a/src/test/java/JsonTest.java b/src/test/java/JsonTest.java deleted file mode 100644 index f147dc3cfd8..00000000000 --- a/src/test/java/JsonTest.java +++ /dev/null @@ -1,43 +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. - */ - -import net.dv8tion.jda.api.utils.data.DataObject; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class JsonTest -{ - private static final String json = "{\"int\":10,\"long\":100,\"boolean\":true,\"string\":\"test\"}"; - - @Test - public void testParse() - { - DataObject object = DataObject.fromJson(json); - Assertions.assertEquals(10, object.getInt("int", 0)); - Assertions.assertEquals(100, object.getLong("long", 0)); - Assertions.assertEquals(true, object.getBoolean("boolean", false)); - Assertions.assertEquals("test", object.getString("string", null)); - } - - @Test - public void testJsonToString() - { - DataObject object = DataObject.fromJson(json); - String result = object.toString(); - DataObject symmetric = DataObject.fromJson(result); - Assertions.assertEquals(object.toMap(), symmetric.toMap()); // lucky that this works here :) - } -} diff --git a/src/test/java/LocalizationTest.java b/src/test/java/LocalizationTest.java deleted file mode 100644 index 7708d56d9d3..00000000000 --- a/src/test/java/LocalizationTest.java +++ /dev/null @@ -1,140 +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. - */ - -import net.dv8tion.jda.api.interactions.DiscordLocale; -import net.dv8tion.jda.api.interactions.commands.Command; -import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.*; -import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; -import net.dv8tion.jda.api.interactions.commands.localization.ResourceBundleLocalizationFunction; -import net.dv8tion.jda.api.utils.data.DataArray; -import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.api.utils.data.DataPath; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class LocalizationTest -{ - private static SlashCommandData slashCommandData; - private static DataObject data; - - @BeforeAll - public static void setup() - { - final LocalizationFunction localizationFunction = ResourceBundleLocalizationFunction - .fromBundles("MyCommands", DiscordLocale.FRENCH) - .build(); - - slashCommandData = Commands.slash("ban", "Bans someone").addSubcommandGroups( - new SubcommandGroupData("user", "Bans a member").addSubcommands( - new SubcommandData("perm", "Bans an user permanently").addOptions( - new OptionData(OptionType.STRING, "user", "The user to ban"), - new OptionData(OptionType.INTEGER, "del_days", "The amount of days to delete messages") - .addChoices( - new Command.Choice("1 Day", "1"), - new Command.Choice("7 Days", "7"), - new Command.Choice("14 Days", "14") - ) - ), - new SubcommandData("temp", "Bans an user temporarily").addOptions( - new OptionData(OptionType.STRING, "user", "The user to ban"), - new OptionData(OptionType.INTEGER, "del_days", "The amount of days to delete messages") - .addChoices( - new Command.Choice("1 Day", "1"), - new Command.Choice("7 Days", "7"), - new Command.Choice("14 Days", "14") - ) - ) - ) - ).setLocalizationFunction(localizationFunction); - - data = slashCommandData.toData(); - } - - @Test - public void commandLocalization() - { - assertEquals("ban", DataPath.getString(data, "name_localizations.fr")); - assertEquals("Bannis un utilisateur", DataPath.getString(data, "description_localizations.fr")); - } - - @Test - public void subcommandLocalization() - { - assertEquals("utilisateur", navigateOptions("user").getObject("name_localizations").getString("fr")); - assertEquals("Bannis un utilisateur", navigateOptions("user").getObject("description_localizations").getString("fr")); - } - - @Test - public void subcommandGroupLocalization() - { - assertEquals("permanent", navigateOptions("user", "perm").getObject("name_localizations").getString("fr")); - assertEquals("Bannis un utilisateur pour toujours", navigateOptions("user", "perm").getObject("description_localizations").getString("fr")); - } - - @Test - public void optionLocalization() - { - assertEquals("utilisateur", navigateOptions("user", "perm", "user").getObject("name_localizations").getString("fr")); - assertEquals("L'utilisateur à bannir", navigateOptions("user", "perm", "user").getObject("description_localizations").getString("fr")); - - assertEquals("nb_jours", navigateOptions("user", "perm", "del_days").getObject("name_localizations").getString("fr")); - assertEquals("Nombre de jours de messages à supprimer", navigateOptions("user", "perm", "del_days").getObject("description_localizations").getString("fr")); - } - - @Test - public void choiceLocalization() - { - assertEquals("1 jour", navigateChoice("1 Day", "user", "perm", "del_days").getObject("name_localizations").getString("fr")); - assertEquals("7 jours", navigateChoice("7 Days", "user", "perm", "del_days").getObject("name_localizations").getString("fr")); - assertEquals("14 jours", navigateChoice("14 Days", "user", "perm", "del_days").getObject("name_localizations").getString("fr")); - } - - @Test - public void reconstructData() - { - final DataObject data = slashCommandData.toData(); - final DataObject reconstitutedData = CommandData.fromData(data).toData(); - assertEquals(data.toMap(), reconstitutedData.toMap()); - } - - private DataObject navigateOptions(String... names) - { - DataObject o = data; - for (String name : names) - { - o = o.getArray("options").stream(DataArray::getObject) - .filter(s -> s.getString("name").equals(name)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Could not find an option with path: " + Arrays.toString(names))); - } - return o; - } - - private DataObject navigateChoice(String choiceName, String... names) - { - return navigateOptions(names) - .getArray("choices") - .stream(DataArray::getObject) - .filter(s -> s.getString("name").equals(choiceName)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Could not find choice '" + choiceName + "' with path: " + Arrays.toString(names))); - } -} diff --git a/src/test/java/MarkdownTest.java b/src/test/java/MarkdownTest.java deleted file mode 100644 index 384934491fd..00000000000 --- a/src/test/java/MarkdownTest.java +++ /dev/null @@ -1,468 +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. - */ - -import net.dv8tion.jda.api.utils.MarkdownSanitizer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class MarkdownTest -{ - private MarkdownSanitizer markdown; - - @BeforeEach - public void setup() - { - markdown = new MarkdownSanitizer().withStrategy(MarkdownSanitizer.SanitizationStrategy.REMOVE); - } - - @Test - public void testComplex() - { - Assertions.assertEquals("ABCDEF", markdown.compute("**A_B||C~~D__E`F`__~~||_**")); - } - - @Test - public void testTrivial() - { - Assertions.assertEquals("", markdown.compute("")); - Assertions.assertEquals("Hello World ~~~~", markdown.compute("Hello World ~~~~")); - Assertions.assertEquals("Hello World ~", markdown.compute("Hello World ~~~~~")); - } - - @Test - public void testBold() - { - Assertions.assertEquals("Hello", markdown.compute("**Hello**")); - Assertions.assertEquals("**Hello", markdown.compute("**Hello")); - Assertions.assertEquals("\\**Hello**", markdown.compute("\\**Hello**")); - } - - @Test - public void testItalics() - { - Assertions.assertEquals("Hello", markdown.compute("*Hello*")); - Assertions.assertEquals("Hello", markdown.compute("_Hello_")); - - Assertions.assertEquals("*Hello", markdown.compute("*Hello")); - Assertions.assertEquals("_Hello", markdown.compute("_Hello")); - - Assertions.assertEquals("\\*Hello*", markdown.compute("\\*Hello*")); - Assertions.assertEquals("\\_Hello_", markdown.compute("\\_Hello_")); - } - - @Test - public void testBoldItalics() - { - Assertions.assertEquals("Hello", markdown.compute("***Hello***")); - Assertions.assertEquals("***Hello", markdown.compute("***Hello")); - Assertions.assertEquals("\\***Hello***", markdown.compute("\\***Hello***")); - } - - @Test - public void testUnderline() - { - Assertions.assertEquals("Hello", markdown.compute("__Hello__")); - Assertions.assertEquals("__Hello", markdown.compute("__Hello")); - Assertions.assertEquals("\\__Hello__", markdown.compute("\\__Hello__")); - } - - @Test - public void testStrike() - { - Assertions.assertEquals("Hello", markdown.compute("~~Hello~~")); - Assertions.assertEquals("~~Hello", markdown.compute("~~Hello")); - Assertions.assertEquals("\\~~Hello~~", markdown.compute("\\~~Hello~~")); - } - - @Test - public void testSpoiler() - { - Assertions.assertEquals("Hello", markdown.compute("||Hello||")); - Assertions.assertEquals("||Hello", markdown.compute("||Hello")); - Assertions.assertEquals("\\||Hello||", markdown.compute("\\||Hello||")); - } - - @Test - public void testMono() - { - Assertions.assertEquals("Hello", markdown.compute("`Hello`")); - Assertions.assertEquals("`Hello", markdown.compute("`Hello")); - Assertions.assertEquals("\\`Hello`", markdown.compute("\\`Hello`")); - - Assertions.assertEquals("Hello **World**", markdown.compute("`Hello **World**`")); - Assertions.assertEquals("`Hello World", markdown.compute("`Hello **World**")); - Assertions.assertEquals("\\`Hello World`", markdown.compute("\\`Hello **World**`")); - } - - @Test - public void testMonoTwo() - { - Assertions.assertEquals("Hello", markdown.compute("``Hello``")); - Assertions.assertEquals("``Hello", markdown.compute("``Hello")); - Assertions.assertEquals("\\``Hello``", markdown.compute("\\``Hello``")); - - Assertions.assertEquals("Hello **World**", markdown.compute("``Hello **World**``")); - Assertions.assertEquals("``Hello World", markdown.compute("``Hello **World**")); - Assertions.assertEquals("\\``Hello World``", markdown.compute("\\``Hello **World**``")); - - Assertions.assertEquals("Hello `to` World", markdown.compute("``Hello `to` World``")); - Assertions.assertEquals("``Hello to World", markdown.compute("``Hello `to` World")); - Assertions.assertEquals("\\``Hello to World``", markdown.compute("\\``Hello `to` World``")); - } - - @Test - public void testBlock() - { - Assertions.assertEquals("Hello", markdown.compute("```Hello```")); - Assertions.assertEquals("```Hello", markdown.compute("```Hello")); - Assertions.assertEquals("\\```Hello```", markdown.compute("\\```Hello```")); - - Assertions.assertEquals("Hello **World**", markdown.compute("```Hello **World**```")); - Assertions.assertEquals("```Hello World", markdown.compute("```Hello **World**")); - Assertions.assertEquals("\\```Hello World```", markdown.compute("\\```Hello **World**```")); - - Assertions.assertEquals("Hello `to` World", markdown.compute("```Hello `to` World```")); - Assertions.assertEquals("```Hello to World", markdown.compute("```Hello `to` World")); - Assertions.assertEquals("\\```Hello to World```", markdown.compute("\\```Hello `to` World```")); - - Assertions.assertEquals("Test", markdown.compute("```java\nTest```")); - } - - @Test - public void testQuote() - { - Assertions.assertEquals("Hello > World", markdown.compute("> Hello > World")); - Assertions.assertEquals("Hello\nWorld", markdown.compute("> Hello\n> World")); - Assertions.assertEquals("Hello\nWorld", markdown.compute(">>> Hello\nWorld")); - Assertions.assertEquals("Hello\nWorld", markdown.compute(">>>\nHello\nWorld")); - Assertions.assertEquals("Hello > World", markdown.compute(">>>\nHello > World")); - Assertions.assertEquals("Hello\n World", markdown.compute("Hello\n > World")); - } -} - -class IgnoreMarkdownTest -{ - private MarkdownSanitizer markdown; - - @BeforeEach - public void setup() - { - markdown = new MarkdownSanitizer().withIgnored(0xFFFFFFFF); - } - - @Test - public void testComplex() - { - Assertions.assertEquals("**A_B||C~~D__E`F`__~~||_**", markdown.compute("**A_B||C~~D__E`F`__~~||_**")); - } - - @Test - public void testBold() - { - Assertions.assertEquals("**Hello**", markdown.compute("**Hello**")); - Assertions.assertEquals("**Hello", markdown.compute("**Hello")); - } - - @Test - public void testItalics() - { - Assertions.assertEquals("*Hello*", markdown.compute("*Hello*")); - Assertions.assertEquals("_Hello_", markdown.compute("_Hello_")); - - Assertions.assertEquals("*Hello", markdown.compute("*Hello")); - Assertions.assertEquals("_Hello", markdown.compute("_Hello")); - } - - @Test - public void testBoldItalics() - { - Assertions.assertEquals("***Hello***", markdown.compute("***Hello***")); - Assertions.assertEquals("***Hello", markdown.compute("***Hello")); - Assertions.assertEquals("\\***Hello***", markdown.compute("\\***Hello***")); - } - - @Test - public void testUnderline() - { - Assertions.assertEquals("__Hello__", markdown.compute("__Hello__")); - Assertions.assertEquals("__Hello", markdown.compute("__Hello")); - } - - @Test - public void testStrike() - { - Assertions.assertEquals("~~Hello~~", markdown.compute("~~Hello~~")); - Assertions.assertEquals("~~Hello", markdown.compute("~~Hello")); - } - - @Test - public void testSpoiler() - { - Assertions.assertEquals("||Hello||", markdown.compute("||Hello||")); - Assertions.assertEquals("||Hello", markdown.compute("||Hello")); - } - - @Test - public void testMono() - { - Assertions.assertEquals("`Hello`", markdown.compute("`Hello`")); - Assertions.assertEquals("`Hello", markdown.compute("`Hello")); - - Assertions.assertEquals("`Hello **World**`", markdown.compute("`Hello **World**`")); - Assertions.assertEquals("`Hello **World**", markdown.compute("`Hello **World**")); - } - - @Test - public void testMonoTwo() - { - Assertions.assertEquals("``Hello``", markdown.compute("``Hello``")); - Assertions.assertEquals("``Hello", markdown.compute("``Hello")); - - Assertions.assertEquals("``Hello **World**``", markdown.compute("``Hello **World**``")); - Assertions.assertEquals("``Hello **World**", markdown.compute("``Hello **World**")); - - Assertions.assertEquals("``Hello `to` World``", markdown.compute("``Hello `to` World``")); - Assertions.assertEquals("``Hello `to` World", markdown.compute("``Hello `to` World")); - } - - @Test - public void testBlock() - { - Assertions.assertEquals("```Hello```", markdown.compute("```Hello```")); - Assertions.assertEquals("```Hello", markdown.compute("```Hello")); - - Assertions.assertEquals("```Hello **World**```", markdown.compute("```Hello **World**```")); - Assertions.assertEquals("```Hello **World**", markdown.compute("```Hello **World**")); - - Assertions.assertEquals("```Hello `to` World```", markdown.compute("```Hello `to` World```")); - Assertions.assertEquals("```Hello `to` World", markdown.compute("```Hello `to` World")); - - Assertions.assertEquals("```java\nTest```", markdown.compute("```java\nTest```")); - } - - @Test - public void testQuote() - { - Assertions.assertEquals("> Hello > World", markdown.compute("> Hello > World")); - Assertions.assertEquals("> Hello\n> World", markdown.compute("> Hello\n> World")); - Assertions.assertEquals(">>> Hello\nWorld", markdown.compute(">>> Hello\nWorld")); - Assertions.assertEquals(">>>\nHello\nWorld", markdown.compute(">>>\nHello\nWorld")); - Assertions.assertEquals(">>>\nHello > World", markdown.compute(">>>\nHello > World")); - Assertions.assertEquals("Hello\n > World", markdown.compute("Hello\n > World")); - } -} - -class EscapeMarkdownTest -{ - private MarkdownSanitizer markdown; - - @BeforeEach - public void setup() - { - markdown = new MarkdownSanitizer().withStrategy(MarkdownSanitizer.SanitizationStrategy.ESCAPE); - } - - @Test - public void testComplex() - { - Assertions.assertEquals("\\*\\*A\\_B\\||C\\~~D\\_\\_E\\`F\\`\\_\\_\\~~\\||\\_\\*\\*", markdown.compute("**A_B||C~~D__E`F`__~~||_**")); - } - - @Test - public void testBold() - { - Assertions.assertEquals("\\*\\*Hello\\*\\*", markdown.compute("**Hello**")); - Assertions.assertEquals("**Hello", markdown.compute("**Hello")); - Assertions.assertEquals("\\**Hello**", markdown.compute("\\**Hello**")); - } - - @Test - public void testItalics() - { - Assertions.assertEquals("\\*Hello\\*", markdown.compute("*Hello*")); - Assertions.assertEquals("\\_Hello\\_", markdown.compute("_Hello_")); - - Assertions.assertEquals("*Hello", markdown.compute("*Hello")); - Assertions.assertEquals("_Hello", markdown.compute("_Hello")); - - Assertions.assertEquals("\\*Hello*", markdown.compute("\\*Hello*")); - Assertions.assertEquals("\\_Hello_", markdown.compute("\\_Hello_")); - } - - @Test - public void testBoldItalics() - { - Assertions.assertEquals("\\*\\*\\*Hello\\*\\*\\*", markdown.compute("***Hello***")); - Assertions.assertEquals("***Hello", markdown.compute("***Hello")); - Assertions.assertEquals("\\***Hello***", markdown.compute("\\***Hello***")); - } - - @Test - public void testUnderline() - { - Assertions.assertEquals("\\_\\_Hello\\_\\_", markdown.compute("__Hello__")); - Assertions.assertEquals("__Hello", markdown.compute("__Hello")); - Assertions.assertEquals("\\__Hello__", markdown.compute("\\__Hello__")); - } - - @Test - public void testStrike() - { - Assertions.assertEquals("\\~~Hello\\~~", markdown.compute("~~Hello~~")); - Assertions.assertEquals("~~Hello", markdown.compute("~~Hello")); - Assertions.assertEquals("\\~~Hello~~", markdown.compute("\\~~Hello~~")); - } - - @Test - public void testSpoiler() - { - Assertions.assertEquals("\\||Hello\\||", markdown.compute("||Hello||")); - Assertions.assertEquals("||Hello", markdown.compute("||Hello")); - Assertions.assertEquals("\\||Hello||", markdown.compute("\\||Hello||")); - } - - @Test - public void testMono() - { - Assertions.assertEquals("\\`Hello\\`", markdown.compute("`Hello`")); - Assertions.assertEquals("`Hello", markdown.compute("`Hello")); - Assertions.assertEquals("\\`Hello`", markdown.compute("\\`Hello`")); - - Assertions.assertEquals("\\`Hello **World**\\`", markdown.compute("`Hello **World**`")); - Assertions.assertEquals("`Hello \\*\\*World\\*\\*", markdown.compute("`Hello **World**")); - Assertions.assertEquals("\\`Hello \\*\\*World\\*\\*`", markdown.compute("\\`Hello **World**`")); - - } - - @Test - public void testMonoTwo() - { - Assertions.assertEquals("\\``Hello\\``", markdown.compute("``Hello``")); - Assertions.assertEquals("``Hello", markdown.compute("``Hello")); - Assertions.assertEquals("\\``Hello``", markdown.compute("\\``Hello``")); - - Assertions.assertEquals("\\``Hello **World**\\``", markdown.compute("``Hello **World**``")); - Assertions.assertEquals("``Hello \\*\\*World\\*\\*", markdown.compute("``Hello **World**")); - Assertions.assertEquals("\\``Hello \\*\\*World\\*\\*``", markdown.compute("\\``Hello **World**``")); - - Assertions.assertEquals("\\``Hello `to` World\\``", markdown.compute("``Hello `to` World``")); - Assertions.assertEquals("``Hello \\`to\\` World", markdown.compute("``Hello `to` World")); - Assertions.assertEquals("\\``Hello \\`to\\` World", markdown.compute("\\``Hello `to` World")); - } - - @Test - public void testBlock() - { - Assertions.assertEquals("\\```Hello\\```", markdown.compute("```Hello```")); - Assertions.assertEquals("```Hello", markdown.compute("```Hello")); - Assertions.assertEquals("\\```Hello", markdown.compute("\\```Hello")); - - Assertions.assertEquals("\\```Hello **World**\\```", markdown.compute("```Hello **World**```")); - Assertions.assertEquals("```Hello \\*\\*World\\*\\*", markdown.compute("```Hello **World**")); - Assertions.assertEquals("\\```Hello \\*\\*World\\*\\*", markdown.compute("\\```Hello **World**")); - - Assertions.assertEquals("\\```Hello `to` World\\```", markdown.compute("```Hello `to` World```")); - Assertions.assertEquals("```Hello \\`to\\` World", markdown.compute("```Hello `to` World")); - Assertions.assertEquals("\\```Hello \\`to\\` World", markdown.compute("\\```Hello `to` World")); - - Assertions.assertEquals("\\```java\nTest\\```", markdown.compute("```java\nTest```")); - } - - @Test - public void testQuote() - { - Assertions.assertEquals("\\> Hello > World", markdown.compute("> Hello > World")); - Assertions.assertEquals("\\> Hello\n\\> World", markdown.compute("> Hello\n> World")); - Assertions.assertEquals("\\>>> Hello\nWorld", markdown.compute(">>> Hello\nWorld")); - Assertions.assertEquals("\\>>>\nHello\nWorld", markdown.compute(">>>\nHello\nWorld")); - Assertions.assertEquals("\\>>>\nHello > World", markdown.compute(">>>\nHello > World")); - Assertions.assertEquals("\\> \\_Hello \n\\> World\\_", markdown.compute("> _Hello \n> World_")); - Assertions.assertEquals("Hello\n \\> World", markdown.compute("Hello\n > World")); - } -} - -class EscapeMarkdownAllTest -{ - @Test - public void testAsterisk() - { - Assertions.assertEquals("Hello\\*World", MarkdownSanitizer.escape("Hello*World", true)); - Assertions.assertEquals("Hello\\*\\*World", MarkdownSanitizer.escape("Hello**World", true)); - Assertions.assertEquals("Hello\\*\\*\\*World", MarkdownSanitizer.escape("Hello***World", true)); - - Assertions.assertEquals("Hello\\*World", MarkdownSanitizer.escape("Hello\\*World", true)); - Assertions.assertEquals("Hello\\*\\*World", MarkdownSanitizer.escape("Hello\\*\\*World", true)); - Assertions.assertEquals("Hello\\*\\*\\*World", MarkdownSanitizer.escape("Hello\\*\\*\\*World", true)); - } - - @Test - public void testUnderscore() - { - Assertions.assertEquals("Hello\\_World", MarkdownSanitizer.escape("Hello_World", true)); - Assertions.assertEquals("Hello\\_\\_World", MarkdownSanitizer.escape("Hello__World", true)); - Assertions.assertEquals("Hello\\_\\_\\_World", MarkdownSanitizer.escape("Hello___World", true)); - - Assertions.assertEquals("Hello\\_World", MarkdownSanitizer.escape("Hello\\_World", true)); - Assertions.assertEquals("Hello\\_\\_World", MarkdownSanitizer.escape("Hello\\_\\_World", true)); - Assertions.assertEquals("Hello\\_\\_\\_World", MarkdownSanitizer.escape("Hello\\_\\_\\_World", true)); - } - - @Test - public void testCodeBlock() - { - Assertions.assertEquals("Hello\\`World", MarkdownSanitizer.escape("Hello`World", true)); - Assertions.assertEquals("Hello\\`\\`World", MarkdownSanitizer.escape("Hello``World", true)); - Assertions.assertEquals("Hello\\`\\`\\`World", MarkdownSanitizer.escape("Hello```World", true)); - - Assertions.assertEquals("Hello\\`World", MarkdownSanitizer.escape("Hello\\`World", true)); - Assertions.assertEquals("Hello\\`\\`World", MarkdownSanitizer.escape("Hello\\`\\`World", true)); - Assertions.assertEquals("Hello\\`\\`\\`World", MarkdownSanitizer.escape("Hello\\`\\`\\`World", true)); - } - - @Test - public void testSpoiler() - { - Assertions.assertEquals("Hello\\|\\|World", MarkdownSanitizer.escape("Hello||World", true)); - Assertions.assertEquals("Hello|World", MarkdownSanitizer.escape("Hello|World", true)); - - Assertions.assertEquals("Hello\\|\\|World", MarkdownSanitizer.escape("Hello\\|\\|World", true)); - Assertions.assertEquals("Hello\\|World", MarkdownSanitizer.escape("Hello\\|World", true)); - } - - @Test - public void testStrike() - { - Assertions.assertEquals("Hello\\~\\~World", MarkdownSanitizer.escape("Hello~~World", true)); - Assertions.assertEquals("Hello\\~\\~World", MarkdownSanitizer.escape("Hello\\~\\~World", true)); - } - - @Test - public void testQuote() - { - Assertions.assertEquals("\\> Hello World", MarkdownSanitizer.escape("> Hello World", true)); - Assertions.assertEquals(">Hello World", MarkdownSanitizer.escape(">Hello World", true)); - Assertions.assertEquals("\\>\\>\\> Hello World", MarkdownSanitizer.escape(">>> Hello World", true)); - Assertions.assertEquals(">>>Hello World", MarkdownSanitizer.escape(">>>Hello World", true)); - Assertions.assertEquals("\\>\\>\\> Hello > World\n\\> Hello >>> World\n<@12345> > Hello\n \\> Hello world", MarkdownSanitizer.escape(">>> Hello > World\n> Hello >>> World\n<@12345> > Hello\n > Hello world", true)); - - Assertions.assertEquals("\\> Hello World", MarkdownSanitizer.escape("\\> Hello World", true)); - Assertions.assertEquals("\\>\\>\\> Hello World", MarkdownSanitizer.escape("\\>\\>\\> Hello World", true)); - Assertions.assertEquals("Hello > World", MarkdownSanitizer.escape("Hello > World")); - Assertions.assertEquals("Hello\n \\> World", MarkdownSanitizer.escape("Hello\n > World")); - Assertions.assertEquals("Hello\n\\> World", MarkdownSanitizer.escape("Hello\n> World")); - } -} diff --git a/src/test/java/MarkdownUtilTest.java b/src/test/java/MarkdownUtilTest.java deleted file mode 100644 index 2501e4f2bbf..00000000000 --- a/src/test/java/MarkdownUtilTest.java +++ /dev/null @@ -1,106 +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. - */ - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import static net.dv8tion.jda.api.utils.MarkdownUtil.*; - -public class MarkdownUtilTest -{ - @Test - public void testBold() - { - Assertions.assertEquals("**Hello World**", bold("Hello World")); - Assertions.assertEquals("**Hello \\*\\*Test\\*\\* World**", bold("Hello **Test** World")); - Assertions.assertEquals("**Hello *Test* World**", bold("Hello *Test* World")); - } - - @Test - public void testItalics() - { - Assertions.assertEquals("_Hello World_", italics("Hello World")); - Assertions.assertEquals("_Hello \\_Test\\_ World_", italics("Hello _Test_ World")); - Assertions.assertEquals("_Hello __Test__ World_", italics("Hello __Test__ World")); - } - - @Test - public void testUnderline() - { - Assertions.assertEquals("__Hello World__", underline("Hello World")); - Assertions.assertEquals("__Hello \\_\\_Test\\_\\_ World__", underline("Hello __Test__ World")); - Assertions.assertEquals("__Hello _Test_ World__", underline("Hello _Test_ World")); - } - - @Test - public void testMonospace() - { - Assertions.assertEquals("`Hello World`", monospace("Hello World")); - Assertions.assertEquals("`Hello \\`Test\\` World`", monospace("Hello `Test` World")); - Assertions.assertEquals("`Hello ``Test`` World`", monospace("Hello ``Test`` World")); - } - - @Test - public void testCodeblock() - { - Assertions.assertEquals("```java\nHello World```", codeblock("java", "Hello World")); - Assertions.assertEquals("```java\nHello \\```java\nTest\\``` World```", codeblock("java", "Hello ```java\nTest``` World")); - Assertions.assertEquals("```java\nHello `Test` World```", codeblock("java", "Hello `Test` World")); - - Assertions.assertEquals("```Hello World```", codeblock("Hello World")); - Assertions.assertEquals("```Hello \\```java\nTest\\``` World```", codeblock("Hello ```java\nTest``` World")); - Assertions.assertEquals("```Hello `Test` World```", codeblock("Hello `Test` World")); - } - - @Test - public void testSpoiler() - { - Assertions.assertEquals("||Hello World||", spoiler("Hello World")); - Assertions.assertEquals("||Hello \\||Test\\|| World||", spoiler("Hello ||Test|| World")); - Assertions.assertEquals("||Hello |Test| World||", spoiler("Hello |Test| World")); - } - - @Test - public void testStrike() - { - Assertions.assertEquals("~~Hello World~~", strike("Hello World")); - Assertions.assertEquals("~~Hello \\~~Test\\~~ World~~", strike("Hello ~~Test~~ World")); - Assertions.assertEquals("~~Hello ~Test~ World~~", strike("Hello ~Test~ World")); - } - - @Test - public void testQuote() - { - Assertions.assertEquals("> Hello World", quote("Hello World")); - Assertions.assertEquals("> Hello \n> \\> Test World", quote("Hello \n> Test World")); - Assertions.assertEquals("> Hello > Test World", quote("Hello > Test World")); - } - - @Test - public void testQuoteBlock() - { - Assertions.assertEquals(">>> Hello World", quoteBlock("Hello World")); - Assertions.assertEquals(">>> Hello \n>>> Test World", quoteBlock("Hello \n>>> Test World")); - } - - @Test - public void testMaskedLink() - { - Assertions.assertEquals("[Hello](World)", maskedLink("Hello", "World")); - Assertions.assertEquals("[Hello](World%29)", maskedLink("Hello", "World)")); - Assertions.assertEquals("[Hello\\]](World%29)", maskedLink("Hello]", "World)")); - } -} diff --git a/src/test/java/net/dv8tion/jda/entities/MessageSerializationTest.java b/src/test/java/net/dv8tion/jda/entities/MessageSerializationTest.java deleted file mode 100644 index d605fe5fa57..00000000000 --- a/src/test/java/net/dv8tion/jda/entities/MessageSerializationTest.java +++ /dev/null @@ -1,80 +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.entities; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.EmbedType; -import net.dv8tion.jda.api.entities.MessageEmbed; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class MessageSerializationTest -{ - @Test - void testEmbedSerialization() - { - EmbedBuilder builder = new EmbedBuilder(); - builder.setDescription("Description Text"); - builder.setTitle("Title Text", "https://example.com/title"); - builder.setAuthor("Author Text", "https://example.com/author", "https://example.com/author_icon"); - builder.setFooter("Footer Text", "https://example.com/footer_icon"); - builder.setImage("https://example.com/image"); - builder.setThumbnail("https://example.com/thumbnail"); - builder.addField("Field 1", "Field 1 Text", true); - builder.addField("Field 2", "Field 2 Text", false); - builder.addField("Field 3", "Field 3 Text", true); - - MessageEmbed embed = builder.build(); - - MessageEmbed dataEmbed = EmbedBuilder.fromData(embed.toData()).build(); - - Assertions.assertEquals(embed.getType(), dataEmbed.getType()); - Assertions.assertEquals(EmbedType.RICH, embed.getType()); - - Assertions.assertEquals(embed.getDescription(), dataEmbed.getDescription()); - Assertions.assertEquals(embed.getTitle(), dataEmbed.getTitle()); - Assertions.assertEquals(embed.getUrl(), dataEmbed.getUrl()); - Assertions.assertEquals(embed.getAuthor(), dataEmbed.getAuthor()); - Assertions.assertEquals(embed.getFooter(), dataEmbed.getFooter()); - Assertions.assertEquals(embed.getImage(), dataEmbed.getImage()); - Assertions.assertEquals(embed.getThumbnail(), dataEmbed.getThumbnail()); - Assertions.assertEquals(embed.getFields(), dataEmbed.getFields()); - - Assertions.assertEquals(embed, dataEmbed); - - Assertions.assertEquals("Description Text", dataEmbed.getDescription()); - Assertions.assertEquals("Title Text", dataEmbed.getTitle()); - Assertions.assertEquals("https://example.com/title", dataEmbed.getUrl()); - Assertions.assertEquals("Author Text", dataEmbed.getAuthor().getName()); - Assertions.assertEquals("https://example.com/author", dataEmbed.getAuthor().getUrl()); - Assertions.assertEquals("https://example.com/author_icon", dataEmbed.getAuthor().getIconUrl()); - Assertions.assertEquals("Footer Text", dataEmbed.getFooter().getText()); - Assertions.assertEquals("https://example.com/footer_icon", dataEmbed.getFooter().getIconUrl()); - Assertions.assertEquals("https://example.com/image", dataEmbed.getImage().getUrl()); - Assertions.assertEquals("https://example.com/thumbnail", dataEmbed.getThumbnail().getUrl()); - Assertions.assertEquals(3, dataEmbed.getFields().size()); - Assertions.assertEquals("Field 1", dataEmbed.getFields().get(0).getName()); - Assertions.assertEquals("Field 1 Text", dataEmbed.getFields().get(0).getValue()); - Assertions.assertTrue(dataEmbed.getFields().get(0).isInline()); - Assertions.assertEquals("Field 2", dataEmbed.getFields().get(1).getName()); - Assertions.assertEquals("Field 2 Text", dataEmbed.getFields().get(1).getValue()); - Assertions.assertFalse(dataEmbed.getFields().get(1).isInline()); - Assertions.assertEquals("Field 3", dataEmbed.getFields().get(2).getName()); - Assertions.assertEquals("Field 3 Text", dataEmbed.getFields().get(2).getValue()); - Assertions.assertTrue(dataEmbed.getFields().get(2).isInline()); - } -} diff --git a/src/test/java/net/dv8tion/jda/entitystring/EntityStringTest.java b/src/test/java/net/dv8tion/jda/entitystring/EntityStringTest.java deleted file mode 100644 index 2ac411cda1b..00000000000 --- a/src/test/java/net/dv8tion/jda/entitystring/EntityStringTest.java +++ /dev/null @@ -1,188 +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.entitystring; - -import net.dv8tion.jda.api.entities.channel.ChannelType; -import net.dv8tion.jda.internal.utils.EntityString; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class EntityStringTest -{ - @Test - @Order(1) - public void testSimple() - { - assertEquals("AnEntity", new EntityString(new AnEntity()).toString()); - assertEquals("AnEntity:AName", new EntityString(new AnEntity()).setName("AName").toString()); - } - - @Test - @Order(2) - public void testClassNameAsString() - { - assertEquals("NotAnEntity", new EntityString("NotAnEntity").toString()); - assertEquals("NotAnEntity:AName", new EntityString("NotAnEntity").setName("AName").toString()); - } - - @Test - @Order(3) - public void testType() - { - assertEquals("AnEntity[AType]", new EntityString(new AnEntity()).setType("AType").toString()); - assertEquals("AnEntity[AType]:AName", new EntityString(new AnEntity()).setType("AType").setName("AName").toString()); - assertEquals("AnEntity[NEWS]:AName", new EntityString(new AnEntity()).setType(ChannelType.NEWS).setName("AName").toString()); - } - - @Test - @Order(4) - public void testMetadata() - { - assertEquals("AnEntity(Metadata1)", new EntityString(new AnEntity()).addMetadata(null, "Metadata1").toString()); - assertEquals("AnEntity(MetaKey=Metadata1)", new EntityString(new AnEntity()).addMetadata("MetaKey", "Metadata1").toString()); - assertEquals("AnEntity(MetaKey=42)", new EntityString(new AnEntity()).addMetadata("MetaKey", 42).toString()); - assertEquals("AnEntity(MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new AnEntity()) - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - } - - @Test - @Order(5) - public void testAll() - { - assertEquals("AnEntity:AName(Metadata1)", new EntityString(new AnEntity()) - .setName("AName") - .addMetadata(null, "Metadata1") - .toString()); - assertEquals("AnEntity:AName(MetaKey=Metadata1)", new EntityString(new AnEntity()) - .setName("AName") - .addMetadata("MetaKey", "Metadata1") - .toString()); - assertEquals("AnEntity:AName(MetaKey=42)", new EntityString(new AnEntity()) - .setName("AName") - .addMetadata("MetaKey", 42) - .toString()); - assertEquals("AnEntity:AName(MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new AnEntity()) - .setName("AName") - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - - assertEquals("AnEntity[Type]:AName(Metadata1)", new EntityString(new AnEntity()) - .setName("AName") - .setType("Type") - .addMetadata(null, "Metadata1") - .toString()); - assertEquals("AnEntity[Type]:AName(MetaKey=Metadata1)", new EntityString(new AnEntity()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey", "Metadata1") - .toString()); - assertEquals("AnEntity[Type]:AName(MetaKey=42)", new EntityString(new AnEntity()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey", 42) - .toString()); - assertEquals("AnEntity[Type]:AName(MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new AnEntity()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - } - - @Test - @Order(6) - public void testSimpleSnowflake() - { - assertEquals("ASnowflake(id=42)", new EntityString(new ASnowflake()).toString()); - assertEquals("ASnowflake:AName(id=42)", new EntityString(new ASnowflake()).setName("AName").toString()); - } - - @Test - @Order(7) - public void testTypeSnowflake() - { - assertEquals("ASnowflake[AType](id=42)", new EntityString(new ASnowflake()).setType("AType").toString()); - assertEquals("ASnowflake[AType]:AName(id=42)", new EntityString(new ASnowflake()).setType("AType").setName("AName").toString()); - assertEquals("ASnowflake[NEWS]:AName(id=42)", new EntityString(new ASnowflake()).setType(ChannelType.NEWS).setName("AName").toString()); - } - - @Test - @Order(8) - public void testMetadataSnowflake() - { - assertEquals("ASnowflake(id=42, Metadata1)", new EntityString(new ASnowflake()).addMetadata(null, "Metadata1").toString()); - assertEquals("ASnowflake(id=42, MetaKey=Metadata1)", new EntityString(new ASnowflake()).addMetadata("MetaKey", "Metadata1").toString()); - assertEquals("ASnowflake(id=42, MetaKey=42)", new EntityString(new ASnowflake()).addMetadata("MetaKey", 42).toString()); - assertEquals("ASnowflake(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new ASnowflake()) - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - } - - @Test - @Order(9) - public void testAllSnowflake() - { - assertEquals("ASnowflake:AName(id=42, Metadata1)", new EntityString(new ASnowflake()) - .setName("AName") - .addMetadata(null, "Metadata1") - .toString()); - assertEquals("ASnowflake:AName(id=42, MetaKey=Metadata1)", new EntityString(new ASnowflake()) - .setName("AName") - .addMetadata("MetaKey", "Metadata1") - .toString()); - assertEquals("ASnowflake:AName(id=42, MetaKey=42)", new EntityString(new ASnowflake()) - .setName("AName") - .addMetadata("MetaKey", 42) - .toString()); - assertEquals("ASnowflake:AName(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new ASnowflake()) - .setName("AName") - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - - assertEquals("ASnowflake[Type]:AName(id=42, Metadata1)", new EntityString(new ASnowflake()) - .setName("AName") - .setType("Type") - .addMetadata(null, "Metadata1") - .toString()); - assertEquals("ASnowflake[Type]:AName(id=42, MetaKey=Metadata1)", new EntityString(new ASnowflake()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey", "Metadata1") - .toString()); - assertEquals("ASnowflake[Type]:AName(id=42, MetaKey=42)", new EntityString(new ASnowflake()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey", 42) - .toString()); - assertEquals("ASnowflake[Type]:AName(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)", new EntityString(new ASnowflake()) - .setName("AName") - .setType("Type") - .addMetadata("MetaKey1", "Metadata1") - .addMetadata("MetaKey2", "Metadata2") - .toString()); - } -} diff --git a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java new file mode 100644 index 00000000000..6138ebfd4d7 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java @@ -0,0 +1,82 @@ +/* + * 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.api.requests.Method; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.requests.Requester; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; + +import javax.annotation.Nonnull; +import java.util.concurrent.ScheduledExecutorService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +public class IntegrationTest +{ + @Mock + protected JDAImpl jda; + @Mock + protected Requester requester; + @Mock + protected ScheduledExecutorService scheduledExecutorService; + + private AutoCloseable closeable; + + @BeforeEach + protected final void setup() + { + closeable = openMocks(this); + when(jda.getRequester()).thenReturn(requester); + } + + @AfterEach + protected final void teardown() throws Exception + { + closeable.close(); + } + + @Nonnull + protected DataObject normalizeRequestBody(@Nonnull DataObject body) + { + return body; + } + + protected void assertNextRequestEquals(Method method, String compiledRoute, DataObject expectedBody) + { + doNothing().when(requester).request(assertArg(request -> { + assertThat(request.getRoute().getMethod()).isEqualTo(method); + assertThat(request.getRoute().getCompiledRoute()).isEqualTo(compiledRoute); + + assertThat(request.getRawBody()) + .isNotNull() + .isInstanceOf(DataObject.class); + DataObject body = normalizeRequestBody((DataObject) request.getRawBody()); + + assertThat(body) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(expectedBody); + })); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/PrettyRepresentation.java b/src/test/java/net/dv8tion/jda/test/PrettyRepresentation.java new file mode 100644 index 00000000000..5080ac4815d --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/PrettyRepresentation.java @@ -0,0 +1,34 @@ +/* + * 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.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import org.assertj.core.presentation.StandardRepresentation; + +public class PrettyRepresentation extends StandardRepresentation +{ + @Override + protected String fallbackToStringOf(Object object) + { + if (object instanceof DataObject) + return ((DataObject) object).toPrettyString(); + else if (object instanceof DataArray) + return ((DataArray) object).toPrettyString(); + return object.toString(); + } +} diff --git a/src/test/java/net/dv8tion/jda/ChannelConsistencyTest.java b/src/test/java/net/dv8tion/jda/test/compliance/ChannelConsistencyComplianceTest.java similarity index 72% rename from src/test/java/net/dv8tion/jda/ChannelConsistencyTest.java rename to src/test/java/net/dv8tion/jda/test/compliance/ChannelConsistencyComplianceTest.java index dac29156fa6..f2bab544279 100644 --- a/src/test/java/net/dv8tion/jda/ChannelConsistencyTest.java +++ b/src/test/java/net/dv8tion/jda/test/compliance/ChannelConsistencyComplianceTest.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package net.dv8tion.jda; +package net.dv8tion.jda.test.compliance; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.attribute.IGuildChannelContainer; import net.dv8tion.jda.api.entities.channel.concrete.Category; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.lang.reflect.Method; @@ -30,7 +29,10 @@ import java.util.Set; import java.util.stream.Collectors; -public class ChannelConsistencyTest +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +public class ChannelConsistencyComplianceTest { private static Set getMethodNames(Class clazz) { @@ -39,11 +41,11 @@ private static Set getMethodNames(Class clazz) private static String getChannelName(ChannelType type) { - return type.name().substring(0, 1) + type.name().substring(1).toLowerCase(Locale.ROOT); + return type.name().charAt(0) + type.name().substring(1).toLowerCase(Locale.ROOT); } @Test - public void checkCreateChannelMethods() + void checkCreateChannelMethods() { Set guildMethods = getMethodNames(Guild.class); @@ -57,7 +59,7 @@ public void checkCreateChannelMethods() { String channelName = getChannelName(type); String methodName = "create" + channelName + "Channel"; - Assertions.assertTrue(guildMethods.contains(methodName), "Missing method Guild#" + methodName); + assertThat(guildMethods).contains(methodName); } Set categoryMethods = getMethodNames(Category.class); @@ -66,12 +68,12 @@ public void checkCreateChannelMethods() { String channelName = getChannelName(type); String methodName = "create" + channelName + "Channel"; - Assertions.assertTrue(categoryMethods.contains(methodName), "Missing method Category#" + methodName); + assertThat(categoryMethods).contains(methodName); } } @Test - public void checkCacheAccessMethods() + void checkCacheAccessMethods() { Set jdaMethods = getMethodNames(IGuildChannelContainer.class); Set categoryMethods = getMethodNames(Category.class); @@ -87,22 +89,22 @@ public void checkCacheAccessMethods() String channelName = getChannelName(type); String methodName = "get" + channelName + "ChannelCache"; - Assertions.assertTrue(jdaMethods.contains(methodName), "Missing method IGuildChannelContainer#" + methodName); + assertThat(jdaMethods).contains(methodName); methodName = "get" + channelName + "ChannelsByName"; - Assertions.assertTrue(jdaMethods.contains(methodName), "Missing method IGuildChannelContainer#" + methodName); + assertThat(jdaMethods).contains(methodName); methodName = "get" + channelName + "ChannelById"; - Assertions.assertTrue(jdaMethods.contains(methodName), "Missing method IGuildChannelContainer#" + methodName); + assertThat(jdaMethods).contains(methodName); methodName = "get" + channelName + "Channels"; - Assertions.assertTrue(jdaMethods.contains(methodName), "Missing method IGuildChannelContainer#" + methodName); - Assertions.assertTrue(categoryMethods.contains(methodName), "Missing method Category#" + methodName); + assertThat(jdaMethods).contains(methodName); + assertThat(categoryMethods).contains(methodName); } } @Test - public void checkManagerExists() + void checkManagerExists() { EnumSet editable = EnumSet.complementOf(EnumSet.of( ChannelType.PRIVATE, ChannelType.GROUP, ChannelType.CATEGORY, @@ -114,9 +116,9 @@ public void checkManagerExists() { String channelName = getChannelName(type); - Assertions.assertDoesNotThrow(() -> { - Class.forName("net.dv8tion.jda.api.managers.channel.concrete." + channelName + "ChannelManager"); - }, "Missing manager interface for ChannelType." + type); + assertThatCode(() -> + Class.forName("net.dv8tion.jda.api.managers.channel.concrete." + channelName + "ChannelManager") + ).as("Missing manager interface for ChannelType." + type).doesNotThrowAnyException(); } } } diff --git a/src/test/java/net/dv8tion/jda/EventConsistencyTest.java b/src/test/java/net/dv8tion/jda/test/compliance/EventConsistencyComplianceTest.java similarity index 80% rename from src/test/java/net/dv8tion/jda/EventConsistencyTest.java rename to src/test/java/net/dv8tion/jda/test/compliance/EventConsistencyComplianceTest.java index 73831791de0..8b246c19b7a 100644 --- a/src/test/java/net/dv8tion/jda/EventConsistencyTest.java +++ b/src/test/java/net/dv8tion/jda/test/compliance/EventConsistencyComplianceTest.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package net.dv8tion.jda; +package net.dv8tion.jda.test.compliance; import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.UpdateEvent; import net.dv8tion.jda.api.events.self.SelfUpdateDiscriminatorEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.reflections.Reflections; @@ -31,7 +30,10 @@ import java.util.HashSet; import java.util.Set; -public class EventConsistencyTest +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +public class EventConsistencyComplianceTest { static Set> eventTypes; static Set> excludedTypes; @@ -61,7 +63,9 @@ void testListenerAdapter() continue; String name = type.getSimpleName(); String methodName = "on" + name.substring(0, name.length() - "Event".length()); - Assertions.assertDoesNotThrow(() -> adapter.getDeclaredMethod(methodName, type), "Method for event " + type + " is missing!"); + assertThatCode(() -> adapter.getDeclaredMethod(methodName, type)) + .as("Method for event " + type + " is missing!") + .doesNotThrowAnyException(); found.add(methodName); } @@ -69,7 +73,9 @@ void testListenerAdapter() { if (!method.isAccessible() || method.getAnnotation(Deprecated.class) != null) continue; - Assertions.assertTrue(found.contains(method.getName()), "Dangling method found in ListenerAdapter " + method.getName()); + assertThat(found.contains(method.getName())) + .as("Dangling method found in ListenerAdapter " + method.getName()) + .isTrue(); } } } diff --git a/src/test/java/net/dv8tion/jda/test/data/DataPathTest.java b/src/test/java/net/dv8tion/jda/test/data/DataPathTest.java new file mode 100644 index 00000000000..39a22b3cc2d --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/data/DataPathTest.java @@ -0,0 +1,117 @@ +/* + * 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.data; + +import net.dv8tion.jda.api.exceptions.ParsingException; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.data.DataPath; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +public class DataPathTest +{ + @Test + void testSimple() + { + DataObject object = DataObject.empty() + .put("foo", "10"); // string to also test parsing + + assertThat(DataPath.getInt(object, "foo")).isEqualTo(10); + + DataArray array = DataArray.empty().add("20"); + assertThat(DataPath.getInt(array, "[0]")).isEqualTo(20); + } + + @Test + void testSimpleMissing() + { + DataObject object = DataObject.empty(); + + assertThat(DataPath.getLong(object, "foo?", 0)).isEqualTo(0L); + assertThatThrownBy(() -> DataPath.getLong(object, "foo")) + .hasMessage("Unable to resolve value with key foo to type long: null") + .isInstanceOf(ParsingException.class); + + DataArray array = DataArray.empty(); + + assertThat(DataPath.getBoolean(array, "[0]?", true)).isTrue(); + assertThatThrownBy(() -> DataPath.getObject(array, "[0]")) + .hasMessage("Could not resolve value of type Object at path \"[0]\"") + .isInstanceOf(ParsingException.class); + } + + @Test + void testObjectInArray() + { + DataObject object = DataObject.empty().put("foo", 10.0); + DataArray array = DataArray.empty().add(object); + + assertThat(DataPath.getDouble(array, "[0].foo")).isEqualTo(10.0); + assertThat(DataPath.getDouble(array, "[1]?.foo", 20.0)).isEqualTo(20.0); + + assertThatIndexOutOfBoundsException() + .isThrownBy(() -> DataPath.getDouble(array, "[1].foo")); + } + + @Test + void testArrayInObject() + { + DataArray array = DataArray.empty().add("hello"); + DataObject object = DataObject.empty().put("foo", array); + + assertThat(DataPath.getString(object, "foo[0]")).isEqualTo("hello"); + assertThat(DataPath.getString(object, "foo[1]?", "world")).isEqualTo("world"); + assertThatIndexOutOfBoundsException() + .isThrownBy(() -> DataPath.getString(object, "foo[1]")); + } + + @Test + void testArrayInArray() + { + DataArray array = DataArray.empty().add(DataArray.empty().add("10")); + + assertThat(DataPath.getUnsignedInt(array, "[0][0]")).isEqualTo(10); + assertThat(DataPath.getUnsignedInt(array, "[0][1]?", 20)).isEqualTo(20); + assertThat(DataPath.getUnsignedInt(array, "[1]?[0]", 20)).isEqualTo(20); + assertThatIndexOutOfBoundsException().isThrownBy(() -> DataPath.getUnsignedInt(array, "[0][1]")); + assertThatIndexOutOfBoundsException().isThrownBy(() -> DataPath.getUnsignedInt(array, "[1][0]")); + assertThatThrownBy(() -> DataPath.getUnsignedInt(array, "[0][1]?")) + .hasMessage("Could not resolve value of type unsigned int at path \"[0][1]?\"") + .isInstanceOf(ParsingException.class); + assertThatThrownBy(() -> DataPath.getUnsignedInt(array, "[1]?[0]")) + .hasMessage("Could not resolve value of type unsigned int at path \"[1]?[0]\"") + .isInstanceOf(ParsingException.class); + } + + @Test + void testComplex() + { + DataObject object = DataObject.empty() + .put("array", DataArray.empty() + .add(DataObject.empty() + .put("foo", DataObject.empty() + .put("bar", "hello")))); + + assertThat(DataPath.getString(object, "array[0].foo.bar")).isEqualTo("hello"); + assertThat(DataPath.getString(object, "array[0].wrong?.bar", "world")).isEqualTo("world"); + assertThatThrownBy(() -> DataPath.getString(object, "array[0].wrong?.bar")) + .hasMessage("Could not resolve value of type String at path \"array[0].wrong?.bar\"") + .isInstanceOf(ParsingException.class); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/data/JsonTest.java b/src/test/java/net/dv8tion/jda/test/data/JsonTest.java new file mode 100644 index 00000000000..581e4be5d64 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/data/JsonTest.java @@ -0,0 +1,353 @@ +/* + * 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.data; + +import net.dv8tion.jda.api.exceptions.ParsingException; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.utils.Helpers; +import net.dv8tion.jda.test.PrettyRepresentation; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class JsonTest +{ + private static final String TEST_TIME_STRING = "2024-01-01T12:34:56.789Z"; + private static final OffsetDateTime TEST_TIME = OffsetDateTime.parse(TEST_TIME_STRING); + private static final String testJson = jsonOf( + kv("int", 10), + kv("long", 100), + kv("boolean", true), + kv("string", "test"), + kv("double", 4.2), + kv("time", TEST_TIME_STRING) + ); + private static final String testJsonArray = Stream + .of(10, 100L, true, "\"test\"", 4.2, 5.2f) + .map(String::valueOf) + .collect(Collectors.joining(",\n", "[", "]")); + + private static final byte[] simpleEtfArray = {-125, 108, 0, 0, 0, 6, 97, 10, 97, 100, 100, 0, 4, 116, 114, 117, 101, 109, 0, 0, 0, 4, 116, 101, 115, 116, 70, 64, 16, -52, -52, -52, -52, -52, -51, 70, 64, 20, -52, -52, -52, -52, -52, -51, 106}; + private static final byte[] complexEtfArray = {-125, 108, 0, 0, 0, 6, 109, 0, 0, 0, 3, 111, 110, 101, 97, 2, 70, 64, 11, -103, -103, -103, -103, -103, -102, 97, 7, 116, 0, 0, 0, 2, 109, 0, 0, 0, 5, 102, 105, 114, 115, 116, 109, 0, 0, 0, 5, 101, 105, 103, 104, 116, 109, 0, 0, 0, 6, 115, 101, 99, 111, 110, 100, 106, 108, 0, 0, 0, 2, 109, 0, 0, 0, 4, 110, 105, 110, 101, 116, 0, 0, 0, 1, 109, 0, 0, 0, 3, 107, 101, 121, 109, 0, 0, 0, 3, 116, 101, 110, 106, 106}; + + @Nested + class DataObjectTest + { + @Test + void testParse() + { + DataObject object = DataObject.fromJson(testJson); + assertThat(object.getInt("int")).isEqualTo(10); + assertThat(object.getLong("long")).isEqualTo(100); + assertThat(object.getDouble("double")).isEqualTo(4.2); + assertThat(object.getBoolean("boolean")).isTrue(); + assertThat(object.getString("string")).isEqualTo("test"); + assertThat(object.getOffsetDateTime("time")).isEqualTo(TEST_TIME); + } + + @Test + void testCoerce() + { + DataObject data = DataObject.empty() + .put("stringified_int", "42") + .put("stringified_boolean", "true") + .put("stringified_long", "86699011792191488") + .put("stringified_datetime", TEST_TIME_STRING) + .put("stringified_double", "123.456"); + + assertThat(data.toMap()).containsOnly( + entry("stringified_int", "42"), + entry("stringified_boolean", "true"), + entry("stringified_long", "86699011792191488"), + entry("stringified_datetime", TEST_TIME_STRING), + entry("stringified_double", "123.456") + ); + + assertThat(data.getInt("stringified_int")).isEqualTo(42); + assertThat(data.getBoolean("stringified_boolean")).isTrue(); + assertThat(data.getLong("stringified_long")).isEqualTo(86699011792191488L); + assertThat(data.getUnsignedLong("stringified_long")).isEqualTo(86699011792191488L); + assertThat(data.getDouble("stringified_double")).isEqualTo(123.456); + assertThat(data.getString("stringified_datetime")).isEqualTo(TEST_TIME_STRING); + } + + @Test + void testFallback() + { + DataObject data = DataObject.fromJson(jsonOf()); + + assertThat(data).isEqualTo(DataObject.empty()); + assertThat(data).hasToString("{}"); + + assertThat(data.isNull("key")).isTrue(); + assertThat(data.hasKey("key")).isFalse(); + + assertThat(data.getDouble("key", 5.3)).isEqualTo(5.3); + assertThat(data.getInt("key", 4)).isEqualTo(4); + assertThat(data.getUnsignedInt("key", 7)).isEqualTo(7); + assertThat(data.getLong("key", 123L)).isEqualTo(123); + assertThat(data.getUnsignedLong("key", 321L)).isEqualTo(321L); + assertThat(data.getBoolean("key")).isFalse(); + assertThat(data.getBoolean("key", true)).isTrue(); + assertThat(data.getOffsetDateTime("key", TEST_TIME)).isEqualTo(TEST_TIME); + assertThat(data.opt("key")).isEmpty(); + assertThat(data.optObject("key")).isEmpty(); + assertThat(data.optArray("key")).isEmpty(); + } + + @Test + void testJsonToString() + { + DataObject object = DataObject.fromJson(testJson); + String result = object.toString(); + DataObject symmetric = DataObject.fromJson(result); + + assertThat(symmetric.toMap()).isNotSameAs(object.toMap()); + assertThat(symmetric.toMap()).isEqualTo(object.toMap()); + assertThat(symmetric.toMap()).hasSize(6); + assertThat(symmetric.toMap()).containsOnly( + entry("int", 10), + entry("long", 100), + entry("boolean", true), + entry("string", "test"), + entry("double", 4.2), + entry("time", TEST_TIME_STRING) + ); + } + + @Test + void testFactories() + { + DataObject reference = DataObject.fromJson(testJson); + + assertThat(DataObject.fromJson(testJson.getBytes(StandardCharsets.UTF_8))) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(reference); + assertThat(DataObject.fromJson(new StringReader(testJson))) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(reference); + assertThat(DataObject.fromJson(new ByteArrayInputStream(testJson.getBytes(StandardCharsets.UTF_8)))) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(reference); + } + } + + @Nested + class DataArrayTest + { + @Test + void testParse() + { + DataArray object = DataArray.fromJson(testJsonArray); + assertThat(object.getInt(0)).isEqualTo(10); + assertThat(object.getLong(1)).isEqualTo(100); + assertThat(object.getBoolean(2)).isTrue(); + assertThat(object.getString(3)).isEqualTo("test"); + assertThat(object.getDouble(4)).isEqualTo(4.2); + assertThat(object.getDouble(5)).isEqualTo(5.2); + } + + @Test + void testCoerce() + { + DataArray array = DataArray.empty() + .add("42") + .add("true") + .add("86699011792191488") + .add(TEST_TIME_STRING) + .add("123.456"); + + assertThat(array.toList()).containsExactly( + "42", "true", "86699011792191488", TEST_TIME_STRING, "123.456" + ); + + assertThat(array.getInt(0)).isEqualTo(42); + assertThat(array.getBoolean(1)).isTrue(); + assertThat(array.getLong(2)).isEqualTo(86699011792191488L); + assertThat(array.getUnsignedLong(2)).isEqualTo(86699011792191488L); + assertThat(array.getString(3)).isEqualTo(TEST_TIME_STRING); + assertThat(array.getDouble(4)).isEqualTo(123.456); + } + + @Test + void testFallback() + { + DataArray data = DataArray.fromJson("[]"); + + assertThat(data).isEqualTo(DataArray.empty()); + assertThat(data).hasToString("[]"); + + assertThat(data.isNull(0)).isTrue(); + assertThat(data.length()).isEqualTo(0); + assertThat(data.isEmpty()).isTrue(); + + assertThat(data.getDouble(0, 5.3)).isEqualTo(5.3); + assertThat(data.getInt(0, 4)).isEqualTo(4); + assertThat(data.getUnsignedInt(0, 7)).isEqualTo(7); + assertThat(data.getLong(0, 123L)).isEqualTo(123); + assertThat(data.getUnsignedLong(0, 321L)).isEqualTo(321L); + assertThat(data.getBoolean(0)).isFalse(); + assertThat(data.getBoolean(0, true)).isTrue(); + assertThat(data.getOffsetDateTime(0, TEST_TIME)).isEqualTo(TEST_TIME); + } + + @Test + void testJsonToString() + { + DataArray object = DataArray.fromJson(testJsonArray); + String result = object.toString(); + DataArray symmetric = DataArray.fromJson(result); + + assertThat(symmetric.toList()).isNotSameAs(object.toList()); + assertThat(symmetric.toList()).isEqualTo(object.toList()); + assertThat(symmetric.toList()).hasSize(6); + assertThat(symmetric.toList()).containsExactly( + 10, 100, true, "test", 4.2, 5.2 + ); + } + + @Test + void testStream() + { + DataArray intArray = IntStream.range(0, 3).boxed().collect(Helpers.toDataArray()); + assertThat(intArray.stream(DataArray::getInt)) + .containsExactly(0, 1, 2); + assertThat(intArray.stream(DataArray::getLong)) + .containsExactly(0L, 1L, 2L); + + DataArray doubleArray = DoubleStream.of(0.1, 0.5, 1.2, 4.2).boxed().collect(Helpers.toDataArray()); + assertThat(doubleArray.stream(DataArray::getDouble)) + .containsExactly(0.1, 0.5, 1.2, 4.2); + + DataArray stringArray = DataArray.empty().add("foo").add("bar"); + assertThat(stringArray.stream(DataArray::getString)) + .containsExactly("foo", "bar"); + + DataArray polyTypedArray = DataArray.empty().add(1).add(2.3).add("four"); + assertThatThrownBy(() -> polyTypedArray.stream(DataArray::getInt).toArray()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> polyTypedArray.stream(DataArray::getDouble).toArray()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> polyTypedArray.stream(DataArray::getObject).toArray()) + .isInstanceOf(ParsingException.class) + .hasMessage("Cannot parse value for index 0 into type Map: 1 instance of Integer"); + assertThat(polyTypedArray.stream(DataArray::getString)) + .containsExactly("1", "2.3", "four"); + + DataArray objectArray = DataArray.empty() + .add(DataObject.empty().put("foo", 1)) + .add(DataObject.empty().put("foo", 2)); + assertThat(objectArray.stream(DataArray::getObject).map(obj -> obj.getInt("foo"))) + .containsExactly(1, 2); + + objectArray.add(DataArray.empty()); + assertThatThrownBy(() -> + objectArray.stream(DataArray::getObject).map(obj -> obj.getInt("foo")).toArray() + ) + .isInstanceOf(ParsingException.class) + .hasMessage("Cannot parse value for index 2 into type Map: [] instance of ArrayList"); + } + + @Test + void testFactories() + { + assertThat(DataArray.fromJson(new StringReader(testJsonArray))) + .withRepresentation(new PrettyRepresentation()) + .containsExactly(10, 100, true, "test", 4.2, 5.2); + assertThat(DataArray.fromJson(new ByteArrayInputStream(testJsonArray.getBytes(StandardCharsets.UTF_8)))) + .withRepresentation(new PrettyRepresentation()) + .containsExactly(10, 100, true, "test", 4.2, 5.2); + assertThat(DataArray.fromJson(testJsonArray)) + .withRepresentation(new PrettyRepresentation()) + .containsExactly(10, 100, true, "test", 4.2, 5.2); + + List inputList = Arrays.asList(10, 100L, true, "test", 4.2, 5.2f); + DataArray array = DataArray.fromCollection(inputList); + assertThat(array) + .withRepresentation(new PrettyRepresentation()) + .as("Do not lose types from input collections") + .containsExactly(10, 100L, true, "test", 4.2, 5.2f); + assertThat(array.toList()).isNotSameAs(inputList); + assertThat(array.toList()).isEqualTo(inputList); + assertThat(array.add("foo").toList()).isNotEqualTo(inputList); + + assertThat(DataArray.fromETF(simpleEtfArray)) + .withRepresentation(new PrettyRepresentation()) + .containsExactly(10, 100, true, "test", 4.2, 5.2); + assertThat(DataArray.fromETF(simpleEtfArray).toETF()) + .isEqualTo(simpleEtfArray); + } + + @Test + void testExTerm() + { + DataArray array = DataArray.empty() + .add("one") + .add(2) + .add(3.45) + .add(7L) + .add(DataObject.empty() + .put("first", "eight") + .put("second", DataArray.empty())) + .add(DataArray.empty() + .add("nine") + .add(DataObject.empty() + .put("key", "ten"))); + + assertThat(array).hasSize(6); + assertThat(array.toETF()).isEqualTo(complexEtfArray); + assertThat(DataArray.fromETF(complexEtfArray).toETF()) + .isEqualTo(complexEtfArray); + assertThat(DataArray.fromETF(complexEtfArray).toPrettyString()) + .isEqualToIgnoringWhitespace(array.toPrettyString()); + } + } + + private static Map.Entry entry(K key, V value) + { + return new AbstractMap.SimpleEntry<>(key, value); + } + + private static String kv(String key, Object value) + { + return String.format(Locale.ROOT, "\"%s\": %s", key, value); + } + + private static String kv(String key, String value) + { + return String.format(Locale.ROOT, "\"%s\": \"%s\"", key, value); + } + + private static String jsonOf(String... keyValueMapping) + { + return Stream.of(keyValueMapping) + .collect(Collectors.joining(",\n", "{", "}")); + } +} diff --git a/src/test/java/net/dv8tion/jda/entities/ActivityTest.java b/src/test/java/net/dv8tion/jda/test/entities/ActivityTest.java similarity index 69% rename from src/test/java/net/dv8tion/jda/entities/ActivityTest.java rename to src/test/java/net/dv8tion/jda/test/entities/ActivityTest.java index d6150f9217b..96565c900d6 100644 --- a/src/test/java/net/dv8tion/jda/entities/ActivityTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/ActivityTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.entities; +package net.dv8tion.jda.test.entities; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.RichPresence; @@ -22,11 +22,12 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.managers.PresenceImpl; +import net.dv8tion.jda.test.PrettyRepresentation; import org.junit.jupiter.api.Test; import java.util.Arrays; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; public class ActivityTest { @@ -43,8 +44,15 @@ private static DataObject formatActivity(int type, String name, String state) return json; } + private static void assertEquals(DataObject expected, DataObject actual) + { + assertThat(actual) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(expected); + } + @Test - public void activitySerializationTest() + void activitySerializationTest() { assertEquals( formatActivity(0, "playing test", null), @@ -102,7 +110,7 @@ public void activitySerializationTest() } @Test - public void activityBasicDeserializationTest() + void activityBasicDeserializationTest() { Activity activity = EntityBuilder.createActivity( DataObject.empty() @@ -110,15 +118,15 @@ public void activityBasicDeserializationTest() .put("name", "Games") ); - assertFalse(activity.isRich()); - assertEquals(Activity.ActivityType.PLAYING, activity.getType()); - assertEquals("Games", activity.getName());; - assertNull(activity.getState()); - assertNull(activity.getUrl()); + assertThat(activity.isRich()).isFalse(); + assertThat(activity.getType()).isEqualTo(Activity.ActivityType.PLAYING); + assertThat(activity.getName()).isEqualTo("Games"); + assertThat(activity.getState()).isNull(); + assertThat(activity.getUrl()).isNull(); } @Test - public void activityRichDeserializationTest() + void activityRichDeserializationTest() { Activity activity = EntityBuilder.createActivity( DataObject.empty() @@ -127,11 +135,11 @@ public void activityRichDeserializationTest() .put("state", "Active") ); - assertFalse(activity.isRich(), "isRich()"); - assertEquals(Activity.ActivityType.PLAYING, activity.getType()); - assertEquals("Games", activity.getName());; - assertEquals("Active", activity.getState()); - assertNull(activity.getUrl(), "url"); + assertThat(activity.isRich()).isFalse(); + assertThat(activity.getType()).isEqualTo(Activity.ActivityType.PLAYING); + assertThat(activity.getName()).isEqualTo("Games");; + assertThat(activity.getState()).isEqualTo("Active"); + assertThat(activity.getUrl()).isNull(); activity = EntityBuilder.createActivity( DataObject.empty() @@ -155,30 +163,31 @@ public void activityRichDeserializationTest() ); RichPresence rich = activity.asRichPresence(); - assertEquals(activity, rich); + assertThat(rich).isNotNull(); + assertThat(rich).isEqualTo(activity); - assertEquals(Activity.ActivityType.PLAYING, rich.getType()); - assertEquals("The Best Game Ever", rich.getName()); - assertEquals("In a Group", rich.getState()); + assertThat(rich.getType()).isEqualTo(Activity.ActivityType.PLAYING); + assertThat(rich.getName()).isEqualTo("The Best Game Ever"); + assertThat(rich.getState()).isEqualTo("In a Group"); - assertNotNull(rich.getParty(), "party"); - assertEquals("1234", rich.getParty().getId()); - assertEquals(3, rich.getParty().getSize()); - assertEquals(6, rich.getParty().getMax()); + assertThat(rich.getParty()).isNotNull(); + assertThat(rich.getParty().getId()).isEqualTo("1234"); + assertThat(rich.getParty().getSize()).isEqualTo(3); + assertThat(rich.getParty().getMax()).isEqualTo(6); - assertNotNull(rich.getTimestamps()); - assertEquals(1507665886, rich.getTimestamps().getStart()); - assertEquals(1507666000, rich.getTimestamps().getEnd()); + assertThat(rich.getTimestamps()).isNotNull(); + assertThat(rich.getTimestamps().getStart()).isEqualTo(1507665886); + assertThat(rich.getTimestamps().getEnd()).isEqualTo(1507666000); - assertNotNull(rich.getLargeImage(), "assets.large_image"); - assertEquals("canary-large", rich.getLargeImage().getKey()); - assertNull(rich.getLargeImage().getText(), "assets.large_text"); + assertThat(rich.getLargeImage()).isNotNull(); + assertThat(rich.getLargeImage().getKey()).isEqualTo("canary-large"); + assertThat(rich.getLargeImage().getText()).isNull(); - assertNotNull(rich.getSmallImage(), "assets.small_image"); - assertEquals("ptb-large", rich.getSmallImage().getKey()); - assertEquals("Small icon", rich.getSmallImage().getText()); + assertThat(rich.getSmallImage()).isNotNull(); + assertThat(rich.getSmallImage().getKey()).isEqualTo("ptb-large"); + assertThat(rich.getSmallImage().getText()).isEqualTo("Small icon"); - assertEquals("4b2fdce12f639de8bfa7e3591b71a0d679d7c93f", rich.getSessionId()); - assertEquals("e7eb30d2ee025ed05c71ea495f770b76454ee4e0", rich.getSyncId()); + assertThat(rich.getSessionId()).isEqualTo("4b2fdce12f639de8bfa7e3591b71a0d679d7c93f"); + assertThat(rich.getSyncId()).isEqualTo("e7eb30d2ee025ed05c71ea495f770b76454ee4e0"); } } diff --git a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java new file mode 100644 index 00000000000..8d4bb8ec336 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.entities; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.test.PrettyRepresentation; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +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"; + private static final String AUTHOR_TEXT = "Author Text"; + private static final String AUTHOR_URL = "https://example.com/author"; + private static final String AUTHOR_ICON = "https://example.com/author_icon"; + private static final String FOOTER_TEXT = "Footer Text"; + private static final String FOOTER_ICON = "https://example.com/footer_icon"; + private static final String IMAGE_URL = "https://example.com/image"; + private static final String THUMBNAIL_URL = "https://example.com/thumbnail"; + private static final String FIELD_1_NAME = "Field 1"; + private static final String FIELD_1_TEXT = "Field 1 Text"; + private static final String FIELD_2_NAME = "Field 2"; + private static final String FIELD_2_TEXT = "Field 2 Text"; + private static final String FIELD_3_NAME = "Field 3"; + private static final String FIELD_3_TEXT = "Field 3 Text"; + + @Test + void testEmbedSerialization() + { + MessageEmbed embed = getTestEmbed(); + + MessageEmbed dataEmbed = EmbedBuilder.fromData(embed.toData()).build(); + + assertThat(dataEmbed).isNotSameAs(embed); + assertThat(dataEmbed).isEqualTo(embed); + + assertThat(dataEmbed.toData()) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty() + .put("title", TITLE_TEXT) + .put("url", TITLE_URL) + .put("description", DESCRIPTION_TEXT) + .put("image", DataObject.empty() + .put("url", IMAGE_URL)) + .put("thumbnail", DataObject.empty() + .put("url", THUMBNAIL_URL)) + .put("footer", DataObject.empty() + .put("icon_url", FOOTER_ICON) + .put("text", FOOTER_TEXT)) + .put("author", DataObject.empty() + .put("icon_url", AUTHOR_ICON) + .put("name", AUTHOR_TEXT) + .put("url", AUTHOR_URL)) + .put("fields", DataArray.empty() + .add(DataObject.empty() + .put("inline", true) + .put("name", FIELD_1_NAME) + .put("value", FIELD_1_TEXT)) + .add(DataObject.empty() + .put("inline", false) + .put("name", FIELD_2_NAME) + .put("value", FIELD_2_TEXT)) + .add(DataObject.empty() + .put("inline", true) + .put("name", FIELD_3_NAME) + .put("value", FIELD_3_TEXT)))); + } + + @NotNull + private static MessageEmbed getTestEmbed() + { + return new EmbedBuilder() + .setDescription(DESCRIPTION_TEXT) + .setTitle(TITLE_TEXT, TITLE_URL) + .setAuthor(AUTHOR_TEXT, AUTHOR_URL, AUTHOR_ICON) + .setFooter(FOOTER_TEXT, FOOTER_ICON) + .setImage(IMAGE_URL) + .setThumbnail(THUMBNAIL_URL) + .addField(FIELD_1_NAME, FIELD_1_TEXT, true) + .addField(FIELD_2_NAME, FIELD_2_TEXT, false) + .addField(FIELD_3_NAME, FIELD_3_TEXT, true) + .build(); + } +} diff --git a/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java b/src/test/java/net/dv8tion/jda/test/entities/channel/ChannelCacheViewTest.java similarity index 83% rename from src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java rename to src/test/java/net/dv8tion/jda/test/entities/channel/ChannelCacheViewTest.java index 70b4bd4eac6..9391e395b14 100644 --- a/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/channel/ChannelCacheViewTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.entities.channel; +package net.dv8tion.jda.test.entities.channel; import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; @@ -40,7 +40,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; class ChannelCacheViewTest @@ -182,10 +182,10 @@ void testSortedStream() { SortedChannelCacheView cache = getMockedGuildCache(); String output = toListString(cache.stream()); - assertEquals(VALID_SORT_ORDER, output); + assertThat(output).isEqualTo(VALID_SORT_ORDER); output = toListString(cache.parallelStream()); - assertEquals(VALID_SORT_ORDER, output); + assertThat(output).isEqualTo(VALID_SORT_ORDER); } @Test @@ -193,13 +193,13 @@ void testUnsortedStream() { SortedChannelCacheView cache = getMockedGuildCache(); String output = toListString(cache.streamUnordered()); - assertNotEquals(VALID_SORT_ORDER, output); + assertThat(output).isNotEqualTo(VALID_SORT_ORDER); output = toListString(cache.parallelStreamUnordered()); - assertNotEquals(VALID_SORT_ORDER, output); + assertThat(output).isNotEqualTo(VALID_SORT_ORDER); output = cache.applyStream(ChannelCacheViewTest::toListString); - assertNotEquals(VALID_SORT_ORDER, output); + assertThat(output).isNotEqualTo(VALID_SORT_ORDER); } @Test @@ -208,15 +208,16 @@ void testAsListWorks() SortedChannelCacheView cache = getMockedGuildCache(); String output = toListString(cache.asList().stream()); - assertEquals(VALID_SORT_ORDER, output); + assertThat(output).isEqualTo(VALID_SORT_ORDER); SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); List fromOfType = voiceView.asList(); List voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toList())); - assertEquals(voiceView.size(), voiceChannelFilter.size()); - assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); - assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + assertThat(voiceChannelFilter) + .hasSameSizeAs(voiceView); + assertThat(voiceChannelFilter) + .hasSameElementsAs(fromOfType); } @Test @@ -225,15 +226,16 @@ void testAsSetWorks() SortedChannelCacheView cache = getMockedGuildCache(); String output = toListString(cache.asSet().stream()); - assertEquals(VALID_SORT_ORDER, output); + assertThat(output).isEqualTo(VALID_SORT_ORDER); SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); Set fromOfType = voiceView.asSet(); Set voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toSet())); - assertEquals(voiceView.size(), voiceChannelFilter.size()); - assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); - assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + assertThat(voiceChannelFilter) + .hasSize((int) voiceView.size()); + assertThat(voiceChannelFilter) + .hasSameElementsAs(fromOfType); } @Test @@ -242,12 +244,12 @@ void testSizeWorks() SortedChannelCacheView cache = getMockedGuildCache(); NavigableSet asSet = cache.asSet(); - assertEquals(asSet.size(), cache.size()); + assertThat(cache).hasSameSizeAs(asSet); SortedChannelCacheView ofTypeMessage = cache.ofType(GuildMessageChannel.class); Set filterMessageType = asSet.stream().filter(GuildMessageChannel.class::isInstance).collect(Collectors.toSet()); - assertEquals(filterMessageType.size(), ofTypeMessage.size()); + assertThat(ofTypeMessage).hasSameSizeAs(filterMessageType); } @Test @@ -255,16 +257,22 @@ void testEmptyWorks() { SortedChannelCacheView empty = new SortedChannelCacheViewImpl<>(GuildChannel.class); - assertTrue(empty.isEmpty(), "New cache must be empty"); + assertThat(empty).isEmpty(); SortedChannelCacheViewImpl filled = getMockedGuildCache(); - assertFalse(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must not be empty before remove"); + assertThat(filled.ofType(GuildMessageChannel.class)) + .as("Filtered cache must not be empty before remove") + .isNotEmpty(); filled.removeIf(GuildMessageChannel.class, (c) -> true); - assertFalse(filled.isEmpty(), "Filled cache must not be empty"); - assertTrue(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must be empty"); + assertThat(filled) + .as("Filled cache must not be empty") + .isNotEmpty(); + assertThat(filled.ofType(GuildMessageChannel.class)) + .as("Filtered cache must be empty") + .isEmpty(); } @Test @@ -276,17 +284,21 @@ void testRemoveWorks() GuildChannel textWithoutParent = getByName.get().get(0); - assertSame(textWithoutParent, cache.remove(textWithoutParent), "Remove returns instance"); - assertTrue(getByName.get().isEmpty(), "Channel should be removed"); + assertThat(textWithoutParent) + .as("Remove returns instance") + .isSameAs(cache.remove(textWithoutParent)); + assertThat(getByName.get()) + .as("Channel should be removed") + .isEmpty(); List messageChannels = getOfType.get(); - assertFalse(messageChannels.isEmpty(), "Message channels should not be removed"); + assertThat(messageChannels).isNotEmpty(); cache.removeIf(GuildChannel.class, GuildMessageChannel.class::isInstance); messageChannels = getOfType.get(); - assertTrue(messageChannels.isEmpty(), "Message channels should be removed"); + assertThat(messageChannels).isEmpty(); } } diff --git a/src/test/java/net/dv8tion/jda/entitystring/ASnowflake.java b/src/test/java/net/dv8tion/jda/test/entitystring/ASnowflake.java similarity index 95% rename from src/test/java/net/dv8tion/jda/entitystring/ASnowflake.java rename to src/test/java/net/dv8tion/jda/test/entitystring/ASnowflake.java index 4370e113c0b..c290926ac03 100644 --- a/src/test/java/net/dv8tion/jda/entitystring/ASnowflake.java +++ b/src/test/java/net/dv8tion/jda/test/entitystring/ASnowflake.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.entitystring; +package net.dv8tion.jda.test.entitystring; import net.dv8tion.jda.api.entities.ISnowflake; diff --git a/src/test/java/net/dv8tion/jda/entitystring/AnEntity.java b/src/test/java/net/dv8tion/jda/test/entitystring/AnEntity.java similarity index 94% rename from src/test/java/net/dv8tion/jda/entitystring/AnEntity.java rename to src/test/java/net/dv8tion/jda/test/entitystring/AnEntity.java index e3aac7407a6..c581091f09b 100644 --- a/src/test/java/net/dv8tion/jda/entitystring/AnEntity.java +++ b/src/test/java/net/dv8tion/jda/test/entitystring/AnEntity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.dv8tion.jda.entitystring; +package net.dv8tion.jda.test.entitystring; public class AnEntity { diff --git a/src/test/java/net/dv8tion/jda/test/entitystring/EntityStringTest.java b/src/test/java/net/dv8tion/jda/test/entitystring/EntityStringTest.java new file mode 100644 index 00000000000..cde9d3edaaa --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entitystring/EntityStringTest.java @@ -0,0 +1,206 @@ +/* + * 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.entitystring; + +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.internal.utils.EntityString; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EntityStringTest +{ + @Test + @Order(1) + void testSimple() + { + assertThat(new EntityString(new AnEntity())) + .hasToString("AnEntity"); + assertThat(new EntityString(new AnEntity()).setName("AName")) + .hasToString("AnEntity:AName"); + } + + @Test + @Order(2) + void testClassNameAsString() + { + assertThat(new EntityString("NotAnEntity")) + .hasToString("NotAnEntity"); + assertThat(new EntityString("NotAnEntity").setName("AName")) + .hasToString("NotAnEntity:AName"); + } + + @Test + @Order(3) + void testType() + { + assertThat(new EntityString(new AnEntity()).setType("AType")) + .hasToString("AnEntity[AType]"); + assertThat(new EntityString(new AnEntity()).setType("AType").setName("AName")) + .hasToString("AnEntity[AType]:AName"); + assertThat(new EntityString(new AnEntity()).setType(ChannelType.NEWS).setName("AName")) + .hasToString("AnEntity[NEWS]:AName"); + } + + @Test + @Order(4) + void testMetadata() + { + assertThat(new EntityString(new AnEntity()).addMetadata(null, "Metadata1")) + .hasToString("AnEntity(Metadata1)"); + assertThat(new EntityString(new AnEntity()).addMetadata("MetaKey", "Metadata1")) + .hasToString("AnEntity(MetaKey=Metadata1)"); + assertThat(new EntityString(new AnEntity()).addMetadata("MetaKey", 42)) + .hasToString("AnEntity(MetaKey=42)"); + assertThat(new EntityString(new AnEntity()) + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("AnEntity(MetaKey1=Metadata1, MetaKey2=Metadata2)"); + } + + @Test + @Order(5) + void testAll() + { + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .addMetadata(null, "Metadata1")) + .hasToString("AnEntity:AName(Metadata1)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .addMetadata("MetaKey", "Metadata1")) + .hasToString("AnEntity:AName(MetaKey=Metadata1)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .addMetadata("MetaKey", 42)) + .hasToString("AnEntity:AName(MetaKey=42)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("AnEntity:AName(MetaKey1=Metadata1, MetaKey2=Metadata2)"); + + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .setType("Type") + .addMetadata(null, "Metadata1")) + .hasToString("AnEntity[Type]:AName(Metadata1)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey", "Metadata1")) + .hasToString("AnEntity[Type]:AName(MetaKey=Metadata1)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey", 42)) + .hasToString("AnEntity[Type]:AName(MetaKey=42)"); + assertThat(new EntityString(new AnEntity()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("AnEntity[Type]:AName(MetaKey1=Metadata1, MetaKey2=Metadata2)"); + } + + @Test + @Order(6) + void testSimpleSnowflake() + { + assertThat(new EntityString(new ASnowflake())) + .hasToString("ASnowflake(id=42)"); + assertThat(new EntityString(new ASnowflake()).setName("AName")) + .hasToString("ASnowflake:AName(id=42)"); + } + + @Test + @Order(7) + void testTypeSnowflake() + { + assertThat(new EntityString(new ASnowflake()).setType("AType")) + .hasToString("ASnowflake[AType](id=42)"); + assertThat(new EntityString(new ASnowflake()).setType("AType").setName("AName")) + .hasToString("ASnowflake[AType]:AName(id=42)"); + assertThat(new EntityString(new ASnowflake()).setType(ChannelType.NEWS).setName("AName")) + .hasToString("ASnowflake[NEWS]:AName(id=42)"); + } + + @Test + @Order(8) + void testMetadataSnowflake() + { + assertThat(new EntityString(new ASnowflake()).addMetadata(null, "Metadata1")) + .hasToString("ASnowflake(id=42, Metadata1)"); + assertThat(new EntityString(new ASnowflake()).addMetadata("MetaKey", "Metadata1")) + .hasToString("ASnowflake(id=42, MetaKey=Metadata1)"); + assertThat(new EntityString(new ASnowflake()).addMetadata("MetaKey", 42)) + .hasToString("ASnowflake(id=42, MetaKey=42)"); + assertThat(new EntityString(new ASnowflake()) + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("ASnowflake(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)"); + } + + @Test + @Order(9) + void testAllSnowflake() + { + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .addMetadata(null, "Metadata1")) + .hasToString("ASnowflake:AName(id=42, Metadata1)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .addMetadata("MetaKey", "Metadata1")) + .hasToString("ASnowflake:AName(id=42, MetaKey=Metadata1)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .addMetadata("MetaKey", 42)) + .hasToString("ASnowflake:AName(id=42, MetaKey=42)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("ASnowflake:AName(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)"); + + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .setType("Type") + .addMetadata(null, "Metadata1")) + .hasToString("ASnowflake[Type]:AName(id=42, Metadata1)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey", "Metadata1")) + .hasToString("ASnowflake[Type]:AName(id=42, MetaKey=Metadata1)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey", 42)) + .hasToString("ASnowflake[Type]:AName(id=42, MetaKey=42)"); + assertThat(new EntityString(new ASnowflake()) + .setName("AName") + .setType("Type") + .addMetadata("MetaKey1", "Metadata1") + .addMetadata("MetaKey2", "Metadata2")) + .hasToString("ASnowflake[Type]:AName(id=42, MetaKey1=Metadata1, MetaKey2=Metadata2)"); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java new file mode 100644 index 00000000000..1dae4f186ce --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java @@ -0,0 +1,238 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +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.test.PrettyRepresentation; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class CommandDataTest +{ + private static DataObject defaultCommand() + { + return DataObject.empty() + .put("type", 1) + .put("dm_permission", true) + .put("name_localizations", DataObject.empty()) + .put("description_localizations", DataObject.empty()) + .put("nsfw", false) + .put("default_member_permissions", null) + .put("options", DataArray.empty()); + } + + private static DataObject defaultOption(OptionType type, String name, String description) + { + return DataObject.empty() + .put("type", type.getKey()) + .put("name", name) + .put("description", description) + .put("required", false) + .put("autocomplete", false) + .put("name_localizations", DataObject.empty()) + .put("description_localizations", DataObject.empty()); + } + + @Test + void testNormal() + { + CommandData command = new CommandDataImpl("ban", "Ban a user from this server") + .setGuildOnly(true) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS)) + .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required + .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false + .addOption(OptionType.INTEGER, "days", "The duration of the ban", false); // test with explicit false + + DataObject data = command.toData(); + + assertThat(data) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(defaultCommand() + .put("type", 1) + .put("name", "ban") + .put("description", "Ban a user from this server") + .put("dm_permission", false) + .put("default_member_permissions", "4") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.USER, "user", "The user to ban").put("required", true)) + .add(defaultOption(OptionType.STRING, "reason", "The ban reason")) + .add(defaultOption(OptionType.INTEGER, "days", "The duration of the ban")))); + } + + @Test + void testDefaultMemberPermissions() + { + CommandData command = new CommandDataImpl("ban", "Ban a user from this server") + .setDefaultPermissions(DefaultMemberPermissions.DISABLED); + + assertThat(command.toData().get("default_member_permissions")).isEqualTo("0"); + + command.setDefaultPermissions(DefaultMemberPermissions.ENABLED); + + assertThat(command.toData().opt("default_member_permissions")).isEmpty(); + } + + @Test + void testSubcommand() + { + CommandDataImpl command = new CommandDataImpl("mod", "Moderation commands") + .addSubcommands(new SubcommandData("ban", "Ban a user from this server") + .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required + .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false + .addOption(OptionType.INTEGER, "days", "The duration of the ban", false)); // test with explicit false + + assertThat(command.toData()) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(defaultCommand() + .put("name", "mod") + .put("description", "Moderation commands") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.SUB_COMMAND, "ban", "Ban a user from this server") + .remove("autocomplete") + .remove("required") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.USER, "user", "The user to ban").put("required", true)) + .add(defaultOption(OptionType.STRING, "reason", "The ban reason")) + .add(defaultOption(OptionType.INTEGER, "days", "The duration of the ban")))))); + } + + @Test + void testSubcommandGroup() + { + CommandDataImpl command = new CommandDataImpl("mod", "Moderation commands") + .addSubcommandGroups(new SubcommandGroupData("ban", "Ban or unban a user from this server") + .addSubcommands(new SubcommandData("add", "Ban a user from this server") + .addOption(OptionType.USER, "user", "The user to ban", true) // required before non-required + .addOption(OptionType.STRING, "reason", "The ban reason") // test that default is false + .addOption(OptionType.INTEGER, "days", "The duration of the ban", false))); // test with explicit false + + assertThat(command.toData()) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(defaultCommand() + .put("name", "mod") + .put("description", "Moderation commands") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.SUB_COMMAND_GROUP, "ban", "Ban or unban a user from this server") + .remove("autocomplete") + .remove("required") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.SUB_COMMAND, "add", "Ban a user from this server") + .remove("autocomplete") + .remove("required") + .put("options", DataArray.empty() + .add(defaultOption(OptionType.USER, "user", "The user to ban").put("required", true)) + .add(defaultOption(OptionType.STRING, "reason", "The ban reason")) + .add(defaultOption(OptionType.INTEGER, "days", "The duration of the ban")))))))); + } + + @Test + void testRequiredThrows() + { + CommandDataImpl command = new CommandDataImpl("ban", "Simple ban command"); + command.addOption(OptionType.STRING, "opt", "desc"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> command.addOption(OptionType.STRING, "other", "desc", true)) + .withMessage("Cannot add required options after non-required options!"); + + SubcommandData subcommand = new SubcommandData("sub", "Simple subcommand"); + subcommand.addOption(OptionType.STRING, "opt", "desc"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> subcommand.addOption(OptionType.STRING, "other", "desc", true)) + .withMessage("Cannot add required options after non-required options!"); + } + + @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"); + } + + @Test + void testChoices() + { + OptionData stringOption = new OptionData(OptionType.STRING, "choice", "Option with choices!"); + 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<>(); + for (int i = 0; i < 25; i++) + { + intOption.addChoice("choice_" + i, i); + choices.add(new Command.Choice("choice_" + i, i)); + } + assertThatIllegalArgumentException() + .isThrownBy(() -> intOption.addChoice("name", 100)) + .withMessage("Cannot have more than 25 choices for an option!"); + assertThat(intOption.getChoices()) + .hasSize(25); + assertThat(intOption.getChoices()) + .isEqualTo(choices); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/LocalizationTest.java b/src/test/java/net/dv8tion/jda/test/interactions/LocalizationTest.java new file mode 100644 index 00000000000..3f9c09cf9f2 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/interactions/LocalizationTest.java @@ -0,0 +1,209 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.interactions.DiscordLocale; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.*; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import net.dv8tion.jda.api.interactions.commands.localization.ResourceBundleLocalizationFunction; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.test.PrettyRepresentation; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LocalizationTest +{ + private static SlashCommandData slashCommandData; + private static DataObject data; + + @BeforeAll + static void setup() + { + LocalizationFunction localizationFunction = ResourceBundleLocalizationFunction + .fromBundles("MyCommands", DiscordLocale.FRENCH) + .build(); + + slashCommandData = Commands.slash("ban", "Bans someone").addSubcommandGroups( + new SubcommandGroupData("user", "Bans a member").addSubcommands( + new SubcommandData("perm", "Bans a user permanently").addOptions( + new OptionData(OptionType.STRING, "user", "The user to ban"), + new OptionData(OptionType.INTEGER, "del_days", "The amount of days to delete messages") + .addChoices( + new Command.Choice("1 Day", "1"), + new Command.Choice("7 Days", "7"), + new Command.Choice("14 Days", "14") + ) + ), + new SubcommandData("temp", "Bans a user temporarily").addOptions( + new OptionData(OptionType.STRING, "user", "The user to ban"), + new OptionData(OptionType.INTEGER, "del_days", "The amount of days to delete messages") + .addChoices( + new Command.Choice("1 Day", "1"), + new Command.Choice("7 Days", "7"), + new Command.Choice("14 Days", "14") + ) + ) + ) + ).setLocalizationFunction(localizationFunction); + + data = slashCommandData.toData(); + } + + @Test + void commandLocalization() + { + assertThat(data.getString("name")).isEqualTo("ban"); + assertThat(data.getString("description")).isEqualTo("Bans someone"); + + assertThat(data.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "ban")); + assertThat(data.getObject("description_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "Bannis un utilisateur")); + } + + @Test + void subcommandLocalization() + { + DataObject subcommandGroup = getOption(data, "user"); + + assertThat(subcommandGroup.getString("name")).isEqualTo("user"); + assertThat(subcommandGroup.getString("description")).isEqualTo("Bans a member"); + + assertThat(subcommandGroup.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "utilisateur")); + assertThat(subcommandGroup.getObject("description_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "Bannis un utilisateur")); + } + + @Test + void subcommandGroupLocalization() + { + DataObject subcommandGroup = getOption(data, "user"); + DataObject subcommand = getOption(subcommandGroup, "perm"); + + assertThat(subcommand.getString("name")).isEqualTo("perm"); + assertThat(subcommand.getString("description")).isEqualTo("Bans a user permanently"); + + assertThat(subcommand.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "permanent")); + assertThat(subcommand.getObject("description_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "Bannis un utilisateur pour toujours")); + } + + @Test + void optionLocalization() + { + DataObject subcommandGroup = getOption(data, "user"); + DataObject subcommand = getOption(subcommandGroup, "perm"); + DataObject userOption = getOption(subcommand, "user"); + DataObject delDaysOption = getOption(subcommand, "del_days"); + + assertThat(userOption.getString("name")).isEqualTo("user"); + assertThat(userOption.getString("description")).isEqualTo("The user to ban"); + + assertThat(delDaysOption.getString("name")).isEqualTo("del_days"); + assertThat(delDaysOption.getString("description")).isEqualTo("The amount of days to delete messages"); + + assertThat(userOption.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "utilisateur")); + assertThat(userOption.getObject("description_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "L'utilisateur à bannir")); + + assertThat(delDaysOption.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "nb_jours")); + assertThat(delDaysOption.getObject("description_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "Nombre de jours de messages à supprimer")); + } + + @Test + void choiceLocalization() + { + DataObject subcommandGroup = getOption(data, "user"); + DataObject subcommand = getOption(subcommandGroup, "perm"); + DataObject delDaysOption = getOption(subcommand, "del_days"); + + DataObject days1 = getChoice(delDaysOption, "1 Day"); + assertThat(days1.getString("name")).isEqualTo("1 Day"); + assertThat(days1.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "1 jour")); + + DataObject days7 = getChoice(delDaysOption, "7 Days"); + assertThat(days7.getString("name")).isEqualTo("7 Days"); + assertThat(days7.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "7 jours")); + + DataObject days14 = getChoice(delDaysOption, "14 Days"); + assertThat(days14.getString("name")).isEqualTo("14 Days"); + assertThat(days14.getObject("name_localizations")) + .withRepresentation(new PrettyRepresentation()) + .isEqualTo(DataObject.empty().put("fr", "14 jours")); + } + + @Test + void reconstructData() + { + final DataObject data = slashCommandData.toData(); + final DataObject reconstitutedData = CommandData.fromData(data).toData(); + assertThat(reconstitutedData.toMap()).isEqualTo(data.toMap()); + } + + private static DataObject getOption(DataObject root, String name) + { + Stream options = root.getArray("options") + .stream(DataArray::getObject) + .filter(option -> option.getString("name").equals(name)); + return assertExactlyOne(options); + } + + private static DataObject getChoice(DataObject root, String name) + { + Stream choices = root.getArray("choices") + .stream(DataArray::getObject) + .filter(choice -> choice.getString("name").equals(name)); + return assertExactlyOne(choices); + } + + private static T assertExactlyOne(Stream stream) + { + List results = stream.collect(Collectors.toList()); + assertThat(results) + .withRepresentation(new PrettyRepresentation()) + .hasSize(1); + return results.get(0); + } +} diff --git a/src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java b/src/test/java/net/dv8tion/jda/test/interactions/SelectMenuTests.java similarity index 56% rename from src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java rename to src/test/java/net/dv8tion/jda/test/interactions/SelectMenuTests.java index 670c7dd30b6..e356ce7b95b 100644 --- a/src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java +++ b/src/test/java/net/dv8tion/jda/test/interactions/SelectMenuTests.java @@ -14,22 +14,22 @@ * limitations under the License. */ -package net.dv8tion.jda.interactions; +package net.dv8tion.jda.test.interactions; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.Builder; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.SelectTarget; import net.dv8tion.jda.api.utils.data.DataObject; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.util.Arrays; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; public class SelectMenuTests { @Test - public void testEntitySelectDefaultValueValid() + void testEntitySelectDefaultValueValid() { Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); builder.setDefaultValues(DefaultValue.role("1234")); @@ -37,9 +37,9 @@ public void testEntitySelectDefaultValueValid() EntitySelectMenu menu = builder.build(); DataObject value = menu.toData().getArray("default_values").getObject(0); - Assertions.assertEquals(Arrays.asList(DefaultValue.role("1234")), menu.getDefaultValues()); - Assertions.assertEquals("role", value.getString("type")); - Assertions.assertEquals("1234", value.getString("id")); + assertThat(menu.getDefaultValues()).containsExactly(DefaultValue.role("1234")); + assertThat(value.getString("type")).isEqualTo("role"); + assertThat(value.getString("id")).isEqualTo("1234"); builder = EntitySelectMenu.create("customid", SelectTarget.USER); builder.setDefaultValues(DefaultValue.user("1234")); @@ -47,9 +47,9 @@ public void testEntitySelectDefaultValueValid() menu = builder.build(); value = menu.toData().getArray("default_values").getObject(0); - Assertions.assertEquals(Arrays.asList(DefaultValue.user("1234")), menu.getDefaultValues()); - Assertions.assertEquals("user", value.getString("type")); - Assertions.assertEquals("1234", value.getString("id")); + assertThat(menu.getDefaultValues()).containsExactly(DefaultValue.user("1234")); + assertThat(value.getString("type")).isEqualTo("user"); + assertThat(value.getString("id")).isEqualTo("1234"); builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); builder.setDefaultValues(DefaultValue.channel("1234")); @@ -57,41 +57,41 @@ public void testEntitySelectDefaultValueValid() menu = builder.build(); value = menu.toData().getArray("default_values").getObject(0); - Assertions.assertEquals(Arrays.asList(DefaultValue.channel("1234")), menu.getDefaultValues()); - Assertions.assertEquals("channel", value.getString("type")); - Assertions.assertEquals("1234", value.getString("id")); + assertThat(menu.getDefaultValues()).containsExactly(DefaultValue.channel("1234")); + assertThat(value.getString("type")).isEqualTo("channel"); + assertThat(value.getString("id")).isEqualTo("1234"); } @Test - public void testEntitySelectDefaultValueInvalid() + void testEntitySelectDefaultValueInvalid() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); builder.setDefaultValues(DefaultValue.user("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.ROLE, but provided default value has type SelectTarget.USER!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); builder.setDefaultValues(DefaultValue.channel("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.ROLE, but provided default value has type SelectTarget.CHANNEL!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE, SelectTarget.USER); builder.setDefaultValues(DefaultValue.channel("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.ROLE and SelectTarget.USER, but provided default value has type SelectTarget.CHANNEL!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.USER); builder.setDefaultValues(DefaultValue.channel("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.USER, but provided default value has type SelectTarget.CHANNEL!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.USER); builder.setDefaultValues(DefaultValue.role("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.USER, but provided default value has type SelectTarget.ROLE!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); builder.setDefaultValues(DefaultValue.user("1234")); - }); - Assertions.assertThrows(IllegalArgumentException.class, () -> { + }).withMessage("The select menu supports types SelectTarget.CHANNEL, but provided default value has type SelectTarget.USER!"); + assertThatIllegalArgumentException().isThrownBy(() -> { Builder builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); builder.setDefaultValues(DefaultValue.role("1234")); - }); + }).withMessage("The select menu supports types SelectTarget.CHANNEL, but provided default value has type SelectTarget.ROLE!"); } } diff --git a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java new file mode 100644 index 00000000000..c7afa5cd0ab --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -0,0 +1,113 @@ +/* + * 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.restaction; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import javax.annotation.Nonnull; + +import static net.dv8tion.jda.api.requests.Method.POST; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class MessageCreateActionTest extends IntegrationTest +{ + private static final String FIXED_CHANNEL_ID = "1234567890"; + private static final String FIXED_NONCE = "123456"; + private static final String ENDPOINT_URL = "channels/" + FIXED_CHANNEL_ID + "/messages"; + + @Mock + protected MessageChannel channel; + + private static DataObject defaultMessageRequest() + { + return DataObject.empty() + .put("allowed_mentions", DataObject.empty() + .put("parse", DataArray.empty() + .add("users") + .add("roles") + .add("everyone")) + .put("replied_user", true)) + .put("components", DataArray.empty()) + .put("content", "") + .put("embeds", DataArray.empty()) + .put("enforce_nonce", true) + .put("flags", 0) + .put("nonce", FIXED_NONCE) + .put("tts", false); + } + + @BeforeEach + void setupChannel() + { + when(channel.getId()).thenReturn(FIXED_CHANNEL_ID); + when(channel.getJDA()).thenReturn(jda); + } + + @Test + void testEmpty() + { + assertThatIllegalStateException().isThrownBy(() -> + new MessageCreateActionImpl(channel) + .queue() + ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + } + + @Test + void testContentOnly() + { + assertNextRequestEquals(POST, ENDPOINT_URL, defaultMessageRequest() + .put("content", "test content")); + + new MessageCreateActionImpl(channel) + .setContent("test content") + .queue(); + + verify(requester, times(1)).request(any()); + } + + @Test + void testEmbedOnly() + { + assertNextRequestEquals(POST, ENDPOINT_URL, defaultMessageRequest() + .put("embeds", DataArray.empty() + .add(DataObject.empty().put("description", "test description")))); + + new MessageCreateActionImpl(channel) + .setEmbeds(new EmbedBuilder() + .setDescription("test description") + .build()) + .queue(); + + verify(requester, times(1)).request(any()); + } + + @Nonnull + protected DataObject normalizeRequestBody(@Nonnull DataObject body) + { + return body.put("nonce", FIXED_NONCE); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/restaction/RestActionTest.java b/src/test/java/net/dv8tion/jda/test/restaction/RestActionTest.java new file mode 100644 index 00000000000..72f226075c0 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/restaction/RestActionTest.java @@ -0,0 +1,115 @@ +/* + * 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.restaction; + +import net.dv8tion.jda.internal.requests.CompletedRestAction; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class RestActionTest extends IntegrationTest +{ + @Test + void testMapOperator() + { + assertThat( + new CompletedRestAction<>(jda, "12345") + .map(Integer::parseInt) + .complete() + ).isEqualTo(12345); + } + + @Test + void testFlatMapOperator() + { + assertThat( + new CompletedRestAction<>(jda, "12345") + .flatMap(value -> new CompletedRestAction<>(jda, Integer.parseInt(value))) + .complete() + ).isEqualTo(12345); + + assertThat( + new CompletedRestAction<>(jda, "12345") + .flatMap( + value -> value.startsWith("123"), + value -> new CompletedRestAction<>(jda, Integer.parseInt(value))) + .complete() + ).isEqualTo(12345); + + assertThatThrownBy(() -> + new CompletedRestAction<>(jda, "12345") + .flatMap( + value -> value.startsWith("wrong"), + value -> new CompletedRestAction<>(jda, Integer.parseInt(value))) + .complete() + ).isInstanceOf(CancellationException.class).hasMessage("FlatMap condition failed"); + + assertThat( + new CompletedRestAction<>(jda, "12345") + .flatMap( + value -> value.startsWith("wrong"), + value -> new CompletedRestAction<>(jda, Integer.parseInt(value))) + .submit() + ) + .failsWithin(Duration.ZERO) + .withThrowableThat() + .havingRootCause() + .isInstanceOf(CancellationException.class); + } + + @Test + void testDelayOperator() + { + when(scheduledExecutorService.schedule(any(Runnable.class), anyLong(), any())) + .thenReturn(null); + + new CompletedRestAction<>(jda, "12345") + .delay(Duration.ofSeconds(2), scheduledExecutorService) + .queue(); + + new CompletedRestAction<>(jda, "12345") + .delay(3, TimeUnit.SECONDS, scheduledExecutorService) + .queue(); + + verify(scheduledExecutorService, times(1)) + .schedule(any(Runnable.class), eq(2000L), eq(TimeUnit.MILLISECONDS)); + + verify(scheduledExecutorService, times(1)) + .schedule(any(Runnable.class), eq(3L), eq(TimeUnit.SECONDS)); + } + + @Test + void testQueueAfter() + { + when(scheduledExecutorService.schedule(any(Runnable.class), anyLong(), any())) + .thenReturn(null); + + new CompletedRestAction<>(jda, "12345") + .queueAfter(2, TimeUnit.SECONDS, scheduledExecutorService); + + verify(scheduledExecutorService, times(1)) + .schedule(any(Runnable.class), eq(2L), eq(TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/util/HelpersTest.java b/src/test/java/net/dv8tion/jda/test/util/HelpersTest.java new file mode 100644 index 00000000000..de13a475812 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/util/HelpersTest.java @@ -0,0 +1,124 @@ +/* + * 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.util; + +import net.dv8tion.jda.internal.utils.Helpers; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HelpersTest +{ + @Test + void testIsEmpty() + { + assertThat(Helpers.isEmpty(null)).isTrue(); + assertThat(Helpers.isEmpty("")).isTrue(); + assertThat(Helpers.isEmpty("null")).isFalse(); + assertThat(Helpers.isEmpty("testing with spaces")).isFalse(); + } + + @Test + void testContainsWhitespace() + { + assertThat(Helpers.containsWhitespace(" ")).isTrue(); + assertThat(Helpers.containsWhitespace("testing with spaces")).isTrue(); + assertThat(Helpers.containsWhitespace(null)).isFalse(); + assertThat(Helpers.containsWhitespace("")).isFalse(); + assertThat(Helpers.containsWhitespace("null")).isFalse(); + } + + @Test + void testIsBlank() + { + assertThat(Helpers.isBlank(" ")).isTrue(); + assertThat(Helpers.isBlank(null)).isTrue(); + assertThat(Helpers.isBlank("")).isTrue(); + assertThat(Helpers.isBlank("testing with spaces")).isFalse(); + assertThat(Helpers.isBlank("null")).isFalse(); + } + + @Test + void testCountMatches() + { + assertThat(Helpers.countMatches("Hello World", 'l')).isEqualTo(3); + assertThat(Helpers.countMatches("Hello World", ' ')).isEqualTo(1); + assertThat(Helpers.countMatches("Hello World", '_')).isEqualTo(0); + assertThat(Helpers.countMatches("", '!')).isEqualTo(0); + assertThat(Helpers.countMatches(null, '?')).isEqualTo(0); + } + + @Test + void testTruncate() + { + assertThat(Helpers.truncate("Hello World", 5)).isEqualTo("Hello"); + assertThat(Helpers.truncate("Hello", 5)).isEqualTo("Hello"); + assertThat(Helpers.truncate("Hello", 10)).isEqualTo("Hello"); + assertThat(Helpers.truncate("", 10)).isEqualTo(""); + assertThat(Helpers.truncate("Test", 0)).isEqualTo(""); + assertThat(Helpers.truncate(null, 10)).isNull(); + } + + @Test + void testRightPad() + { + assertThat(Helpers.rightPad("Hello", 9)).isEqualTo("Hello "); + assertThat(Helpers.rightPad("Hello World", 9)).isEqualTo("Hello World"); + assertThat(Helpers.rightPad("Hello", 5)).isEqualTo("Hello"); + } + + @Test + void testLeftPad() + { + assertThat(Helpers.leftPad("Hello", 9)).isEqualTo(" Hello"); + assertThat(Helpers.leftPad("Hello World", 9)).isEqualTo("Hello World"); + assertThat(Helpers.leftPad("Hello", 5)).isEqualTo("Hello"); + } + + @Test + void testIsNumeric() + { + assertThat(Helpers.isNumeric("10")).isTrue(); + assertThat(Helpers.isNumeric("1")).isTrue(); + assertThat(Helpers.isNumeric("0")).isTrue(); + assertThat(Helpers.isNumeric(String.valueOf(Long.MAX_VALUE))).isTrue(); + assertThat(Helpers.isNumeric(null)).isFalse(); + assertThat(Helpers.isNumeric("")).isFalse(); + assertThat(Helpers.isNumeric("Test")).isFalse(); + assertThat(Helpers.isNumeric("1.0")).isFalse(); + assertThat(Helpers.isNumeric("1e10")).isFalse(); + } + + @Test + void testDeepEquals() + { + List a = Arrays.asList("A", "B", "C"); + List b = Arrays.asList("B", "A", "C"); + List c = Arrays.asList("A", "B"); + List d = Arrays.asList("A", "B", "C"); + + assertThat(Helpers.deepEquals(a, a)).isTrue(); + assertThat(Helpers.deepEquals(a, d)).isTrue(); + assertThat(Helpers.deepEqualsUnordered(a, b)).isTrue(); + assertThat(Helpers.deepEquals(a, b)).isFalse(); + assertThat(Helpers.deepEquals(a, c)).isFalse(); + assertThat(Helpers.deepEqualsUnordered(b, c)).isFalse(); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/util/MarkdownTest.java b/src/test/java/net/dv8tion/jda/test/util/MarkdownTest.java new file mode 100644 index 00000000000..12388cdc688 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/util/MarkdownTest.java @@ -0,0 +1,471 @@ +/* + * 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.util; + +import net.dv8tion.jda.api.utils.MarkdownSanitizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MarkdownTest +{ + private MarkdownSanitizer markdown; + + @BeforeEach + void setup() + { + markdown = new MarkdownSanitizer().withStrategy(MarkdownSanitizer.SanitizationStrategy.REMOVE); + } + + @Test + void testComplex() + { + assertThat(markdown.compute("**A_B||C~~D__E`F`__~~||_**")).isEqualTo("ABCDEF"); + } + + @Test + void testTrivial() + { + assertThat(markdown.compute("")).isEqualTo(""); + assertThat(markdown.compute("Hello World ~~~~")).isEqualTo("Hello World ~~~~"); + assertThat(markdown.compute("Hello World ~~~~~")).isEqualTo("Hello World ~"); + } + + @Test + void testBold() + { + assertThat(markdown.compute("**Hello**")).isEqualTo("Hello"); + assertThat(markdown.compute("**Hello")).isEqualTo("**Hello"); + assertThat(markdown.compute("\\**Hello**")).isEqualTo("\\**Hello**"); + } + + @Test + void testItalics() + { + assertThat(markdown.compute("*Hello*")).isEqualTo("Hello"); + assertThat(markdown.compute("_Hello_")).isEqualTo("Hello"); + + assertThat(markdown.compute("*Hello")).isEqualTo("*Hello"); + assertThat(markdown.compute("_Hello")).isEqualTo("_Hello"); + + assertThat(markdown.compute("\\*Hello*")).isEqualTo("\\*Hello*"); + assertThat(markdown.compute("\\_Hello_")).isEqualTo("\\_Hello_"); + } + + @Test + void testBoldItalics() + { + assertThat(markdown.compute("***Hello***")).isEqualTo("Hello"); + assertThat(markdown.compute("***Hello")).isEqualTo("***Hello"); + assertThat(markdown.compute("\\***Hello***")).isEqualTo("\\***Hello***"); + } + + @Test + void testUnderline() + { + assertThat(markdown.compute("__Hello__")).isEqualTo("Hello"); + assertThat(markdown.compute("__Hello")).isEqualTo("__Hello"); + assertThat(markdown.compute("\\__Hello__")).isEqualTo("\\__Hello__"); + } + + @Test + void testStrike() + { + assertThat(markdown.compute("~~Hello~~")).isEqualTo("Hello"); + assertThat(markdown.compute("~~Hello")).isEqualTo("~~Hello"); + assertThat(markdown.compute("\\~~Hello~~")).isEqualTo("\\~~Hello~~"); + } + + @Test + void testSpoiler() + { + assertThat(markdown.compute("||Hello||")).isEqualTo("Hello"); + assertThat(markdown.compute("||Hello")).isEqualTo("||Hello"); + assertThat(markdown.compute("\\||Hello||")).isEqualTo("\\||Hello||"); + } + + @Test + void testMono() + { + assertThat(markdown.compute("`Hello`")).isEqualTo("Hello"); + assertThat(markdown.compute("`Hello")).isEqualTo("`Hello"); + assertThat(markdown.compute("\\`Hello`")).isEqualTo("\\`Hello`"); + + assertThat(markdown.compute("`Hello **World**`")).isEqualTo("Hello **World**"); + assertThat(markdown.compute("`Hello **World**")).isEqualTo("`Hello World"); + assertThat(markdown.compute("\\`Hello **World**`")).isEqualTo("\\`Hello World`"); + } + + @Test + void testMonoTwo() + { + assertThat(markdown.compute("``Hello``")).isEqualTo("Hello"); + assertThat(markdown.compute("``Hello")).isEqualTo("``Hello"); + assertThat(markdown.compute("\\``Hello``")).isEqualTo("\\``Hello``"); + + assertThat(markdown.compute("``Hello **World**``")).isEqualTo("Hello **World**"); + assertThat(markdown.compute("``Hello **World**")).isEqualTo("``Hello World"); + assertThat(markdown.compute("\\``Hello **World**``")).isEqualTo("\\``Hello World``"); + + assertThat(markdown.compute("``Hello `to` World``")).isEqualTo("Hello `to` World"); + assertThat(markdown.compute("``Hello `to` World")).isEqualTo("``Hello to World"); + assertThat(markdown.compute("\\``Hello `to` World``")).isEqualTo("\\``Hello to World``"); + } + + @Test + void testBlock() + { + assertThat(markdown.compute("```Hello```")).isEqualTo("Hello"); + assertThat(markdown.compute("```Hello")).isEqualTo("```Hello"); + assertThat(markdown.compute("\\```Hello```")).isEqualTo("\\```Hello```"); + + assertThat(markdown.compute("```Hello **World**```")).isEqualTo("Hello **World**"); + assertThat(markdown.compute("```Hello **World**")).isEqualTo("```Hello World"); + assertThat(markdown.compute("\\```Hello **World**```")).isEqualTo("\\```Hello World```"); + + assertThat(markdown.compute("```Hello `to` World```")).isEqualTo("Hello `to` World"); + assertThat(markdown.compute("```Hello `to` World")).isEqualTo("```Hello to World"); + assertThat(markdown.compute("\\```Hello `to` World```")).isEqualTo("\\```Hello to World```"); + + assertThat(markdown.compute("```java\nTest```")).isEqualTo("Test"); + } + + @Test + void testQuote() + { + assertThat(markdown.compute("> Hello > World")).isEqualTo("Hello > World"); + assertThat(markdown.compute("> Hello\n> World")).isEqualTo("Hello\nWorld"); + assertThat(markdown.compute(">>> Hello\nWorld")).isEqualTo("Hello\nWorld"); + assertThat(markdown.compute(">>>\nHello\nWorld")).isEqualTo("Hello\nWorld"); + assertThat(markdown.compute(">>>\nHello > World")).isEqualTo("Hello > World"); + assertThat(markdown.compute("Hello\n > World")).isEqualTo("Hello\n World"); + } +} + +class IgnoreMarkdownTest +{ + private MarkdownSanitizer markdown; + + @BeforeEach + void setup() + { + markdown = new MarkdownSanitizer().withIgnored(0xFFFFFFFF); + } + + @Test + void testComplex() + { + assertThat(markdown.compute("**A_B||C~~D__E`F`__~~||_**")).isEqualTo("**A_B||C~~D__E`F`__~~||_**"); + } + + @Test + void testBold() + { + assertThat(markdown.compute("**Hello**")).isEqualTo("**Hello**"); + assertThat(markdown.compute("**Hello")).isEqualTo("**Hello"); + } + + @Test + void testItalics() + { + assertThat(markdown.compute("*Hello*")).isEqualTo("*Hello*"); + assertThat(markdown.compute("_Hello_")).isEqualTo("_Hello_"); + + assertThat(markdown.compute("*Hello")).isEqualTo("*Hello"); + assertThat(markdown.compute("_Hello")).isEqualTo("_Hello"); + } + + @Test + void testBoldItalics() + { + assertThat(markdown.compute("***Hello***")).isEqualTo("***Hello***"); + assertThat(markdown.compute("***Hello")).isEqualTo("***Hello"); + assertThat(markdown.compute("\\***Hello***")).isEqualTo("\\***Hello***"); + } + + @Test + void testUnderline() + { + assertThat(markdown.compute("__Hello__")).isEqualTo("__Hello__"); + assertThat(markdown.compute("__Hello")).isEqualTo("__Hello"); + } + + @Test + void testStrike() + { + assertThat(markdown.compute("~~Hello~~")).isEqualTo("~~Hello~~"); + assertThat(markdown.compute("~~Hello")).isEqualTo("~~Hello"); + } + + @Test + void testSpoiler() + { + assertThat(markdown.compute("||Hello||")).isEqualTo("||Hello||"); + assertThat(markdown.compute("||Hello")).isEqualTo("||Hello"); + } + + @Test + void testMono() + { + assertThat(markdown.compute("`Hello`")).isEqualTo("`Hello`"); + assertThat(markdown.compute("`Hello")).isEqualTo("`Hello"); + + assertThat(markdown.compute("`Hello **World**`")).isEqualTo("`Hello **World**`"); + assertThat(markdown.compute("`Hello **World**")).isEqualTo("`Hello **World**"); + } + + @Test + void testMonoTwo() + { + assertThat(markdown.compute("``Hello``")).isEqualTo("``Hello``"); + assertThat(markdown.compute("``Hello")).isEqualTo("``Hello"); + + assertThat(markdown.compute("``Hello **World**``")).isEqualTo("``Hello **World**``"); + assertThat(markdown.compute("``Hello **World**")).isEqualTo("``Hello **World**"); + + assertThat(markdown.compute("``Hello `to` World``")).isEqualTo("``Hello `to` World``"); + assertThat(markdown.compute("``Hello `to` World")).isEqualTo("``Hello `to` World"); + } + + @Test + void testBlock() + { + assertThat(markdown.compute("```Hello```")).isEqualTo("```Hello```"); + assertThat(markdown.compute("```Hello")).isEqualTo("```Hello"); + + assertThat(markdown.compute("```Hello **World**```")).isEqualTo("```Hello **World**```"); + assertThat(markdown.compute("```Hello **World**")).isEqualTo("```Hello **World**"); + + assertThat(markdown.compute("```Hello `to` World```")).isEqualTo("```Hello `to` World```"); + assertThat(markdown.compute("```Hello `to` World")).isEqualTo("```Hello `to` World"); + + assertThat(markdown.compute("```java\nTest```")).isEqualTo("```java\nTest```"); + } + + @Test + void testQuote() + { + assertThat(markdown.compute("> Hello > World")).isEqualTo("> Hello > World"); + assertThat(markdown.compute("> Hello\n> World")).isEqualTo("> Hello\n> World"); + assertThat(markdown.compute(">>> Hello\nWorld")).isEqualTo(">>> Hello\nWorld"); + assertThat(markdown.compute(">>>\nHello\nWorld")).isEqualTo(">>>\nHello\nWorld"); + assertThat(markdown.compute(">>>\nHello > World")).isEqualTo(">>>\nHello > World"); + assertThat(markdown.compute("Hello\n > World")).isEqualTo("Hello\n > World"); + } +} + +class EscapeMarkdownTest +{ + private MarkdownSanitizer markdown; + + @BeforeEach + void setup() + { + markdown = new MarkdownSanitizer().withStrategy(MarkdownSanitizer.SanitizationStrategy.ESCAPE); + } + + @Test + void testComplex() + { + assertThat(markdown.compute("**A_B||C~~D__E`F`__~~||_**")).isEqualTo("\\*\\*A\\_B\\||C\\~~D\\_\\_E\\`F\\`\\_\\_\\~~\\||\\_\\*\\*"); + } + + @Test + void testBold() + { + assertThat(markdown.compute("**Hello**")).isEqualTo("\\*\\*Hello\\*\\*"); + assertThat(markdown.compute("**Hello")).isEqualTo("**Hello"); + assertThat(markdown.compute("\\**Hello**")).isEqualTo("\\**Hello**"); + } + + @Test + void testItalics() + { + assertThat(markdown.compute("*Hello*")).isEqualTo("\\*Hello\\*"); + assertThat(markdown.compute("_Hello_")).isEqualTo("\\_Hello\\_"); + + assertThat(markdown.compute("*Hello")).isEqualTo("*Hello"); + assertThat(markdown.compute("_Hello")).isEqualTo("_Hello"); + + assertThat(markdown.compute("\\*Hello*")).isEqualTo("\\*Hello*"); + assertThat(markdown.compute("\\_Hello_")).isEqualTo("\\_Hello_"); + } + + @Test + void testBoldItalics() + { + assertThat(markdown.compute("***Hello***")).isEqualTo("\\*\\*\\*Hello\\*\\*\\*"); + assertThat(markdown.compute("***Hello")).isEqualTo("***Hello"); + assertThat(markdown.compute("\\***Hello***")).isEqualTo("\\***Hello***"); + } + + @Test + void testUnderline() + { + assertThat(markdown.compute("__Hello__")).isEqualTo("\\_\\_Hello\\_\\_"); + assertThat(markdown.compute("__Hello")).isEqualTo("__Hello"); + assertThat(markdown.compute("\\__Hello__")).isEqualTo("\\__Hello__"); + } + + @Test + void testStrike() + { + assertThat(markdown.compute("~~Hello~~")).isEqualTo("\\~~Hello\\~~"); + assertThat(markdown.compute("~~Hello")).isEqualTo("~~Hello"); + assertThat(markdown.compute("\\~~Hello~~")).isEqualTo("\\~~Hello~~"); + } + + @Test + void testSpoiler() + { + assertThat(markdown.compute("||Hello||")).isEqualTo("\\||Hello\\||"); + assertThat(markdown.compute("||Hello")).isEqualTo("||Hello"); + assertThat(markdown.compute("\\||Hello||")).isEqualTo("\\||Hello||"); + } + + @Test + void testMono() + { + assertThat(markdown.compute("`Hello`")).isEqualTo("\\`Hello\\`"); + assertThat(markdown.compute("`Hello")).isEqualTo("`Hello"); + assertThat(markdown.compute("\\`Hello`")).isEqualTo("\\`Hello`"); + + assertThat(markdown.compute("`Hello **World**`")).isEqualTo("\\`Hello **World**\\`"); + assertThat(markdown.compute("`Hello **World**")).isEqualTo("`Hello \\*\\*World\\*\\*"); + assertThat(markdown.compute("\\`Hello **World**`")).isEqualTo("\\`Hello \\*\\*World\\*\\*`"); + + } + + @Test + void testMonoTwo() + { + assertThat(markdown.compute("``Hello``")).isEqualTo("\\``Hello\\``"); + assertThat(markdown.compute("``Hello")).isEqualTo("``Hello"); + assertThat(markdown.compute("\\``Hello``")).isEqualTo("\\``Hello``"); + + assertThat(markdown.compute("``Hello **World**``")).isEqualTo("\\``Hello **World**\\``"); + assertThat(markdown.compute("``Hello **World**")).isEqualTo("``Hello \\*\\*World\\*\\*"); + assertThat(markdown.compute("\\``Hello **World**``")).isEqualTo("\\``Hello \\*\\*World\\*\\*``"); + + assertThat(markdown.compute("``Hello `to` World``")).isEqualTo("\\``Hello `to` World\\``"); + assertThat(markdown.compute("``Hello `to` World")).isEqualTo("``Hello \\`to\\` World"); + assertThat(markdown.compute("\\``Hello `to` World")).isEqualTo("\\``Hello \\`to\\` World"); + } + + @Test + void testBlock() + { + assertThat(markdown.compute("```Hello```")).isEqualTo("\\```Hello\\```"); + assertThat(markdown.compute("```Hello")).isEqualTo("```Hello"); + assertThat(markdown.compute("\\```Hello")).isEqualTo("\\```Hello"); + + assertThat(markdown.compute("```Hello **World**```")).isEqualTo("\\```Hello **World**\\```"); + assertThat(markdown.compute("```Hello **World**")).isEqualTo("```Hello \\*\\*World\\*\\*"); + assertThat(markdown.compute("\\```Hello **World**")).isEqualTo("\\```Hello \\*\\*World\\*\\*"); + + assertThat(markdown.compute("```Hello `to` World```")).isEqualTo("\\```Hello `to` World\\```"); + assertThat(markdown.compute("```Hello `to` World")).isEqualTo("```Hello \\`to\\` World"); + assertThat(markdown.compute("\\```Hello `to` World")).isEqualTo("\\```Hello \\`to\\` World"); + + assertThat(markdown.compute("```java\nTest```")).isEqualTo("\\```java\nTest\\```"); + } + + @Test + void testQuote() + { + assertThat(markdown.compute("> Hello > World")).isEqualTo("\\> Hello > World"); + assertThat(markdown.compute("> Hello\n> World")).isEqualTo("\\> Hello\n\\> World"); + assertThat(markdown.compute(">>> Hello\nWorld")).isEqualTo("\\>>> Hello\nWorld"); + assertThat(markdown.compute(">>>\nHello\nWorld")).isEqualTo("\\>>>\nHello\nWorld"); + assertThat(markdown.compute(">>>\nHello > World")).isEqualTo("\\>>>\nHello > World"); + assertThat(markdown.compute("> _Hello \n> World_")).isEqualTo("\\> \\_Hello \n\\> World\\_"); + assertThat(markdown.compute("Hello\n > World")).isEqualTo("Hello\n \\> World"); + } +} + +class EscapeMarkdownAllTest +{ + @Test + void testAsterisk() + { + assertThat(MarkdownSanitizer.escape("Hello*World", true)).isEqualTo("Hello\\*World"); + assertThat(MarkdownSanitizer.escape("Hello**World", true)).isEqualTo("Hello\\*\\*World"); + assertThat(MarkdownSanitizer.escape("Hello***World", true)).isEqualTo("Hello\\*\\*\\*World"); + + assertThat(MarkdownSanitizer.escape("Hello\\*World", true)).isEqualTo("Hello\\*World"); + assertThat(MarkdownSanitizer.escape("Hello\\*\\*World", true)).isEqualTo("Hello\\*\\*World"); + assertThat(MarkdownSanitizer.escape("Hello\\*\\*\\*World", true)).isEqualTo("Hello\\*\\*\\*World"); + } + + @Test + void testUnderscore() + { + assertThat(MarkdownSanitizer.escape("Hello_World", true)).isEqualTo("Hello\\_World"); + assertThat(MarkdownSanitizer.escape("Hello__World", true)).isEqualTo("Hello\\_\\_World"); + assertThat(MarkdownSanitizer.escape("Hello___World", true)).isEqualTo("Hello\\_\\_\\_World"); + + assertThat(MarkdownSanitizer.escape("Hello\\_World", true)).isEqualTo("Hello\\_World"); + assertThat(MarkdownSanitizer.escape("Hello\\_\\_World", true)).isEqualTo("Hello\\_\\_World"); + assertThat(MarkdownSanitizer.escape("Hello\\_\\_\\_World", true)).isEqualTo("Hello\\_\\_\\_World"); + } + + @Test + void testCodeBlock() + { + assertThat(MarkdownSanitizer.escape("Hello`World", true)).isEqualTo("Hello\\`World"); + assertThat(MarkdownSanitizer.escape("Hello``World", true)).isEqualTo("Hello\\`\\`World"); + assertThat(MarkdownSanitizer.escape("Hello```World", true)).isEqualTo("Hello\\`\\`\\`World"); + + assertThat(MarkdownSanitizer.escape("Hello\\`World", true)).isEqualTo("Hello\\`World"); + assertThat(MarkdownSanitizer.escape("Hello\\`\\`World", true)).isEqualTo("Hello\\`\\`World"); + assertThat(MarkdownSanitizer.escape("Hello\\`\\`\\`World", true)).isEqualTo("Hello\\`\\`\\`World"); + } + + @Test + void testSpoiler() + { + assertThat(MarkdownSanitizer.escape("Hello||World", true)).isEqualTo("Hello\\|\\|World"); + assertThat(MarkdownSanitizer.escape("Hello|World", true)).isEqualTo("Hello|World"); + + assertThat(MarkdownSanitizer.escape("Hello\\|\\|World", true)).isEqualTo("Hello\\|\\|World"); + assertThat(MarkdownSanitizer.escape("Hello\\|World", true)).isEqualTo("Hello\\|World"); + } + + @Test + void testStrike() + { + assertThat(MarkdownSanitizer.escape("Hello~~World", true)).isEqualTo("Hello\\~\\~World"); + assertThat(MarkdownSanitizer.escape("Hello\\~\\~World", true)).isEqualTo("Hello\\~\\~World"); + } + + @Test + void testQuote() + { + assertThat(MarkdownSanitizer.escape("> Hello World", true)).isEqualTo("\\> Hello World"); + assertThat(MarkdownSanitizer.escape(">Hello World", true)).isEqualTo(">Hello World"); + assertThat(MarkdownSanitizer.escape(">>> Hello World", true)).isEqualTo("\\>\\>\\> Hello World"); + assertThat(MarkdownSanitizer.escape(">>>Hello World", true)).isEqualTo(">>>Hello World"); + assertThat(MarkdownSanitizer.escape(">>> Hello > World\n> Hello >>> World\n<@12345> > Hello\n > Hello world", true)).isEqualTo("\\>\\>\\> Hello > World\n\\> Hello >>> World\n<@12345> > Hello\n \\> Hello world"); + + assertThat(MarkdownSanitizer.escape("\\> Hello World", true)).isEqualTo("\\> Hello World"); + assertThat(MarkdownSanitizer.escape("\\>\\>\\> Hello World", true)).isEqualTo("\\>\\>\\> Hello World"); + assertThat(MarkdownSanitizer.escape("Hello > World")).isEqualTo("Hello > World"); + assertThat(MarkdownSanitizer.escape("Hello\n > World")).isEqualTo("Hello\n \\> World"); + assertThat(MarkdownSanitizer.escape("Hello\n> World")).isEqualTo("Hello\n\\> World"); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/util/MarkdownUtilTest.java b/src/test/java/net/dv8tion/jda/test/util/MarkdownUtilTest.java new file mode 100644 index 00000000000..c9143bcf24d --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/util/MarkdownUtilTest.java @@ -0,0 +1,108 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; + +import static net.dv8tion.jda.api.utils.MarkdownUtil.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class MarkdownUtilTest +{ + @Test + void testBold() + { + assertThat(bold("Hello World")).isEqualTo("**Hello World**"); + assertThat(bold("Hello **Test** World")).isEqualTo("**Hello \\*\\*Test\\*\\* World**"); + assertThat(bold("Hello *Test* World")).isEqualTo("**Hello *Test* World**"); + } + + @Test + void testItalics() + { + assertThat(italics("Hello World")).isEqualTo("_Hello World_"); + assertThat(italics("Hello _Test_ World")).isEqualTo("_Hello \\_Test\\_ World_"); + assertThat(italics("Hello __Test__ World")).isEqualTo("_Hello __Test__ World_"); + } + + @Test + void testUnderline() + { + assertThat(underline("Hello World")).isEqualTo("__Hello World__"); + assertThat(underline("Hello __Test__ World")).isEqualTo("__Hello \\_\\_Test\\_\\_ World__"); + assertThat(underline("Hello _Test_ World")).isEqualTo("__Hello _Test_ World__"); + } + + @Test + void testMonospace() + { + assertThat(monospace("Hello World")).isEqualTo("`Hello World`"); + assertThat(monospace("Hello `Test` World")).isEqualTo("`Hello \\`Test\\` World`"); + assertThat(monospace("Hello ``Test`` World")).isEqualTo("`Hello ``Test`` World`"); + } + + @Test + void testCodeblock() + { + assertThat(codeblock("java", "Hello World")).isEqualTo("```java\nHello World```"); + assertThat(codeblock("java", "Hello ```java\nTest``` World")).isEqualTo("```java\nHello \\```java\nTest\\``` World```"); + assertThat(codeblock("java", "Hello `Test` World")).isEqualTo("```java\nHello `Test` World```"); + + assertThat(codeblock("Hello World")).isEqualTo("```Hello World```"); + assertThat(codeblock("Hello ```java\nTest``` World")).isEqualTo("```Hello \\```java\nTest\\``` World```"); + assertThat(codeblock("Hello `Test` World")).isEqualTo("```Hello `Test` World```"); + } + + @Test + void testSpoiler() + { + assertThat(spoiler("Hello World")).isEqualTo("||Hello World||"); + assertThat(spoiler("Hello ||Test|| World")).isEqualTo("||Hello \\||Test\\|| World||"); + assertThat(spoiler("Hello |Test| World")).isEqualTo("||Hello |Test| World||"); + } + + @Test + void testStrike() + { + assertThat(strike("Hello World")).isEqualTo("~~Hello World~~"); + assertThat(strike("Hello ~~Test~~ World")).isEqualTo("~~Hello \\~~Test\\~~ World~~"); + assertThat(strike("Hello ~Test~ World")).isEqualTo("~~Hello ~Test~ World~~"); + } + + @Test + void testQuote() + { + assertThat(quote("Hello World")).isEqualTo("> Hello World"); + assertThat(quote("Hello \n> Test World")).isEqualTo("> Hello \n> \\> Test World"); + assertThat(quote("Hello > Test World")).isEqualTo("> Hello > Test World"); + } + + @Test + void testQuoteBlock() + { + assertThat(quoteBlock("Hello World")).isEqualTo(">>> Hello World"); + assertThat(quoteBlock("Hello \n>>> Test World")).isEqualTo(">>> Hello \n>>> Test World"); + } + + @Test + void testMaskedLink() + { + assertThat(maskedLink("Hello", "World")).isEqualTo("[Hello](World)"); + assertThat(maskedLink("Hello", "World)")).isEqualTo("[Hello](World%29)"); + assertThat(maskedLink("Hello]", "World)")).isEqualTo("[Hello\\]](World%29)"); + } +} From 398c23d9e129484348f1273c963fb47b2411ee8e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:26:53 +0200 Subject: [PATCH 13/25] Create an exception when receiving UNKNOWN_WEBHOOK in interaction hooks (#2621) --- .../net/dv8tion/jda/api/requests/Request.java | 17 ++++++++-- .../internal/entities/ReceivedMessage.java | 32 +++++++++++++++++-- .../jda/internal/requests/ErrorMapper.java | 31 ++++++++++++++++++ .../jda/internal/requests/RestActionImpl.java | 20 +++++++++++- 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/internal/requests/ErrorMapper.java diff --git a/src/main/java/net/dv8tion/jda/api/requests/Request.java b/src/main/java/net/dv8tion/jda/api/requests/Request.java index 468a2df017d..cdb41171117 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Request.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Request.java @@ -138,15 +138,26 @@ public void onFailure(Response response) { if (response.code == 429) { - onFailure(new RateLimitedException(route, response.retryAfter)); + onRateLimited(response); } else { - onFailure(ErrorResponseException.create( - ErrorResponse.fromJSON(response.optObject().orElse(null)), response)); + onFailure(createErrorResponseException(response)); } } + public void onRateLimited(Response response) + { + onFailure(new RateLimitedException(route, response.retryAfter)); + } + + @Nonnull + public ErrorResponseException createErrorResponseException(@Nonnull Response response) + { + return ErrorResponseException.create( + ErrorResponse.fromJSON(response.optObject().orElse(null)), response); + } + public void onFailure(Throwable failException) { if (done) 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 69a4f8ef3d7..7b598e57887 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -39,6 +39,7 @@ import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.Route; @@ -51,7 +52,9 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageEditData; import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.interactions.InteractionHookImpl; import net.dv8tion.jda.internal.requests.CompletedRestAction; +import net.dv8tion.jda.internal.requests.ErrorMapper; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.requests.restaction.MessageEditActionImpl; @@ -774,7 +777,9 @@ public AuditableRestAction delete() if (isWebhookRequest()) { Route.CompiledRoute route = Route.Webhooks.EXECUTE_WEBHOOK_DELETE.compile(webhook.getId(), webhook.getToken(), getId()); - return new AuditableRestActionImpl<>(getJDA(), route); + final AuditableRestActionImpl action = new AuditableRestActionImpl<>(getJDA(), route); + action.setErrorMapper(getUnknownWebhookErrorMapper()); + return action; } SelfUser self = getJDA().getSelfUser(); @@ -839,7 +844,9 @@ public AuditableRestAction suppressEmbeds(boolean suppressed) newFlags &= ~suppressionValue; DataObject body = DataObject.empty().put("flags", newFlags); - return new AuditableRestActionImpl<>(api, route, body); + final AuditableRestActionImpl action = new AuditableRestActionImpl<>(api, route, body); + action.setErrorMapper(getUnknownWebhookErrorMapper()); + return action; } @Nonnull @@ -980,8 +987,27 @@ private boolean isWebhookRequest() @Nonnull private MessageEditActionImpl editRequest() { - return hasChannel() + final MessageEditActionImpl messageEditAction = hasChannel() ? new MessageEditActionImpl(getChannel(), getId()) : new MessageEditActionImpl(getJDA(), hasGuild() ? getGuild() : null, getChannelId(), getId()); + + messageEditAction.setErrorMapper(getUnknownWebhookErrorMapper()); + return messageEditAction; + } + + private ErrorMapper getUnknownWebhookErrorMapper() + { + if (!isWebhookRequest()) + return null; + + return (response, request, exception) -> + { + if (webhook instanceof InteractionHookImpl + && !((InteractionHookImpl) webhook).isAck() + && exception.getErrorResponse() == ErrorResponse.UNKNOWN_WEBHOOK) + return new IllegalStateException("Sending a webhook request requires the interaction to be acknowledged before expiration", exception); + else + return null; + }; } } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/ErrorMapper.java b/src/main/java/net/dv8tion/jda/internal/requests/ErrorMapper.java new file mode 100644 index 00000000000..92d785dbac8 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/ErrorMapper.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.internal.requests; + +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +@FunctionalInterface +public interface ErrorMapper +{ + @Nullable + Throwable apply(@Nonnull Response response, @Nonnull Request request, @Nonnull ErrorResponseException exception); +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java index 3184d67d1c1..48eb4fb741b 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java @@ -63,6 +63,7 @@ else if (t.getCause() != null) private final Route.CompiledRoute route; private final RequestBody data; private final BiFunction, T> handler; + private ErrorMapper errorMapper = null; private boolean priority = false; private long deadline = 0; @@ -146,6 +147,11 @@ public RestActionImpl(JDA api, Route.CompiledRoute route, RequestBody data, BiFu this.handler = handler; } + public void setErrorMapper(ErrorMapper errorMapper) + { + this.errorMapper = errorMapper; + } + public RestActionImpl priority() { priority = true; @@ -277,8 +283,20 @@ public void handleResponse(Response response, Request request) { if (response.isOk()) handleSuccess(response, request); + else if (response.isRateLimit()) + request.onRateLimited(response); else - request.onFailure(response); + { + final ErrorResponseException exception = request.createErrorResponseException(response); + final Throwable mappedThrowable = this.errorMapper != null + ? this.errorMapper.apply(response, request, exception) + : null; + + if (mappedThrowable != null) + request.onFailure(mappedThrowable); + else + request.onFailure(exception); + } } protected void handleSuccess(Response response, Request request) From c3b85d97cf5866ca892fe3de089ade471a058cfd Mon Sep 17 00:00:00 2001 From: Giuliopime <60524738+Giuliopime@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:30:27 +0200 Subject: [PATCH 14/25] Add support for premium app subscriptions (#2583) - New gateway events - New interaction response type - Contextual entitlement information in interactions --- src/main/java/net/dv8tion/jda/api/JDA.java | 11 + .../dv8tion/jda/api/entities/Entitlement.java | 188 ++++++++++++++++++ .../entitlement/EntitlementCreateEvent.java | 35 ++++ .../entitlement/EntitlementDeleteEvent.java | 40 ++++ .../entitlement/EntitlementUpdateEvent.java | 41 ++++ .../entitlement/GenericEntitlementEvent.java | 52 +++++ .../api/events/entitlement/package-info.java | 21 ++ .../GenericInteractionCreateEvent.java | 9 + .../GenericCommandInteractionEvent.java | 8 + ...enericComponentInteractionCreateEvent.java | 8 + .../jda/api/hooks/ListenerAdapter.java | 10 + .../jda/api/interactions/Interaction.java | 17 +- .../IPremiumRequiredReplyCallback.java | 37 ++++ .../commands/CommandInteraction.java | 3 +- .../components/ComponentInteraction.java | 3 +- .../net/dv8tion/jda/api/requests/Route.java | 1 + .../InteractionCallbackAction.java | 2 + .../PremiumRequiredCallbackAction.java | 29 +++ .../EntitlementPaginationAction.java | 149 ++++++++++++++ .../net/dv8tion/jda/internal/JDAImpl.java | 9 + .../entities/AbstractWebhookClient.java | 3 +- .../internal/entities/EntitlementImpl.java | 106 ++++++++++ .../jda/internal/entities/EntityBuilder.java | 15 ++ .../entities/automod/AutoModRuleImpl.java | 24 +-- .../handle/EntitlementCreateHandler.java | 36 ++++ .../handle/EntitlementDeleteHandler.java | 36 ++++ .../handle/EntitlementUpdateHandler.java | 35 ++++ .../interactions/InteractionImpl.java | 22 +- .../command/CommandInteractionImpl.java | 9 + .../component/ComponentInteractionImpl.java | 9 + .../component/EntitySelectMenuImpl.java | 3 +- .../requests/IncomingWebhookClientImpl.java | 3 +- .../internal/requests/WebSocketClient.java | 3 + .../restaction/CommandCreateActionImpl.java | 7 +- .../PremiumRequiredCallbackActionImpl.java | 58 ++++++ .../EntitlementPaginationActionImpl.java | 183 +++++++++++++++++ 36 files changed, 1194 insertions(+), 31 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/entities/Entitlement.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementCreateEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementDeleteEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementUpdateEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/entitlement/GenericEntitlementEvent.java create mode 100644 src/main/java/net/dv8tion/jda/api/events/entitlement/package-info.java create mode 100644 src/main/java/net/dv8tion/jda/api/interactions/callbacks/IPremiumRequiredReplyCallback.java create mode 100644 src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/PremiumRequiredCallbackAction.java create mode 100644 src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/EntitlementPaginationAction.java create mode 100644 src/main/java/net/dv8tion/jda/internal/entities/EntitlementImpl.java create mode 100644 src/main/java/net/dv8tion/jda/internal/handle/EntitlementCreateHandler.java create mode 100644 src/main/java/net/dv8tion/jda/internal/handle/EntitlementDeleteHandler.java create mode 100644 src/main/java/net/dv8tion/jda/internal/handle/EntitlementUpdateHandler.java create mode 100644 src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/PremiumRequiredCallbackActionImpl.java create mode 100644 src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/EntitlementPaginationActionImpl.java diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 48581126ae5..eefea53fc87 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -37,6 +37,7 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.Route; import net.dv8tion.jda.api.requests.restaction.*; +import net.dv8tion.jda.api.requests.restaction.pagination.EntitlementPaginationAction; import net.dv8tion.jda.api.sharding.ShardManager; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.cache.CacheFlag; @@ -1883,6 +1884,16 @@ default List getEmojisByName(@Nonnull String name, boolean igno @CheckReturnValue RestAction retrieveApplicationInfo(); + /** + * A {@link net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction PaginationAction} implementation + * which allows you to {@link Iterable iterate} over {@link Entitlement}s that are applicable to the logged in application. + * + * @return {@link EntitlementPaginationAction EntitlementPaginationAction} + */ + @Nonnull + @CheckReturnValue + EntitlementPaginationAction retrieveEntitlements(); + /** * Configures the required scopes applied to the {@link #getInviteUrl(Permission...)} and similar methods. *
    To use slash commands you must add {@code "applications.commands"} to these scopes. The scope {@code "bot"} is always applied. diff --git a/src/main/java/net/dv8tion/jda/api/entities/Entitlement.java b/src/main/java/net/dv8tion/jda/api/entities/Entitlement.java new file mode 100644 index 00000000000..94fb94dfa35 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/Entitlement.java @@ -0,0 +1,188 @@ +/* + * 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; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.OffsetDateTime; + +/** + * Represents a user or guild that has access to a premium offering in your application. + * + * @see Discord Docs about Entitlements + */ +public interface Entitlement extends ISnowflake +{ + + /** + * The id of the SKU related to this {@link Entitlement Entitlement} + * + * @return The id of the SKU related to this {@link Entitlement Entitlement} + */ + long getSkuIdLong(); + + /** + * The id of the SKU related to this {@link Entitlement Entitlement} + * + * @return The id of the SKU related to this {@link Entitlement Entitlement} + */ + @Nonnull + default String getSkuId() + { + return Long.toUnsignedString(getSkuIdLong()); + } + + /** + * The id of the parent application of this {@link Entitlement Entitlement} + * + * @return The id of the parent application of this {@link Entitlement Entitlement} + */ + long getApplicationIdLong(); + + /** + * The id of the parent application of this {@link Entitlement Entitlement} + * + * @return The id of the parent application of this {@link Entitlement Entitlement} + */ + @Nonnull + default String getApplicationId() + { + return Long.toUnsignedString(getApplicationIdLong()); + } + + /** + * The id of the user that purchased the {@link Entitlement Entitlement} + * + * @return The id of the user that purchased the {@link Entitlement Entitlement} + */ + long getUserIdLong(); + + /** + * The id of the user that purchased the {@link Entitlement Entitlement} + * + * @return The id of the user that purchased the {@link Entitlement Entitlement} + */ + default String getUserId() + { + return Long.toUnsignedString(getUserIdLong()); + } + + /** + * The guild id that is granted access to the {@link Entitlement Entitlement}s SKU + * + * @return The id of the guild that purchased the {@link Entitlement Entitlement} or 0 if this is not a guild subscription + */ + long getGuildIdLong(); + + /** + * The guild id that is granted access to the {@link Entitlement Entitlement}s SKU + * + * @return The id of the guild that purchased the {@link Entitlement Entitlement} or {@code null} if this is not a guild subscription + */ + @Nullable + default String getGuildId() + { + if (getGuildIdLong() == 0) + return null; + + return Long.toUnsignedString(getGuildIdLong()); + } + + /** + * The type of the Entitlement + *
    The only possible type of Entitlement currently is {@link EntitlementType#APPLICATION_SUBSCRIPTION} + *
    Discord doesn't currently support other types for entitlements. + * + * @return the {@link Entitlement Entitlement} type + */ + @Nonnull + EntitlementType getType(); + + /** + * Whether the {@link Entitlement Entitlement} has been deleted or not. + * + * @return True if the {@link Entitlement Entitlement} was deleted, False otherwise + * + * @see net.dv8tion.jda.api.events.entitlement.EntitlementDeleteEvent + */ + boolean isDeleted(); + + /** + * The start date at which the {@link Entitlement Entitlement} is valid. + * + * @return Start date at which the {@link Entitlement Entitlement} is valid. Not present when using test entitlements. + */ + @Nullable + OffsetDateTime getTimeStarting(); + + /** + * Date at which the {@link Entitlement Entitlement} is no longer valid. + * + * @return Date at which the {@link Entitlement Entitlement} is no longer valid. Not present when using test entitlements. + */ + @Nullable + OffsetDateTime getTimeEnding(); + + /** + * Represents the type of this Entitlement + */ + enum EntitlementType + { + APPLICATION_SUBSCRIPTION(8), + /** + * Placeholder for unsupported types. + */ + UNKNOWN(-1); + + private final int key; + + EntitlementType(int key) + { + this.key = key; + } + + /** + * The Discord defined id key for this EntitlementType. + * + * @return the id key. + */ + public int getKey() + { + return key; + } + + /** + * Gets the EntitlementType related to the provided key. + *
    If an unknown key is provided, this returns {@link #UNKNOWN} + * + * @param key + * The Discord key referencing a EntitlementType. + * + * @return The EntitlementType that has the key provided, or {@link #UNKNOWN} for unknown key. + */ + @Nonnull + public static EntitlementType fromKey(int key) + { + for (EntitlementType type : values()) + { + if (type.getKey() == key) + return type; + } + return UNKNOWN; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementCreateEvent.java b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementCreateEvent.java new file mode 100644 index 00000000000..b52fe994dfa --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementCreateEvent.java @@ -0,0 +1,35 @@ +/* + * 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.entitlement; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; + +import javax.annotation.Nonnull; + +/** + * Indicates that a user subscribed to a SKU. + * + * @see #getEntitlement() + */ +public class EntitlementCreateEvent extends GenericEntitlementEvent +{ + public EntitlementCreateEvent(@Nonnull JDA api, long responseNumber, @Nonnull Entitlement entitlement) + { + super(api, responseNumber, entitlement); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementDeleteEvent.java b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementDeleteEvent.java new file mode 100644 index 00000000000..3325ed636c0 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementDeleteEvent.java @@ -0,0 +1,40 @@ +/* + * 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.entitlement; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; + +import javax.annotation.Nonnull; + +/** + * Indicates that an {@link Entitlement Entitlement} was deleted. {@link Entitlement Entitlement} deletions are infrequent and occur strictly when: + *
      + *
    • Discord issues a refund for a subscription; or
    • + *
    • Discord removes an {@link Entitlement Entitlement} via internal tooling
    • + *
    + * + * @see #getEntitlement() + * @see EntitlementUpdateEvent + */ +public class EntitlementDeleteEvent extends GenericEntitlementEvent +{ + public EntitlementDeleteEvent(@Nonnull JDA api, long responseNumber, @Nonnull Entitlement entitlement) + { + super(api, responseNumber, entitlement); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementUpdateEvent.java b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementUpdateEvent.java new file mode 100644 index 00000000000..8602dad9601 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/entitlement/EntitlementUpdateEvent.java @@ -0,0 +1,41 @@ +/* + * 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.entitlement; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; + +import javax.annotation.Nonnull; + +/** + * Indicates an {@link Entitlement Entitlement} has renewed for the next billing period. + * The {@link Entitlement#getTimeEnding() timeEnding} will have an updated value with the new expiration date. + * + *

    Notice
    + * The {@link Entitlement#getTimeEnding() timeEnding} is updated for active subscriptions at the end of every billing period to + * indicate renewal. When an {@link Entitlement Entitlement} has not been renewed, Discord will indicate this by not emitting + * an {@link EntitlementUpdateEvent} with the new {@link Entitlement#getTimeEnding() timeEnding} date + * + * @see #getEntitlement() + */ +public class EntitlementUpdateEvent extends GenericEntitlementEvent +{ + public EntitlementUpdateEvent(@Nonnull JDA api, long responseNumber, @Nonnull Entitlement entitlement) + { + super(api, responseNumber, entitlement); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/entitlement/GenericEntitlementEvent.java b/src/main/java/net/dv8tion/jda/api/events/entitlement/GenericEntitlementEvent.java new file mode 100644 index 00000000000..fa8071aeeb5 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/entitlement/GenericEntitlementEvent.java @@ -0,0 +1,52 @@ +/* + * 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.entitlement; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; +import net.dv8tion.jda.api.events.Event; + +import javax.annotation.Nonnull; + +/** + * Indicates that an {@link Entitlement Entitlement} was either created, updated, or deleted + * + * @see EntitlementCreateEvent + * @see EntitlementUpdateEvent + * @see EntitlementDeleteEvent + */ +public abstract class GenericEntitlementEvent extends Event +{ + protected final Entitlement entitlement; + + protected GenericEntitlementEvent(@Nonnull JDA api, long responseNumber, @Nonnull Entitlement entitlement) + { + super(api, responseNumber); + this.entitlement = entitlement; + } + + /** + * The {@link Entitlement Entitlement} + * + * @return The {@link Entitlement Entitlement} + */ + @Nonnull + public Entitlement getEntitlement() + { + return entitlement; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/entitlement/package-info.java b/src/main/java/net/dv8tion/jda/api/events/entitlement/package-info.java new file mode 100644 index 00000000000..a963879c610 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/entitlement/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Events that indicate that a {@link net.dv8tion.jda.api.entities.Entitlement Entitlement} + * is either created, updated, or deleted. + */ +package net.dv8tion.jda.api.events.entitlement; diff --git a/src/main/java/net/dv8tion/jda/api/events/interaction/GenericInteractionCreateEvent.java b/src/main/java/net/dv8tion/jda/api/events/interaction/GenericInteractionCreateEvent.java index 2f1eac6ced3..24d40d8f20b 100644 --- a/src/main/java/net/dv8tion/jda/api/events/interaction/GenericInteractionCreateEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/interaction/GenericInteractionCreateEvent.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.api.events.interaction; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; @@ -27,6 +28,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.List; /** * Indicates that an {@link Interaction} was created. @@ -121,6 +123,13 @@ public User getUser() return interaction.getUser(); } + @Nonnull + @Override + public List getEntitlements() + { + return interaction.getEntitlements(); + } + @Override public long getIdLong() { diff --git a/src/main/java/net/dv8tion/jda/api/events/interaction/command/GenericCommandInteractionEvent.java b/src/main/java/net/dv8tion/jda/api/events/interaction/command/GenericCommandInteractionEvent.java index 37f69103cbe..b81d5c5fb28 100644 --- a/src/main/java/net/dv8tion/jda/api/events/interaction/command/GenericCommandInteractionEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/interaction/command/GenericCommandInteractionEvent.java @@ -24,6 +24,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import javax.annotation.Nonnull; @@ -118,4 +119,11 @@ public ModalCallbackAction replyModal(@Nonnull Modal modal) { return getInteraction().replyModal(modal); } + + @Nonnull + @Override + public PremiumRequiredCallbackAction replyWithPremiumRequired() + { + return getInteraction().replyWithPremiumRequired(); + } } diff --git a/src/main/java/net/dv8tion/jda/api/events/interaction/component/GenericComponentInteractionCreateEvent.java b/src/main/java/net/dv8tion/jda/api/events/interaction/component/GenericComponentInteractionCreateEvent.java index 82ef4c9a60e..2a04315b098 100644 --- a/src/main/java/net/dv8tion/jda/api/events/interaction/component/GenericComponentInteractionCreateEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/interaction/component/GenericComponentInteractionCreateEvent.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.requests.restaction.interactions.MessageEditCallbackAction; import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import javax.annotation.Nonnull; @@ -124,4 +125,11 @@ public ModalCallbackAction replyModal(@Nonnull Modal modal) { return interaction.replyModal(modal); } + + @Nonnull + @Override + public PremiumRequiredCallbackAction replyWithPremiumRequired() + { + return interaction.replyWithPremiumRequired(); + } } 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 3df7247bae9..fa764c405ec 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -36,6 +36,10 @@ import net.dv8tion.jda.api.events.emoji.update.EmojiUpdateNameEvent; import net.dv8tion.jda.api.events.emoji.update.EmojiUpdateRolesEvent; import net.dv8tion.jda.api.events.emoji.update.GenericEmojiUpdateEvent; +import net.dv8tion.jda.api.events.entitlement.EntitlementCreateEvent; +import net.dv8tion.jda.api.events.entitlement.EntitlementDeleteEvent; +import net.dv8tion.jda.api.events.entitlement.EntitlementUpdateEvent; +import net.dv8tion.jda.api.events.entitlement.GenericEntitlementEvent; import net.dv8tion.jda.api.events.guild.*; import net.dv8tion.jda.api.events.guild.invite.GenericGuildInviteEvent; import net.dv8tion.jda.api.events.guild.invite.GuildInviteCreateEvent; @@ -363,6 +367,11 @@ public void onGuildStickerUpdateTags(@Nonnull GuildStickerUpdateTagsEvent event) public void onGuildStickerUpdateDescription(@Nonnull GuildStickerUpdateDescriptionEvent event) {} public void onGuildStickerUpdateAvailable(@Nonnull GuildStickerUpdateAvailableEvent event) {} + // Entitlement events + public void onEntitlementCreate(@Nonnull EntitlementCreateEvent event) {} + public void onEntitlementUpdate(@Nonnull EntitlementUpdateEvent event) {} + public void onEntitlementDelete(@Nonnull EntitlementDeleteEvent event) {} + // Debug Events public void onHttpRequest(@Nonnull HttpRequestEvent event) {} @@ -403,6 +412,7 @@ public void onGenericEmoji(@Nonnull GenericEmojiEvent event) {} public void onGenericEmojiUpdate(@Nonnull GenericEmojiUpdateEvent event) {} public void onGenericGuildSticker(@Nonnull GenericGuildStickerEvent event) {} public void onGenericGuildStickerUpdate(@Nonnull GenericGuildStickerUpdateEvent event) {} + public void onGenericEntitlement(@Nonnull GenericEntitlementEvent event) {} public void onGenericPermissionOverride(@Nonnull GenericPermissionOverrideEvent event) {} public void onGenericScheduledEventUpdate(@Nonnull GenericScheduledEventUpdateEvent event) {} public void onGenericScheduledEventGateway(@Nonnull GenericScheduledEventGatewayEvent event) {} diff --git a/src/main/java/net/dv8tion/jda/api/interactions/Interaction.java b/src/main/java/net/dv8tion/jda/api/interactions/Interaction.java index 19f773c4341..32fbb0d5452 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/Interaction.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/Interaction.java @@ -17,10 +17,7 @@ package net.dv8tion.jda.api.interactions; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.ISnowflake; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; @@ -33,6 +30,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.List; /** * Abstract representation for any kind of Discord interaction. @@ -50,6 +48,8 @@ *
    Which supports choice suggestions for auto-complete interactions via {@link IAutoCompleteCallback#replyChoices(Command.Choice...)} *

  • {@link IModalCallback} *
    Which supports replying using a {@link Modal} via {@link IModalCallback#replyModal(Modal)}
  • + *
  • {@link IPremiumRequiredReplyCallback} + *
    Which will reply stating that an {@link Entitlement Entitlement} is required
  • * * *

    Once the interaction is acknowledged, you can not reply with these methods again. If the interaction is a {@link IDeferrableCallback deferrable}, @@ -231,6 +231,15 @@ default DiscordLocale getGuildLocale() return getGuild().getLocale(); } + /** + * Returns the list of {@link Entitlement entitlements} for the current guild and user. + *
    If this interaction is not from a guild, it will only contain entitlements of the user. + * + * @return The {@link List List} of {@link Entitlement Entitlement} + */ + @Nonnull + List getEntitlements(); + /** * Returns the {@link net.dv8tion.jda.api.JDA JDA} instance of this interaction * diff --git a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IPremiumRequiredReplyCallback.java b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IPremiumRequiredReplyCallback.java new file mode 100644 index 00000000000..a25b923c945 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IPremiumRequiredReplyCallback.java @@ -0,0 +1,37 @@ +/* + * 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.interactions.callbacks; + +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; +import net.dv8tion.jda.api.entities.Entitlement; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; + +/** + * Replies with an in-built client message stating that an {@link Entitlement Entitlement} is required. + * + *

    Replying with {@link #replyWithPremiumRequired()} will automatically acknowledge this interaction. + * + *

    Note:This interaction requires monetization to be enabled. + */ +public interface IPremiumRequiredReplyCallback extends IDeferrableCallback +{ + @Nonnull + @CheckReturnValue + PremiumRequiredCallbackAction replyWithPremiumRequired(); +} diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteraction.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteraction.java index 73125956428..d0a87b1ed5f 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteraction.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteraction.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.api.interactions.commands; import net.dv8tion.jda.api.interactions.callbacks.IModalCallback; +import net.dv8tion.jda.api.interactions.callbacks.IPremiumRequiredReplyCallback; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.context.ContextInteraction; @@ -28,6 +29,6 @@ * @see ContextInteraction * @see SlashCommandInteraction */ -public interface CommandInteraction extends IReplyCallback, CommandInteractionPayload, IModalCallback +public interface CommandInteraction extends IReplyCallback, CommandInteractionPayload, IModalCallback, IPremiumRequiredReplyCallback { } diff --git a/src/main/java/net/dv8tion/jda/api/interactions/components/ComponentInteraction.java b/src/main/java/net/dv8tion/jda/api/interactions/components/ComponentInteraction.java index f56895df120..2b2290c2135 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/components/ComponentInteraction.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/components/ComponentInteraction.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; import net.dv8tion.jda.api.interactions.callbacks.IMessageEditCallback; import net.dv8tion.jda.api.interactions.callbacks.IModalCallback; +import net.dv8tion.jda.api.interactions.callbacks.IPremiumRequiredReplyCallback; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import javax.annotation.Nonnull; @@ -31,7 +32,7 @@ *

    Instead of {@link #deferReply()} and {@link #reply(String)} you can use {@link #deferEdit()} and {@link #editMessage(String)} with these interactions! * You can only acknowledge an interaction once! */ -public interface ComponentInteraction extends IReplyCallback, IMessageEditCallback, IModalCallback +public interface ComponentInteraction extends IReplyCallback, IMessageEditCallback, IModalCallback, IPremiumRequiredReplyCallback { /** * The custom component ID provided to the component when it was originally created. 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 d4542550485..a522a1943e0 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Route.java @@ -45,6 +45,7 @@ public static class Applications public static final Route GET_BOT_APPLICATION = new Route(GET, "oauth2/applications/@me"); public static final Route GET_ROLE_CONNECTION_METADATA = new Route(GET, "applications/{application_id}/role-connections/metadata"); public static final Route UPDATE_ROLE_CONNECTION_METADATA = new Route(PUT, "applications/{application_id}/role-connections/metadata"); + public static final Route GET_ENTITLEMENTS = new Route(GET, "applications/{application_id}/entitlements"); } public static class Interactions diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/InteractionCallbackAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/InteractionCallbackAction.java index e15775a0043..90d3b045bca 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/InteractionCallbackAction.java +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/InteractionCallbackAction.java @@ -55,6 +55,8 @@ enum ResponseType COMMAND_AUTOCOMPLETE_CHOICES(8), /** Respond with a modal */ MODAL(9), + /** Respond with the "Premium required" default Discord message for premium App subscriptions **/ + PREMIUM_REQUIRED(10), ; private final int raw; diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/PremiumRequiredCallbackAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/PremiumRequiredCallbackAction.java new file mode 100644 index 00000000000..4d2031865a6 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/interactions/PremiumRequiredCallbackAction.java @@ -0,0 +1,29 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.requests.FluentRestAction; + +/** + * An {@link InteractionCallbackAction} that can be used to send the "Premium required" interaction response. + * + * @see net.dv8tion.jda.api.interactions.callbacks.IPremiumRequiredReplyCallback + */ +public interface PremiumRequiredCallbackAction extends InteractionCallbackAction, FluentRestAction +{ + +} diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/EntitlementPaginationAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/EntitlementPaginationAction.java new file mode 100644 index 00000000000..e850d0f7f0f --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/pagination/EntitlementPaginationAction.java @@ -0,0 +1,149 @@ +package net.dv8tion.jda.api.requests.restaction.pagination; + +import net.dv8tion.jda.api.entities.Entitlement; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.UserSnowflake; +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; + +/** + * {@link PaginationAction PaginationAction} that paginates the application entitlements endpoint. + *

    By default, JDA will include {@link Entitlement Entitlement}s which have ended, that is, {@link Entitlement Entitlement}s which + * have gone past their {@link Entitlement#getTimeEnding() timeEnding}. You may use {@link EntitlementPaginationAction#excludeEnded excludeEnded(true)} + * to only return {@link Entitlement}s which are still active + * + *

    Limits
    + * Minimum - 1
    + * Maximum - 100 + *
    Default - 100 + * + *

    Example
    + *

    {@code
    + * //Fetch all entitlements for a given SKU id
    + * public static void fetchEntitlements(JDA api, String skuId, Consumer> callback) {
    + *     List entitlements = new ArrayList<>()
    + *     EntitlementPaginationAction action = api.retrieveEntitlements().skuIds(skuId).excludeEnded(true)
    + *     action.forEachAsync((entitlement) -> {
    + *           entitlements.add(entitlement)
    + *           return true; //continues to retrieve all entitlements until there are none left to retrieve
    + *     }.thenRun(() -> callback.accept(entitlements));
    + * }
    + * }
    + */ +public interface EntitlementPaginationAction extends PaginationAction +{ + /** + * Filter {@link Entitlement Entitlement}s to retrieve by the given user ID + * + * @param user + * The {@link UserSnowflake UserSnowflake} used to filter or {@code null} to remove user filtering. + * This can be a member or user instance of {@link User#fromId(long)} + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction user(@Nullable UserSnowflake user); + + /** + * Filters {@link Entitlement Entitlement}s by their SKU id + * + * @param skuIds + * The SKU ids to filter by + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction skuIds(long... skuIds); + + /** + * Filters {@link Entitlement Entitlement}s by their SKU id + * + * @param skuIds + * The SKU ids to filter by + * + * @throws java.lang.IllegalArgumentException + * If any of the provided {@code skuIds} are {@code null}, empty or are not a valid snowflake + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction skuIds(@Nonnull String... skuIds); + + /** + * Filters {@link Entitlement Entitlement}s by their SKU id + * + * @param skuIds + * The SKU ids to filter by + * + * @throws java.lang.IllegalArgumentException + * If any of the provided {@code skuIds} are {@code null}, empty or invalid snowflakes + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction skuIds(@Nonnull Collection skuIds); + + /** + * Filters {@link Entitlement Entitlement}s by a guild id + * + * @param guildId + * The guild id to filter by + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction guild(long guildId); + + /** + * Filters {@link Entitlement Entitlement}s by a guild id + * + * @param guildId + * The guild id to filter by + * + * @throws java.lang.IllegalArgumentException + * If the provided {@code guildId} is {@code null}, empty or is not a valid snowflake + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + default EntitlementPaginationAction guild(@Nonnull String guildId) + { + Checks.notNull(guildId, "guildId"); + Checks.isSnowflake(guildId, "guildId"); + return guild(Long.parseUnsignedLong(guildId)); + } + + /** + * Filters {@link Entitlement Entitlement}s by a {@link Guild Guild} + * + * @param guild + * The {@link Guild Guild} to filter by + * + * @throws java.lang.IllegalArgumentException + * If the provided {@code guild} is {@code null} + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + default EntitlementPaginationAction guild(@Nonnull Guild guild) + { + Checks.notNull(guild, "guild"); + return guild(guild.getIdLong()); + } + + /** + * Whether to exclude subscriptions which have gone past their end date. + *

    Test entitlements which are created through the API do not have an end date. + * + * @param excludeEnded + * Whether to exclude ended subscriptions from returned {@link Entitlement Entitlement}s + * + * @return The current {@link EntitlementPaginationAction EntitlementPaginationAction} for chaining convenience + */ + @Nonnull + EntitlementPaginationAction excludeEnded(boolean excludeEnded); +} diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index f55f9801110..01f84c3a505 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -52,6 +52,7 @@ import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; import net.dv8tion.jda.api.requests.restaction.CommandEditAction; import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; +import net.dv8tion.jda.api.requests.restaction.pagination.EntitlementPaginationAction; import net.dv8tion.jda.api.sharding.ShardManager; import net.dv8tion.jda.api.utils.*; import net.dv8tion.jda.api.utils.cache.CacheFlag; @@ -75,6 +76,7 @@ import net.dv8tion.jda.internal.requests.restaction.CommandEditActionImpl; import net.dv8tion.jda.internal.requests.restaction.CommandListUpdateActionImpl; import net.dv8tion.jda.internal.requests.restaction.GuildActionImpl; +import net.dv8tion.jda.internal.requests.restaction.pagination.EntitlementPaginationActionImpl; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.*; import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; @@ -1164,6 +1166,13 @@ public RestAction retrieveApplicationInfo() }); } + @Nonnull + @Override + public EntitlementPaginationAction retrieveEntitlements() + { + return new EntitlementPaginationActionImpl(this); + } + @Nonnull @Override public JDA setRequiredScopes(@Nonnull Collection scopes) 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 87d0c8d18bf..1e636eb5e00 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java @@ -33,7 +33,6 @@ import net.dv8tion.jda.internal.requests.restaction.WebhookMessageDeleteActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; import net.dv8tion.jda.internal.utils.Checks; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import java.util.Collection; @@ -91,7 +90,7 @@ public WebhookMessageCreateAction sendMessageEmbeds(@Nonnull Collection sendMessageComponents(@NotNull Collection components) + public WebhookMessageCreateAction sendMessageComponents(@Nonnull Collection components) { return sendRequest().setComponents(components); } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntitlementImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/EntitlementImpl.java new file mode 100644 index 00000000000..e77d790477d --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntitlementImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.entities; + +import net.dv8tion.jda.api.entities.Entitlement; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.OffsetDateTime; + +public class EntitlementImpl implements Entitlement +{ + private long id; + private long skuId; + private long applicationId; + private long userId; + private long guildId; + private EntitlementType type; + private boolean deleted; + private OffsetDateTime startsAt; + private OffsetDateTime endsAt; + + public EntitlementImpl(long id, long skuId, long applicationId, long userId, long guildId, EntitlementType type, boolean deleted, @Nullable OffsetDateTime startsAt, @Nullable OffsetDateTime endsAt) + { + this.id = id; + this.skuId = skuId; + this.applicationId = applicationId; + this.userId = userId; + this.guildId = guildId; + this.type = type; + this.deleted = deleted; + this.startsAt = startsAt; + this.endsAt = endsAt; + } + + @Override + public long getIdLong() + { + return id; + } + + @Override + public long getSkuIdLong() + { + return skuId; + } + + @Override + public long getApplicationIdLong() + { + return applicationId; + } + + @Override + public long getUserIdLong() + { + return userId; + } + + @Override + public long getGuildIdLong() + { + return guildId; + } + + @Nonnull + @Override + public EntitlementType getType() + { + return type; + } + + @Override + public boolean isDeleted() + { + return deleted; + } + + @Nullable + @Override + public OffsetDateTime getTimeStarting() + { + return startsAt; + } + + @Nullable + @Override + public OffsetDateTime getTimeEnding() + { + return endsAt; + } +} 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 00440cd5349..65fef6f6399 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -2557,6 +2557,21 @@ public AuditLogChange createAuditLogChange(DataObject change) return new AuditLogChange(oldValue, newValue, key); } + public Entitlement createEntitlement(DataObject object) + { + return new EntitlementImpl( + object.getLong("id"), + object.getLong("sku_id"), + object.getLong("application_id"), + object.getLong("user_id", 0), + object.getLong("guild_id", 0), + Entitlement.EntitlementType.fromKey(object.getInt("type")), + object.getBoolean("deleted"), + object.getOffsetDateTime("starts_at", null), + object.getOffsetDateTime("ends_at", null) + ); + } + private Map changeToMap(Set changesList) { return changesList.stream().collect(Collectors.toMap(AuditLogChange::getKey, UnaryOperator.identity())); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/automod/AutoModRuleImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/automod/AutoModRuleImpl.java index 9bfbd75cade..2f07ecc677d 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/automod/AutoModRuleImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/automod/AutoModRuleImpl.java @@ -29,8 +29,8 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.utils.EntityString; import net.dv8tion.jda.internal.utils.Helpers; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -68,7 +68,7 @@ public long getIdLong() return id; } - @NotNull + @Nonnull @Override public Guild getGuild() { @@ -84,21 +84,21 @@ public long getCreatorIdLong() return ownerId; } - @NotNull + @Nonnull @Override public String getName() { return name; } - @NotNull + @Nonnull @Override public AutoModEventType getEventType() { return eventType; } - @NotNull + @Nonnull @Override public AutoModTriggerType getTriggerType() { @@ -111,7 +111,7 @@ public boolean isEnabled() return enabled; } - @NotNull + @Nonnull @Override public List getExemptRoles() { @@ -126,7 +126,7 @@ public List getExemptRoles() return Collections.unmodifiableList(roles); } - @NotNull + @Nonnull @Override public List getExemptChannels() { @@ -141,35 +141,35 @@ public List getExemptChannels() return Collections.unmodifiableList(channels); } - @NotNull + @Nonnull @Override public List getActions() { return actions; } - @NotNull + @Nonnull @Override public List getFilteredKeywords() { return filteredKeywords; } - @NotNull + @Nonnull @Override public List getFilteredRegex() { return filteredRegex; } - @NotNull + @Nonnull @Override public EnumSet getFilteredPresets() { return Helpers.copyEnumSet(KeywordPreset.class, filteredPresets); } - @NotNull + @Nonnull @Override public List getAllowlist() { diff --git a/src/main/java/net/dv8tion/jda/internal/handle/EntitlementCreateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementCreateHandler.java new file mode 100644 index 00000000000..32367ecff4a --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementCreateHandler.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.internal.handle; + +import net.dv8tion.jda.api.events.entitlement.EntitlementCreateEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; + +public class EntitlementCreateHandler extends SocketHandler +{ + public EntitlementCreateHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + getJDA().handleEvent(new EntitlementCreateEvent(getJDA(), responseNumber, getJDA().getEntityBuilder().createEntitlement(content))); + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/EntitlementDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementDeleteHandler.java new file mode 100644 index 00000000000..22a54e3bbd4 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementDeleteHandler.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.internal.handle; + +import net.dv8tion.jda.api.events.entitlement.EntitlementDeleteEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; + +public class EntitlementDeleteHandler extends SocketHandler +{ + public EntitlementDeleteHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + getJDA().handleEvent(new EntitlementDeleteEvent(getJDA(), responseNumber, getJDA().getEntityBuilder().createEntitlement(content))); + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/EntitlementUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementUpdateHandler.java new file mode 100644 index 00000000000..c2a86177528 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/EntitlementUpdateHandler.java @@ -0,0 +1,35 @@ +/* + * 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.events.entitlement.EntitlementUpdateEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; + +public class EntitlementUpdateHandler extends SocketHandler +{ + public EntitlementUpdateHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + getJDA().handleEvent(new EntitlementUpdateEvent(getJDA(), responseNumber, getJDA().getEntityBuilder().createEntitlement(content))); + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java index 9c334a9d380..cce804a8bdd 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.internal.interactions; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Entitlement; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; @@ -26,15 +27,17 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.interactions.DiscordLocale; import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.entities.GuildImpl; -import net.dv8tion.jda.internal.entities.MemberImpl; -import net.dv8tion.jda.internal.entities.UserImpl; +import net.dv8tion.jda.internal.entities.*; import net.dv8tion.jda.internal.entities.channel.concrete.PrivateChannelImpl; +import net.dv8tion.jda.internal.utils.Helpers; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; public class InteractionImpl implements Interaction { @@ -47,6 +50,7 @@ public class InteractionImpl implements Interaction protected final User user; protected final Channel channel; protected final DiscordLocale userLocale; + protected final List entitlements; protected final JDAImpl api; //This is used to give a proper error when an interaction is ack'd twice @@ -104,6 +108,11 @@ public InteractionImpl(JDAImpl jda, DataObject data) } this.user = user; } + + this.entitlements = data.optArray("entitlements").orElseGet(DataArray::empty) + .stream(DataArray::getObject) + .map(jda.getEntityBuilder()::createEntitlement) + .collect(Helpers.toUnmodifiableList()); } // Used to allow interaction hook to send messages after acknowledgements @@ -183,6 +192,13 @@ public Member getMember() return member; } + @Nonnull + @Override + public List getEntitlements() + { + return entitlements; + } + @Nonnull @Override public JDA getJDA() diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/command/CommandInteractionImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/command/CommandInteractionImpl.java index 2940a5ad99e..9d437bac2ce 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/command/CommandInteractionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/command/CommandInteractionImpl.java @@ -20,11 +20,13 @@ import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.interactions.DeferrableInteractionImpl; import net.dv8tion.jda.internal.requests.restaction.interactions.ModalCallbackActionImpl; +import net.dv8tion.jda.internal.requests.restaction.interactions.PremiumRequiredCallbackActionImpl; import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -60,4 +62,11 @@ public ModalCallbackAction replyModal(@Nonnull Modal modal) Checks.notNull(modal, "Modal"); return new ModalCallbackActionImpl(this, modal); } + + @Nonnull + @Override + public PremiumRequiredCallbackAction replyWithPremiumRequired() + { + return new PremiumRequiredCallbackActionImpl(this); + } } diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/component/ComponentInteractionImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/component/ComponentInteractionImpl.java index e6e735eae3f..e7ec45268b3 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/component/ComponentInteractionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/component/ComponentInteractionImpl.java @@ -23,6 +23,7 @@ import net.dv8tion.jda.api.interactions.components.ComponentInteraction; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; @@ -30,6 +31,7 @@ import net.dv8tion.jda.internal.interactions.DeferrableInteractionImpl; import net.dv8tion.jda.internal.requests.restaction.interactions.MessageEditCallbackActionImpl; import net.dv8tion.jda.internal.requests.restaction.interactions.ModalCallbackActionImpl; +import net.dv8tion.jda.internal.requests.restaction.interactions.PremiumRequiredCallbackActionImpl; import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -115,4 +117,11 @@ public ModalCallbackAction replyModal(@Nonnull Modal modal) return new ModalCallbackActionImpl(this, modal); } + + @Nonnull + @Override + public PremiumRequiredCallbackAction replyWithPremiumRequired() + { + return new PremiumRequiredCallbackActionImpl(this); + } } diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java index a760810b6e0..f2a7540aea3 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java @@ -22,7 +22,6 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.utils.Helpers; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import java.util.Collections; @@ -99,7 +98,7 @@ public List getDefaultValues() return defaultValues; } - @NotNull + @Nonnull @Override public DataObject toData() { diff --git a/src/main/java/net/dv8tion/jda/internal/requests/IncomingWebhookClientImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/IncomingWebhookClientImpl.java index 9f7bd21f561..2f32deb30c3 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/IncomingWebhookClientImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/IncomingWebhookClientImpl.java @@ -33,7 +33,6 @@ import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageRetrieveActionImpl; import net.dv8tion.jda.internal.utils.Checks; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import java.util.function.Function; @@ -82,7 +81,7 @@ public WebhookMessageRetrieveAction retrieveMessageById(@Nonnull String messageI @Nonnull @Override - public WebhookMessageDeleteAction deleteMessageById(@NotNull String messageId) + public WebhookMessageDeleteAction deleteMessageById(@Nonnull String messageId) { WebhookMessageDeleteActionImpl action = (WebhookMessageDeleteActionImpl) super.deleteMessageById(messageId); action.run(); 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 1a7c1fca7ff..08f24d1ad5a 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -1361,6 +1361,9 @@ protected void setupHandlers() handlers.put("CHANNEL_CREATE", new ChannelCreateHandler(api)); handlers.put("CHANNEL_DELETE", new ChannelDeleteHandler(api)); handlers.put("CHANNEL_UPDATE", new ChannelUpdateHandler(api)); + handlers.put("ENTITLEMENT_CREATE", new EntitlementCreateHandler(api)); + handlers.put("ENTITLEMENT_UPDATE", new EntitlementUpdateHandler(api)); + handlers.put("ENTITLEMENT_DELETE", new EntitlementDeleteHandler(api)); handlers.put("GUILD_AUDIT_LOG_ENTRY_CREATE", new GuildAuditLogEntryCreateHandler(api)); handlers.put("GUILD_BAN_ADD", new GuildBanHandler(api, true)); handlers.put("GUILD_BAN_REMOVE", new GuildBanHandler(api, false)); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/CommandCreateActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/CommandCreateActionImpl.java index ba7873b7f50..fdd02e0db16 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/CommandCreateActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/CommandCreateActionImpl.java @@ -34,7 +34,6 @@ import net.dv8tion.jda.internal.interactions.command.CommandImpl; import net.dv8tion.jda.internal.requests.RestActionImpl; import okhttp3.RequestBody; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import java.util.List; @@ -225,19 +224,19 @@ public LocalizationMap getDescriptionLocalizations() } @Override - public boolean removeOptions(@NotNull Predicate condition) + public boolean removeOptions(@Nonnull Predicate condition) { return data.removeOptions(condition); } @Override - public boolean removeSubcommands(@NotNull Predicate condition) + public boolean removeSubcommands(@Nonnull Predicate condition) { return data.removeSubcommands(condition); } @Override - public boolean removeSubcommandGroups(@NotNull Predicate condition) + public boolean removeSubcommandGroups(@Nonnull Predicate condition) { return data.removeSubcommandGroups(condition); } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/PremiumRequiredCallbackActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/PremiumRequiredCallbackActionImpl.java new file mode 100644 index 00000000000..45ca3752f22 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/PremiumRequiredCallbackActionImpl.java @@ -0,0 +1,58 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.interactions.callbacks.IPremiumRequiredReplyCallback; +import net.dv8tion.jda.api.requests.restaction.interactions.InteractionCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.PremiumRequiredCallbackAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.interactions.InteractionImpl; +import okhttp3.RequestBody; + +import javax.annotation.Nonnull; +import java.util.function.BooleanSupplier; + +public class PremiumRequiredCallbackActionImpl extends InteractionCallbackImpl implements PremiumRequiredCallbackAction +{ + + public PremiumRequiredCallbackActionImpl(IPremiumRequiredReplyCallback interaction) + { + super((InteractionImpl) interaction); + } + + @Override + protected RequestBody finalizeData() + { + return getRequestBody(DataObject.empty() + .put("type", InteractionCallbackAction.ResponseType.PREMIUM_REQUIRED.getRaw()) + .put("data", DataObject.empty())); + } + + @Nonnull + @Override + public PremiumRequiredCallbackAction setCheck(BooleanSupplier checks) + { + return (PremiumRequiredCallbackAction) super.setCheck(checks); + } + + @Nonnull + @Override + public PremiumRequiredCallbackAction deadline(long timestamp) + { + return (PremiumRequiredCallbackAction) super.deadline(timestamp); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/EntitlementPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/EntitlementPaginationActionImpl.java new file mode 100644 index 00000000000..c51e54c534b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/EntitlementPaginationActionImpl.java @@ -0,0 +1,183 @@ +/* + * 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.Entitlement; +import net.dv8tion.jda.api.entities.UserSnowflake; +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.EntitlementPaginationAction; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.EntityBuilder; +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +public class EntitlementPaginationActionImpl + extends PaginationActionImpl + implements EntitlementPaginationAction +{ + protected List skuIds; + protected long guildId; + protected long userId; + protected boolean excludeEnded; + + public EntitlementPaginationActionImpl(JDA api) + { + super(api, Route.Applications.GET_ENTITLEMENTS.compile(api.getSelfUser().getApplicationId()), 1, 100, 100); + this.skuIds = new ArrayList<>(); + this.guildId = 0; + this.userId = 0; + } + + @Nonnull + @Override + public EnumSet getSupportedOrders() + { + return EnumSet.of(PaginationOrder.BACKWARD, PaginationOrder.FORWARD); + } + + @Nonnull + @Override + public EntitlementPaginationAction user(@Nullable UserSnowflake user) + { + if (user == null) + userId = 0; + else + userId = user.getIdLong(); + return this; + } + + @Nonnull + @Override + public EntitlementPaginationAction skuIds(long... skuIds) + { + this.skuIds.clear(); + for (long skuId : skuIds) + this.skuIds.add(Long.toUnsignedString(skuId)); + return this; + } + + @Nonnull + @Override + public EntitlementPaginationAction skuIds(@Nonnull String... skuIds) + { + Checks.noneNull(skuIds, "skuIds"); + for (String skuId : skuIds) + Checks.isSnowflake(skuId, "skuId"); + + this.skuIds.clear(); + + Collections.addAll(this.skuIds, skuIds); + return this; + } + + @Nonnull + @Override + public EntitlementPaginationAction skuIds(@Nonnull Collection skuIds) + { + Checks.noneNull(skuIds, "skuIds"); + + this.skuIds.clear(); + for (String skuId : skuIds) + { + Checks.isSnowflake(skuId, "skuId"); + this.skuIds.add(skuId); + } + + return this; + } + + @Nonnull + @Override + public EntitlementPaginationAction guild(long guildId) + { + this.guildId = guildId; + return this; + } + + @Nonnull + @Override + public EntitlementPaginationAction excludeEnded(boolean excludeEnded) + { + this.excludeEnded = excludeEnded; + return this; + } + + @Override + protected Route.CompiledRoute finalizeRoute() + { + Route.CompiledRoute route = super.finalizeRoute(); + + if (userId != 0) + route = route.withQueryParams("user_id", Long.toUnsignedString(userId)); + + if (!skuIds.isEmpty()) + route = route.withQueryParams("sku_ids", String.join(",", skuIds)); + + if (guildId != 0) + route = route.withQueryParams("guild_id", Long.toUnsignedString(guildId)); + + if (excludeEnded) + route = route.withQueryParams("exclude_ended", String.valueOf(true)); + + return route; + } + + @Override + protected void handleSuccess(Response response, Request> request) + { + DataArray array = response.getArray(); + List entitlements = new ArrayList<>(array.length()); + EntityBuilder builder = api.getEntityBuilder(); + for (int i = 0; i < array.length(); i++) + { + try + { + DataObject object = array.getObject(i); + Entitlement entitlement = builder.createEntitlement(object); + entitlements.add(entitlement); + } + catch(ParsingException | NullPointerException e) + { + LOG.warn("Encountered an exception in EntitlementPaginationAction", e); + } + } + + if (!entitlements.isEmpty()) + { + if (useCache) + cached.addAll(entitlements); + last = entitlements.get(entitlements.size() - 1); + lastKey = last.getIdLong(); + } + + request.onSuccess(entitlements); + } + + @Override + protected long getKey(Entitlement it) + { + return it.getIdLong(); + } +} From 1bcfb7334b2da4d8695c2597def4132b3dd7844c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 6 Apr 2024 23:23:34 +0200 Subject: [PATCH 15/25] Bump version to 5.0.0-beta.22 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index cab68f5e665..f7b22d2bbd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,7 @@ plugins { } val javaVersion = JavaVersion.current() -val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.21") +val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.22") val isCI = System.getProperty("BUILD_NUMBER") != null // jenkins || System.getenv("BUILD_NUMBER") != null || System.getProperty("GIT_COMMIT") != null // jitpack From 31419c09af28272466d5746f12ee1237cf489f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 7 Apr 2024 14:09:51 +0200 Subject: [PATCH 16/25] Use typed assertions for RestActions (#2643) * Use typed assertions for RestActions * Test bulk-ban deduplication * Add entitlement pagination test --- .../jda/internal/entities/EntityBuilder.java | 10 +- .../java/net/dv8tion/jda/test/Constants.java | 24 +++ .../net/dv8tion/jda/test/IntegrationTest.java | 66 ++++-- .../jda/test/RestActionAssertions.java | 161 ++++++++++++++ .../entities/guild/AbstractGuildTest.java | 65 ++++++ .../jda/test/entities/guild/BulkBanTest.java | 120 +++++++++++ .../EntitlementPaginationTest.java | 203 ++++++++++++++++++ .../restaction/MessageCreateActionTest.java | 39 ++-- 8 files changed, 644 insertions(+), 44 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/test/Constants.java create mode 100644 src/test/java/net/dv8tion/jda/test/RestActionAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/entities/guild/AbstractGuildTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java create mode 100644 src/test/java/net/dv8tion/jda/test/interactions/EntitlementPaginationTest.java 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 65fef6f6399..97d7258a771 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -2560,11 +2560,11 @@ public AuditLogChange createAuditLogChange(DataObject change) public Entitlement createEntitlement(DataObject object) { return new EntitlementImpl( - object.getLong("id"), - object.getLong("sku_id"), - object.getLong("application_id"), - object.getLong("user_id", 0), - object.getLong("guild_id", 0), + object.getUnsignedLong("id"), + object.getUnsignedLong("sku_id"), + object.getUnsignedLong("application_id"), + object.getUnsignedLong("user_id", 0), + object.getUnsignedLong("guild_id", 0), Entitlement.EntitlementType.fromKey(object.getInt("type")), object.getBoolean("deleted"), object.getOffsetDateTime("starts_at", null), diff --git a/src/test/java/net/dv8tion/jda/test/Constants.java b/src/test/java/net/dv8tion/jda/test/Constants.java new file mode 100644 index 00000000000..0e1c4c9ef47 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/Constants.java @@ -0,0 +1,24 @@ +/* + * 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 interface Constants +{ + long GUILD_ID = 125227483518861312L; + long MINN_USER_ID = 86699011792191488L; + long BUTLER_USER_ID = 150203841827045376L; +} diff --git a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java index 6138ebfd4d7..a884a14a06d 100644 --- a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java +++ b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java @@ -16,25 +16,32 @@ package net.dv8tion.jda.test; -import net.dv8tion.jda.api.requests.Method; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.Requester; +import net.dv8tion.jda.internal.requests.RestActionImpl; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; import org.mockito.Mock; +import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; +import java.util.Random; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.assertArg; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.openMocks; public class IntegrationTest { + protected Random random = new Random(); @Mock protected JDAImpl jda; @Mock @@ -43,17 +50,25 @@ public class IntegrationTest protected ScheduledExecutorService scheduledExecutorService; private AutoCloseable closeable; + private int expectedRequestCount; @BeforeEach protected final void setup() { + random.setSeed(4242); + expectedRequestCount = 0; closeable = openMocks(this); when(jda.getRequester()).thenReturn(requester); + when(jda.getEntityBuilder()).thenReturn(new EntityBuilder(jda)); } @AfterEach - protected final void teardown() throws Exception + protected final void teardown(TestInfo testInfo) throws Exception { + verify( + requester, + times(expectedRequestCount).description("Requests sent by " + testInfo.getDisplayName()) + ).request(any()); closeable.close(); } @@ -63,20 +78,31 @@ protected DataObject normalizeRequestBody(@Nonnull DataObject body) return body; } - protected void assertNextRequestEquals(Method method, String compiledRoute, DataObject expectedBody) + @CheckReturnValue + protected RestActionAssertions assertThatRequestFrom(@Nonnull RestAction action) { - doNothing().when(requester).request(assertArg(request -> { - assertThat(request.getRoute().getMethod()).isEqualTo(method); - assertThat(request.getRoute().getCompiledRoute()).isEqualTo(compiledRoute); - - assertThat(request.getRawBody()) - .isNotNull() - .isInstanceOf(DataObject.class); - DataObject body = normalizeRequestBody((DataObject) request.getRawBody()); - - assertThat(body) - .withRepresentation(new PrettyRepresentation()) - .isEqualTo(expectedBody); - })); + expectedRequestCount += 1; + return RestActionAssertions.assertThatNextAction(requester, action) + .withNormalizedBody(this::normalizeRequestBody); + } + + protected void whenSuccess(RestActionImpl action, DataArray array, Consumer assertion) + { + Response response = mock(); + Request request = mock(); + + when(response.isOk()).thenReturn(true); + when(response.getArray()).thenReturn(array); + + doNothing().when(request).onSuccess(assertArg(assertion)); + + action.handleResponse(response, request); + + verify(request, times(1)).onSuccess(any()); + } + + protected String randomSnowflake() + { + return Long.toUnsignedString(random.nextLong()); } } diff --git a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java b/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java new file mode 100644 index 00000000000..37f9f245189 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java @@ -0,0 +1,161 @@ +/* + * 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.api.requests.Method; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.requests.Requester; +import net.dv8tion.jda.internal.utils.EncodingUtil; +import org.jetbrains.annotations.Contract; +import org.mockito.ThrowingConsumer; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import java.util.*; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.doNothing; + +public class RestActionAssertions implements ThrowingConsumer> +{ + private final RestAction action; + private final List>> assertions = new ArrayList<>(); + private Consumer normalizeRequestBody = (v) -> {}; + + public RestActionAssertions(RestAction action) + { + this.action = action; + } + + @CheckReturnValue + public static RestActionAssertions assertThatNextAction(Requester requester, RestAction action) + { + RestActionAssertions assertions = new RestActionAssertions(action); + doNothing().when(requester).request(assertArg(assertions::acceptThrows)); + return assertions; + } + + public void whenQueueCalled() + { + action.queue(); + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions withNormalizedBody(@Nonnull Consumer normalizer) + { + this.normalizeRequestBody = normalizer; + return this; + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions checkAssertions(@Nonnull ThrowingConsumer> assertion) + { + assertions.add(assertion); + return this; + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions hasBodyEqualTo(@Nonnull DataObject expected) + { + return checkAssertions(request -> { + Object body = request.getRawBody(); + assertThat(body) + .isNotNull() + .isInstanceOf(DataObject.class); + + DataObject dataObject = (DataObject) body; + normalizeRequestBody.accept(dataObject); + normalizeRequestBody.accept(expected); + + assertThat(dataObject.toPrettyString()) + .as("RestAction should send request using expected request body") + .isEqualTo(expected.toPrettyString()); + }); + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions hasMethod(@Nonnull Method method) + { + return checkAssertions(request -> + assertThat(request.getRoute().getMethod()) + .as("RestAction should send request using expected HTTP Method") + .isEqualTo(method) + ); + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions hasCompiledRoute(@Nonnull String route) + { + return checkAssertions(request -> + assertThat(request.getRoute().getCompiledRoute()) + .as("RestAction should send request using expected REST endpoint") + .isEqualTo(route) + ); + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions hasQueryParams(@Nonnull Object... params) + { + assertThat(params.length).isEven(); + + Map expectedQuery = new LinkedHashMap<>(); + + for (int i = 0; i < params.length; i += 2) + expectedQuery.put(String.valueOf(params[i]), EncodingUtil.encodeUTF8(String.valueOf(params[i + 1]))); + + return checkAssertions(request -> { + Map actualQuery = new LinkedHashMap<>(); + String[] query = request.getRoute().getCompiledRoute().split("[?&=]"); + + for (int i = 1; i < query.length; i += 2) + actualQuery.put(query[i], query[i + 1]); + + assertThat(actualQuery) + .containsExactlyEntriesOf(expectedQuery); + }); + } + + @CheckReturnValue + @Contract("_->this") + public RestActionAssertions hasAuditReason(@Nonnull String reason) + { + return checkAssertions(request -> + assertThat(request.getHeaders()) + .as("RestAction should set header") + .contains(new AbstractMap.SimpleEntry<>("X-Audit-Log-Reason", reason)) + ); + } + + @Override + public void acceptThrows(Request request) throws Throwable + { + for (ThrowingConsumer> assertion : assertions) + { + assertion.acceptThrows(request); + } + } +} diff --git a/src/test/java/net/dv8tion/jda/test/entities/guild/AbstractGuildTest.java b/src/test/java/net/dv8tion/jda/test/entities/guild/AbstractGuildTest.java new file mode 100644 index 00000000000..0dd7263ff3e --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/guild/AbstractGuildTest.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.entities.guild; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.entities.MemberImpl; +import net.dv8tion.jda.internal.entities.SelfUserImpl; +import net.dv8tion.jda.internal.utils.UnlockHook; +import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; +import net.dv8tion.jda.test.Constants; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; + +import java.util.EnumSet; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public abstract class AbstractGuildTest extends IntegrationTest +{ + @Mock + protected SelfUserImpl selfUser; + @Mock + protected MemberImpl selfMember; + + protected GuildImpl guild; + + @BeforeEach + final void setupGuild() + { + when(selfUser.getIdLong()).thenReturn(Constants.MINN_USER_ID); + when(jda.getSelfUser()).thenReturn(selfUser); + when(jda.getCacheFlags()).thenReturn(EnumSet.allOf(CacheFlag.class)); + + guild = new GuildImpl(jda, Constants.GUILD_ID); + + MemberCacheViewImpl members = guild.getMembersView(); + try (UnlockHook ignored = members.writeLock()) + { + members.getMap().put(Constants.MINN_USER_ID, selfMember); + } + } + + protected void hasPermission(boolean has) + { + when(selfMember.hasPermission(any(Permission[].class))).thenReturn(has); + } +} 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 new file mode 100644 index 00000000000..8192df657b7 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java @@ -0,0 +1,120 @@ +/* + * 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.guild; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.UserSnowflake; +import net.dv8tion.jda.api.exceptions.HierarchyException; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import net.dv8tion.jda.api.requests.Method; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.utils.Helpers; +import net.dv8tion.jda.test.Constants; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BulkBanTest extends AbstractGuildTest +{ + @Test + void testMissingPermissions() + { + hasPermission(false); + + assertThatThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ZERO)) + .isInstanceOf(InsufficientPermissionException.class) + .hasMessage("Cannot perform action due to a lack of Permission. Missing permission: " + Permission.BAN_MEMBERS); + } + + @Test + void testBanOwner() + { + hasPermission(true); + + guild.setOwnerId(Constants.BUTLER_USER_ID); + + Set users = Collections.singleton(User.fromId(Constants.BUTLER_USER_ID)); + + assertThatThrownBy(() -> guild.ban(users, Duration.ZERO)) + .isInstanceOf(HierarchyException.class) + .hasMessage("Cannot ban the owner of a guild."); + } + + @Test + void testInvalidInputs() + { + hasPermission(true); + + Set users = Collections.singleton(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> guild.ban(users, Duration.ZERO).queue()) + .withMessage("Users may not be null"); + 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( + LongStream.range(1, 300) + .map(i -> random.nextLong()) + .mapToObj(User::fromId) + .collect(Collectors.toList()), + null + ).queue() + ).withMessage("Cannot ban more than 200 users at once"); + } + + @Test + void testDuplicates() + { + hasPermission(true); + + Duration duration = Duration.ofSeconds(random.nextInt(10000)); + String reason = Helpers.format("User %d was banned by %d for %s", Constants.BUTLER_USER_ID, Constants.MINN_USER_ID, duration); + List users = Arrays.asList( + User.fromId(Constants.BUTLER_USER_ID), + User.fromId(Constants.BUTLER_USER_ID) + ); + + assertThatRequestFrom(guild.ban(users, duration).reason(reason)) + .hasMethod(Method.POST) + .hasCompiledRoute("guilds/" + Constants.GUILD_ID + "/bulk-ban") + .hasAuditReason(reason) + .hasBodyEqualTo(DataObject.empty() + .put("delete_message_seconds", duration.getSeconds()) + .put("user_ids", DataArray.empty().add(Constants.BUTLER_USER_ID))) + .whenQueueCalled(); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/EntitlementPaginationTest.java b/src/test/java/net/dv8tion/jda/test/interactions/EntitlementPaginationTest.java new file mode 100644 index 00000000000..53062e8d232 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/interactions/EntitlementPaginationTest.java @@ -0,0 +1,203 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.entities.Entitlement; +import net.dv8tion.jda.api.entities.SelfUser; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.requests.restaction.pagination.EntitlementPaginationActionImpl; +import net.dv8tion.jda.test.Constants; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Spy; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static net.dv8tion.jda.api.requests.Method.GET; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class EntitlementPaginationTest extends IntegrationTest +{ + protected static final String routeTemplate = "applications/%d/entitlements%s"; + + @Spy + protected SelfUser selfUser; + + protected EntitlementPaginationActionImpl action; + + private DataObject fakeEntitlement(String id) + { + return DataObject.empty() + .put("id", id) + .put("sku_id", randomSnowflake()) + .put("application_id", randomSnowflake()) + .put("user_id", randomSnowflake()) + .put("type", 8) + .put("deleted", false); + } + + @BeforeEach + void setupSelfUser() + { + when(selfUser.getApplicationIdLong()).thenReturn(Constants.BUTLER_USER_ID); + when(jda.getSelfUser()).thenReturn(selfUser); + + action = new EntitlementPaginationActionImpl(jda); + } + + @Test + void testParsingFailure() + { + assertThatRequestFrom(action) + .hasMethod(GET) + .hasCompiledRoute(String.format(routeTemplate, Constants.BUTLER_USER_ID, "?limit=100")) + .whenQueueCalled(); + + DataArray responseBody = DataArray.empty() + .add(DataObject.empty()); // Invalid entitlement object + + whenSuccess(action, responseBody, response -> + assertThat(response).isEmpty() // Is logged and skipped + ); + } + + @Test + void testDefaultPagination() + { + assertThatRequestFrom(action) + .hasQueryParams("limit", "100") + .whenQueueCalled(); + + DataArray array = DataArray.empty() + .add(fakeEntitlement("2")) + .add(fakeEntitlement("1")); + + whenSuccess(action, array, response -> + assertThat(response) + .hasSize(2) + .map(Entitlement::getId) + .containsExactly("2", "1") + ); + + assertThatRequestFrom(action) + .hasQueryParams("limit", "100", "before", "1") + .whenQueueCalled(); + + whenSuccess(action, DataArray.empty(), response -> + assertThat(response) + .isEmpty() + ); + + assertThat(action.cacheSize()).isEqualTo(2); + assertThat(action.getCached()) + .hasSize(2) + .map(Entitlement::getId) + .containsExactly("2", "1"); + + } + + @Test + void testReversePagination() + { + assertThatRequestFrom(action.reverse()) + .hasQueryParams("limit", 100, "after", 0) + .whenQueueCalled(); + + DataArray array = DataArray.empty() + .add(fakeEntitlement("1")) + .add(fakeEntitlement("2")); + + whenSuccess(action, array, response -> + assertThat(response) + .hasSize(2) + .map(Entitlement::getId) + .containsExactly("1", "2") + ); + + assertThatRequestFrom(action) + .hasQueryParams("limit", "100", "after", "2") + .whenQueueCalled(); + + whenSuccess(action, DataArray.empty(), response -> + assertThat(response) + .isEmpty() + ); + + assertThat(action.cacheSize()).isEqualTo(2); + assertThat(action.getCached()) + .hasSize(2) + .map(Entitlement::getId) + .containsExactly("1", "2"); + } + + @Test + void testSkipTo() + { + long skipId = Math.abs(random.nextLong()); + + assertThatRequestFrom(action.skipTo(skipId)) + .hasQueryParams("limit", "100", "before", skipId) + .whenQueueCalled(); + } + + @Nested + class Filter + { + @Test + void byExcludeEnded() + { + assertThatRequestFrom(action.excludeEnded(true)) + .hasQueryParams("limit", "100", "exclude_ended", "true") + .whenQueueCalled(); + } + + @Test + void bySkuIds() + { + Set sku = new LinkedHashSet<>(); + + for (int i = -5; i < random.nextInt(10); i++) + sku.add(Long.toUnsignedString(random.nextLong())); + + assertThatRequestFrom(action.skuIds(sku)) + .hasQueryParams("limit", "100", "sku_ids", String.join(",", sku)) + .whenQueueCalled(); + } + + @Test + void byUserId() + { + assertThatRequestFrom(action.user(User.fromId(Constants.MINN_USER_ID))) + .hasQueryParams( "limit", 100, "user_id", Constants.MINN_USER_ID) + .whenQueueCalled(); + } + + @Test + void byGuildId() + { + assertThatRequestFrom(action.guild(Constants.GUILD_ID)) + .hasQueryParams("limit", 100, "guild_id", Constants.GUILD_ID) + .whenQueueCalled(); + } + } +} 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 c7afa5cd0ab..57456b018d7 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -18,6 +18,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +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.internal.requests.restaction.MessageCreateActionImpl; @@ -30,8 +31,7 @@ import static net.dv8tion.jda.api.requests.Method.POST; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; public class MessageCreateActionTest extends IntegrationTest { @@ -79,30 +79,31 @@ void testEmpty() @Test void testContentOnly() { - assertNextRequestEquals(POST, ENDPOINT_URL, defaultMessageRequest() - .put("content", "test content")); - - new MessageCreateActionImpl(channel) - .setContent("test content") - .queue(); - - verify(requester, times(1)).request(any()); + MessageCreateAction action = new MessageCreateActionImpl(channel) + .setContent("test content"); + + assertThatRequestFrom(action) + .hasMethod(POST) + .hasCompiledRoute(ENDPOINT_URL) + .hasBodyEqualTo(defaultMessageRequest().put("content", "test content")) + .whenQueueCalled(); } @Test void testEmbedOnly() { - assertNextRequestEquals(POST, ENDPOINT_URL, defaultMessageRequest() - .put("embeds", DataArray.empty() - .add(DataObject.empty().put("description", "test description")))); - - new MessageCreateActionImpl(channel) + MessageCreateAction action = new MessageCreateActionImpl(channel) .setEmbeds(new EmbedBuilder() .setDescription("test description") - .build()) - .queue(); - - verify(requester, times(1)).request(any()); + .build()); + + assertThatRequestFrom(action) + .hasMethod(POST) + .hasCompiledRoute(ENDPOINT_URL) + .hasBodyEqualTo(defaultMessageRequest() + .put("embeds", DataArray.empty() + .add(DataObject.empty().put("description", "test description")))) + .whenQueueCalled(); } @Nonnull From 6062ea4e4da83cdde8c7c8448382330d9fa861a6 Mon Sep 17 00:00:00 2001 From: GitMilchi <117216918+GitMilchi@users.noreply.github.com> Date: Sat, 13 Apr 2024 19:52:18 +0200 Subject: [PATCH 17/25] added USER_MUST_BE_VERIFIED ErrorResponse (#2651) --- src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java | 1 + 1 file changed, 1 insertion(+) 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 450f4f98447..d2de7b32baf 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -164,6 +164,7 @@ public enum ErrorResponse SERVER_NOT_ENOUGH_BOOSTS( 50101, "This server needs more boosts to perform this action"), MIXED_PREMIUM_ROLES_FOR_EMOJI( 50144, "Cannot mix subscription and non subscription roles for an emoji"), ILLEGAL_EMOJI_CONVERSION( 50145, "Cannot convert between premium emoji and normal emoji"), + USER_MUST_BE_VERIFIED( 50178, "The user account must first be verified"), MFA_NOT_ENABLED( 60003, "MFA auth required but not enabled"), NO_USER_WITH_TAG_EXISTS( 80004, "No users with DiscordTag exist"), REACTION_BLOCKED( 90001, "Reaction Blocked"), From 827f29bdc7a475e8224621c262fc1ddf3091527d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Tue, 16 Apr 2024 20:14:30 +0200 Subject: [PATCH 18/25] Update dependencies and use version catalog (#2652) --- build.gradle.kts | 31 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 26 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f7b22d2bbd9..b9a13115be5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,7 +36,7 @@ plugins { id("io.codearte.nexus-staging") version "0.30.0" id("de.marcphilipp.nexus-publish") version "0.4.0" - id("com.github.johnrengelman.shadow") version "7.1.2" + id("com.github.johnrengelman.shadow") version "8.1.1" } val javaVersion = JavaVersion.current() @@ -97,33 +97,32 @@ dependencies { /* ABI dependencies */ //Code safety - compileOnly("com.google.code.findbugs:jsr305:3.0.2") - compileOnly("org.jetbrains:annotations:23.0.0") + compileOnly(libs.findbugs) + compileOnly(libs.jetbrains.annotations) //Logger - api("org.slf4j:slf4j-api:1.7.36") + api(libs.slf4j) //Web Connection Support - api("com.neovisionaries:nv-websocket-client:2.14") - api("com.squareup.okhttp3:okhttp:4.12.0") + api(libs.websocket.client) + api(libs.okhttp) //Opus library support - api("club.minnced:opus-java:1.1.1") + api(libs.opus) //Collections Utility - api("org.apache.commons:commons-collections4:4.4") + api(libs.commons.collections) //we use this only together with opus-java // if that dependency is excluded it also doesn't need jna anymore // since jna is a transitive runtime dependency of opus-java we don't include it explicitly as dependency - compileOnly("net.java.dev.jna:jna:4.4.0") + compileOnly(libs.jna) /* Internal dependencies */ //General Utility - implementation("net.sf.trove4j:trove4j:3.0.3") - implementation("com.fasterxml.jackson.core:jackson-core:2.16.0") - implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0") + implementation(libs.trove4j) + implementation(libs.bundles.jackson) //Sets the dependencies for the examples configurations["examplesImplementation"].withDependencies { @@ -132,10 +131,10 @@ dependencies { addAll(configurations["compileOnly"].allDependencies) } - testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") - testImplementation("org.reflections:reflections:0.10.2") - testImplementation("org.mockito:mockito-core:5.8.0") - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation(libs.junit) + testImplementation(libs.reflections) + testImplementation(libs.mockito) + testImplementation(libs.assertj) } val compileJava: JavaCompile by tasks diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fceae6..48c0a02ca41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 4ce7e83eb4b..8f29929de9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,27 @@ rootProject.name = "JDA" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + version("jackson", "2.16.0") + library("jackson-core", "com.fasterxml.jackson.core", "jackson-core").versionRef("jackson") + library("jackson-databind", "com.fasterxml.jackson.core", "jackson-databind").versionRef("jackson") + bundle("jackson", listOf("jackson-core", "jackson-databind")) + + library("opus", "club.minnced", "opus-java" ).version("1.1.1") + library("findbugs", "com.google.code.findbugs", "jsr305" ).version("3.0.2") + library("websocket-client", "com.neovisionaries", "nv-websocket-client" ).version("2.14") + library("okhttp", "com.squareup.okhttp3", "okhttp" ).version("4.12.0") + library("jna", "net.java.dev.jna", "jna" ).version("5.14.0") + library("trove4j", "net.sf.trove4j", "core" ).version("3.1.0") + library("commons-collections", "org.apache.commons", "commons-collections4").version("4.4") + library("commons-lang3", "org.apache.commons", "commons-lang3" ).version("3.14.0") + library("assertj", "org.assertj", "assertj-core" ).version("3.25.3") + library("jetbrains-annotations", "org.jetbrains", "annotations" ).version("24.1.0") + library("junit", "org.junit.jupiter", "junit-jupiter" ).version("5.10.2") + library("mockito", "org.mockito", "mockito-core" ).version("5.11.0") + library("reflections", "org.reflections", "reflections" ).version("0.10.2") + library("slf4j", "org.slf4j", "slf4j-api" ).version("1.7.36") + } + } +} From cf795c93a62eace46a53c3cd3329a7ce49f05703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 20 Apr 2024 13:25:51 +0200 Subject: [PATCH 19/25] Update permission enum (#2654) --- src/main/java/net/dv8tion/jda/api/Permission.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 22e99f903d7..798ffba081a 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -15,8 +15,6 @@ */ package net.dv8tion.jda.api; -import net.dv8tion.jda.annotations.ForRemoval; -import net.dv8tion.jda.annotations.ReplaceWith; import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.Nonnull; @@ -33,19 +31,17 @@ public enum Permission // General Server / Channel Permissions MANAGE_CHANNEL( 4, true, true, "Manage Channels"), MANAGE_SERVER( 5, true, false, "Manage Server"), - VIEW_AUDIT_LOGS( 7, true, false, "View Audit Logs"), - VIEW_CHANNEL( 10, true, true, "View Channel(s)"), + VIEW_AUDIT_LOGS( 7, true, false, "View Audit Log"), + VIEW_CHANNEL( 10, true, true, "View Channels"), VIEW_GUILD_INSIGHTS( 19, true, false, "View Server Insights"), MANAGE_ROLES( 28, true, false, "Manage Roles"), MANAGE_PERMISSIONS( 28, false, true, "Manage Permissions"), MANAGE_WEBHOOKS( 29, true, true, "Manage Webhooks"), - @Deprecated - @ForRemoval(deadline = "5.0.0") - @ReplaceWith("MANAGE_GUILD_EXPRESSIONS") - MANAGE_EMOJIS_AND_STICKERS( 30, true, false, "Manage Emojis and Stickers"), - MANAGE_GUILD_EXPRESSIONS( 30, true, false, "Manage Emojis, Stickers, and Soundboards"), + MANAGE_GUILD_EXPRESSIONS( 30, true, false, "Manage Expressions"), MANAGE_EVENTS( 33, true, true, "Manage Events"), VIEW_CREATOR_MONETIZATION_ANALYTICS(41, true, false, "View Creator Analytics"), + CREATE_GUILD_EXPRESSIONS( 43, true, false, "Create Expressions"), + CREATE_SCHEDULED_EVENTS( 44, true, false, "Create Events"), // Membership Permissions CREATE_INSTANT_INVITE(0, true, true, "Create Instant Invite"), From 48a41110c5ff1f7ae62385fc03dfa024b9831fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:11:38 +0200 Subject: [PATCH 20/25] Rewrite JDA readme (#2639) --- .github/CONTRIBUTING.md | 41 +-- README.md | 730 ++++++++++++---------------------------- 2 files changed, 225 insertions(+), 546 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 14140121ad8..bdf5ab06f81 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,33 +1,16 @@ - ## Making Changes -Depending on your changes there are certain rules you have to follow if you expect -your Pull Request to be merged. - -**Note**: It is recommended to create a new remote branch for each Pull Request. -Based on the current `master` changes! - -1. Adding a new Method or Class - - If your addition is not internal (e.g. an impl class or private method) you have to write documentation. - - For that please follow the [JavaDoc template](https://jda.wiki/contributing/structure-guide/#javadoc) - - Keep your code consistent! [example](https://jda.wiki/contributing/contributing/#making-changes) - - Follow the guides provided at [JDA Structure Guide](https://jda.wiki/contributing/structure-guide/) - - Compare your code style to the one used all over JDA and ensure you - do not break the consistency (if you find issues in the JDA style you can include and update it) - - Do not remove existing functionality, use deprecation instead (for reference [deprecation policy](https://github.com/discord-jda/JDA?tab=readme-ov-file#versioning-and-deprecation-policy)) +Depending on your changes there are certain rules you have to follow if you expect your Pull Request (PR) to be merged. -2. Making a Commit - - While having multiple commits can help the reader understand your changes, it might sometimes be - better to include more changes in a single commit. - - When you commit your changes write a proper commit caption which explains what you have done +1. Check for existing pull requests that might already implement your changes. + You can review those pull requests with your suggestions for improvements. +1. Reference documentation or issues that are relevant to your changes. + For instance, if you're implementing a new Discord feature, include a link to the [api docs](https://github.com/discord/discord-api-docs) pull request that documents the feature. +1. If you implement a new feature, include some examples on how you intend it to be used in your PR description. +1. Write documentation for all public API methods and types. +1. Follow the JDA coding style, even if unconventional, we want to keep it consistent to maintain readability. +1. Try to limit your PR to one logical change or closely related changes. + For instance, do not refactor other code that is irrelevant to your changes. If you want to refactor something, make a dedicated PR. -3. Updating your Fork - - Before you start committing make sure your fork is updated. - (See [Syncing a Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) - or [Keeping a Fork Updated](https://thoughtbot.com/blog/keeping-a-github-fork-updated)) - -4. Only open Pull Requests to master - - Look at the [Repository Structure](https://jda.wiki/contributing/repository-structure/) for further details - -For more information please consult the [Contributing](https://jda.wiki/contributing/contributing/) -section of our wiki. +> [!TIP] +> Before starting to implement your changes, you can post in our [lib-dev](https://discord.gg/qcy8K58zWb) channel on our Discord Server. Getting early feedback will help you and the reviewers save a lot of time. diff --git a/README.md b/README.md index bcf76265f70..4d33420b846 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [jitpack]: https://img.shields.io/badge/Snapshots-JitPack-blue [download]: #download [discord-invite]: https://discord.gg/0hMr4ce0tIl3SLv5 -[migration]: https://jda.wiki/introduction/migration-v3-v4/ +[migration]: https://jda.wiki/introduction/migration-v4-v5/ [jenkins]: https://ci.dv8tion.net/job/JDA5 [license]: https://github.com/discord-jda/JDA/tree/master/LICENSE [faq]: https://jda.wiki/introduction/faq/ @@ -15,534 +15,294 @@ [troubleshooting-shield]: https://img.shields.io/badge/Wiki-Troubleshooting-darkgreen.svg [jenkins-shield]: https://img.shields.io/badge/Download-Jenkins-purple.svg [license-shield]: https://img.shields.io/badge/License-Apache%202.0-white.svg -[migration-shield]: https://img.shields.io/badge/Wiki-Migrating%20from%20V3-darkgreen.svg +[migration-shield]: https://img.shields.io/badge/Wiki-Migrating%20from%20V4-darkgreen.svg +[GatewayIntent]: https://docs.jda.wiki/net/dv8tion/jda/api/requests/GatewayIntent.html +[JDABuilder]: https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html +[DefaultShardManagerBuilder]: https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.html - + -[ ![maven-central][] ][download] -[ ![jitpack][] ](https://jitpack.io/#discord-jda/JDA) -[ ![jenkins-shield][] ][jenkins] -[ ![license-shield][] ][license] - -[ ![discord-shield][] ][discord-invite] -[ ![faq-shield] ][faq] -[ ![docs-shield] ][docs] -[ ![troubleshooting-shield] ][troubleshooting] -[ ![migration-shield][] ][migration] +[![maven-central][]][download] +[![jitpack][]](https://jitpack.io/#discord-jda/JDA) +[![jenkins-shield][]][jenkins] +[![license-shield][]][license] +[![discord-shield][]][discord-invite] +[![faq-shield]][faq] +[![docs-shield]][docs] +[![troubleshooting-shield]][troubleshooting] +[![migration-shield][]][migration] # JDA (Java Discord API) -JDA strives to provide a clean and full wrapping of the Discord REST API and its WebSocket-Events for Java. -This library is a helpful tool that provides the functionality to create a Discord bot in Java. - -## Summary - -1. [Introduction](#creating-the-jda-object) -2. [Sharding](#sharding-a-bot) -3. [Entity Lifetimes](#entity-lifetimes) -4. [Download](#download) -5. [Documentation](#documentation) -6. [Support](#getting-help) -7. [Extensions And Plugins](#third-party-recommendations) -8. [Contributing](#contributing-to-jda) -9. [Dependencies](#dependencies) -10. [Other Libraries](#related-projects) +This open source library is intended for implementing bots on Discord using the real-time gateway and REST API. It provides event based functionality to implement bots of any kind, allowing for effective and scalable applications. -## UserBots and SelfBots +## 📖 Overview -Discord is currently prohibiting creation and usage of automated client accounts (AccountType.CLIENT). -We have officially dropped support for client login as of version **4.2.0**! -If you need a bot, use a bot account from the [Application Dashboard](https://discord.com/developers/applications). +The core concepts of JDA have been developed to make building scalable apps easy: -[Read More](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) +1. Event System + Providing simplified events from the gateway API, to respond to any platform events in real-time without much hassle. +1. Rest Actions + Easy to use and scalable implementation of REST API functionality, letting you choose between callbacks with combinators, futures, and blocking. + The library also handles rate-limits imposed by Discord automatically, while still offering ways to replace the default implementation. +1. Customizable Cache + Trading memory usage for better performance where necessary, with sane default presets to choose from and customize. -## Creating the JDA Object +You can learn more by visiting our [wiki][wiki] or referencing our [Javadocs][docs]. -Creating the JDA Object is done via the JDABuilder class. After setting the token and other options via setters, -the JDA Object is then created by calling the `build()` method. When `build()` returns, -JDA might not have finished starting up. However, you can use `awaitReady()` -on the JDA object to ensure that the entire cache is loaded before proceeding. -Note that this method is blocking and will cause the thread to sleep until startup has completed. +## 🔬 Installation -**Example**: +[![maven-central][]](https://mvnrepository.com/artifact/net.dv8tion/JDA/latest) +[![jitpack][]](https://jitpack.io/#discord-jda/JDA) -```java -JDA jda = JDABuilder.createDefault("token").build(); -``` +This library is available on maven central. The latest version is always shown in the [GitHub Release](https://github.com/discord-jda/JDA/releases/latest). -### Configuration +The minimum java version supported by JDA is **Java SE 8**. JDA also uses JSR 305 to support solid interoperability with Kotlin out of the box. -Both the `JDABuilder` and the `DefaultShardManagerBuilder` allow a set of configurations to improve the experience. +### Gradle -**Example**: +```gradle +repositories { + mavenCentral() +} -```java -public static void main(String[] args) { - JDABuilder builder = JDABuilder.createDefault(args[0]); - - // Disable parts of the cache - builder.disableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE); - // Enable the bulk delete event - builder.setBulkDeleteSplittingEnabled(false); - // Set activity (like "playing Something") - builder.setActivity(Activity.watching("TV")); - - builder.build(); +dependencies { + implementation("net.dv8tion:JDA:$version") { // replace $version with the latest version + // Optionally disable audio natives to reduce jar size by excluding `opus-java` + // Gradle DSL: + // exclude module: 'opus-java' + // Kotlin DSL: + // exclude(module="opus-java") + } } ``` -> See [JDABuilder](https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html) - and [DefaultShardManagerBuilder](https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.html) +### Maven -You can configure the memory usage by changing enabled `CacheFlags` on the `JDABuilder`. -Additionally, you can change the handling of the member/user cache by disabling **intents** or changing the **member cache policy**. -To learn more about intents and member loading/caching, read the [Gateway Intents Guide](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/). +```xml + + net.dv8tion + JDA + $version + + +``` -```java -public void configureMemoryUsage(JDABuilder builder) { - // Disable cache for member activities (streaming/games/spotify) - builder.disableCache(CacheFlag.ACTIVITY); +## 🤖 Creating a Bot - // Only cache members who are either in a voice channel or owner of the guild - builder.setMemberCachePolicy(MemberCachePolicy.VOICE.or(MemberCachePolicy.OWNER)); +To use this library, you have to create an Application in the [Discord Application Dashboard](https://discord.com/developers/applications) and grab your bot token. You can find a step-by-step guide for this in our wiki page [Creating a Discord Bot](https://jda.wiki/using-jda/getting-started/#creating-a-discord-bot). - // Disable member chunking on startup - builder.setChunkingFilter(ChunkingFilter.NONE); +## 🏃‍♂️ Getting Started - // Disable presence updates and typing events - builder.disableIntents(GatewayIntent.GUILD_PRESENCE, GatewayIntent.GUILD_MESSAGE_TYPING); +We provide a number of [examples](https://github.com/discord-jda/JDA/tree/master/src/examples/java) to introduce you to JDA. You can also take a look at our official [Wiki][wiki], [Documentation][docs], and [FAQ][faq]. - // Consider guilds with more than 50 members as "large". - // Large guilds will only provide online members in their setup and thus reduce bandwidth if chunking is disabled. - builder.setLargeThreshold(50); -} -``` +Every bot implemented by JDA starts out using the [JDABuilder][JDABuilder] or [DefaultShardManagerBuilder][DefaultShardManagerBuilder]. Both builders provide a set of default presets for cache usage and events it wants to receive: -### Listening for Events +- `createDefault` - Enables cache for users who are active in voice channels and all cache flags +- `createLight` - Disables all user cache and cache flags +- `create` - Enables member chunking, caches all users, and enables all cache flags -The event system in JDA is configured through a hierarchy of classes/interfaces. -We offer two implementations for the `IEventManager`: +We recommend reading the guide on [caching and intents](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/) to get a feel for configuring your bot properly. Here are some possible use-cases: -- **InterfacedEventManager** which uses an `EventListener` interface and the `ListenerAdapter` abstract class -- **AnnotatedEventManager** which uses the `@SubscribeEvent` annotation which can be applied to methods +### Example: Message Logging -By default, the **InterfacedEventManager** is used. -Since you can create your own implementation of `IEventManager`, this is a very versatile and configurable system. -If the aforementioned implementations don't suit your use-case you can simply create a custom implementation and -configure it on the `JDABuilder` with `setEventManager(...)`. +> [!NOTE] +> The following example makes use of the **privileged intent** `GatewayIntent.MESSAGE_CONTENT`, which must be explicitly enabled in your application dashboard. You can find out more about intents in our [wiki guide](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/). -#### Examples: +Simply logging messages to the console. Making use of [JDABuilder][JDABuilder], the intended entry point for smaller bots that don't intend to grow to thousands of guilds. -**Using EventListener**: +Starting your bot and attaching an event listener, using the right [intents][GatewayIntent]: ```java -public class ReadyListener implements EventListener { - public static void main(String[] args) throws InterruptedException { - // Note: It is important to register your ReadyListener before building - JDA jda = JDABuilder.createDefault("token") - .addEventListeners(new ReadyListener()) - .build(); - - // optionally block until JDA is ready - jda.awaitReady(); - } - - @Override - public void onEvent(GenericEvent event) { - if (event instanceof ReadyEvent) { - System.out.println("API is ready!"); - } - } +public static void main(String[] args) { + JDABuilder.createLight(token, EnumSet.of(GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)) + .addEventListener(new MessageReceiveListener()) + .build(); } ``` -**Using ListenerAdapter**: +Your event listener could look like this: ```java -public class MessageListener extends ListenerAdapter { - public static void main(String[] args) { - JDA jda = JDABuilder.createDefault("token") - .enableIntents(GatewayIntent.MESSAGE_CONTENT) // enables explicit access to message.getContentDisplay() - .build(); - //You can also add event listeners to the already built JDA instance - // Note that some events may not be received if the listener is added after calling build() - // This includes events such as the ReadyEvent - jda.addEventListener(new MessageListener()); - } - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.isFromType(ChannelType.PRIVATE)) { - System.out.printf("[PM] %s: %s\n", event.getAuthor().getName(), - event.getMessage().getContentDisplay()); - } else { - System.out.printf("[%s][%s] %s: %s\n", event.getGuild().getName(), - event.getTextChannel().getName(), event.getMember().getEffectiveName(), - event.getMessage().getContentDisplay()); - } - } +public class MessageReceiveListener extends ListenerAdapter { + @Override + public void onMessageReceived(MessageReceivedEvent event) { + System.out.printf("[%s] %#s: %s\n", + event.getChannel(), + event.getAuthor(), + event.getMessage().getContentDisplay()); + } } ``` -**Slash-Commands**: - -```java -public class Bot extends ListenerAdapter { - public static void main(String[] args) { - if (args.length < 1) { - System.out.println("You have to provide a token as first argument!"); - System.exit(1); - } - // args[0] would be the token (using an environment variable or config file is preferred for security) - // We don't need any intents for this bot. Slash commands work without any intents! - JDA jda = JDABuilder.createLight(args[0], Collections.emptyList()) - .addEventListeners(new Bot()) - .setActivity(Activity.playing("Type /ping")) - .build(); - - // Sets the global command list to the provided commands (removing all others) - jda.updateCommands().addCommands( - Commands.slash("ping", "Calculate ping of the bot"), - Commands.slash("ban", "Ban a user from the server") - .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS)) // only usable with ban permissions - .setGuildOnly(true) // Ban command only works inside a guild - .addOption(OptionType.USER, "user", "The user to ban", true) // required option of type user (target to ban) - .addOption(OptionType.STRING, "reason", "The ban reason") // optional reason - ).queue(); - } - - @Override - public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { - // make sure we handle the right command - switch (event.getName()) { - case "ping": - long time = System.currentTimeMillis(); - event.reply("Pong!").setEphemeral(true) // reply or acknowledge - .flatMap(v -> - event.getHook().editOriginalFormat("Pong: %d ms", System.currentTimeMillis() - time) // then edit original - ).queue(); // Queue both reply and edit - break; - case "ban": - // double check permissions, don't trust Discord on this! - if (!event.getMember().hasPermission(Permission.BAN_MEMBERS)) { - event.reply("You cannot ban members! Nice try ;)").setEphemeral(true).queue(); - break; - } - User target = event.getOption("user", OptionMapping::getUser); - // optionally check for member information - Member member = event.getOption("user", OptionMapping::getMember); - if (!event.getMember().canInteract(member)) { - event.reply("You cannot ban this user.").setEphemeral(true).queue(); - break; - } - // Before starting our ban request, tell the user we received the command - // This sends a "Bot is thinking..." message which is later edited once we finished - event.deferReply().queue(); - String reason = event.getOption("reason", OptionMapping::getAsString); - AuditableRestAction action = event.getGuild().ban(target, 0); // Start building our ban request - if (reason != null) // reason is optional - action = action.reason(reason); // set the reason for the ban in the audit logs and ban log - action.queue(v -> { - // Edit the thinking message with our response on success - event.getHook().editOriginal("**" + target.getAsTag() + "** was banned by **" + event.getUser().getAsTag() + "**!").queue(); - }, error -> { - // Tell the user we encountered some error - event.getHook().editOriginal("Some error occurred, try again!").queue(); - error.printStackTrace(); - }); - break; - default: - System.out.printf("Unknown command %s used by %#s%n", event.getName(), event.getUser()); - } - } -} -``` +You can find a more thorough example with the [MessageLoggerExample](https://github.com/discord-jda/JDA/blob/master/src/examples/java/MessageLoggerExample.java) class. -### RestAction +### Example: Slash Command Bot -Through [RestAction](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html) we provide request handling with - - - [callbacks](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#queue%28java.util.function.Consumer%29) - - [promises](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#submit%28%29) - - and [sync](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#complete%28%29) +This is a bot that makes use of [interactions](https://jda.wiki/using-jda/interactions/) to respond to user commands. Unlike the message logging bot, this bot can work without any enabled intents, since interactions are always available. -and it is up to the user to decide which pattern to utilize. -It can be combined with reactive libraries such as [reactor-core](https://github.com/reactor/reactor-core) due to being lazy. +```java +public static void main(String[] args) { + JDA jda = JDABuilder.createLight(token, Collections.emptyList()) + .addEventListener(new SlashCommandListener()) + .build(); -The RestAction interface also supports a number of operators to avoid callback hell: + // Register your commands to make them visible globally on Discord: -- [`map`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#map%28java.util.function.Function%29) - Convert the result of the `RestAction` to a different value -- [`flatMap`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#flatMap%28java.util.function.Function%29) - Chain another `RestAction` on the result -- [`delay`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#delay%28java.time.Duration%29) - Delay the element of the previous step + CommandListUpdateAction commands = jda.updateCommands(); -**Example**: + // Add all your commands on this action instance + commands.addCommands( + Commands.slash("say", "Makes the bot say what you tell it to") + .addOption(STRING, "content", "What the bot should say", true), // Accepting a user input + Commands.slash("leave", "Makes the bot leave the server") + .setGuildOnly(true) // this doesn't make sense in DMs + .setDefaultPermissions(DefaultMemberPermissions.DISABLED) // only admins should be able to use this command. + ); -```java -public RestAction selfDestruct(MessageChannel channel, String content) { - return channel.sendMessage("The following message will destroy itself in 1 minute!") - .delay(10, SECONDS, scheduler) // edit 10 seconds later - .flatMap((it) -> it.editMessage(content)) - .delay(1, MINUTES, scheduler) // delete 1 minute later - .flatMap(Message::delete); + // Then finally send your commands to discord using the API + commands.queue(); } ``` -### More Examples - -We provide a small set of Examples in the [Example Directory](https://github.com/discord-jda/JDA/tree/master/src/examples/java). - -## Sharding a Bot - -When your bot joins over 2500 guilds, it is required to perform **Sharding**. -This means, your connection is split up into multiple **shards**, each only accessing a fraction of your total available guilds. -A shard can at most contain 2500 guilds when starting up the bot. - -Each shard is assigned a **shard id** and **shard total** (usually shown as `id / total`), which uniquely identifies which guilds are accessible on that shard. -For instance, the first of 2 shards would be `0 / 2` and the second would be `1 / 2`. - -If you want to use sharding with your bot, make use of the [DefaultShardManager](https://docs.jda.wiki/net/dv8tion/jda/api/sharding/DefaultShardManager.html) as seen in the example below. -This manager automatically assigns the right number of shards to your bot, so you do not need to do any math yourself. - -Internally, this shard manager also handles the proper scaling of threads for connections and handles the login rate-limit (Identify Rate-Limit) to properly startup without issues. - -If you do not want to use the shard manager, and instead manage sharding yourself, you can use [JDABuilder#useSharding](https://docs.jda.wiki/net/dv8tion/jda/api/JDABuilder.html#useSharding(int,int)) and [ConcurrentSessionController](https://docs.jda.wiki/net/dv8tion/jda/api/utils/ConcurrentSessionController.html). - - -### Example Sharding - Using DefaultShardManager +An event listener that responds to commands could look like this: ```java -public static void main(String[] args) { - DefaultShardManagerBuilder builder = DefaultShardManagerBuilder.createDefault(args[0]); - builder.addEventListeners(new MessageListener()); - builder.build(); +public class SlashCommandListener extends ListenerAdapter { + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + switch (event.getName()) { + case "say" -> { + String content = event.getOption("content", OptionMapping::getAsString); + event.reply(content).queue(); + }; + case "leave" -> { + event.reply("I'm leaving the server now!") + .setEphemeral(true) // this message is only visible to the command user + .flatMap(m -> event.getGuild().leave()) // append a follow-up action using flatMap + .queue(); // enqueue both actions to run in sequence (send message -> leave guild) + }; + default -> return; + } + } } ``` -## Entity Lifetimes - -An **Entity** is the term used to describe types such as **GuildChannel**/**Message**/**User** and other entities that Discord provides. -Instances of these entities are created and deleted by JDA when Discord instructs it. This means the lifetime depends on signals provided by the Discord API which are used to create/update/delete entities. -This is done through Gateway Events known as "dispatches" that are handled by the JDA WebSocket handlers. -When Discord instructs JDA to delete entities, they are simply removed from the JDA cache and lose their references. -Once that happens, nothing in JDA interacts or updates the instances of those entities, and they become outdated. -Discord may instruct to delete these entities randomly for cache synchronization with the API. - -**It is not recommended to store _any_ of these entities for a longer period of time!** -Instead of keeping (e.g.) a `User` instance in some field, an ID should be used. With the ID of a user, -you can use `getUserById(id)` to get and keep the user reference in a local variable (see below). +You can find a more thorough example with the [SlashBotExample](https://github.com/discord-jda/JDA/blob/master/src/examples/java/SlashBotExample.java) class. -### Entity Updates +## 🚀 RestAction -When an entity is updated through its manager, they will send a request to the Discord API which will update the state -of the entity. The success of this request **does not** imply the entity has been updated yet. All entities are updated -by the aforementioned **Gateway Events** which means you cannot rely on the cache being updated yet once the -execution of a RestAction has completed. Some requests rely on the cache being updated to correctly update the entity. -An example of this is updating roles of a member which overrides all roles of the member by sending a list of the -new set of roles. This is done by first checking the current cache, the roles the member has right now, and appending -or removing the requested roles. If the cache has not yet been updated by an event, this will result in unexpected behavior. - -### Entity Deletion - -Discord may request that a client (the JDA session) invalidates its entire cache. When this happens, JDA will remove all of its current entities and reconnect the session. This is signaled through the `SessionRecreateEvent`. When entities are removed from the JDA cache, your instance will keep stale entities in memory. This results in memory duplication, potential memory leaks, and outdated state. It is **highly recommended** to only keep references to entities by storing their **id** and using the respective `get...ById(id)` method when needed. Alternatively, keep the entity stored and make sure to replace it as soon as possible when the cache is replaced. - -#### Example +In this library, the [RestAction](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html) interface is used as a request builder for all API endpoints. +This interface represents a lazy request builder, as shown in this simple example: ```java -public class UserLogger extends ListenerAdapter { - private final User user; - - public UserLogger(User user) { - this.user = user; - } - - private User getUser(JDA api) { - // Acquire a reference to the User instance through the id - User newUser = api.getUserById(this.user.getIdLong()); - if (newUser != null) - this.user = newUser; - return this.user; - } - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - User author = event.getAuthor(); - Message message = event.getMessage(); - if (author.getIdLong() == this.user.getIdLong()) { - // Update user from message instance (likely more up-to-date) - this.user = author; - // Print the message of the user - System.out.println(author.getAsTag() + ": " + message.getContentDisplay()); - } - } - - @Override - public void onGuildJoin(GuildJoinEvent event) { - JDA api = event.getJDA(); - User user = getUser(); // use getter to refresh user automatically on access - user.openPrivateChannel().queue((channel) -> { - // Send a private message to the user - channel.sendMessageFormat("I have joined a new guild: **%s**", event.getGuild().getName()).queue(); - }); - } -} +channel.sendMessage("Hello Friend!") + .addFiles(FileUpload.fromData(greetImage)) // Chain builder methods to configure the request + .queue() // Send the request asynchronously ``` -## Download +> [!IMPORTANT] +> The final call to [`queue()`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#queue%28%29) sends the request. +> You can also send the request synchronously or using futures, check out our extended guide in the [RestAction Wiki](https://jda.wiki/using-jda/using-restaction/). -[ ![maven-central][] ](https://mvnrepository.com/artifact/net.dv8tion/JDA/latest) -[ ![jitpack][] ](https://jitpack.io/#discord-jda/JDA) +The RestAction interface also supports a number of operators to avoid callback hell: -Latest Release: [GitHub Release](https://github.com/discord-jda/JDA/releases/latest)
    +- [`map`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#map%28java.util.function.Function%29) + Convert the result of the `RestAction` to a different value +- [`flatMap`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#flatMap%28java.util.function.Function%29) + Chain another `RestAction` on the result +- [`delay`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#delay%28java.time.Duration%29) + Delay the element of the previous step -Be sure to replace the **VERSION** key below with the one of the versions shown above! For snapshots, please use the instructions provided by [JitPack](https://jitpack.io/#discord-jda/JDA). +As well as combinators like: -**Maven** -```xml - - net.dv8tion - JDA - VERSION - -``` +- [`and`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#and(net.dv8tion.jda.api.requests.RestAction,java.util.function.BiFunction)) + Require another RestAction to complete successfully, running in parallel +- [`allOf`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#allOf(java.util.Collection)) + Accumulate a list of many actions into one (see also [`mapToResult`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#mapToResult())) +- [`zip`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#zip(net.dv8tion.jda.api.requests.RestAction,net.dv8tion.jda.api.requests.RestAction...)) + Similar to `and`, but combines the results into a list + -**Maven without Audio** -```xml - - net.dv8tion - JDA - VERSION - - - club.minnced - opus-java - - - -``` +And configurators like: -**Gradle** -```gradle -repositories { - mavenCentral() -} +- [`timeout`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#timeout(long,java.util.concurrent.TimeUnit)) and [`deadline`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#deadline(long)) + Configure how long the action is allowed to be in queue, cancelling if it takes too long +- [`setCheck`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/RestAction.html#setCheck(java.util.function.BooleanSupplier)) + Running some checks right before the request is sent, this can be helpful when it is in queue for a while +- [`reason`](https://docs.jda.wiki/net/dv8tion/jda/api/requests/restaction/AuditableRestAction.html#reason(java.lang.String)) + The [audit log reason](https://discord.com/developers/docs/resources/audit-log) for an action -dependencies { - //Change 'implementation' to 'compile' in old Gradle versions - implementation("net.dv8tion:JDA:VERSION") -} -``` +**Example**: -**Gradle without Audio** -```gradle -dependencies { - //Change 'implementation' to 'compile' in old Gradle versions - implementation("net.dv8tion:JDA:VERSION") { - exclude module: 'opus-java' - } +```java +public RestAction selfDestruct(MessageChannel channel, String content) { + return channel.sendMessage("The following message will destroy itself in 1 minute!") + .addActionRow(Button.danger("delete", "Delete now")) // further amend message before sending + .delay(10, SECONDS, scheduler) // after sending, wait 10 seconds + .flatMap((it) -> it.editMessage(content)) // then edit the message + .delay(1, MINUTES, scheduler) // wait another minute + .flatMap(Message::delete); // then delete } ``` -The snapshot builds are only available via JitPack and require adding the JitPack resolver, you need to specify specific commits to access those builds. -Stable releases are published to [maven-central](https://mvnrepository.com/artifact/net.dv8tion/JDA). - -If you do not need any opus de-/encoding done by JDA (voice receive/send with PCM) you can exclude `opus-java` entirely. -This can be done if you only send audio with an `AudioSendHandler` which only sends opus (`isOpus() = true`). (See [lavaplayer](https://github.com/sedmelluq/lavaplayer)) - -If you want to use a custom opus library you can provide the absolute path to `OpusLibrary.loadFrom(String)` before using -the audio API of JDA. This works without `opus-java-natives` as it only requires `opus-java-api`. -
    _For this setup you should only exclude `opus-java-natives` as `opus-java-api` is a requirement for en-/decoding._ - -See [opus-java](https://github.com/discord-java/opus-java) - -### Logging Framework - SLF4J +This could then be used in code: -JDA is using [SLF4J](https://www.slf4j.org/) to log its messages. - -That means you should add some SLF4J implementation to your build path in addition to JDA. -If no implementation is found, following message will be printed to the console on startup: -``` -SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". -SLF4J: Defaulting to no-operation (NOP) logger implementation -SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. +```java +selfDestruct(channel, "Hello friend, this is my secret message").queue(); ``` -JDA currently provides a fallback Logger in case that no SLF4J implementation is present. -We strongly recommend to use one though, as that can improve speed and allows you to customize the Logger as well as log to files - -There is a guide for logback-classic available in our wiki: [Logging Setup](https://jda.wiki/setup/logging/) - -## Documentation - -Docs can be found on the [GitHub Pages][docs] -
    We also have a wiki filled with information and troubleshooting guides at [jda.wiki][wiki] +## 🧩 Extensions -### Annotations - -We use a number of annotations to indicate future plans for implemented functionality such as new features of -the Discord API. - -- [Incubating](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/Incubating.java) -
    This annotation is used to indicate that functionality may change in the future. Often used when a new feature is added. -- [ReplaceWith](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/ReplaceWith.java) -
    Paired with `@Deprecated` this is used to inform you how the new code-fragment is supposed to look once the hereby annotated functionality is removed. -- [ForRemoval](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/ForRemoval.java) -
    Paired with `@Deprecated` this indicates that we plan to entirely remove the hereby annotated functionality in the future. -- [DeprecatedSince](https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/annotations/DeprecatedSince.java) -
    Paired with `@Deprecated` this specifies when a feature was marked as deprecated. - -[Sources](https://github.com/discord-jda/JDA/tree/master/src/main/java/net/dv8tion/jda/annotations) - -## Getting Help +### [jda-ktx](https://github.com/MinnDevelopment/jda-ktx) -For general troubleshooting you can visit our wiki [Troubleshooting][troubleshooting] and [FAQ][faq]. -
    If you need help, or just want to talk with the JDA or other Devs, you can join the [Official JDA Discord Guild][discord-invite]. +Created and maintained by [MinnDevelopment](https://github.com/MinnDevelopment). +Provides [Kotlin](https://kotlinlang.org/) extensions for **RestAction** and events that provide a more idiomatic Kotlin experience. -Alternatively you can also join the [Unofficial Discord API Guild](https://discord.gg/discord-api). -Once you joined, you can find JDA-specific help in the `#java_jda` channel. +```kotlin +fun main() { + val jda = light(BOT_TOKEN) + + jda.onCommand("ping") { event -> + val time = measureTime { + event.reply("Pong!").await() // suspending + }.inWholeMilliseconds -For guides and setup help you can also take a look at the [wiki](https://jda.wiki/) -
    Especially interesting are the [Getting Started](https://jda.wiki/introduction/jda/) -and [Setup](https://jda.wiki/setup/intellij/) Pages. + event.hook.editOriginal("Pong: $time ms").queue() + } +} +``` -## Third Party Recommendations +There are a number of examples available in the [README](https://github.com/MinnDevelopment/jda-ktx/#jda-ktx). ### [Lavaplayer](https://github.com/lavalink-devs/lavaplayer) -Created by [sedmelluq](https://github.com/sedmelluq) and now maintained by the [lavalink community](https://github.com/lavalink-devs) -
    Lavaplayer is the most popular library used by Music Bots created in Java. -It is highly compatible with JDA and Discord4J and allows to play audio from -Youtube, Soundcloud, Twitch, Bandcamp and [more providers](https://github.com/lavalink-devs/lavaplayer#supported-formats). -
    The library can easily be expanded to more services by implementing your own AudioSourceManager and registering it. +Created by [sedmelluq](https://github.com/sedmelluq) and now maintained by the [lavalink community](https://github.com/lavalink-devs) +Lavaplayer is the most popular library used by Music Bots created in Java. +It is highly compatible with JDA and Discord4J and allows playing audio from +YouTube, Soundcloud, Twitch, Bandcamp and [more providers](https://github.com/lavalink-devs/lavaplayer#supported-formats). +The library can easily be expanded to more services by implementing your own AudioSourceManager and registering it. +We recommend to also use [udpqueue](#udpqueue-an-extension-of-jda-nas) in addition to lavaplayer, to avoid stuttering issues caused by GC pauses. It is recommended to read the [Usage](https://github.com/lavalink-devs/lavaplayer#usage) section of Lavaplayer -to understand a proper implementation. -
    Sedmelluq provided a demo in his repository which presents an example implementation for JDA: +to understand a proper implementation. +Sedmelluq provided a demo in his repository which presents an example implementation for JDA: https://github.com/lavalink-devs/lavaplayer/tree/master/demo-jda -### [Lavalink](https://github.com/lavalink-devs/Lavalink) - -Created by [Freya Arbjerg](https://github.com/freyacodes) and now maintained by the [lavalink community](https://github.com/lavalink-devs). +### [udpqueue](https://github.com/MinnDevelopment/udpqueue.rs) (an extension of [jda-nas](https://github.com/sedmelluq/jda-nas)) -Lavalink is a popular standalone audio sending node based on Lavaplayer. Lavalink was built with scalability in mind, -and allows streaming music via many servers. It supports most of Lavaplayer's features. +Created and maintained by [sedmelluq](https://github.com/sedmelluq) and extended by [MinnDevelopment](https://github.com/MinnDevelopment) +Provides a native implementation for the JDA Audio Send-System **to avoid GC pauses potentially causing problems** with continuous audio playback. -Lavalink is used by many large bots, as well as bot developers who can not use a Java library like Lavaplayer. -If you plan on serving music on a smaller scale with JDA it is often preferable to just use Lavaplayer directly -as it is easier. - -[Lavalink-Client](https://github.com/FredBoat/Lavalink-Client) is the official Lavalink client for JDA. - - -### [jda-nas](https://github.com/sedmelluq/jda-nas) and [udpqueue](https://github.com/MinnDevelopment/udpqueue.rs) - -Created and maintained by [sedmelluq](https://github.com/sedmelluq) and extended by [MinnDevelopment](https://github.com/MinnDevelopment) -
    Provides a native implementation for the JDA Audio Send-System to avoid GC pauses. - -Note that this send system creates an extra UDP-Client which causes audio receive to no longer function properly, +Note that this send-system creates an extra UDP-Client which causes audio receive to no longer function properly, since Discord identifies the sending UDP-Client as the receiver. ```java @@ -550,95 +310,31 @@ JDABuilder builder = JDABuilder.createDefault(BOT_TOKEN) .setAudioSendFactory(new NativeAudioSendFactory()); ``` -### [jda-ktx](https://github.com/MinnDevelopment/jda-ktx) - -Created and maintained by [MinnDevelopment](https://github.com/MinnDevelopment). -
    Provides [Kotlin](https://kotlinlang.org/) extensions for **RestAction** and events that provide a more idiomatic Kotlin experience. - -```kotlin -fun main() { - val jda = light(BOT_TOKEN) - - jda.onCommand("ping") { event -> - val time = measureTime { - event.reply("Pong!").await() // suspending - }.inWholeMilliseconds +### [Lavalink](https://github.com/lavalink-devs/Lavalink) - event.hook.editOriginal("Pong: $time ms").queue() - } -} -``` +Created by [Freya Arbjerg](https://github.com/freyacodes) and now maintained by the [lavalink community](https://github.com/lavalink-devs). -There is a number of examples available in the [README](https://github.com/MinnDevelopment/jda-ktx/#jda-ktx). +Lavalink is a popular standalone audio sending node based on Lavaplayer. Lavalink was built with scalability in mind, +and allows streaming music via many servers. It supports most of Lavaplayer's features. ------- +Lavalink is used by many large bots, as well as bot developers who can not use a Java library like Lavaplayer. +If you plan on serving music on a smaller scale with JDA, it is often preferable to just use Lavaplayer directly +as it is easier. -More can be found in our github organization: [JDA-Applications](https://github.com/JDA-Applications) +[Lavalink-Client](https://github.com/FredBoat/Lavalink-Client) is the official Lavalink client for JDA. -## Contributing to JDA +## 🛠️ Contributing to JDA If you want to contribute to JDA, make sure to base your branch off of our **master** branch (or a feature-branch) and create your PR into that **same** branch. -More information can be found at the wiki page [Contributing](https://jda.wiki/contributing/contributing/). +Please follow our [Contributing Guidelines](https://github.com/discord-jda/JDA/blob/master/.github/CONTRIBUTING.md). -## Versioning and Deprecation Policy +Do not expect your pull request to get immediate attention, sometimes it will take a long time to get a response. +You can join our [discord server][discord-invite] and ask in [#lib-dev](https://discord.com/channels/125227483518861312/869965829024915466) before starting to work on a new PR, to get more immediate feedback from our community members. -Since the Discord API is in itself a moving standard, the stability is never guaranteed. For this reason, JDA does not follow the common semver versioning strategy. +## 🚨 Breaking Changes -The JDA version is structured with a looser definition, where the version change indicates the significance of changes. -For instance, using `5.1.2` as a baseline: - -- A change to the major like `6.0.0` indicates that a lot of code has to be adjusted due to major changes to the interfaces. A change like this always comes with a full migration guide like [Migrating from 4.X to 5.X](https://jda.wiki/introduction/migration-v4-v5/). -- A change to the minor like `5.2.0` indicates some code may need to be adjusted due to the removal or change of interfaces. You can usually find the necessary changes in the release documentation. -- A change to the patch like `5.1.3` indicates bug fixes and new feature additions that are backwards compatible. - -If a feature is marked as deprecated, it usually also indicates an alternative. For instance: - -```java -@Deprecated -@DeprecatedSince("5.1.2") -@ForRemoval(deadline="5.2.0") -@ReplaceWith("setFoo(foo)") -public void changeFoo(Foo foo) { ... } -``` +Due to the nature of the Discord API, the library will regularly introduce breaking changes to allow for a quick adoption of newer features. We try to keep these breaking changes minimal, but cannot avoid them entirely. -The method `changeFoo` was deprecated in release `5.1.2` and is going to be removed in `5.2.0`. Your change should replace all usage of `changeFoo(foo)` with `setFoo(foo)`. - -Sometimes, a feature might be removed without a replacement. This will be clearly explained in the documentation. - - -## Dependencies: - -This project requires **Java 8+**.
    -All dependencies are managed automatically by Gradle. - * NV WebSocket Client - * Version: **2.14** - * [Github](https://github.com/TakahikoKawasaki/nv-websocket-client) - * OkHttp - * Version: **4.10.0** - * [Github](https://github.com/square/okhttp) - * Apache Commons Collections4 - * Version: **4.4** - * [Website](https://commons.apache.org/proper/commons-collections) - * jackson - * Version: **2.14.1** - * [Github](https://github.com/FasterXML/jackson) - * Trove4j - * Version: **3.0.3** - * [BitBucket](https://bitbucket.org/trove4j/trove) - * slf4j-api - * Version: **1.7.36** - * [Website](https://www.slf4j.org/) - * opus-java (optional) - * Version: **1.1.1** - * [GitHub](https://github.com/discord-java/opus-java) - -## Related Projects - -- [Discord4J](https://github.com/Discord4J/Discord4J) -- [Discord.NET](https://github.com/discord-net/Discord.Net) -- [discord.py](https://github.com/Rapptz/discord.py) -- [serenity](https://github.com/serenity-rs/serenity) - -**See also:** [Discord API Community Libraries](https://github.com/apacheli/discord-api-libs) +Most breaking changes will result in a **minor** version bump (`5.1.2` → `5.2.0`). From 0ce30fd4502179a5562886c0734d1f9398b5745b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Apr 2024 11:14:36 +0200 Subject: [PATCH 21/25] Fix `CommandInteractionPayload#getCommandString` in autocomplete interactions (#2659) --- .../commands/CommandInteractionPayload.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java index 36103c8c72d..35b31f8454f 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/commands/CommandInteractionPayload.java @@ -149,6 +149,20 @@ default String getCommandString() for (OptionMapping o : getOptions()) { builder.append(" ").append(o.getName()).append(": "); + // Discord doesn't send the resolved entities on autocomplete interactions + if (this instanceof CommandAutoCompleteInteraction) + { + switch (o.getType()) + { + case CHANNEL: + case USER: + case ROLE: + case MENTIONABLE: + builder.append(o.getAsLong()); + continue; + } + } + switch (o.getType()) { case CHANNEL: From e4860b4d23ef3e990cba06686900cd3ff67b1382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:20:04 +0200 Subject: [PATCH 22/25] Poll support (#2649) --- build.gradle.kts | 1 + .../java/net/dv8tion/jda/api/Permission.java | 1 + .../net/dv8tion/jda/api/entities/Message.java | 83 ++++++ .../jda/api/entities/WebhookClient.java | 39 +++ .../channel/middleman/MessageChannel.java | 151 ++++++++++ .../api/entities/messages/MessagePoll.java | 273 ++++++++++++++++++ .../entities/messages/MessagePollImpl.java | 81 ++++++ .../poll/GenericMessagePollVoteEvent.java | 137 +++++++++ .../message/poll/MessagePollVoteAddEvent.java | 43 +++ .../poll/MessagePollVoteRemoveEvent.java | 43 +++ .../jda/api/hooks/ListenerAdapter.java | 6 + .../callbacks/IReplyCallback.java | 49 ++++ .../commands/build/SubcommandData.java | 5 +- .../commands/build/SubcommandGroupData.java | 8 +- .../jda/api/requests/ErrorResponse.java | 6 + .../jda/api/requests/GatewayIntent.java | 10 + .../net/dv8tion/jda/api/requests/Route.java | 3 + .../PollVotersPaginationAction.java | 31 ++ .../utils/messages/MessageCreateBuilder.java | 24 +- .../api/utils/messages/MessageCreateData.java | 17 +- .../utils/messages/MessageCreateRequest.java | 22 ++ .../utils/messages/MessagePollBuilder.java | 228 +++++++++++++++ .../api/utils/messages/MessagePollData.java | 106 +++++++ .../entities/AbstractWebhookClient.java | 9 + .../jda/internal/entities/EntityBuilder.java | 48 ++- .../jda/internal/entities/GuildImpl.java | 3 +- .../internal/entities/ReceivedMessage.java | 28 +- .../mixin/middleman/MessageChannelMixin.java | 29 ++ .../handle/MessagePollVoteHandler.java | 84 ++++++ .../interactions/CommandDataImpl.java | 3 +- .../internal/requests/WebSocketClient.java | 2 + .../restaction/MessageCreateActionImpl.java | 2 +- .../PollVotersPaginationActionImpl.java | 85 ++++++ .../dv8tion/jda/internal/utils/Checks.java | 14 + .../dv8tion/jda/internal/utils/Helpers.java | 24 ++ .../message/MessageCreateBuilderMixin.java | 16 + .../net/dv8tion/jda/test/ChecksHelper.java | 96 ++++++ .../java/net/dv8tion/jda/test/Constants.java | 1 + .../net/dv8tion/jda/test/IntegrationTest.java | 1 + .../checks/AbstractChecksAssertions.java | 48 +++ .../checks/DurationChecksAssertions.java | 57 ++++ .../checks/EnumChecksAssertions.java | 33 +++ .../checks/LongChecksAssertions.java | 36 +++ .../checks/SimpleChecksAssertions.java | 27 ++ .../checks/StringChecksAssertions.java | 75 +++++ .../events/EventFiredAssertions.java | 61 ++++ .../restaction}/RestActionAssertions.java | 2 +- .../entities/MessageSerializationTest.java | 1 - .../jda/test/entities/guild/BulkBanTest.java | 12 +- .../entities/message/MessagePollDataTest.java | 75 +++++ .../message/PollVotersPaginationTest.java | 59 ++++ .../events/AbstractSocketHandlerTest.java | 60 ++++ .../test/events/MessagePollHandlerTests.java | 83 ++++++ .../test/interactions/CommandDataTest.java | 67 +++-- .../restaction/MessageCreateActionTest.java | 62 +++- 55 files changed, 2509 insertions(+), 61 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 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/api/requests/restaction/pagination/PollVotersPaginationAction.java create mode 100644 src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollBuilder.java create mode 100644 src/main/java/net/dv8tion/jda/api/utils/messages/MessagePollData.java create mode 100644 src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.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/ChecksHelper.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/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 create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java create mode 100644 src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.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 create mode 100644 src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.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/build.gradle.kts b/build.gradle.kts index b9a13115be5..cd79b01c47c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { testImplementation(libs.reflections) testImplementation(libs.mockito) testImplementation(libs.assertj) + testImplementation(libs.commons.lang3) } val compileJava: JavaCompile by tasks diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 798ffba081a..1b15040d788 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -64,6 +64,7 @@ public enum Permission USE_APPLICATION_COMMANDS( 31, true, true, "Use Application Commands"), MESSAGE_EXT_STICKER( 37, true, true, "Use External Stickers"), MESSAGE_ATTACH_VOICE_MESSAGE(46, true, true, "Send Voice Messages"), + MESSAGE_SEND_POLLS( 49, true, true, "Create Polls"), // Thread Permissions MANAGE_THREADS( 34, true, true, "Manage Threads"), diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index d10b6cdd8d4..15c4d4c07ab 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -31,6 +31,7 @@ import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; import net.dv8tion.jda.api.entities.sticker.GuildSticker; import net.dv8tion.jda.api.entities.sticker.Sticker; import net.dv8tion.jda.api.entities.sticker.StickerItem; @@ -48,6 +49,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.requests.restaction.MessageEditAction; import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.AttachmentProxy; @@ -55,10 +57,12 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.requests.FunctionalCallback; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.IOUtil; @@ -681,6 +685,43 @@ default String getGuildId() @Nonnull List getComponents(); + /** + * The {@link MessagePoll} attached to this message. + * + * @return Possibly-null poll instance for this message + * + * @see #endPoll() + */ + @Nullable + MessagePoll getPoll(); + + /** + * End the poll attached to this message. + * + * @throws IllegalStateException + * If this poll was not sent by the currently logged in account or no poll was attached to this message + * + * @return {@link AuditableRestAction} - Type: {@link Message} + */ + @Nonnull + @CheckReturnValue + AuditableRestAction endPoll(); + + /** + * Paginate the users who voted for a poll answer. + * + * @param answerId + * The id of the poll answer, usually the ordinal position of the answer (first is 1) + * + * @return {@link PollVotersPaginationAction} + */ + @Nonnull + @CheckReturnValue + default PollVotersPaginationAction retrievePollVoters(long answerId) + { + return new PollVotersPaginationActionImpl(getJDA(), getChannelId(), getId(), answerId); + } + /** * Rows of interactive components such as {@link Button Buttons}. *
    You can use {@link MessageRequest#setComponents(LayoutComponent...)} to update these. @@ -1359,6 +1400,48 @@ default MessageCreateAction reply(@Nonnull MessageCreateData msg) return getChannel().sendMessage(msg).setMessageReference(this); } + /** + * Shortcut for {@code getChannel().sendMessagePoll(data).setMessageReference(this)}. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Currently always {@link #DEFAULT}. + */ + enum LayoutType + { + DEFAULT(1), + UNKNOWN(-1); + + private final int key; + + LayoutType(int key) + { + this.key = key; + } + + /** + * The raw API key used to identify this layout. + * + * @return The API key + */ + public int getKey() + { + return key; + } + + /** + * Resolves the provided raw API key to the layout enum constant. + * + * @param key + * The API key + * + * @return The layout type or {@link #UNKNOWN} + */ + public static LayoutType fromKey(int key) + { + for (LayoutType type : values()) + { + if (type.key == key) + return type; + } + return UNKNOWN; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java new file mode 100644 index 00000000000..f5e6dbbb1d5 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.entities.messages; + +import javax.annotation.Nonnull; +import java.time.OffsetDateTime; +import java.util.List; + +public class MessagePollImpl implements MessagePoll +{ + private final LayoutType layout; + private final Question question; + private final List answers; + private final OffsetDateTime expiresAt; + private final boolean isMultiAnswer; + private final boolean isFinalizedVotes; + + public MessagePollImpl(LayoutType layout, Question question, List answers, OffsetDateTime expiresAt, boolean isMultiAnswer, boolean isFinalizedVotes) + { + this.layout = layout; + this.question = question; + this.answers = answers; + this.expiresAt = expiresAt; + this.isMultiAnswer = isMultiAnswer; + this.isFinalizedVotes = isFinalizedVotes; + } + + @Nonnull + @Override + public LayoutType getLayout() + { + return layout; + } + + @Nonnull + @Override + public Question getQuestion() + { + return question; + } + + @Nonnull + @Override + public List getAnswers() + { + return answers; + } + + @Nonnull + @Override + public OffsetDateTime getTimeExpiresAt() + { + return expiresAt; + } + + @Override + public boolean isMultiAnswer() + { + return isMultiAnswer; + } + + @Override + public boolean isFinalizedVotes() + { + return isFinalizedVotes; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/poll/GenericMessagePollVoteEvent.java new file mode 100644 index 00000000000..f8ada658eb1 --- /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 00000000000..24fa90eefb8 --- /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 00000000000..5c261f88fec --- /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 fa764c405ec..0ee9eb61605 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -61,6 +61,9 @@ import net.dv8tion.jda.api.events.interaction.command.*; import net.dv8tion.jda.api.events.interaction.component.*; import net.dv8tion.jda.api.events.message.*; +import net.dv8tion.jda.api.events.message.poll.GenericMessagePollVoteEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; import net.dv8tion.jda.api.events.message.react.*; import net.dv8tion.jda.api.events.role.GenericRoleEvent; import net.dv8tion.jda.api.events.role.RoleCreateEvent; @@ -186,6 +189,8 @@ public void onMessageReactionAdd(@Nonnull MessageReactionAddEvent event) {} public void onMessageReactionRemove(@Nonnull MessageReactionRemoveEvent event) {} public void onMessageReactionRemoveAll(@Nonnull MessageReactionRemoveAllEvent event) {} public void onMessageReactionRemoveEmoji(@Nonnull MessageReactionRemoveEmojiEvent event) {} + public void onMessagePollVoteAdd(@Nonnull MessagePollVoteAddEvent event) {} + public void onMessagePollVoteRemove(@Nonnull MessagePollVoteRemoveEvent event) {} //PermissionOverride Events public void onPermissionOverrideDelete(@Nonnull PermissionOverrideDeleteEvent event) {} @@ -389,6 +394,7 @@ public void onGenericContextInteraction(@Nonnull GenericContextInteractionEvent< public void onGenericSelectMenuInteraction(@Nonnull GenericSelectMenuInteractionEvent event) {} public void onGenericMessage(@Nonnull GenericMessageEvent event) {} public void onGenericMessageReaction(@Nonnull GenericMessageReactionEvent event) {} + public void onGenericMessagePollVote(@Nonnull GenericMessagePollVoteEvent event) {} public void onGenericUser(@Nonnull GenericUserEvent event) {} public void onGenericUserPresence(@Nonnull GenericUserPresenceEvent event) {} public void onGenericUserUpdate(@Nonnull GenericUserUpdateEvent event) {} diff --git a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java index 2988de9137c..dbaca131ea2 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/callbacks/IReplyCallback.java @@ -26,6 +26,8 @@ import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -146,6 +148,53 @@ default ReplyCallbackAction reply(@Nonnull MessageCreateData message) return action.applyData(message); } + /** + * Reply to this interaction and acknowledge it. + *
    This will send a reply message for this interaction. + * You can use {@link ReplyCallbackAction#setEphemeral(boolean) setEphemeral(true)} to only let the target user see the message. + * Replies are non-ephemeral by default. + * + *

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

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

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

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

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

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

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

    Example
    + *

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

    A poll must have at least one answer. + * + * @param title + * The poll title (up to {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters) + * + * @throws IllegalArgumentException + * If the title is blank or longer than {@value MessagePoll#MAX_QUESTION_TEXT_LENGTH} characters + * + * @return {@link MessagePollBuilder} + */ + @Nonnull + public static MessagePollBuilder builder(@Nonnull String title) + { + return new MessagePollBuilder(title); + } + + @NotNull + @Override + public DataObject toData() + { + DataObject data = DataObject.empty(); + + data.put("duration", TimeUnit.SECONDS.toHours(duration.getSeconds())); + data.put("allow_multiselect", isMultiAnswer); + data.put("layout_type", layout.getKey()); + + data.put("question", DataObject.empty() + .put("text", question.getText())); + + data.put("answers", answers.stream() + .map(answer -> DataObject.empty() + .put("answer_id", answer.getId()) + .put("poll_media", DataObject.empty() + .put("text", answer.getText()) + .put("emoji", answer.getEmoji()))) + .collect(Helpers.toDataArray())); + + return data; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java index 1e636eb5e00..386e43c3373 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractWebhookClient.java @@ -29,6 +29,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageCreateActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageDeleteActionImpl; import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl; @@ -102,6 +103,14 @@ public WebhookMessageCreateAction sendMessage(@Nonnull MessageCreateData mess return sendRequest().applyData(message); } + @Nonnull + @Override + public WebhookMessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + Checks.notNull(poll, "Message Poll"); + return sendRequest().setPoll(poll); + } + @Nonnull @Override public WebhookMessageCreateAction sendFiles(@Nonnull Collection files) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 97d7258a771..3c69ee7ecb6 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -39,8 +39,11 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.EmojiUnion; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.entities.messages.MessagePollImpl; import net.dv8tion.jda.api.entities.sticker.*; import net.dv8tion.jda.api.entities.templates.Template; import net.dv8tion.jda.api.entities.templates.TemplateChannel; @@ -1836,6 +1839,8 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) ); } + MessagePoll poll = jsonObject.optObject("poll").map(EntityBuilder::createMessagePoll).orElse(null); + // Message Components List components = Collections.emptyList(); Optional componentsArrayOpt = jsonObject.optArray("components"); @@ -1866,7 +1871,7 @@ else if (MISSING_CHANNEL.equals(ex.getMessage())) int position = jsonObject.getInt("position", -1); return new ReceivedMessage(id, channelId, guildId, api, guild, channel, type, messageReference, fromWebhook, applicationId, tts, pinned, - content, nonce, user, member, activity, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, + content, nonce, user, member, activity, poll, editTime, mentions, reactions, attachments, embeds, stickers, components, flags, messageInteraction, startedThread, position); } @@ -1897,6 +1902,47 @@ private static MessageActivity createMessageActivity(DataObject jsonObject) return new MessageActivity(activityType, partyId, application); } + public static MessagePollImpl createMessagePoll(DataObject data) + { + MessagePoll.LayoutType layout = MessagePoll.LayoutType.fromKey(data.getInt("layout_type")); + OffsetDateTime expiresAt = data.isNull("expiry") ? null : data.getOffsetDateTime("expiry"); + boolean isMultiAnswer = data.getBoolean("allow_multiselect"); + + DataArray answersData = data.getArray("answers"); + DataObject questionData = data.getObject("question"); + + DataObject resultsData = data.optObject("results").orElseGet( + () -> DataObject.empty().put("answer_counts", DataArray.empty()) // FIXME: Discord bug + ); + boolean isFinalized = resultsData.getBoolean("is_finalized"); + + DataArray resultVotes = resultsData.getArray("answer_counts"); + TLongObjectMap voteMapping = new TLongObjectHashMap<>(); + resultVotes.stream(DataArray::getObject) + .forEach(votes -> voteMapping.put(votes.getLong("id"), votes)); + + MessagePoll.Question question = new MessagePoll.Question( + questionData.getString("text"), + questionData.optObject("emoji").map(Emoji::fromData).orElse(null)); + + List answers = answersData.stream(DataArray::getObject) + .map(answer -> { + long answerId = answer.getLong("answer_id"); + DataObject media = answer.getObject("poll_media"); + DataObject votes = voteMapping.get(answerId); + return new MessagePoll.Answer( + answerId, + media.getString("text"), + media.optObject("emoji").map(Emoji::fromData).orElse(null), + votes != null ? votes.getInt("count") : 0, + votes != null && votes.getBoolean("me_voted") + ); + }) + .collect(Helpers.toUnmodifiableList()); + + return new MessagePollImpl(layout, question, answers, expiresAt, isMultiAnswer, isFinalized); + } + public MessageReaction createMessageReaction(MessageChannel chan, long channelId, long messageId, DataObject obj) { DataObject emoji = obj.getObject("emoji"); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index e0dedbf78b3..9965195cd24 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -82,7 +82,6 @@ import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; import okhttp3.MediaType; import okhttp3.MultipartBody; -import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -1553,7 +1552,7 @@ public AuditableRestAction ban(@Nonnull Collection reactions, List attachments, List embeds, List stickers, List components, int flags, Message.Interaction interaction, ThreadChannel startedThread, int position) @@ -151,6 +153,7 @@ public ReceivedMessage( this.interaction = interaction; this.startedThread = startedThread; this.position = position; + this.poll = poll; } private void checkSystem(String comment) @@ -613,6 +616,29 @@ public List getComponents() return components; } + @Override + public MessagePoll getPoll() + { + checkIntent(); + return poll; + } + + @Nonnull + @Override + public AuditableRestAction endPoll() + { + checkUser(); + if (poll == null) + throw new IllegalStateException("This message does not contain a poll"); + return new AuditableRestActionImpl<>(getJDA(), Route.Messages.END_POLL.compile(getChannelId(), getId()), (response, request) -> { + JDAImpl jda = (JDAImpl) getJDA(); + EntityBuilder entityBuilder = jda.getEntityBuilder(); + if (hasChannel()) + return entityBuilder.createMessageWithChannel(response.getObject(), channel, false); + return entityBuilder.createMessageFromWebhook(response.getObject(), hasGuild() ? getGuild() : null); + }); + } + @Nonnull @Override public Mentions getMentions() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java index 8f33c5990dd..e2992ae8428 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/mixin/middleman/MessageChannelMixin.java @@ -40,7 +40,9 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.api.utils.messages.MessageCreateData; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import net.dv8tion.jda.internal.requests.RestActionImpl; +import org.jetbrains.annotations.NotNull; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -155,6 +157,33 @@ default MessageCreateAction sendMessageEmbeds(@Nonnull Collection components) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessageComponents(components); + } + + @Nonnull + @Override + default MessageCreateAction sendMessagePoll(@Nonnull MessagePollData poll) + { + checkCanAccessChannel(); + checkCanSendMessage(); + return MessageChannelUnion.super.sendMessagePoll(poll); + } + @Nonnull @CheckReturnValue default MessageCreateAction sendMessage(@Nonnull MessageCreateData msg) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java new file mode 100644 index 00000000000..1e38887ce93 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/MessagePollVoteHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.requests.WebSocketClient; + +public class MessagePollVoteHandler extends SocketHandler +{ + private final boolean add; + + public MessagePollVoteHandler(JDAImpl api, boolean add) + { + super(api); + this.add = add; + } + + @Override + protected Long handleInternally(DataObject content) + { + long answerId = content.getLong("answer_id"); + long userId = content.getUnsignedLong("user_id"); + long messageId = content.getUnsignedLong("message_id"); + long channelId = content.getUnsignedLong("channel_id"); + long guildId = content.getUnsignedLong("guild_id", 0); + + if (api.getGuildSetupController().isLocked(guildId)) + return guildId; + + Guild guild = api.getGuildById(guildId); + MessageChannel channel = api.getChannelById(MessageChannel.class, channelId); + if (channel == null) + { + if (guild != null) + { + GuildChannel actual = guild.getGuildChannelById(channelId); + if (actual != null) + { + WebSocketClient.LOG.debug("Dropping message poll vote event for unexpected channel of type {}", actual.getType()); + return null; + } + } + + if (guildId != 0) + { + api.getEventCache().cache(EventCache.Type.CHANNEL, channelId, responseNumber, allContent, this::handle); + EventCache.LOG.debug("Received a vote for a channel that JDA does not currently have cached"); + return null; + } + + channel = getJDA().getEntityBuilder().createPrivateChannel( + DataObject.empty() + .put("id", channelId) + ); + } + + if (add) + api.handleEvent(new MessagePollVoteAddEvent(channel, responseNumber, messageId, userId, answerId)); + else + api.handleEvent(new MessagePollVoteRemoveEvent(channel, responseNumber, messageId, userId, answerId)); + + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java index c3bf85f68be..88bc202595a 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/CommandDataImpl.java @@ -87,8 +87,7 @@ public void checkName(@Nonnull String name) public void checkDescription(@Nonnull String description) { checkType(Command.Type.SLASH, "set description"); - Checks.notEmpty(description, "Description"); - Checks.notLonger(description, MAX_DESCRIPTION_LENGTH, "Description"); + Checks.inRange(description, 1, MAX_DESCRIPTION_LENGTH, "Description"); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index 08f24d1ad5a..924c45724ae 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -1395,6 +1395,8 @@ protected void setupHandlers() handlers.put("MESSAGE_REACTION_REMOVE", new MessageReactionHandler(api, false)); handlers.put("MESSAGE_REACTION_REMOVE_ALL", new MessageReactionBulkRemoveHandler(api)); handlers.put("MESSAGE_REACTION_REMOVE_EMOJI", new MessageReactionClearEmojiHandler(api)); + handlers.put("MESSAGE_POLL_VOTE_ADD", new MessagePollVoteHandler(api, true)); + handlers.put("MESSAGE_POLL_VOTE_REMOVE", new MessagePollVoteHandler(api, false)); handlers.put("MESSAGE_UPDATE", new MessageUpdateHandler(api)); handlers.put("PRESENCE_UPDATE", new PresenceUpdateHandler(api)); handlers.put("READY", new ReadyHandler(api)); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java index 6f80cd8167c..6e76e56a5b4 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageCreateActionImpl.java @@ -79,7 +79,7 @@ protected RequestBody finalizeData() { if (!stickers.isEmpty()) return getRequestBody(DataObject.empty().put("sticker_ids", stickers)); - throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + throw new IllegalStateException("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } try (MessageCreateData data = builder.build()) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java new file mode 100644 index 00000000000..5ac181ee7b5 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/PollVotersPaginationActionImpl.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.requests.restaction.pagination; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.exceptions.ParsingException; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; +import net.dv8tion.jda.api.requests.Route; +import net.dv8tion.jda.api.requests.restaction.pagination.PollVotersPaginationAction; +import net.dv8tion.jda.api.utils.data.DataArray; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.EntityBuilder; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class PollVotersPaginationActionImpl extends PaginationActionImpl implements PollVotersPaginationAction +{ + public PollVotersPaginationActionImpl(JDA jda, String channelId, String messageId, long answerId) + { + super(jda, Route.Messages.GET_POLL_ANSWER_VOTERS.compile(channelId, messageId, Long.toString(answerId)), 1, 1000, 1000); + this.order = PaginationOrder.FORWARD; + } + + @NotNull + @Override + public EnumSet getSupportedOrders() + { + return EnumSet.of(PaginationOrder.FORWARD); + } + + @Override + protected long getKey(User it) + { + return it.getIdLong(); + } + + @Override + protected void handleSuccess(Response response, Request> request) + { + DataArray array = response.getObject().getArray("users"); + List users = new ArrayList<>(array.length()); + EntityBuilder builder = api.getEntityBuilder(); + for (int i = 0; i < array.length(); i++) + { + try + { + DataObject object = array.getObject(i); + users.add(builder.createUser(object)); + } + catch(ParsingException | NullPointerException e) + { + LOG.warn("Encountered an exception in PollVotersPaginationAction", e); + } + } + + if (!users.isEmpty()) + { + if (useCache) + cached.addAll(users); + last = users.get(users.size() - 1); + lastKey = last.getIdLong(); + } + + request.onSuccess(users); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java index 5e913595b8e..edd1fe10fd2 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 b8a70512132..703f7d1e84e 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -26,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.ToLongFunction; import java.util.stream.Collector; @@ -316,4 +317,27 @@ public static boolean hasCause(Throwable throwable, Class c { return Collector.of(DataArray::empty, DataArray::add, DataArray::addAll); } + + public static String durationToString(Duration duration, TimeUnit resolutionUnit) + { + long actual = resolutionUnit.convert(duration.getSeconds(), TimeUnit.SECONDS); + String raw = actual + " " + resolutionUnit.toString().toLowerCase(Locale.ROOT); + + long days = duration.toDays(); + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() - TimeUnit.DAYS.toSeconds(days) - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); + + StringJoiner joiner = new StringJoiner(" "); + if (days > 0) + joiner.add(days + " days"); + if (hours > 0) + joiner.add(hours + " hours"); + if (minutes > 0) + joiner.add(minutes + " minutes"); + if (seconds > 0) + joiner.add(seconds + " seconds"); + + return raw + " (" + joiner + ")"; + } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java index af8f154de3e..7cf498d9892 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/message/MessageCreateBuilderMixin.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; import net.dv8tion.jda.api.utils.messages.MessageCreateRequest; +import net.dv8tion.jda.api.utils.messages.MessagePollData; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -62,6 +63,21 @@ default R addFiles(@Nonnull Collection files) return (R) this; } + @Nullable + @Override + default MessagePollData getPoll() + { + return getBuilder().getPoll(); + } + + @Nonnull + @Override + default R setPoll(@Nullable MessagePollData poll) + { + getBuilder().setPoll(poll); + return (R) this; + } + @Nonnull @Override default R setTTS(boolean tts) diff --git a/src/test/java/net/dv8tion/jda/test/ChecksHelper.java b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java new file mode 100644 index 00000000000..142f2792d93 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/ChecksHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test; + +import net.dv8tion.jda.test.assertions.checks.*; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.regex.Pattern; + +public class ChecksHelper +{ + public static String tooLongError(String name, int maxLength, String value) + { + return name + " may not be longer than " + maxLength + " characters! Provided: \"" + value + "\""; + } + + public static String notInRangeError(String name, int minLength, int maxLength, String value) + { + return name + " must be between " + minLength + " and " + maxLength + " characters long! Provided: \"" + value + "\""; + } + + public static String isNullError(String name) + { + return name + " may not be null"; + } + + public static String isEmptyError(String name) + { + return name + " may not be empty"; + } + + public static String isBlankError(String name) + { + return name + " may not be blank"; + } + + public static String isNotLowercase(String name, String value) + { + return name + " must be lowercase only! Provided: \"" + value + "\""; + } + + public static String notRegexMatch(String name, Pattern pattern, String value) + { + return name + " must match regex ^" + pattern + "$. Provided: \"" + value + "\""; + } + + public static String isNegativeError(String name) + { + return name + " may not be negative"; + } + + public static String notPositiveError(String name) + { + return name + " may not be negative or zero"; + } + + public static StringChecksAssertions assertStringChecks(String name, ThrowingConsumer callable) + { + return new StringChecksAssertions(name, callable); + } + + public static > EnumChecksAssertions assertEnumChecks(String name, ThrowingConsumer callable) + { + return new EnumChecksAssertions<>(name, callable); + } + + public static DurationChecksAssertions assertDurationChecks(String name, ThrowingConsumer callable) + { + return new DurationChecksAssertions(name, callable); + } + + public static LongChecksAssertions assertLongChecks(String name, ThrowingConsumer callable) + { + return new LongChecksAssertions(name, callable); + } + + public static SimpleChecksAssertions assertChecks(String name, ThrowingConsumer callable) + { + return new SimpleChecksAssertions<>(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/Constants.java b/src/test/java/net/dv8tion/jda/test/Constants.java index 0e1c4c9ef47..4dc3e449bf7 100644 --- a/src/test/java/net/dv8tion/jda/test/Constants.java +++ b/src/test/java/net/dv8tion/jda/test/Constants.java @@ -19,6 +19,7 @@ public interface Constants { long GUILD_ID = 125227483518861312L; + long CHANNEL_ID = 125227483518861312L; long MINN_USER_ID = 86699011792191488L; long BUTLER_USER_ID = 150203841827045376L; } diff --git a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java index a884a14a06d..ebb0952457e 100644 --- a/src/test/java/net/dv8tion/jda/test/IntegrationTest.java +++ b/src/test/java/net/dv8tion/jda/test/IntegrationTest.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.requests.Requester; import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.test.assertions.restaction.RestActionAssertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java new file mode 100644 index 00000000000..f8f964a7c4a --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/AbstractChecksAssertions.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static net.dv8tion.jda.test.ChecksHelper.isNullError; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class AbstractChecksAssertions> +{ + protected final String name; + protected final ThrowingConsumer callable; + + public AbstractChecksAssertions(String name, ThrowingConsumer callable) + { + this.name = name; + this.callable = callable; + } + + public S checksNotNull() + { + return throwsFor(null, isNullError(name)); + } + + @SuppressWarnings("unchecked") + public S throwsFor(T input, String expectedError) + { + assertThatIllegalArgumentException() + .isThrownBy(() -> callable.accept(input)) + .withMessage(expectedError); + return (S) this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java new file mode 100644 index 00000000000..ba50121459f --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/DurationChecksAssertions.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.internal.utils.Helpers.durationToString; +import static net.dv8tion.jda.test.ChecksHelper.isNegativeError; +import static net.dv8tion.jda.test.ChecksHelper.notPositiveError; + +public class DurationChecksAssertions extends AbstractChecksAssertions +{ + public DurationChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public DurationChecksAssertions checksNotNegative() + { + throwsFor(Duration.ofSeconds(-1), isNegativeError(name)); + return this; + } + + public DurationChecksAssertions checksPositive() + { + throwsFor(Duration.ofSeconds(-1), notPositiveError(name)); + throwsFor(Duration.ZERO, notPositiveError(name)); + return this; + } + + public DurationChecksAssertions checksNotLonger(Duration maxDuration, TimeUnit resolution) + { + Duration input = maxDuration.plusSeconds(resolution.toSeconds(1)); + throwsFor(input, + String.format(Locale.ROOT, "%s may not be longer than %s. Provided: %s", + name, durationToString(maxDuration, resolution), durationToString(input, resolution))); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/EnumChecksAssertions.java new file mode 100644 index 00000000000..1ca7712fdf9 --- /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 00000000000..6ba7b8bafc5 --- /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 00000000000..91441c2660a --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/SimpleChecksAssertions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.junit.jupiter.api.function.ThrowingConsumer; + +public class SimpleChecksAssertions extends AbstractChecksAssertions> +{ + public SimpleChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java new file mode 100644 index 00000000000..91e89207fd0 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/checks/StringChecksAssertions.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.checks; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.regex.Pattern; + +import static net.dv8tion.jda.test.ChecksHelper.*; + +public class StringChecksAssertions extends AbstractChecksAssertions +{ + public StringChecksAssertions(String name, ThrowingConsumer callable) + { + super(name, callable); + } + + public StringChecksAssertions checksNotEmpty() + { + throwsFor(null, isNullError(name)); + throwsFor("", isEmptyError(name)); + return this; + } + + public StringChecksAssertions checksNotBlank() + { + throwsFor(null, isNullError(name)); + throwsFor("", isBlankError(name)); + throwsFor(" ", isBlankError(name)); + return this; + } + + public StringChecksAssertions checksNotLonger(int maxLength) + { + String invalidInput = StringUtils.repeat("s", maxLength + 1); + throwsFor(invalidInput, tooLongError(name, maxLength, invalidInput)); + return this; + } + + public StringChecksAssertions checksLowercaseOnly() + { + throwsFor("InvalidCasing", isNotLowercase(name, "InvalidCasing")); + return this; + } + + public StringChecksAssertions checksRange(int minLength, int maxLength) + { + String tooLong = StringUtils.repeat("s", maxLength + 1); + String tooShort = StringUtils.repeat("s", minLength - 1); + throwsFor(tooShort, notInRangeError(name, minLength, maxLength, tooShort)); + throwsFor(tooLong, notInRangeError(name, minLength, maxLength, tooLong)); + return this; + } + + public StringChecksAssertions checksRegex(String input, Pattern regex) + { + throwsFor(input, notRegexMatch(name, regex, input)); + return this; + } +} diff --git a/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java new file mode 100644 index 00000000000..3cb518678c1 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/assertions/events/EventFiredAssertions.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.assertions.events; + +import net.dv8tion.jda.internal.JDAImpl; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.*; + +public class EventFiredAssertions +{ + private final Class eventType; + private final JDAImpl jda; + private final List> assertions = new ArrayList<>(); + + public EventFiredAssertions(Class eventType, JDAImpl jda) + { + this.eventType = eventType; + this.jda = jda; + } + + public EventFiredAssertions hasGetterWithValueEqualTo(Function getter, V value) + { + assertions.add(event -> assertThat(getter.apply(event)).isEqualTo(value)); + return this; + } + + public void isFiredBy(Runnable runnable) + { + doNothing().when(jda).handleEvent(assertArg(arg -> { + assertThat(arg).isInstanceOf(eventType); + T casted = eventType.cast(arg); + for (ThrowingConsumer assertion : assertions) + assertion.accept(casted); + })); + + runnable.run(); + + verify(jda, times(1)).handleEvent(any()); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/RestActionAssertions.java b/src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java similarity index 99% rename from src/test/java/net/dv8tion/jda/test/RestActionAssertions.java rename to src/test/java/net/dv8tion/jda/test/assertions/restaction/RestActionAssertions.java index 37f9f245189..3e124582917 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 8d4bb8ec336..87e28a2072a 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/MessageSerializationTest.java @@ -28,7 +28,6 @@ public class MessageSerializationTest { - private static final String DESCRIPTION_TEXT = "Description Text"; private static final String TITLE_TEXT = "Title Text"; private static final String TITLE_URL = "https://example.com/title"; diff --git a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java index 8192df657b7..d5d126d338d 100644 --- a/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java +++ b/src/test/java/net/dv8tion/jda/test/entities/guild/BulkBanTest.java @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import java.util.stream.LongStream; +import static net.dv8tion.jda.test.ChecksHelper.assertDurationChecks; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -70,6 +71,10 @@ void testInvalidInputs() { hasPermission(true); + assertDurationChecks("Deletion timeframe", duration -> guild.ban(Collections.emptyList(), duration)) + .checksNotNegative() + .throwsFor(Duration.ofDays(100), "Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + Set users = Collections.singleton(null); assertThatIllegalArgumentException() @@ -78,12 +83,7 @@ void testInvalidInputs() assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban(null, null).queue()) .withMessage("Users may not be null"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofSeconds(-1)).queue()) - .withMessage("Deletion time cannot be negative"); - assertThatIllegalArgumentException() - .isThrownBy(() -> guild.ban(Collections.emptyList(), Duration.ofDays(100)).queue()) - .withMessage("Deletion timeframe must not be larger than 7 days. Provided: 8640000 seconds"); + assertThatIllegalArgumentException() .isThrownBy(() -> guild.ban( diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java new file mode 100644 index 00000000000..6cd1f67b5b7 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/MessagePollDataTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; +import net.dv8tion.jda.test.ChecksHelper; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static net.dv8tion.jda.test.ChecksHelper.*; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +public class MessagePollDataTest +{ + @Test + void testInvalidInputs() + { + assertStringChecks("Title", MessagePollBuilder::new) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(300); + + MessagePollBuilder builder = new MessagePollBuilder("test title"); + + assertEnumChecks("Layout", builder::setLayout) + .checksNotNull() + .checkIsNot(MessagePoll.LayoutType.UNKNOWN); + + assertDurationChecks("Duration", builder::setDuration) + .checksNotNull() + .checksPositive() + .checksNotLonger(Duration.ofHours(7 * 24), TimeUnit.HOURS); + + ChecksHelper.assertChecks("TimeUnit", (unit) -> builder.setDuration(1, unit)) + .checksNotNull(); + + assertLongChecks("Duration", (duration) -> builder.setDuration(duration, TimeUnit.SECONDS)) + .checksPositive() + .throwsFor(TimeUnit.DAYS.toSeconds(8), "Duration may not be longer than 168 hours (7 days). Provided: 192 hours (8 days)"); + + assertStringChecks("Answer title", builder::addAnswer) + .checksNotNull() + .checksNotBlank() + .checksNotLonger(55); + + assertThatIllegalStateException() + .isThrownBy(builder::build) + .withMessage("Cannot build a poll without answers"); + + for (int i = 0; i < 10; i++) + builder.addAnswer("Answer " + i); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.addAnswer("Answer " + 10)) + .withMessage("Poll cannot have more than 10 answers"); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java new file mode 100644 index 00000000000..57010d81c6b --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/entities/message/PollVotersPaginationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.entities.message; + +import net.dv8tion.jda.api.requests.Method; +import net.dv8tion.jda.internal.requests.restaction.pagination.PollVotersPaginationActionImpl; +import net.dv8tion.jda.test.IntegrationTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class PollVotersPaginationTest extends IntegrationTest +{ + private PollVotersPaginationActionImpl newAction() + { + return new PollVotersPaginationActionImpl(jda, "381886978205155338", "1228092239079804968", 5); + } + + @Test + void testDefaults() + { + assertThatRequestFrom(newAction()) + .hasMethod(Method.GET) + .hasCompiledRoute("channels/381886978205155338/polls/1228092239079804968/answers/5?limit=1000&after=0") + .whenQueueCalled(); + } + + @Test + void testSkipTo() + { + long randomId = random.nextLong(); + assertThatRequestFrom(newAction().skipTo(randomId)) + .hasMethod(Method.GET) + .hasQueryParams("limit", "1000", "after", Long.toUnsignedString(randomId)) + .whenQueueCalled(); + } + + @Test + void testOrder() + { + assertThatIllegalArgumentException() + .isThrownBy(() -> newAction().reverse()) + .withMessage("Cannot use PaginationOrder.BACKWARD for this pagination endpoint."); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java b/src/test/java/net/dv8tion/jda/test/events/AbstractSocketHandlerTest.java new file mode 100644 index 00000000000..686a00df44d --- /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 00000000000..e3acc34a373 --- /dev/null +++ b/src/test/java/net/dv8tion/jda/test/events/MessagePollHandlerTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.test.events; + +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteAddEvent; +import net.dv8tion.jda.api.events.message.poll.MessagePollVoteRemoveEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.handle.MessagePollVoteHandler; +import net.dv8tion.jda.test.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class MessagePollHandlerTests extends AbstractSocketHandlerTest +{ + @Mock + protected GuildMessageChannel channel; + + @BeforeEach + final void setupMessageContext() + { + when(jda.getChannelById(eq(MessageChannel.class), eq(Constants.CHANNEL_ID))).thenReturn(channel); + } + + @Test + void testMinimalVoteAdd() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, true); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteAddEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteAddEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_ADD", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } + + @Test + void testMinimalVoteRemove() + { + MessagePollVoteHandler handler = new MessagePollVoteHandler(jda, false); + + String messageId = randomSnowflake(); + + assertThatEvent(MessagePollVoteRemoveEvent.class) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getMessageId, messageId) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getAnswerId, 1L) + .hasGetterWithValueEqualTo(MessagePollVoteRemoveEvent::getUserIdLong, Constants.MINN_USER_ID) + .isFiredBy(() -> { + handler.handle(random.nextLong(), event("MESSAGE_POLL_VOTE_REMOVE", DataObject.empty() + .put("answer_id", 1) + .put("message_id", messageId) + .put("channel_id", Constants.CHANNEL_ID) + .put("user_id", Constants.MINN_USER_ID))); + }); + } +} diff --git a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java index 1dae4f186ce..e568089b5d7 100644 --- a/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java +++ b/src/test/java/net/dv8tion/jda/test/interactions/CommandDataTest.java @@ -27,12 +27,14 @@ import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.interactions.CommandDataImpl; +import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.test.PrettyRepresentation; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import static net.dv8tion.jda.test.ChecksHelper.assertStringChecks; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -175,50 +177,51 @@ void testRequiredThrows() @Test void testNameChecks() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new CommandDataImpl("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandData("valid_name", "")) - .withMessage("Description may not be empty"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalid name", "Valid description")) - .withMessage("Name must match regex ^[\\w-]+$. Provided: \"invalid name\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("invalidName", "Valid description")) - .withMessage("Name must be lowercase only! Provided: \"invalidName\""); - assertThatIllegalArgumentException() - .isThrownBy(() -> new SubcommandGroupData("valid_name", "")) - .withMessage("Description may not be empty"); + assertStringChecks("Name", input -> new CommandDataImpl(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Name", input -> new SubcommandGroupData(input, "Valid description")) + .checksNotNull() + .checksRange(1, 32) + .checksLowercaseOnly() + .checksRegex("invalid name", Checks.ALPHANUMERIC_WITH_DASH); + + assertStringChecks("Description", input -> new CommandDataImpl("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); + + assertStringChecks("Description", input -> new SubcommandGroupData("valid_name", input)) + .checksNotNull() + .checksRange(1, 100); } @Test void testChoices() { OptionData stringOption = new OptionData(OptionType.STRING, "choice", "Option with choices!"); + + assertStringChecks("Value", value -> stringOption.addChoice("valid_name", value)) + .checksNotEmpty(); + assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalid name", 0)) .withMessage("Cannot add long choice for OptionType.STRING"); assertThatIllegalArgumentException() .isThrownBy(() -> stringOption.addChoice("invalidName", 0.0)) .withMessage("Cannot add double choice for OptionType.STRING"); - assertThatIllegalArgumentException() - .isThrownBy(() -> stringOption.addChoice("valid_name", "")) - .withMessage("Value may not be empty"); OptionData intOption = new OptionData(OptionType.INTEGER, "choice", "Option with choices!"); List choices = new ArrayList<>(); diff --git a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java index 57456b018d7..8b9ee1c397f 100644 --- a/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java +++ b/src/test/java/net/dv8tion/jda/test/restaction/MessageCreateActionTest.java @@ -18,9 +18,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl; import net.dv8tion.jda.test.IntegrationTest; import org.junit.jupiter.api.BeforeEach; @@ -28,8 +30,11 @@ import org.mockito.Mock; import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; import static net.dv8tion.jda.api.requests.Method.POST; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.emoji; +import static net.dv8tion.jda.test.restaction.MessageCreateActionTest.Data.pollAnswer; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.when; @@ -54,6 +59,7 @@ private static DataObject defaultMessageRequest() .put("components", DataArray.empty()) .put("content", "") .put("embeds", DataArray.empty()) + .put("poll", null) .put("enforce_nonce", true) .put("flags", 0) .put("nonce", FIXED_NONCE) @@ -73,7 +79,7 @@ void testEmpty() assertThatIllegalStateException().isThrownBy(() -> new MessageCreateActionImpl(channel) .queue() - ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, or stickers"); + ).withMessage("Cannot build empty messages! Must provide at least one of: content, embed, file, poll, or stickers"); } @Test @@ -106,9 +112,63 @@ void testEmbedOnly() .whenQueueCalled(); } + @Test + void testPollOnly() + { + MessageCreateAction action = new MessageCreateActionImpl(channel) + .setPoll(new MessagePollBuilder("Test poll") + .setDuration(3, TimeUnit.DAYS) + .setMultiAnswer(true) + .addAnswer("Test answer 1") + .addAnswer("Test answer 2", Emoji.fromUnicode("🤔")) + .addAnswer("Test answer 3", Emoji.fromCustom("minn", 821355005788684298L, true)) + .build()); + + assertThatRequestFrom(action) + .hasMethod(POST) + .hasCompiledRoute(ENDPOINT_URL) + .hasBodyEqualTo(defaultMessageRequest() + .put("poll", DataObject.empty() + .put("duration", 72) + .put("allow_multiselect", true) + .put("layout_type", 1) + .put("question", DataObject.empty() + .put("text", "Test poll")) + .put("answers", DataArray.empty() + .add(pollAnswer(1, "Test answer 1", null)) + .add(pollAnswer(2, "Test answer 2", emoji("🤔"))) + .add(pollAnswer(3, "Test answer 3", emoji("minn", 821355005788684298L, true)))))) + .whenQueueCalled(); + } + @Nonnull protected DataObject normalizeRequestBody(@Nonnull DataObject body) { return body.put("nonce", FIXED_NONCE); } + + static class Data + { + static DataObject pollAnswer(long id, String title, DataObject emoji) + { + return DataObject.empty() + .put("answer_id", id) + .put("poll_media", DataObject.empty() + .put("text", title) + .put("emoji", emoji)); + } + + static DataObject emoji(String name) + { + return DataObject.empty().put("name", name); + } + + static DataObject emoji(String name, long id, boolean animated) + { + return DataObject.empty() + .put("name", name) + .put("id", id) + .put("animated", animated); + } + } } From d65b0ffa2ac6f8eeff31a917c0267b21cd6af93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:33:15 +0200 Subject: [PATCH 23/25] Refactor nexus publishing logic (#2653) --- build.gradle.kts | 354 +++++++++++++++++++++++------------------------ 1 file changed, 176 insertions(+), 178 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index cd79b01c47c..e0f7318ee2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,28 +17,28 @@ //to build everything: "gradlew build" //to build and upload everything: "gradlew release" -import Build_gradle.Pom import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import de.marcphilipp.gradle.nexus.InitializeNexusStagingRepository -import de.marcphilipp.gradle.nexus.NexusPublishExtension -import io.codearte.gradle.nexus.BaseStagingTask -import io.codearte.gradle.nexus.NexusStagingExtension +import io.github.gradlenexus.publishplugin.AbstractNexusStagingRepositoryTask import org.apache.tools.ant.filters.ReplaceTokens import java.time.Duration -// Don't remove this, its needed for reasons.... -typealias Pom = org.gradle.api.publish.maven.MavenPom - plugins { signing `java-library` `maven-publish` - id("io.codearte.nexus-staging") version "0.30.0" - id("de.marcphilipp.nexus-publish") version "0.4.0" + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("com.github.johnrengelman.shadow") version "8.1.1" } + +//////////////////////////////////// +// // +// Project Configuration // +// // +//////////////////////////////////// + + val javaVersion = JavaVersion.current() val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.22") val isCI = System.getProperty("BUILD_NUMBER") != null // jenkins @@ -53,13 +53,13 @@ val commitHash: String by lazy { val commit = System.getenv("GIT_COMMIT") ?: System.getProperty("GIT_COMMIT") ?: System.getenv("GITHUB_SHA") // We only set the commit hash on CI builds since we don't want dirty local repos to set a wrong commit if (isCI && commit != null) - commit.substring(0, 7) + commit.take(7) else "DEV" } val previousVersion: Version by lazy { - val file = File(".version") + val file = layout.projectDirectory.file(".version").asFile if (file.canRead()) Version.parse(file.readText().trim()) else @@ -67,12 +67,15 @@ val previousVersion: Version by lazy { } val isNewVersion = previousVersion != versionObj + // Use normal version string for new releases and commitHash for other builds project.version = "$versionObj" + if (isNewVersion) "" else "_$commitHash" - project.group = "net.dv8tion" -val archivesBaseName = "JDA" + +base { + archivesName.set("JDA") +} java { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -88,6 +91,13 @@ configure { } +//////////////////////////////////// +// // +// Dependency Configuration // +// // +//////////////////////////////////// + + repositories { mavenLocal() mavenCentral() @@ -138,44 +148,48 @@ dependencies { testImplementation(libs.commons.lang3) } -val compileJava: JavaCompile by tasks -val shadowJar: ShadowJar by tasks -val javadoc: Javadoc by tasks -val jar: Jar by tasks -val build: Task by tasks -val clean: Task by tasks -val test: Test by tasks -val check: Task by tasks -shadowJar.archiveClassifier.set("withDependencies") +//////////////////////////////////// +// // +// Build Task Configuration // +// // +//////////////////////////////////// -fun nullable(string: String?): String { - return if (string == null) "null" - else "\"$string\"" + +val jar by tasks.getting(Jar::class) { + archiveBaseName.set(project.name) + manifest.attributes( + "Implementation-Version" to project.version, + "Automatic-Module-Name" to "net.dv8tion.jda") +} + +val shadowJar by tasks.getting(ShadowJar::class) { + archiveClassifier.set("withDependencies") + exclude("*.pom") } -val sourcesForRelease = task("sourcesForRelease") { +val sourcesForRelease by tasks.creating(Copy::class) { from("src/main/java") { include("**/JDAInfo.java") val tokens = mapOf( - "versionMajor" to versionObj.major, - "versionMinor" to versionObj.minor, - "versionRevision" to versionObj.revision, - "versionClassifier" to nullable(versionObj.classifier), - "commitHash" to commitHash + "versionMajor" to versionObj.major, + "versionMinor" to versionObj.minor, + "versionRevision" to versionObj.revision, + "versionClassifier" to nullableReplacement(versionObj.classifier), + "commitHash" to commitHash ) // Allow for setting null on some strings without breaking the source // for this, we have special tokens marked with "!@...@!" which are replaced to @...@ filter { it.replace(Regex("\"!@|@!\""), "@") } // Then we can replace the @...@ with the respective values here - filter(mapOf("tokens" to tokens)) + filter("tokens" to tokens) } into("build/filteredSrc") includeEmptyDirs = false } -val generateJavaSources = task("generateJavaSources") { +val generateJavaSources by tasks.creating(SourceTask::class) { val javaSources = sourceSets["main"].allJava.filter { it.name != "JDAInfo.java" }.asFileTree @@ -184,7 +198,7 @@ val generateJavaSources = task("generateJavaSources") { dependsOn(sourcesForRelease) } -val noOpusJar = task("noOpusJar") { +val noOpusJar by tasks.creating(ShadowJar::class) { dependsOn(shadowJar) archiveClassifier.set(shadowJar.archiveClassifier.get() + "-no-opus") @@ -198,7 +212,7 @@ val noOpusJar = task("noOpusJar") { manifest.inheritFrom(jar.manifest) } -val minimalJar = task("minimalJar") { +val minimalJar by tasks.creating(ShadowJar::class) { dependsOn(shadowJar) minimize() archiveClassifier.set(shadowJar.archiveClassifier.get() + "-min") @@ -212,7 +226,7 @@ val minimalJar = task("minimalJar") { manifest.inheritFrom(jar.manifest) } -val sourcesJar = task("sourcesJar") { +val sourcesJar by tasks.creating(Jar::class) { archiveClassifier.set("sources") from("src/main/java") { exclude("**/JDAInfo.java") @@ -222,42 +236,7 @@ val sourcesJar = task("sourcesJar") { dependsOn(sourcesForRelease) } -val javadocJar = task("javadocJar") { - dependsOn(javadoc) - archiveClassifier.set("javadoc") - from(javadoc.destinationDir) -} - -tasks.withType { - exclude("*.pom") -} - -tasks.withType { - val arguments = mutableListOf("-Xlint:deprecation", "-Xlint:unchecked") - options.encoding = "UTF-8" - options.isIncremental = true - if (javaVersion.isJava9Compatible) doFirst { - arguments += "--release" - arguments += "8" - } - doFirst { - options.compilerArgs = arguments - } -} - -compileJava.apply { - source = generateJavaSources.source - dependsOn(generateJavaSources) -} - -jar.apply { - archiveBaseName.set(project.name) - manifest.attributes(mapOf( - "Implementation-Version" to project.version, - "Automatic-Module-Name" to "net.dv8tion.jda")) -} - -javadoc.apply { +val javadoc by tasks.getting(Javadoc::class) { isFailOnError = isCI options.memberLevel = JavadocMemberLevel.PUBLIC options.encoding = "UTF-8" @@ -301,7 +280,34 @@ javadoc.apply { exclude("com/iwebpp/crypto") } -build.apply { +val javadocJar by tasks.creating(Jar::class) { + dependsOn(javadoc) + archiveClassifier.set("javadoc") + from(javadoc.destinationDir) +} + +tasks.withType { + options.encoding = "UTF-8" + options.isIncremental = true + + val args = mutableListOf("-Xlint:deprecation", "-Xlint:unchecked") + + if (javaVersion.isJava9Compatible) { + args.add("--release") + args.add("8") + } + + doFirst { + options.compilerArgs = args + } +} + +val compileJava by tasks.getting(JavaCompile::class) { + dependsOn(generateJavaSources) + source = generateJavaSources.source +} + +val build by tasks.getting(Task::class) { dependsOn(jar) dependsOn(javadocJar) dependsOn(sourcesJar) @@ -309,75 +315,43 @@ build.apply { dependsOn(noOpusJar) dependsOn(minimalJar) - jar.mustRunAfter(clean) + jar.mustRunAfter(tasks.clean) shadowJar.mustRunAfter(sourcesJar) } -test.apply { +val test by tasks.getting(Test::class) { useJUnitPlatform() failFast = true } -fun getProjectProperty(name: String) = project.properties[name] as? String - -class Version( - val major: String, - val minor: String, - val revision: String, - val classifier: String? = null -) { - companion object { - fun parse(string: String): Version { - val (major, minor, revision) = string.substringBefore("-").split(".") - val classifier = if ("-" in string) string.substringAfter("-") else null - return Version(major, minor, revision, classifier) - } - } - - override fun equals(other: Any?): Boolean { - if (other === this) return true - if (other !is Version) return false - return major == other.major - && minor == other.minor - && revision == other.revision - && classifier == other.classifier - } - - override fun toString(): String { - return "$major.$minor.$revision" + if (classifier != null) "-$classifier" else "" - } -} - +//////////////////////////////////// +// // +// Publishing And Signing // +// // +//////////////////////////////////// -//////////////////////////////////////// -//////////////////////////////////////// -//// //// -//// Publishing And Signing //// -//// //// -//////////////////////////////////////// -//////////////////////////////////////// // Generate pom file for maven central -fun generatePom(pom: Pom) { - pom.packaging = "jar" - pom.name.set(project.name) - pom.description.set("Java wrapper for the popular chat & VOIP service: Discord https://discord.com") - pom.url.set("https://github.com/discord-jda/JDA") - pom.scm { +fun MavenPom.populate() { + packaging = "jar" + name.set(project.name) + description.set("Java wrapper for the popular chat & VOIP service: Discord https://discord.com") + url.set("https://github.com/discord-jda/JDA") + scm { url.set("https://github.com/discord-jda/JDA") connection.set("scm:git:git://github.com/discord-jda/JDA") developerConnection.set("scm:git:ssh:git@github.com:discord-jda/JDA") } - pom.licenses { + licenses { license { name.set("The Apache Software License, Version 2.0") url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") distribution.set("repo") } } - pom.developers { + developers { developer { id.set("Minn") name.set("Florian Spieß") @@ -391,17 +365,14 @@ fun generatePom(pom: Pom) { } } - -// Publish - // Skip fat jar publication (See https://github.com/johnrengelman/shadow/issues/586) components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements.get()) { skip() } val SoftwareComponentContainer.java - get() = components.getByName("java") as AdhocComponentWithVariants + get() = components.getByName("java") publishing { publications { - register("Release", MavenPublication::class) { + register("Release") { from(components["java"]) artifactId = project.name @@ -411,88 +382,115 @@ publishing { artifact(sourcesJar) artifact(javadocJar) - generatePom(pom) + pom.populate() } } } - -// Turn off sign tasks if we don't have a key +val ossrhConfigured = getProjectProperty("ossrhUser") != null val canSign = getProjectProperty("signing.keyId") != null +val shouldPublish = isNewVersion && canSign && ossrhConfigured + if (canSign) { signing { sign(publishing.publications.getByName("Release")) } } -// Staging and Promotion +nexusPublishing { + repositories.sonatype { + username.set(getProjectProperty("ossrhUser")) + password.set(getProjectProperty("ossrhPassword")) + stagingProfileId.set(getProjectProperty("stagingProfileId")) + } -configure { - username = getProjectProperty("ossrhUser") ?: "" - password = getProjectProperty("ossrhPassword") ?: "" - stagingProfileId = getProjectProperty("stagingProfileId") ?: "" -} + connectTimeout.set(Duration.ofMinutes(1)) + clientTimeout.set(Duration.ofMinutes(10)) -configure { - nexusPublishing { - repositories.sonatype { - username.set(getProjectProperty("ossrhUser") ?: "") - password.set(getProjectProperty("ossrhPassword") ?: "") - stagingProfileId.set(getProjectProperty("stagingProfileId") ?: "") - } - // Sonatype is very slow :) - connectTimeout.set(Duration.ofMinutes(1)) - clientTimeout.set(Duration.ofMinutes(10)) + transitionCheckOptions { + maxRetries.set(100) + delayBetween.set(Duration.ofSeconds(5)) } } -// This links the close/release tasks to the right repository (from the publication above) -val ossrhConfigured = getProjectProperty("ossrhUser") != null -val shouldPublish = isNewVersion && canSign && ossrhConfigured +//////////////////////////////////// +// // +// Release Task Configuration // +// // +//////////////////////////////////// -// Turn off the staging tasks if we don't want to publish -tasks.withType { - enabled = shouldPublish + +val rebuild by tasks.creating(Task::class) { + group = "build" + + dependsOn(build) + dependsOn(tasks.clean) + build.mustRunAfter(tasks.clean) } -tasks.withType { +val publishingTasks = tasks.withType { enabled = shouldPublish - // We give each step an hour because it takes very long sometimes ... - numberOfRetries = 30 // 30 tries - delayBetweenRetriesInMillis = 2 * 60 * 1000 // 2 minutes + mustRunAfter(rebuild) + dependsOn(rebuild) } -// Getting staging profile is fine though -tasks.getByName("getStagingProfile").enabled = ossrhConfigured - -tasks.create("release") { - // Only close repository after release is published - val closeRepository by tasks - closeRepository.mustRunAfter(tasks.withType()) - dependsOn(tasks.withType()) - - // Closes the sonatype repository and publishes to maven central - val closeAndReleaseRepository: Task by tasks - dependsOn(closeAndReleaseRepository) +tasks.withType { + enabled = shouldPublish +} - // Builds all jars for publications - dependsOn(build) +val release by tasks.creating(Task::class) { + group = "publishing" enabled = shouldPublish + dependsOn(publishingTasks) + doLast { // Only runs when shouldPublish = true - println("Saving version $versionObj to .version") - val file = File(".version") - file.createNewFile() - file.writeText(versionObj.toString()) + logger.lifecycle("Saving version $versionObj to .version") + val file = layout.projectDirectory.file(".version") + file.asFile.createNewFile() + file.asFile.writeText(versionObj.toString()) } } -tasks.withType { - enabled = shouldPublish +afterEvaluate { + val closeAndReleaseSonatypeStagingRepository by tasks.getting + closeAndReleaseSonatypeStagingRepository.apply { + release.dependsOn(this) + mustRunAfter(publishingTasks) + } +} + + +//////////////////////////////////// +// // +// Helpers // +// // +//////////////////////////////////// + + +fun getProjectProperty(name: String) = project.properties[name] as? String + +fun nullableReplacement(string: String?): String { + return if (string == null) "null" + else "\"$string\"" } -// Gradle stop complaining please -tasks.withType { - duplicatesStrategy = DuplicatesStrategy.INCLUDE +data class Version( + val major: String, + val minor: String, + val revision: String, + val classifier: String? = null +) { + companion object { + fun parse(string: String): Version { + val (major, minor, revision) = string.substringBefore("-").split(".") + val classifier = string.substringAfter("-").takeIf { "-" in string } + return Version(major, minor, revision, classifier) + } + } + + override fun toString(): String { + return "$major.$minor.$revision" + if (classifier != null) "-$classifier" else "" + } } From 7fa598494ff9d099c5795668de75867ce747d01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:35:44 +0200 Subject: [PATCH 24/25] Bump version to 5.0.0-beta.23 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e0f7318ee2a..3339f8d4dd2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,7 @@ plugins { val javaVersion = JavaVersion.current() -val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.22") +val versionObj = Version(major = "5", minor = "0", revision = "0", classifier = "beta.23") val isCI = System.getProperty("BUILD_NUMBER") != null // jenkins || System.getenv("BUILD_NUMBER") != null || System.getProperty("GIT_COMMIT") != null // jitpack From fe12aac25c73a25185baa64f0da7f0b441a8e81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 21 Apr 2024 11:47:58 +0200 Subject: [PATCH 25/25] Move MessagePollImpl to internal package --- .../java/net/dv8tion/jda/internal/entities/EntityBuilder.java | 2 +- .../{api => internal}/entities/messages/MessagePollImpl.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename src/main/java/net/dv8tion/jda/{api => internal}/entities/messages/MessagePollImpl.java (94%) 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 3c69ee7ecb6..a07eeddbb54 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -43,7 +43,6 @@ 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; @@ -68,6 +67,7 @@ import net.dv8tion.jda.internal.entities.emoji.CustomEmojiImpl; import net.dv8tion.jda.internal.entities.emoji.RichCustomEmojiImpl; import net.dv8tion.jda.internal.entities.emoji.UnicodeEmojiImpl; +import net.dv8tion.jda.internal.entities.messages.MessagePollImpl; import net.dv8tion.jda.internal.entities.sticker.*; import net.dv8tion.jda.internal.handle.EventCache; import net.dv8tion.jda.internal.utils.Helpers; diff --git a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/messages/MessagePollImpl.java similarity index 94% rename from src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java rename to src/main/java/net/dv8tion/jda/internal/entities/messages/MessagePollImpl.java index f5e6dbbb1d5..5614e2b4559 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/messages/MessagePollImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/messages/MessagePollImpl.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package net.dv8tion.jda.api.entities.messages; +package net.dv8tion.jda.internal.entities.messages; + +import net.dv8tion.jda.api.entities.messages.MessagePoll; import javax.annotation.Nonnull; import java.time.OffsetDateTime;