From 411d676f68dcface6a453c8b0bdb0f878fdf1cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 18 Feb 2023 11:06:42 +0100 Subject: [PATCH] Necessary additions for role subscriptions (#2375) * Add new role tags * Add new message type * Add new error responses * Add new invite target type * Add support for connection metadata configuration --- src/main/java/net/dv8tion/jda/api/JDA.java | 30 ++ .../net/dv8tion/jda/api/entities/Invite.java | 6 + .../dv8tion/jda/api/entities/MessageType.java | 8 + .../net/dv8tion/jda/api/entities/Role.java | 62 +++ .../api/entities/RoleConnectionMetadata.java | 402 ++++++++++++++++++ .../jda/api/requests/ErrorResponse.java | 3 + .../net/dv8tion/jda/internal/JDAImpl.java | 34 ++ .../jda/internal/entities/RoleImpl.java | 41 +- .../handle/GuildRoleUpdateHandler.java | 2 + .../dv8tion/jda/internal/requests/Route.java | 4 +- .../dv8tion/jda/internal/utils/Checks.java | 1 + 11 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/entities/RoleConnectionMetadata.java diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 8983f35808..522a5c2e06 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -828,6 +828,36 @@ default RestAction deleteCommandById(long commandId) return deleteCommandById(Long.toUnsignedString(commandId)); } + /** + * Retrieves the currently configured {@link RoleConnectionMetadata} records for this application. + * + * @return {@link RestAction} - Type: {@link List} of {@link RoleConnectionMetadata} + * + * @see Configuring App Metadata for Linked Roles + */ + @Nonnull + @CheckReturnValue + RestAction> retrieveRoleConnectionMetadata(); + + /** + * Updates the currently configured {@link RoleConnectionMetadata} records for this application. + * + *

Returns the updated connection metadata records on success. + * + * @param records + * The new records to set + * + * @throws IllegalArgumentException + * If null is provided or more than {@value RoleConnectionMetadata#MAX_RECORDS} records are configured. + * + * @return {@link RestAction} - Type: {@link List} of {@link RoleConnectionMetadata} + * + * @see Configuring App Metadata for Linked Roles + */ + @Nonnull + @CheckReturnValue + RestAction> updateRoleConnectionMetadata(@Nonnull Collection records); + /** * Constructs a new {@link Guild Guild} with the specified name *
Use the returned {@link GuildAction GuildAction} to provide diff --git a/src/main/java/net/dv8tion/jda/api/entities/Invite.java b/src/main/java/net/dv8tion/jda/api/entities/Invite.java index 86dc67ec99..f2cae6590f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Invite.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Invite.java @@ -723,6 +723,12 @@ enum TargetType */ EMBEDDED_APPLICATION(2), + /** + * The invite points to a role subscription listing in a guild. + *
These cannot be created by bots. + */ + ROLE_SUBSCRIPTIONS_PURCHASE(3), + /** * Unknown Discord invite target type. Should never happen and would only possibly happen if Discord implemented a new * target type and JDA had yet to implement support for it. diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageType.java b/src/main/java/net/dv8tion/jda/api/entities/MessageType.java index 429abb4ca6..0845eda458 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageType.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageType.java @@ -150,6 +150,14 @@ public enum MessageType */ AUTO_MODERATION_ACTION(24, true, true), + /** + * Sent when someone purchases a role subscription. + * + * @see Role.RoleTags#isAvailableForPurchase() + * @see Role.RoleTags#hasSubscriptionListing() + */ + ROLE_SUBSCRIPTION_PURCHASE(25, true, true), + /** * Unknown MessageType. */ diff --git a/src/main/java/net/dv8tion/jda/api/entities/Role.java b/src/main/java/net/dv8tion/jda/api/entities/Role.java index 3747326b92..10698fdb75 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Role.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Role.java @@ -373,5 +373,67 @@ default String getIntegrationId() { return isIntegration() ? Long.toUnsignedString(getIntegrationIdLong()) : null; } + + /** + * Whether this role can be acquired through a premium subscription purchase. + * A role would also need {@link #isAvailableForPurchase()} to also be true for a user to actually be + * able to purchase the role. + * + * @return True, if this is a subscription role + * + * @see #getSubscriptionIdLong() + * @see #isAvailableForPurchase() + */ + default boolean hasSubscriptionListing() + { + return getSubscriptionIdLong() != 0; + } + + /** + * The subscription listing id for this role. If a role has a subscription id then it is a premium role that + * can be acquired by users via purchase. + * + * @return The listing id, or 0 if this role is not for a subscription listing + * + * @see #isAvailableForPurchase() + */ + long getSubscriptionIdLong(); + + /** + * The subscription listing id for this role. If a role has a subscription id then it is a premium role that + * can be acquired by users via purchase. + * + * @return The listing id, or null if this role is not for a subscription listing + * + * @see #isAvailableForPurchase() + */ + @Nullable + default String getSubscriptionId() + { + return hasSubscriptionListing() ? Long.toUnsignedString(getSubscriptionIdLong()) : null; + } + + /** + * Whether this role has been published for user purchasing. Only {@link #hasSubscriptionListing() premium roles} + * can be purchased. However, a premium role must be published before it can be purchased. + * Additionally, a premium role can be unpublished after it has been published. Doing so will make it + * no longer available for purchase but will not remove the role from users who have already purchased it. + * + * @return True, if this role is purchasable + * + * @see #hasSubscriptionListing() + */ + boolean isAvailableForPurchase(); + + /** + * Whether this role is acquired through a user connection. + *
Such as external services like twitter or reddit. + * This also includes custom third-party applications, such as those managed by bots via {@link RoleConnectionMetadata}. + * + * @return True, if this role is acquired through a user connection + * + * @see Configuring App Metadata for Linked Roles + */ + boolean isLinkedRole(); } } diff --git a/src/main/java/net/dv8tion/jda/api/entities/RoleConnectionMetadata.java b/src/main/java/net/dv8tion/jda/api/entities/RoleConnectionMetadata.java new file mode 100644 index 0000000000..c6a99614c4 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/RoleConnectionMetadata.java @@ -0,0 +1,402 @@ +/* + * 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 net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.interactions.DiscordLocale; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.data.SerializableData; +import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.EntityString; +import net.dv8tion.jda.internal.utils.localization.LocalizationUtils; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * A metadata record used for role connections. + * + * @see Configuring App Metadata for Linked Roles + * @see Role.RoleTags#isLinkedRole() + */ +public class RoleConnectionMetadata implements SerializableData +{ + /** The maximum length a name can be ({@value}) */ + public static final int MAX_NAME_LENGTH = 100; + /** The maximum length a description can be ({@value}) */ + public static final int MAX_DESCRIPTION_LENGTH = 200; + /** The maximum length a key can be ({@value}) */ + public static final int MAX_KEY_LENGTH = 50; + /** The maximum number of records that can be configured ({@value}) */ + public static final int MAX_RECORDS = 5; + + private final MetadataType type; + private final String key; + private final String name; + private final String description; + private final LocalizationMap nameLocalization = new LocalizationMap(RoleConnectionMetadata::checkName); + private final LocalizationMap descriptionLocalization = new LocalizationMap(RoleConnectionMetadata::checkDescription); + + /** + * Creates a new RoleConnectionMetadata instance. + * + * @param type + * The {@link MetadataType} + * @param name + * The display name of the metadata + * @param key + * The key of the metadata (to update the value later) + * @param description + * The description of the metadata + * + * @throws java.lang.IllegalArgumentException + *

    + *
  • If null is provided
  • + *
  • If the provided name is empty or more than {@value MAX_NAME_LENGTH} characters long
  • + *
  • If the provided key is empty or more than {@value MAX_KEY_LENGTH} characters long
  • + *
  • If the provided description is empty or more than {@value MAX_DESCRIPTION_LENGTH} characters long
  • + *
  • If the provided type is {@link MetadataType#UNKNOWN}
  • + *
  • If the provided key contains any characters other than {@code a-z}, {@code 0-9}, or {@code _}
  • + *
+ */ + public RoleConnectionMetadata(@Nonnull MetadataType type, @Nonnull String name, @Nonnull String key, @Nonnull String description) + { + Checks.check(type != MetadataType.UNKNOWN, "Type must not be UNKNOWN"); + Checks.notNull(type, "Type"); + Checks.notNull(key, "Key"); + Checks.inRange(key, 1, MAX_KEY_LENGTH, "Key"); + Checks.matches(key, Checks.LOWERCASE_ASCII_ALPHANUMERIC, "Key"); + checkName(name); + checkDescription(description); + + this.type = type; + this.name = name; + this.key = key; + this.description = description; + } + + private static void checkName(String name) + { + Checks.notNull(name, "Name"); + Checks.inRange(name, 1, MAX_NAME_LENGTH, "Name"); + } + + private static void checkDescription(String description) + { + Checks.notNull(description, "Description"); + Checks.inRange(description, 1, MAX_DESCRIPTION_LENGTH, "Description"); + } + + /** + * The type of the metadata. + * + * @return The type, or {@link MetadataType#UNKNOWN} if unknown + */ + @Nonnull + public MetadataType getType() + { + return type; + } + + /** + * The display name of the metadata. + * + * @return The display name + */ + @Nonnull + public String getName() + { + return name; + } + + /** + * The key of the metadata. + * + * @return The key + */ + @Nonnull + public String getKey() + { + return key; + } + + /** + * The description of the metadata. + * + * @return The description + */ + @Nonnull + public String getDescription() + { + return description; + } + + /** + * The localizations of this record's name for {@link DiscordLocale various languages}. + * + * @return The {@link LocalizationMap} containing the mapping from {@link DiscordLocale} to the localized name + */ + @Nonnull + public LocalizationMap getNameLocalizations() + { + return nameLocalization; + } + + /** + * The localizations of this record's description for {@link DiscordLocale various languages}. + * + * @return The {@link LocalizationMap} containing the mapping from {@link DiscordLocale} to the localized description + */ + @Nonnull + public LocalizationMap getDescriptionLocalizations() + { + return descriptionLocalization; + } + + /** + * Sets a {@link DiscordLocale language-specific} localization of this record's name. + * + *

This change will not take effect in Discord until you update the role connection metadata using {@link JDA#updateRoleConnectionMetadata(Collection)}. + * + * @param locale + * The locale to associate the translated name with + * @param name + * The translated name to put + * + * @throws IllegalArgumentException + *

    + *
  • If the locale is null
  • + *
  • If the name is null
  • + *
  • If the locale is {@link DiscordLocale#UNKNOWN}
  • + *
  • If the provided name is empty or more than {@value MAX_NAME_LENGTH} characters long
  • + *
+ * + * @return This updated record instance + */ + @Nonnull + public RoleConnectionMetadata setNameLocalization(@Nonnull DiscordLocale locale, @Nonnull String name) + { + this.nameLocalization.setTranslation(locale, name); + return this; + } + + /** + * Sets multiple {@link DiscordLocale language-specific} localizations of this record's name. + * + *

This change will not take effect in Discord until you update the role connection metadata using {@link JDA#updateRoleConnectionMetadata(Collection)}. + * + * @param map + * The map from which to transfer the translated names + * + * @throws IllegalArgumentException + *

    + *
  • If the map is null
  • + *
  • If the map contains an {@link DiscordLocale#UNKNOWN} key
  • + *
  • If the map contains a name which is empty or more than {@value MAX_NAME_LENGTH} characters long
  • + *
+ * + * @return This updated record instance + */ + @Nonnull + public RoleConnectionMetadata setNameLocalizations(@Nonnull Map map) + { + this.nameLocalization.setTranslations(map); + return this; + } + + /** + * Sets a {@link DiscordLocale language-specific} localization of this record's description. + * + *

This change will not take effect in Discord until you update the role connection metadata using {@link JDA#updateRoleConnectionMetadata(Collection)}. + * + * @param locale + * The locale to associate the translated description with + * @param description + * The translated description to put + * + * @throws IllegalArgumentException + *

    + *
  • If the locale is null
  • + *
  • If the description is null
  • + *
  • If the locale is {@link DiscordLocale#UNKNOWN}
  • + *
  • If the provided description is empty or more than {@value MAX_DESCRIPTION_LENGTH} characters long
  • + *
+ * + * @return This updated record instance + */ + @Nonnull + public RoleConnectionMetadata setDescriptionLocalization(@Nonnull DiscordLocale locale, @Nonnull String description) + { + this.descriptionLocalization.setTranslation(locale, description); + return this; + } + + /** + * Sets multiple {@link DiscordLocale language-specific} localizations of this record's description. + * + *

This change will not take effect in Discord until you update the role connection metadata using {@link JDA#updateRoleConnectionMetadata(Collection)}. + * + * @param map + * The map from which to transfer the translated descriptions + * + * @throws IllegalArgumentException + *

    + *
  • If the map is null
  • + *
  • If the map contains an {@link DiscordLocale#UNKNOWN} key
  • + *
  • If the map contains a description which is empty or more than {@value MAX_DESCRIPTION_LENGTH} characters long
  • + *
+ * + * @return This updated record instance + */ + @Nonnull + public RoleConnectionMetadata setDescriptionLocalizations(@Nonnull Map map) + { + this.descriptionLocalization.setTranslations(map); + return this; + } + + @Override + public String toString() + { + return new EntityString(this) + .setType(type) + .setName(name) + .addMetadata("key", key) + .toString(); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof RoleConnectionMetadata)) + return false; + if (this == obj) + return true; + RoleConnectionMetadata o = (RoleConnectionMetadata) obj; + return this.type == o.type + && this.key.equals(o.key) + && this.name.equals(o.name) + && this.description.equals(o.description); + } + + @Override + public int hashCode() + { + return Objects.hash(type, key, name, description); + } + + @Nonnull + @Override + public DataObject toData() + { + return DataObject.empty() + .put("type", type.value) + .put("name", name) + .put("key", key) + .put("description", description) + .put("name_localizations", nameLocalization) + .put("description_localizations", descriptionLocalization); + } + + /** + * Parses a {@link RoleConnectionMetadata} from a {@link DataObject}. + *
This is the reverse of {@link #toData()}. + * + * @param data + * The data object to parse values from# + * + * @throws IllegalArgumentException + * If the provided data object is null + * @throws net.dv8tion.jda.api.exceptions.ParsingException + * If the provided data does not have a valid int type value + * + * @return The parsed metadata instance + */ + @Nonnull + public static RoleConnectionMetadata fromData(@Nonnull DataObject data) + { + Checks.notNull(data, "Data"); + RoleConnectionMetadata metadata = new RoleConnectionMetadata( + MetadataType.fromValue(data.getInt("type")), + data.getString("name", null), + data.getString("key", null), + data.getString("description", null) + ); + return metadata.setNameLocalizations(LocalizationUtils.mapFromProperty(data, "name_localizations")) + .setDescriptionLocalizations(LocalizationUtils.mapFromProperty(data, "description_localizations")); + } + + /** + * The type of metadata. + *
Each metadata type offers a comparison operation that allows guilds to configure role requirements based on metadata values stored by the bot. + * Bots specify a metadata value for each user and guilds specify the required guild's configured value within the guild role settings. + * + *

For example, you could use {@link #INTEGER_GREATER_THAN_OR_EQUAL} on a connection to require a certain metadata value to be at least the desired minimum value. + */ + public enum MetadataType + { + INTEGER_LESS_THAN_OR_EQUAL(1), + INTEGER_GREATER_THAN_OR_EQUAL(2), + INTEGER_EQUALS(3), + INTEGER_NOT_EQUALS(4), + DATETIME_LESS_THAN_OR_EQUAL(5), + DATETIME_GREATER_THAN_OR_EQUAL(6), + BOOLEAN_EQUAL(7), + BOOLEAN_NOT_EQUAL(8), + UNKNOWN(-1); + + private final int value; + + MetadataType(int value) + { + this.value = value; + } + + + /** + * The raw value used by Discord. + * + * @return The raw value + */ + public int getValue() + { + return value; + } + + /** + * The MetadataType for the provided raw value. + * + * @param value + * The raw value + * + * @return The MetadataType for the provided raw value, or {@link #UNKNOWN} if none is found + */ + @Nonnull + public static MetadataType fromValue(int value) + { + for (MetadataType type : values()) + { + if (type.value == value) + return type; + } + return UNKNOWN; + } + } +} 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 8bba7163fd..e39190b742 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -108,6 +108,7 @@ public enum ErrorResponse MAX_STICKERS( 30039, "Maximum number of stickers reached"), MAX_PRUNE_REQUESTS( 30040, "Maximum number of prune requests has been reached. Try again later"), MAX_GUILD_WIDGET_UPDATES( 30042, "Maximum number of guild widget settings updates has been reached. Try again later"), + MAX_PREMIUM_EMOJIS( 30056, "Maximum number of premium emojis reached (25)"), UNAUTHORIZED( 40001, "Unauthorized"), NOT_VERIFIED( 40002, "You need to verify your account in order to perform this action"), OPEN_DM_TOO_FAST( 40003, "You are opening direct messages too fast"), @@ -161,6 +162,8 @@ public enum ErrorResponse SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION( 50095, "This server is not available in your location"), SERVER_MONETIZATION_DISABLED( 50097, "This server needs monetization enabled in order to perform this action"), 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"), 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"), diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 6d9fdb16fe..a15b033519 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -86,6 +86,7 @@ import net.dv8tion.jda.internal.utils.config.SessionConfig; import net.dv8tion.jda.internal.utils.config.ThreadingConfig; import okhttp3.OkHttpClient; +import okhttp3.RequestBody; import org.slf4j.Logger; import org.slf4j.MDC; @@ -1032,6 +1033,39 @@ public RestAction deleteCommandById(@Nonnull String commandId) return new RestActionImpl<>(this, route); } + @Nonnull + @Override + public RestAction> retrieveRoleConnectionMetadata() + { + Route.CompiledRoute route = Route.Applications.GET_ROLE_CONNECTION_METADATA.compile(getSelfUser().getApplicationId()); + return new RestActionImpl<>(this, route, + (response, request) -> + response.getArray() + .stream(DataArray::getObject) + .map(RoleConnectionMetadata::fromData) + .collect(Helpers.toUnmodifiableList())); + } + + @Nonnull + @Override + public RestAction> updateRoleConnectionMetadata(@Nonnull Collection records) + { + Checks.noneNull(records, "Records"); + Checks.check(records.size() <= RoleConnectionMetadata.MAX_RECORDS, "An application can have a maximum of %d metadata records", RoleConnectionMetadata.MAX_RECORDS); + + Route.CompiledRoute route = Route.Applications.UPDATE_ROLE_CONNECTION_METADATA.compile(getSelfUser().getApplicationId()); + + DataArray array = DataArray.fromCollection(records); + RequestBody body = RequestBody.create(array.toJson(), Requester.MEDIA_TYPE_JSON); + + return new RestActionImpl<>(this, route, body, + (response, request) -> + response.getArray() + .stream(DataArray::getObject) + .map(RoleConnectionMetadata::fromData) + .collect(Helpers.toUnmodifiableList())); + } + @Nonnull @Override public GuildActionImpl createGuild(@Nonnull String name) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java index 5b3c9194c4..2db0c5c3b6 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java @@ -475,20 +475,29 @@ public static class RoleTagsImpl implements RoleTags public static final RoleTags EMPTY = new RoleTagsImpl(); private final long botId; private final long integrationId; + private final long subscriptionListingId; private final boolean premiumSubscriber; + private final boolean availableForPurchase; + private final boolean isGuildConnections; public RoleTagsImpl() { this.botId = 0L; this.integrationId = 0L; + this.subscriptionListingId = 0L; this.premiumSubscriber = false; + this.availableForPurchase = false; + this.isGuildConnections = false; } public RoleTagsImpl(DataObject tags) { - this.botId = tags.hasKey("bot_id") ? tags.getUnsignedLong("bot_id") : 0L; - this.integrationId = tags.hasKey("integration_id") ? tags.getUnsignedLong("integration_id") : 0L; + this.botId = tags.getUnsignedLong("bot_id", 0L); + this.integrationId = tags.getUnsignedLong("integration_id", 0L); + this.subscriptionListingId = tags.getUnsignedLong("subscription_listing_id", 0L); this.premiumSubscriber = tags.hasKey("premium_subscriber"); + this.availableForPurchase = tags.hasKey("available_for_purchase"); + this.isGuildConnections = tags.hasKey("guild_connections"); } @Override @@ -521,10 +530,28 @@ public long getIntegrationIdLong() return integrationId; } + @Override + public long getSubscriptionIdLong() + { + return subscriptionListingId; + } + + @Override + public boolean isAvailableForPurchase() + { + return availableForPurchase; + } + + @Override + public boolean isLinkedRole() + { + return isGuildConnections; + } + @Override public int hashCode() { - return Objects.hash(botId, integrationId, premiumSubscriber); + return Objects.hash(botId, integrationId, premiumSubscriber, availableForPurchase, subscriptionListingId, isGuildConnections); } @Override @@ -537,7 +564,10 @@ public boolean equals(Object obj) RoleTagsImpl other = (RoleTagsImpl) obj; return botId == other.botId && integrationId == other.integrationId - && premiumSubscriber == other.premiumSubscriber; + && premiumSubscriber == other.premiumSubscriber + && availableForPurchase == other.availableForPurchase + && subscriptionListingId == other.subscriptionListingId + && isGuildConnections == other.isGuildConnections; } @Override @@ -546,7 +576,10 @@ public String toString() return new EntityString(this) .addMetadata("bot", getBotId()) .addMetadata("integration", getIntegrationId()) + .addMetadata("subscriptionListing", getSubscriptionId()) .addMetadata("isBoost", isBoost()) + .addMetadata("isAvailableForPurchase", isAvailableForPurchase()) + .addMetadata("isGuildConnections", isLinkedRole()) .toString(); } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildRoleUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildRoleUpdateHandler.java index 880bde21d5..ec743fcea7 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildRoleUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildRoleUpdateHandler.java @@ -68,6 +68,8 @@ protected Long handleInternally(DataObject content) String iconId = rolejson.getString("icon", null); String emoji = rolejson.getString("unicode_emoji", null); + rolejson.optObject("tags").ifPresent(role::setTags); + if (!Objects.equals(name, role.getName())) { String oldName = role.getName(); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/Route.java b/src/main/java/net/dv8tion/jda/internal/requests/Route.java index 48e5b90317..f070d434cc 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/Route.java @@ -41,7 +41,9 @@ public static class Misc public static class Applications { // Bot only - public static final Route GET_BOT_APPLICATION = new Route(GET, "oauth2/applications/@me"); + 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"); // Client only public static final Route GET_APPLICATIONS = new Route(GET, "oauth2/applications"); 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 e20c2d3407..fd9270c6ec 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Checks.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Checks.java @@ -39,6 +39,7 @@ public class Checks { public static final Pattern ALPHANUMERIC_WITH_DASH = Pattern.compile("[\\w-]+", Pattern.UNICODE_CHARACTER_CLASS); public static final Pattern ALPHANUMERIC = Pattern.compile("[\\w]+", Pattern.UNICODE_CHARACTER_CLASS); + public static final Pattern LOWERCASE_ASCII_ALPHANUMERIC = Pattern.compile("[a-z0-9_]+"); @Contract("null -> fail") public static void isSnowflake(final String snowflake)