Skip to content

Commit

Permalink
Member Avatars & Avatar->Icon refactor (#401)
Browse files Browse the repository at this point in the history
* Member Avatars & Avatar->Icon refactor

* Default to webp instead

* Make user avatar optional, add defaultAvatar

* Move CDN urls to DiscordCDN

* Stuff

* Naming

* This naming fits better imo
  • Loading branch information
ByteAlex authored Oct 1, 2021
1 parent 28fab1a commit f7c5543
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 113 deletions.
12 changes: 8 additions & 4 deletions common/src/main/kotlin/entity/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ data class DiscordGuildMember(
val premiumSince: Optional<String?> = Optional.Missing(),
val deaf: OptionalBoolean = OptionalBoolean.Missing,
val mute: OptionalBoolean = OptionalBoolean.Missing,
val pending: OptionalBoolean = OptionalBoolean.Missing
val pending: OptionalBoolean = OptionalBoolean.Missing,
val avatar: Optional<String?> = Optional.Missing(),
)


Expand All @@ -39,7 +40,8 @@ data class DiscordInteractionGuildMember(
@SerialName("premium_since")
val premiumSince: Optional<String?> = Optional.Missing(),
val permissions: Permissions,
val pending: OptionalBoolean = OptionalBoolean.Missing
val pending: OptionalBoolean = OptionalBoolean.Missing,
val avatar: Optional<String?> = Optional.Missing(),
)


Expand All @@ -60,7 +62,8 @@ data class DiscordAddedGuildMember(
val mute: Boolean,
@SerialName("guild_id")
val guildId: Snowflake,
val pending: OptionalBoolean = OptionalBoolean.Missing
val pending: OptionalBoolean = OptionalBoolean.Missing,
val avatar: Optional<String?> = Optional.Missing(),
)

@Serializable
Expand All @@ -81,7 +84,8 @@ data class DiscordUpdatedGuildMember(
val joinedAt: String,
@SerialName("premium_since")
val premiumSince: Optional<String?> = Optional.Missing(),
val pending: OptionalBoolean = OptionalBoolean.Missing
val pending: OptionalBoolean = OptionalBoolean.Missing,
val avatar: Optional<String?> = Optional.Missing(),
)

@Serializable
Expand Down
11 changes: 6 additions & 5 deletions core/src/main/kotlin/cache/data/MemberData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,28 @@ data class MemberData(
val roles: List<Snowflake>,
val joinedAt: String,
val premiumSince: Optional<String?> = Optional.Missing(),
val pending: OptionalBoolean = OptionalBoolean.Missing
val pending: OptionalBoolean = OptionalBoolean.Missing,
val avatar: Optional<String?> = Optional.Missing()
) {

companion object {
val description = description(MemberData::id)

fun from(userId: Snowflake, guildId: Snowflake, entity: DiscordGuildMember) = with(entity) {
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince)
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince, avatar = avatar)
}


fun from(userId: Snowflake, guildId: Snowflake, entity: DiscordInteractionGuildMember) = with(entity) {
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince)
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince, avatar = avatar)
}

fun from(userId: Snowflake, entity: DiscordAddedGuildMember) = with(entity) {
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince)
MemberData(userId = userId, guildId = guildId, nick, roles, joinedAt, premiumSince, avatar = avatar)
}

