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: + *

    + *
  1. On the member
  2. + *
  3. On the user
  4. + *
  5. {@code null}
  6. + *
+ * + * @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 + *

+ * + * @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 + *

+ * + * @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",