Skip to content

Commit

Permalink
Implement avatar decorations.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nihlus committed Jul 14, 2023
1 parent 3f88783 commit 8f8d27d
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@ public interface IPartialUser

/// <inheritdoc cref="IUser.PublicFlags" />
Optional<UserFlags> PublicFlags { get; }

/// <inheritdoc cref="IUser.AvatarDecoration" />
Optional<IImageHash?> AvatarDecoration { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public interface IUser : IPartialUser
/// </summary>
new Optional<UserFlags> PublicFlags { get; }

/// <summary>
/// Gets the user's avatar decoration.
/// </summary>
new Optional<IImageHash?> AvatarDecoration { get; }

/// <inheritdoc/>
Optional<Snowflake> IPartialUser.ID => this.ID;

Expand Down Expand Up @@ -165,4 +170,7 @@ public interface IUser : IPartialUser

/// <inheritdoc/>
Optional<UserFlags> IPartialUser.PublicFlags => this.PublicFlags;

/// <inheritdoc/>
Optional<IImageHash?> IPartialUser.AvatarDecoration => this.AvatarDecoration;
}
76 changes: 76 additions & 0 deletions Backend/Remora.Discord.API/API/CDN.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,82 @@ public static Result<Uri> GetGuildMemberAvatarUrl
return ub.Uri;
}

/// <summary>
/// Gets the CDN URI of the given user's avatar decoration.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="imageFormat">The requested image format.</param>
/// <param name="imageSize">The requested image size. May be any power of two between 16 and 4096.</param>
/// <returns>A result which may or may not have succeeded.</returns>
public static Result<Uri> GetUserAvatarDecorationUrl
(
IUser user,
Optional<CDNImageFormat> imageFormat = default,
Optional<ushort> imageSize = default
)
{
if (!user.AvatarDecoration.IsDefined(out var avatarDecoration))
{
return new ImageNotFoundError();
}

// Prefer the animated version, if available
if (avatarDecoration.HasGif && !imageFormat.HasValue)
{
imageFormat = CDNImageFormat.GIF;
}

return GetUserAvatarDecorationUrl(user.ID, avatarDecoration, imageFormat, imageSize);
}

/// <summary>
/// Gets the CDN URI of the given user's avatar decoration.
/// </summary>
/// <param name="userID">The ID of the user.</param>
/// <param name="avatarDecorationHash">The image hash of the user's avatar decoration.</param>
/// <param name="imageFormat">The requested image format.</param>
/// <param name="imageSize">The requested image size. May be any power of two between 16 and 4096.</param>
/// <returns>A result which may or may not have succeeded.</returns>
public static Result<Uri> GetUserAvatarDecorationUrl
(
Snowflake userID,
IImageHash avatarDecorationHash,
Optional<CDNImageFormat> imageFormat = default,
Optional<ushort> imageSize = default
)
{
var formatValidation = ValidateOrDefaultImageFormat
(
imageFormat,
CDNImageFormat.PNG
);

if (!formatValidation.IsSuccess)
{
return Result<Uri>.FromError(formatValidation);
}

var format = formatValidation.Entity;

var checkImageSize = CheckImageSize(imageSize);
if (!checkImageSize.IsSuccess)
{
return Result<Uri>.FromError(checkImageSize);
}

var ub = new UriBuilder(Constants.CDNBaseURL)
{
Path = $"avatar-decorations/{userID}/{avatarDecorationHash.Value}.{format.ToFileExtension()}"
};

if (imageSize.TryGet(out var size))
{
ub.Query = $"size={size}";
}

return ub.Uri;
}