fun from(entity: DiscordUpdatedGuildMember) = with(entity) {
MemberData(userId = user.id, guildId = guildId, nick, roles, joinedAt, premiumSince, pending)
MemberData(userId = user.id, guildId = guildId, nick, roles, joinedAt, premiumSince, pending, avatar = avatar)
}

}
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/kotlin/entity/GuildEmoji.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ class GuildEmoji(
*/
val user: UserBehavior? get() = userId?.let { UserBehavior(it, kord) }

/**
* The image as [Icon] object for the emoji
*/
val image: Icon get() = Icon.EmojiIcon(data.animated.discordBoolean, data.id, kord)

/**
* Requests to delete this emoji, with the given [reason].
*
Expand Down
54 changes: 54 additions & 0 deletions core/src/main/kotlin/entity/Icon.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.kord.core.entity

import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.rest.Image
import dev.kord.rest.route.CdnUrl
import dev.kord.rest.route.DiscordCdn

sealed class Icon(val animated: Boolean, val cdnUrl: CdnUrl, override val kord: Kord) : KordObject {

val format: Image.Format
get() = when {
animated -> Image.Format.GIF
else -> Image.Format.WEBP
}

val url: String
get() = cdnUrl.toUrl {
this.format = this@Icon.format
}

suspend fun getImage(): Image = Image.fromUrl(kord.resources.httpClient, cdnUrl.toUrl())

suspend fun getImage(size: Image.Size): Image =
Image.fromUrl(kord.resources.httpClient, cdnUrl.toUrl {
this.size = size
})

suspend fun getImage(format: Image.Format): Image =
Image.fromUrl(kord.resources.httpClient, cdnUrl.toUrl {
this.format = format
})

suspend fun getImage(format: Image.Format, size: Image.Size): Image =
Image.fromUrl(kord.resources.httpClient, cdnUrl.toUrl {
this.format = format
this.size = size
})

override fun toString(): String {
return "Icon(type=${javaClass.name},animated=$animated,cdnUrl=$cdnUrl,kord=$kord)"
}

class EmojiIcon(animated: Boolean, emojiId: Snowflake, kord: Kord) : Icon(animated, DiscordCdn.emoji(emojiId), kord)

class DefaultUserAvatar(discriminator: Int, kord: Kord) : Icon(false, DiscordCdn.defaultAvatar(discriminator), kord)

class UserAvatar(userId: Snowflake, avatarHash: String, kord: Kord) :
Icon(avatarHash.startsWith("a_"), DiscordCdn.userAvatar(userId, avatarHash), kord)

class MemberAvatar(guildId: Snowflake, userId: Snowflake, avatarHash: String, kord: Kord) :
Icon(avatarHash.startsWith("a_"), DiscordCdn.memberAvatar(guildId, userId, avatarHash), kord)
}
6 changes: 6 additions & 0 deletions core/src/main/kotlin/entity/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class Member(
*/
val displayName: String get() = nickname ?: username

/**
* The members guild avatar as [Icon] object
*/
val memberAvatar: Icon?
get() = memberData.avatar.value?.let { Icon.MemberAvatar(memberData.guildId, data.id, it, kord) }

/**
* When the user joined this [guild].
*/
Expand Down
110 changes: 7 additions & 103 deletions core/src/main/kotlin/entity/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.UserFlags
import dev.kord.common.entity.UserPremium
import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.cache.data.UserData
import dev.kord.core.supplier.EntitySupplier
Expand All @@ -24,7 +23,13 @@ open class User(
override val id: Snowflake
get() = data.id

val avatar: Avatar get() = Avatar(data, kord)
/**
* The users avatar as [Icon] object
*/
val avatar: Icon?
get() = data.avatar?.let { Icon.UserAvatar(data.id, it, kord) }

val defaultAvatar: Icon get() = Icon.DefaultUserAvatar(data.discriminator.toInt(), kord)

/**
* The username of this user.
Expand Down Expand Up @@ -97,105 +102,4 @@ open class User(
return "User(data=$data, kord=$kord, supplier=$supplier)"
}

data class Avatar(val data: UserData, override val kord: Kord) : KordObject {

/**
* The default avatar url for this user. Discord uses this for users who don't have a custom avatar set.
*/
val defaultUrl: String get() = "https://cdn.discordapp.com/embed/avatars/${data.discriminator.toInt() % 5}.png"

/**
* Whether the user has set their avatar.
*/
val isCustom: Boolean get() = data.avatar != null

/**
* Whether the user has an animated avatar.
*/
val isAnimated: Boolean get() = data.avatar?.startsWith("a_") ?: false

/**
* A supported format, prioritizing [Image.Format.GIF] for animated avatars and [Image.Format.PNG] for others.
*/
val supportedFormat: Image.Format
get() = when {
isAnimated -> Image.Format.GIF
else -> Image.Format.PNG
}

/**
* Gets the avatar url in a supported format (defined by [supportedFormat]) and default size.
*/
val url: String
get() = getUrl(supportedFormat) ?: defaultUrl

/**
* Gets the avatar url in given [format], or returns null if the [format] is not supported.
*/
fun getUrl(format: Image.Format): String? {
val hash = data.avatar ?: return defaultUrl
if (!isAnimated && format == Image.Format.GIF) return null

return "https://cdn.discordapp.com/avatars/${data.id.value}/$hash.${format.extension}"
}

/**
* Gets the avatar url in a supported format and given [size].
*/
fun getUrl(size: Image.Size): String {
return getUrl(supportedFormat, size)!!
}

/**
* Gets the avatar url in given [format] and [size], or returns null if the [format] is not supported.
*/
fun getUrl(format: Image.Format, size: Image.Size): String? {
val hash = data.avatar ?: return defaultUrl
if (!isAnimated && format == Image.Format.GIF) return null

return "https://cdn.discordapp.com/avatars/${data.id.value}/$hash.${format.extension}?size=${size.maxRes}"
}

/**
* Requests to get the [defaultUrl] as an [Image].
*/
suspend fun getDefaultImage(): Image = Image.fromUrl(kord.resources.httpClient, defaultUrl)

/**
* Requests to get the avatar of the user as an [Image], prioritizing gif for animated avatars and png for others.
*/
suspend fun getImage(): Image = Image.fromUrl(kord.resources.httpClient, url)

/**
* Requests to get the avatar of the user as an [Image] given [format], or returns null if the format is not supported.
*/
suspend fun getImage(format: Image.Format): Image? {
val url = getUrl(format) ?: return null

return Image.fromUrl(kord.resources.httpClient, url)
}

/**
* Requests to get the avatar of the user as an [Image] in given [size].
*/
suspend fun getImage(size: Image.Size): Image {
return Image.fromUrl(kord.resources.httpClient, getUrl(size))
}

/**
* Requests to get the avatar of the user as an [Image] given [format] and [size], or returns null if the
* [format] is not supported.
*/
suspend fun getImage(format: Image.Format, size: Image.Size): Image? {
val url = getUrl(format, size) ?: return null

return Image.fromUrl(kord.resources.httpClient, url)
}

override fun toString(): String {
return "Avatar(data=$data, kord=$kord)"
}

}

}
3 changes: 2 additions & 1 deletion core/src/test/kotlin/live/LiveMemberTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class LiveMemberTest : AbstractLiveEntityTest<LiveMember>() {
guildId = guildId,
roles = emptyList(),
joinedAt = "",
premiumSince = Optional.Missing()
premiumSince = Optional.Missing(),
avatar = Optional.Missing(),
),
userData = UserData(
id = userId,
Expand Down
27 changes: 27 additions & 0 deletions rest/src/main/kotlin/route/CdnUrl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.kord.rest.route

import dev.kord.rest.Image

class CdnUrl(private val rawAssetUri: String) {

fun toUrl(): String {
return toUrl(UrlFormatBuilder())
}

inline fun toUrl(format: UrlFormatBuilder.() -> Unit): String {
val config = UrlFormatBuilder().apply(format)
return toUrl(config)
}

fun toUrl(config: UrlFormatBuilder): String {
val urlBuilder = StringBuilder(rawAssetUri).append(".").append(config.format.extension)
config.size?.let { urlBuilder.append("?size=").append(it.maxRes) }
return urlBuilder.toString()
}

override fun toString(): String {
return "CdnUrl(rawAssetUri=$rawAssetUri)"
}

data class UrlFormatBuilder(var format: Image.Format = Image.Format.WEBP, var size: Image.Size? = null)
}
18 changes: 18 additions & 0 deletions rest/src/main/kotlin/route/DiscordCdn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.kord.rest.route

import dev.kord.common.entity.Snowflake

object DiscordCdn {

private const val BASE_URL = "https://cdn.discordapp.com"

fun emoji(emojiId: Snowflake): CdnUrl = CdnUrl("$BASE_URL/emojis/${emojiId.asString}")

fun defaultAvatar(discriminator: Int): CdnUrl = CdnUrl("$BASE_URL/embed/avatars/${discriminator % 5}")

fun userAvatar(userId: Snowflake, hash: String): CdnUrl = CdnUrl("$BASE_URL/avatars/${userId.asString}/$hash")

fun memberAvatar(guildId: Snowflake, userId: Snowflake, hash: String) =
CdnUrl("$BASE_URL/guilds/${guildId.asString}/users/${userId.asString}/avatars/$hash")

}

0 comments on commit f7c5543

Please sign in to comment.