Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Member Avatars & Avatar->Icon refactor #401

Merged
merged 7 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = [email protected]
}

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
35 changes: 35 additions & 0 deletions rest/src/main/kotlin/route/DiscordCDN.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.kord.rest.route

import dev.kord.common.entity.Snowflake
import dev.kord.rest.Image

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")

}

class CDNUrl(private val rawAssetUri: String) {

fun toUrl(builder: UrlBuilder.() -> Unit = {}): String {
val config = UrlBuilder().apply(builder)
var cdnUrl = "$rawAssetUri.${config.format.extension}"
config.size?.let { cdnUrl += "?size=${it.maxRes}" }
return cdnUrl
}
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved

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

data class UrlBuilder(var format: Image.Format = Image.Format.WEBP, var size: Image.Size? = null)
}
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved