diff --git a/src/main/java/net/dv8tion/jda/api/entities/Collectibles.java b/src/main/java/net/dv8tion/jda/api/entities/Collectibles.java
new file mode 100644
index 0000000000..52420382e7
--- /dev/null
+++ b/src/main/java/net/dv8tion/jda/api/entities/Collectibles.java
@@ -0,0 +1,134 @@
+/*
+ * 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.utils.DiscordAssets;
+import net.dv8tion.jda.api.utils.ImageFormat;
+import net.dv8tion.jda.api.utils.ImageProxy;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * The collectibles a user has equipped.
+ *
+ *
This excludes avatar decorations and profile effects.
+ */
+public interface Collectibles {
+ /**
+ * Returns the equipped nameplate.
+ *
+ * @return The displayed nameplate
+ */
+ @Nullable
+ Nameplate getNameplate();
+
+ /**
+ * A decoration visible in Direct Messages and a guild's member list.
+ */
+ interface Nameplate {
+ /**
+ * Returns the SKU ID of this nameplate.
+ *
+ *
This is unique and will never change.
+ *
+ * @return The SKU ID
+ */
+ @Nonnull
+ String getSkuId();
+
+ /**
+ * Returns the asset path of this nameplate.
+ *
+ *
This is unique but not necessarily immutable.
+ *
+ * @return The asset path
+ */
+ @Nonnull
+ String getAssetPath();
+
+ /**
+ * Returns a URL of this nameplate, as an static asset.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @return The URL to this nameplate's static asset
+ *
+ * @see #getAnimatedAssetUrl()
+ * @see #getStaticAsset()
+ */
+ @Nonnull
+ default String getStaticAssetUrl() {
+ return getStaticAsset().getUrl();
+ }
+
+ /**
+ * Returns an {@link ImageProxy} of this nameplate, as a static asset.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @return The proxy to this nameplate's static asset
+ *
+ * @see #getAnimatedAsset()
+ * @see #getStaticAssetUrl()
+ */
+ @Nonnull
+ default ImageProxy getStaticAsset() {
+ return DiscordAssets.staticNameplate(ImageFormat.PNG, getAssetPath());
+ }
+
+ /**
+ * Returns a URL of this nameplate, as an animated asset.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @return The URL to this nameplate's animated asset
+ *
+ * @see #getStaticAssetUrl()
+ * @see #getAnimatedAsset()
+ */
+ @Nonnull
+ default String getAnimatedAssetUrl() {
+ return getAnimatedAsset().getUrl();
+ }
+
+ /**
+ * Returns an {@link ImageProxy} of this nameplate, as an animated asset.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @return The proxy to this nameplate's animated asset
+ *
+ * @see #getStaticAsset()
+ * @see #getAnimatedAssetUrl()
+ */
+ @Nonnull
+ default ImageProxy getAnimatedAsset() {
+ return DiscordAssets.animatedNameplate(ImageFormat.WEBM, getAssetPath());
+ }
+
+ /**
+ * Returns the name of the background color of this nameplate.
+ *
+ * @return Palette name of this nameplate's background color
+ *
+ * @see Official documentation for nameplates
+ */
+ @Nonnull
+ String getPalette();
+ }
+}
diff --git a/src/main/java/net/dv8tion/jda/api/entities/Member.java b/src/main/java/net/dv8tion/jda/api/entities/Member.java
index f39bf6e2c1..ce11b4435a 100644
--- a/src/main/java/net/dv8tion/jda/api/entities/Member.java
+++ b/src/main/java/net/dv8tion/jda/api/entities/Member.java
@@ -31,6 +31,7 @@
import net.dv8tion.jda.api.utils.ImageFormat;
import net.dv8tion.jda.api.utils.ImageProxy;
import net.dv8tion.jda.api.utils.data.DataObject;
+import net.dv8tion.jda.internal.entities.CollectiblesImpl;
import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.Helpers;
@@ -417,6 +418,31 @@ default ImageProxy getEffectiveAvatar(@Nonnull ImageFormat preferredFormat) {
return avatar == null ? getUser().getEffectiveAvatar(preferredFormat) : avatar;
}
+ /**
+ * Returns the collectibles this member has equipped on this guild specifically.
+ *
+ * @return The collectibles equipped by this member
+ */
+ @Nonnull
+ Collectibles getCollectibles();
+
+ /**
+ * Returns the collectibles currently displayed for this member, in the guild.
+ *
+ *
Each collectible will return the first value found:
+ *
+ * - On the member
+ * - On the user
+ * - {@code null}
+ *
+ *
+ * @return The collectibles effectively equipped by this member
+ */
+ @Nonnull
+ default Collectibles getEffectiveCollectibles() {
+ return new CollectiblesImpl.Effective(this);
+ }
+
/**
* The roles applied to this Member.
*
The roles are ordered based on their position. The highest role being at index 0
diff --git a/src/main/java/net/dv8tion/jda/api/entities/User.java b/src/main/java/net/dv8tion/jda/api/entities/User.java
index bbfd15d496..ef44a3a45d 100644
--- a/src/main/java/net/dv8tion/jda/api/entities/User.java
+++ b/src/main/java/net/dv8tion/jda/api/entities/User.java
@@ -338,6 +338,14 @@ default ImageProxy getEffectiveAvatar(@Nonnull ImageFormat preferredFormat) {
return avatar == null ? getDefaultAvatar() : avatar;
}
+ /**
+ * Returns the collectibles this user has equipped.
+ *
+ * @return The collectibles equipped by this user
+ */
+ @Nonnull
+ Collectibles getCollectibles();
+
/**
* Loads the user's {@link User.Profile} data.
* Returns a completed RestAction if this User has been retrieved using {@link JDA#retrieveUserById(long)}.
diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateCollectiblesEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateCollectiblesEvent.java
new file mode 100644
index 0000000000..d2c655a44a
--- /dev/null
+++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateCollectiblesEvent.java
@@ -0,0 +1,71 @@
+/*
+ * 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.guild.member.update;
+
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Collectibles;
+import net.dv8tion.jda.api.entities.Member;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Indicates that a {@link Member} updated their {@link net.dv8tion.jda.api.entities.Guild Guild} {@link Collectibles}.
+ *
+ * Can be used to retrieve members who change their per guild collectibles, the triggering guild, the old collectibles and the new collectibles.
+ *
+ *
Identifier: {@value #IDENTIFIER}
+ *
+ *
Requirements
+ *
+ *
This event requires the {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MEMBERS GUILD_MEMBERS} intent to be enabled.
+ *
{@link net.dv8tion.jda.api.JDABuilder#createDefault(String) createDefault(String)} and
+ * {@link net.dv8tion.jda.api.JDABuilder#createLight(String) createLight(String)} disable this by default!
+ *
+ *
Additionally, this event requires the {@link net.dv8tion.jda.api.utils.MemberCachePolicy MemberCachePolicy}
+ * to cache the updated members. Discord does not specifically tell us about the updates, but merely tells us the
+ * member was updated and gives us the updated member object. In order to fire a specific event like this we
+ * need to have the old member cached to compare against.
+ */
+public class GuildMemberUpdateCollectiblesEvent extends GenericGuildMemberUpdateEvent {
+ public static final String IDENTIFIER = "collectibles";
+
+ public GuildMemberUpdateCollectiblesEvent(
+ @Nonnull JDA api, long responseNumber, @Nonnull Member member, @Nullable Collectibles oldAvatarId) {
+ super(api, responseNumber, member, oldAvatarId, member.getCollectibles(), IDENTIFIER);
+ }
+
+ /**
+ * The old collectibles
+ *
+ * @return The old collectibles
+ */
+ @Nullable
+ public Collectibles getOldAvatarId() {
+ return getOldValue();
+ }
+
+ /**
+ * The new collectibles
+ *
+ * @return The new collectibles
+ */
+ @Nullable
+ public Collectibles getNewAvatarId() {
+ return getNewValue();
+ }
+}
diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateCollectiblesEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateCollectiblesEvent.java
new file mode 100644
index 0000000000..fad8fc822e
--- /dev/null
+++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateCollectiblesEvent.java
@@ -0,0 +1,71 @@
+/*
+ * 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.user.update;
+
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Collectibles;
+import net.dv8tion.jda.api.entities.User;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Indicates that the {@link Collectibles} of a {@link User} changed.
+ *
+ * Can be used to retrieve the User who changed their collectibles and their previous collectibles.
+ *
+ *
Identifier: {@value #IDENTIFIER}
+ *
+ *
Requirements
+ *
+ *
This event requires the {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_MEMBERS GUILD_MEMBERS} intent to be enabled.
+ *
{@link net.dv8tion.jda.api.JDABuilder#createDefault(String) createDefault(String)} and
+ * {@link net.dv8tion.jda.api.JDABuilder#createLight(String) createLight(String)} disable this by default!
+ *
+ *
Additionally, this event requires the {@link net.dv8tion.jda.api.utils.MemberCachePolicy MemberCachePolicy}
+ * to cache the updated members. Discord does not specifically tell us about the updates, but merely tells us the
+ * member was updated and gives us the updated member object. In order to fire a specific event like this we
+ * need to have the old member cached to compare against.
+ */
+public class UserUpdateCollectiblesEvent extends GenericUserUpdateEvent {
+ public static final String IDENTIFIER = "collectibles";
+
+ public UserUpdateCollectiblesEvent(
+ @Nonnull JDA api, long responseNumber, @Nonnull User user, @Nullable Collectibles oldCollectibles) {
+ super(api, responseNumber, user, oldCollectibles, user.getCollectibles(), IDENTIFIER);
+ }
+
+ /**
+ * The previous collectibles
+ *
+ * @return The previous collectibles
+ */
+ @Nullable
+ public Collectibles getOldCollectibles() {
+ return getOldValue();
+ }
+
+ /**
+ * The new collectibles
+ *
+ * @return The new collectibles
+ */
+ @Nullable
+ public Collectibles getNewCollectibles() {
+ return getNewValue();
+ }
+}
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 33a9147d05..b789dfccdb 100644
--- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java
+++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java
@@ -179,6 +179,8 @@ public void onUserUpdateDiscriminator(@Nonnull UserUpdateDiscriminatorEvent even
public void onUserUpdateAvatar(@Nonnull UserUpdateAvatarEvent event) {}
+ public void onUserUpdateCollectibles(@Nonnull UserUpdateCollectiblesEvent event) {}
+
public void onUserUpdateOnlineStatus(@Nonnull UserUpdateOnlineStatusEvent event) {}
public void onUserUpdateActivityOrder(@Nonnull UserUpdateActivityOrderEvent event) {}
@@ -450,6 +452,8 @@ public void onGuildMemberUpdateNickname(@Nonnull GuildMemberUpdateNicknameEvent
public void onGuildMemberUpdateAvatar(@Nonnull GuildMemberUpdateAvatarEvent event) {}
+ public void onGuildMemberUpdateCollectibles(@Nonnull GuildMemberUpdateCollectiblesEvent event) {}
+
public void onGuildMemberUpdateBoostTime(@Nonnull GuildMemberUpdateBoostTimeEvent event) {}
public void onGuildMemberUpdatePending(@Nonnull GuildMemberUpdatePendingEvent event) {}
diff --git a/src/main/java/net/dv8tion/jda/api/utils/DiscordAssets.java b/src/main/java/net/dv8tion/jda/api/utils/DiscordAssets.java
index de94112c1e..d48a2de679 100644
--- a/src/main/java/net/dv8tion/jda/api/utils/DiscordAssets.java
+++ b/src/main/java/net/dv8tion/jda/api/utils/DiscordAssets.java
@@ -20,6 +20,8 @@
import okhttp3.HttpUrl;
import org.jetbrains.annotations.Contract;
+import java.util.regex.Pattern;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -27,6 +29,8 @@
* Utility class to retrieve an {@link ImageProxy} of most Discord assets.
*/
public final class DiscordAssets {
+ private static final Pattern ALPHANUMERIC_PATH_SEGMENT_PATTERN = Pattern.compile("[\\w\\-/]+");
+
private DiscordAssets() {}
/**
@@ -362,6 +366,78 @@ public static ImageProxy memberAvatar(
return format.finishProxy(builder, avatarId);
}
+ /**
+ * Returns an {@link ImageProxy} of a static nameplate.
+ *
+ * At the time of writing, the only supported format is {@link ImageFormat#PNG PNG}.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @param nameplatePath
+ * The nameplate asset's path
+ *
+ * @throws IllegalArgumentException
+ *
+ * - If an argument is {@code null}, except for the nameplate path
+ * - If the nameplate path starts with a {@code /}
+ * - If the nameplate path contains non-alphanumeric characters other than {@code _}, {@code -} and {@code /}
+ *
+ *
+ * @return An {@link ImageProxy} of the static nameplate
+ *
+ * @see #animatedNameplate(ImageFormat, String)
+ */
+ @Contract("_, null -> null; _, !null -> !null")
+ public static ImageProxy staticNameplate(@Nonnull ImageFormat format, @Nullable String nameplatePath) {
+ Checks.notNull(format, "Format");
+ if (nameplatePath == null) {
+ return null;
+ }
+ Checks.check(!nameplatePath.startsWith("/"), "Nameplate path must not start with /");
+ Checks.matches(nameplatePath, ALPHANUMERIC_PATH_SEGMENT_PATTERN, "Nameplate path");
+
+ HttpUrl.Builder builder = newUrl().addEncodedPathSegment("assets")
+ .addEncodedPathSegment("collectibles")
+ .addPathSegments(nameplatePath);
+ return format.finishProxy(builder, "static");
+ }
+
+ /**
+ * Returns an {@link ImageProxy} of an animated nameplate.
+ *
+ * At the time of writing, the only supported format is {@link ImageFormat#WEBM WEBM}.
+ *
+ *
Size parameters are ignored by this endpoint.
+ *
+ * @param nameplatePath
+ * The nameplate asset's path
+ *
+ * @throws IllegalArgumentException
+ *
+ * - If an argument is {@code null}, except for the nameplate path
+ * - If the nameplate path starts with a {@code /}
+ * - If the nameplate path contains non-alphanumeric characters other than {@code _}, {@code -} and {@code /}
+ *
+ *
+ * @return An {@link ImageProxy} of the animated nameplate
+ *
+ * @see #staticNameplate(ImageFormat, String)
+ */
+ @Contract("_, null -> null; _, !null -> !null")
+ public static ImageProxy animatedNameplate(@Nonnull ImageFormat format, @Nullable String nameplatePath) {
+ Checks.notNull(format, "Format");
+ if (nameplatePath == null) {
+ return null;
+ }
+ Checks.check(!nameplatePath.startsWith("/"), "Nameplate path must not start with /");
+ Checks.matches(nameplatePath, ALPHANUMERIC_PATH_SEGMENT_PATTERN, "Nameplate path");
+
+ HttpUrl.Builder builder = newUrl().addEncodedPathSegment("assets")
+ .addEncodedPathSegment("collectibles")
+ .addPathSegments(nameplatePath);
+ return format.finishProxy(builder, "asset");
+ }
+
/**
* Returns an {@link ImageProxy} of a role's icon.
*
This returns {@code null} if the icon ID is {@code null}.
diff --git a/src/main/java/net/dv8tion/jda/api/utils/ImageFormat.java b/src/main/java/net/dv8tion/jda/api/utils/ImageFormat.java
index 229d4fa8f2..d55830ee09 100644
--- a/src/main/java/net/dv8tion/jda/api/utils/ImageFormat.java
+++ b/src/main/java/net/dv8tion/jda/api/utils/ImageFormat.java
@@ -82,6 +82,11 @@ public final class ImageFormat {
*/
public static final ImageFormat ANIMATED_WEBP = new ImageFormat("webp", Arrays.asList("animated", "true"));
+ /**
+ * Audiovisual media container, which supports multiple video and audio codecs.
+ */
+ public static final ImageFormat WEBM = new ImageFormat("webm", Collections.emptyList());
+
private final String extension;
private final List queryParameters;
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractEntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractEntityBuilder.java
index 3c82b28184..b8dbcfa1e2 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractEntityBuilder.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractEntityBuilder.java
@@ -195,6 +195,7 @@ protected void configureGroupChannel(DataObject json, GroupChannelMixin> chann
protected void configureMember(DataObject memberJson, MemberMixin> member) {
member.setNickname(memberJson.getString("nick", null));
member.setAvatarId(memberJson.getString("avatar", null));
+ member.setCollectibles(CollectiblesImpl.extractFrom(memberJson));
if (!memberJson.isNull("flags")) {
member.setFlags(memberJson.getInt("flags"));
}
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/CollectiblesImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/CollectiblesImpl.java
new file mode 100644
index 0000000000..4e424db0d2
--- /dev/null
+++ b/src/main/java/net/dv8tion/jda/internal/entities/CollectiblesImpl.java
@@ -0,0 +1,152 @@
+/*
+ * 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.Collectibles;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.utils.data.DataObject;
+import net.dv8tion.jda.internal.utils.EntityString;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public class CollectiblesImpl implements Collectibles {
+ public static final Collectibles EMPTY = new CollectiblesImpl(DataObject.empty());
+
+ private final NameplateImpl nameplate;
+
+ private CollectiblesImpl(DataObject o) {
+ this.nameplate = o.optObject("nameplate").map(NameplateImpl::new).orElse(null);
+ }
+
+ @Nonnull
+ public static Collectibles extractFrom(@Nonnull DataObject o) {
+ if (o.isNull("collectibles")) {
+ return EMPTY;
+ }
+ DataObject object = o.getObject("collectibles");
+ // Avoid allocations if there are no collectibles
+ if (object.isNull("nameplate")) {
+ return EMPTY;
+ }
+ return new CollectiblesImpl(object);
+ }
+
+ @Nullable
+ @Override
+ public NameplateImpl getNameplate() {
+ return nameplate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CollectiblesImpl)) {
+ return false;
+ }
+
+ CollectiblesImpl that = (CollectiblesImpl) o;
+ return Objects.equals(nameplate, that.nameplate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(nameplate);
+ }
+
+ @Override
+ public String toString() {
+ return new EntityString(this).addMetadata("nameplate", nameplate).toString();
+ }
+
+ public static class NameplateImpl implements Nameplate {
+ private final String sku, asset, palette;
+
+ public NameplateImpl(DataObject o) {
+ this.sku = o.getString("sku_id");
+ this.asset = o.getString("asset");
+ this.palette = o.getString("palette");
+ }
+
+ @Nonnull
+ @Override
+ public String getSkuId() {
+ return sku;
+ }
+
+ @Nonnull
+ @Override
+ public String getAssetPath() {
+ return asset;
+ }
+
+ @Nonnull
+ @Override
+ public String getPalette() {
+ return palette;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof NameplateImpl)) {
+ return false;
+ }
+
+ NameplateImpl nameplate = (NameplateImpl) o;
+ return Objects.equals(sku, nameplate.sku)
+ && Objects.equals(asset, nameplate.asset)
+ && Objects.equals(palette, nameplate.palette);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sku, asset, palette);
+ }
+
+ @Override
+ public String toString() {
+ return new EntityString(this)
+ .addMetadata("asset_path", asset)
+ .addMetadata("palette", palette)
+ .toString();
+ }
+ }
+
+ public static class Effective implements Collectibles {
+ private final Member member;
+
+ public Effective(Member member) {
+ this.member = member;
+ }
+
+ @Nullable
+ @Override
+ public Nameplate getNameplate() {
+ Nameplate memberNameplate = member.getCollectibles().getNameplate();
+ return memberNameplate != null
+ ? memberNameplate
+ : member.getUser().getCollectibles().getNameplate();
+ }
+ }
+}
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 18871593c4..ba78197b52 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java
@@ -148,6 +148,7 @@ public SelfUser createSelfUser(DataObject self) {
.setGlobalName(self.getString("global_name", null))
.setDiscriminator(Short.parseShort(self.getString("discriminator", "0")))
.setAvatarId(self.getString("avatar", null))
+ .setCollectibles(CollectiblesImpl.EMPTY)
.setBot(self.getBoolean("bot"))
.setSystem(false);
@@ -510,6 +511,7 @@ public UserImpl createUser(DataObject user) {
.setGlobalName(user.getString("global_name", null))
.setDiscriminator(Short.parseShort(user.getString("discriminator", "0")))
.setAvatarId(user.getString("avatar", null))
+ .setCollectibles(CollectiblesImpl.extractFrom(user))
.setBot(user.getBoolean("bot"))
.setSystem(user.getBoolean("system"))
.setFlags(user.getInt("public_flags", 0))
@@ -532,6 +534,8 @@ public void updateUser(UserImpl userObj, DataObject user) {
short newDiscriminator = Short.parseShort(user.getString("discriminator", "0"));
String oldAvatar = userObj.getAvatarId();
String newAvatar = user.getString("avatar", null);
+ Collectibles oldCollectibles = userObj.getCollectibles();
+ Collectibles newCollectibles = CollectiblesImpl.extractFrom(user);
int oldFlags = userObj.getFlagsRaw();
int newFlags = user.getInt("public_flags", 0);
User.PrimaryGuild oldPrimaryGuild = userObj.getPrimaryGuild();
@@ -569,6 +573,13 @@ public void updateUser(UserImpl userObj, DataObject user) {
userObj, oldAvatar));
}
+ if (!Objects.equals(oldCollectibles, newCollectibles)) {
+ userObj.setCollectibles(newCollectibles);
+ jda.handleEvent(new UserUpdateCollectiblesEvent(
+ jda, responseNumber,
+ userObj, oldCollectibles));
+ }
+
if (oldFlags != newFlags) {
userObj.setFlags(newFlags);
jda.handleEvent(new UserUpdateFlagsEvent(jda, responseNumber, userObj, User.UserFlag.getFlags(oldFlags)));
@@ -745,6 +756,15 @@ public void updateMember(GuildImpl guild, MemberImpl member, DataObject content,
getJDA().handleEvent(new GuildMemberUpdateAvatarEvent(getJDA(), responseNumber, member, oldAvatarId));
}
}
+ if (content.hasKey("collectibles")) {
+ Collectibles oldCollectibles = member.getCollectibles();
+ Collectibles newCollectibles = CollectiblesImpl.extractFrom(content);
+ if (!Objects.equals(oldCollectibles, newCollectibles)) {
+ member.setCollectibles(newCollectibles);
+ getJDA().handleEvent(new GuildMemberUpdateCollectiblesEvent(
+ getJDA(), responseNumber, member, oldCollectibles));
+ }
+ }
if (content.hasKey("premium_since")) {
long epoch = 0;
if (!content.isNull("premium_since")) {
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java
index 70b4c2252a..3ea608b202 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java
@@ -52,6 +52,7 @@ public class MemberImpl implements Member, MemberMixin {
private User user;
private String nickname;
private String avatarId;
+ private Collectibles collectibles;
private long joinDate, boostDate, timeOutEnd;
private boolean pending = false;
private int flags;
@@ -183,6 +184,12 @@ public String getAvatarId() {
return avatarId;
}
+ @Nonnull
+ @Override
+ public Collectibles getCollectibles() {
+ return collectibles;
+ }
+
@Nonnull
@Override
public String getEffectiveName() {
@@ -366,6 +373,12 @@ public MemberImpl setAvatarId(String avatarId) {
return this;
}
+ @Override
+ public MemberImpl setCollectibles(Collectibles collectibles) {
+ this.collectibles = collectibles;
+ return this;
+ }
+
@Override
public MemberImpl setJoinDate(long joinDate) {
this.joinDate = joinDate;
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/SelfUserImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/SelfUserImpl.java
index b065a6115c..c929c70d19 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/SelfUserImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/SelfUserImpl.java
@@ -100,6 +100,7 @@ public static SelfUserImpl copyOf(SelfUserImpl other, JDAImpl jda) {
selfUser.setName(other.name)
.setGlobalName(other.globalName)
.setAvatarId(other.avatarId)
+ .setCollectibles(other.collectibles)
.setDiscriminator(other.getDiscriminatorInt())
.setBot(other.bot);
return selfUser.setVerified(other.verified)
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
index 2e7e7c8eda..f876e2b6f7 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
@@ -16,6 +16,7 @@
package net.dv8tion.jda.internal.entities;
+import net.dv8tion.jda.api.entities.Collectibles;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
@@ -45,6 +46,7 @@ public class UserImpl extends UserSnowflakeImpl implements User {
protected String name;
protected String globalName;
protected String avatarId;
+ protected Collectibles collectibles;
protected Profile profile;
protected long privateChannelId = 0L;
protected boolean bot;
@@ -81,6 +83,12 @@ public String getAvatarId() {
return avatarId;
}
+ @Nonnull
+ @Override
+ public Collectibles getCollectibles() {
+ return collectibles;
+ }
+
@Nonnull
@Override
public CacheRestAction retrieveProfile() {
@@ -207,6 +215,11 @@ public UserImpl setAvatarId(String avatarId) {
return this;
}
+ public UserImpl setCollectibles(Collectibles collectibles) {
+ this.collectibles = collectibles;
+ return this;
+ }
+
public UserImpl setProfile(Profile profile) {
this.profile = profile;
return this;
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/detached/DetachedMemberImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/detached/DetachedMemberImpl.java
index 329e7b3a91..015825f354 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/detached/DetachedMemberImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/detached/DetachedMemberImpl.java
@@ -49,6 +49,7 @@ public class DetachedMemberImpl implements Member, MemberMixin> extends Member, IDetachab
T setAvatarId(String avatarId);
+ T setCollectibles(Collectibles collectibles);
+
T setJoinDate(long joinDate);
T setBoostDate(long boostDate);
diff --git a/src/test/java/net/dv8tion/jda/test/util/DiscordAssetsTest.java b/src/test/java/net/dv8tion/jda/test/util/DiscordAssetsTest.java
index bf48ad4864..f2c444d794 100644
--- a/src/test/java/net/dv8tion/jda/test/util/DiscordAssetsTest.java
+++ b/src/test/java/net/dv8tion/jda/test/util/DiscordAssetsTest.java
@@ -31,6 +31,7 @@ public class DiscordAssetsTest extends AbstractSnapshotTest {
private static final ImageFormat EXAMPLE_FORMAT = ImageFormat.ANIMATED_WEBP;
private static final String EXAMPLE_SNOWFLAKE = "222046562543468545";
private static final String EXAMPLE_HASH = "86185a18d168f88b91c";
+ private static final String EXAMPLE_NAMEPLATE_ASSET = "nameplates/nameplates_v3/touch_grass/";
@Test
void testDiscordAssetsOutputIsChecked() {
@@ -64,6 +65,12 @@ void testDiscordAssetsOutputIsChecked() {
"memberAvatar",
memberAvatar(EXAMPLE_FORMAT, EXAMPLE_SNOWFLAKE, EXAMPLE_SNOWFLAKE, EXAMPLE_HASH)
.getUrl());
+ data.put(
+ "staticNameplate",
+ staticNameplate(EXAMPLE_FORMAT, EXAMPLE_NAMEPLATE_ASSET).getUrl());
+ data.put(
+ "animatedNameplate",
+ animatedNameplate(EXAMPLE_FORMAT, EXAMPLE_NAMEPLATE_ASSET).getUrl());
data.put(
"roleIcon",
roleIcon(EXAMPLE_FORMAT, EXAMPLE_SNOWFLAKE, EXAMPLE_HASH).getUrl());
diff --git a/src/test/resources/net/dv8tion/jda/test/util/DiscordAssetsTest/testDiscordAssetsOutputIsChecked.json b/src/test/resources/net/dv8tion/jda/test/util/DiscordAssetsTest/testDiscordAssetsOutputIsChecked.json
index bd64975eac..a0394993fa 100644
--- a/src/test/resources/net/dv8tion/jda/test/util/DiscordAssetsTest/testDiscordAssetsOutputIsChecked.json
+++ b/src/test/resources/net/dv8tion/jda/test/util/DiscordAssetsTest/testDiscordAssetsOutputIsChecked.json
@@ -1,4 +1,5 @@
{
+ "animatedNameplate" : "https://cdn.discordapp.com/assets/collectibles/nameplates/nameplates_v3/touch_grass/asset.webp?animated=true",
"applicationCover" : "https://cdn.discordapp.com/application/222046562543468545/86185a18d168f88b91c.webp?animated=true",
"applicationIcon" : "https://cdn.discordapp.com/app-icons/222046562543468545/86185a18d168f88b91c.webp?animated=true",
"applicationTeamIcon" : "https://cdn.discordapp.com/team-icons/222046562543468545/86185a18d168f88b91c.webp?animated=true",
@@ -10,6 +11,7 @@
"memberAvatar" : "https://cdn.discordapp.com/guilds/222046562543468545/users/222046562543468545/avatars/86185a18d168f88b91c.webp?animated=true",
"roleIcon" : "https://cdn.discordapp.com/role-icons/222046562543468545/86185a18d168f88b91c.webp?animated=true",
"scheduledEventCoverImage" : "https://cdn.discordapp.com/guild-events/222046562543468545/86185a18d168f88b91c.webp?animated=true",
+ "staticNameplate" : "https://cdn.discordapp.com/assets/collectibles/nameplates/nameplates_v3/touch_grass/static.webp?animated=true",
"stickerPackBanner" : "https://cdn.discordapp.com/app-assets/710982414301790216/store/86185a18d168f88b91c.webp?animated=true",
"userAvatar" : "https://cdn.discordapp.com/avatars/222046562543468545/86185a18d168f88b91c.webp?animated=true",
"userBanner" : "https://cdn.discordapp.com/banners/222046562543468545/86185a18d168f88b91c.webp?animated=true",