/// <summary>
/// Gets the CDN URI of the given application's icon.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ public record UserUpdate
Optional<string?> Email = default,
Optional<UserFlags> Flags = default,
Optional<PremiumType> PremiumType = default,
Optional<UserFlags> PublicFlags = default
Optional<UserFlags> PublicFlags = default,
Optional<IImageHash?> AvatarDecoration = default
) : IUserUpdate;
3 changes: 2 additions & 1 deletion Backend/Remora.Discord.API/API/Objects/Users/PartialUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ public record PartialUser
Optional<string?> Email = default,
Optional<UserFlags> Flags = default,
Optional<PremiumType> PremiumType = default,
Optional<UserFlags> PublicFlags = default
Optional<UserFlags> PublicFlags = default,
Optional<IImageHash?> AvatarDecoration = default
) : IPartialUser;
3 changes: 2 additions & 1 deletion Backend/Remora.Discord.API/API/Objects/Users/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ public record User
Optional<string?> Email = default,
Optional<UserFlags> Flags = default,
Optional<PremiumType> PremiumType = default,
Optional<UserFlags> PublicFlags = default
Optional<UserFlags> PublicFlags = default,
Optional<IImageHash?> AvatarDecoration = default
) : IUser;
23 changes: 3 additions & 20 deletions Backend/Remora.Discord.API/API/Objects/Users/UserMention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,6 @@ public record UserMention
Optional<UserFlags> Flags = default,
Optional<PremiumType> PremiumType = default,
Optional<UserFlags> PublicFlags = default,
Optional<IPartialGuildMember> Member = default
) : User
(
ID,
Username,
Discriminator,
GlobalName,
Avatar,
IsBot,
IsSystem,
IsMFAEnabled,
Banner,
AccentColour,
Locale,
IsVerified,
Email,
Flags,
PremiumType,
PublicFlags
), IUserMention;
Optional<IPartialGuildMember> Member = default,
Optional<IImageHash?> AvatarDecoration = default
) : IUserMention;
78 changes: 78 additions & 0 deletions Tests/Remora.Discord.API.Tests/API/CDNTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,84 @@ protected override IEnumerable<Result<Uri>> GetImageUris
}
}

/// <summary>
/// Tests the <see cref="CDN.GetUserAvatarDecorationUrl(IUser, Optional{CDNImageFormat}, Optional{ushort})"/> method and
/// its overloads.
/// </summary>
public class GetUserAvatarDecorationUrl : CDNTestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="GetUserAvatarDecorationUrl"/> class.
/// </summary>
public GetUserAvatarDecorationUrl()
: base
(
new Uri("https://cdn.discordapp.com/avatar-decorations/0/1"),
new[] { CDNImageFormat.PNG }
)
{
}

/// <summary>
/// Tests whether the correct address is returned when the instance has no image set.
/// </summary>
[Fact]
public void ReturnsUnsuccessfulResultIfInstanceHasNoImage()
{
var userID = DiscordSnowflake.New(0);

var mockedUser = new Mock<IUser>();
mockedUser.SetupGet(g => g.AvatarDecoration).Returns(default(Optional<IImageHash?>));
mockedUser.SetupGet(g => g.ID).Returns(userID);

var user = mockedUser.Object;

var getActual = CDN.GetUserAvatarDecorationUrl(user, CDNImageFormat.PNG);

Assert.False(getActual.IsSuccess);
Assert.IsType<ImageNotFoundError>(getActual.Error);
}

/// <summary>
/// Tests whether the correct address is returned when the instance has an image set to null.
/// </summary>
[Fact]
public void ReturnsUnsuccessfulResultIfInstanceHasNullImage()
{
var userID = DiscordSnowflake.New(0);

var mockedUser = new Mock<IUser>();
mockedUser.SetupGet(g => g.AvatarDecoration).Returns(new Optional<IImageHash?>(null));
mockedUser.SetupGet(g => g.ID).Returns(userID);

var user = mockedUser.Object;

var getActual = CDN.GetUserAvatarDecorationUrl(user, CDNImageFormat.PNG);

Assert.False(getActual.IsSuccess);
Assert.IsType<ImageNotFoundError>(getActual.Error);
}

/// <inheritdoc />
protected override IEnumerable<Result<Uri>> GetImageUris
(
Optional<CDNImageFormat> imageFormat = default,
Optional<ushort> imageSize = default
)
{
var userID = DiscordSnowflake.New(0);
var imageHash = new ImageHash("1");

var mockedUser = new Mock<IUser>();
mockedUser.SetupGet(g => g.AvatarDecoration).Returns(imageHash);
mockedUser.SetupGet(g => g.ID).Returns(userID);

var user = mockedUser.Object;
yield return CDN.GetUserAvatarDecorationUrl(user, imageFormat, imageSize);
yield return CDN.GetUserAvatarDecorationUrl(userID, imageHash, imageFormat, imageSize);
}
}

/// <summary>
/// Tests the <see cref="CDN.GetApplicationIconUrl(IApplication, Optional{CDNImageFormat}, Optional{ushort})"/>
/// method and its overloads.
Expand Down
3 changes: 2 additions & 1 deletion Tests/Remora.Discord.Tests/Samples/Objects/USER/USER.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"email": "[email protected]",
"flags": 1,
"premium_type": 1,
"public_flags": 1
"public_flags": 1,
"avatar_decoration": "68b329da9893e34099c7d8ad5cb9c940"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"email": null,
"flags": 1,
"premium_type": 1,
"public_flags": 1
"public_flags": 1,
"avatar_decoration": null
}

0 comments on commit 8f8d27d

Please sign in to comment.