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 2 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(kord, data)

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

import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.core.cache.data.EmojiData
import dev.kord.core.cache.data.MemberData
import dev.kord.core.cache.data.UserData
import dev.kord.rest.Image

sealed class Icon(override val kord: Kord, val animated: Boolean, private val rawAssetUri: String) : KordObject {

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

val url: String
get() = "$rawAssetUri.${format.extension}"

fun getUrl(format: Image.Format): String? {
if (format == Image.Format.GIF && !animated) return null
return "$rawAssetUri.${format.extension}"
}

fun getUrl(size: Image.Size): String {
return "$url?size=${size.maxRes}"
}

fun getUrl(format: Image.Format, size: Image.Size): String? {
if (format == Image.Format.GIF && !animated) return null
return "$rawAssetUri.${format.extension}?size=${size.maxRes}"
}

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

suspend fun getImage(size: Image.Size): Image = Image.fromUrl(kord.resources.httpClient, getUrl(size))

suspend fun getImage(format: Image.Format): Image? =
getUrl(format)?.let { Image.fromUrl(kord.resources.httpClient, it) }

suspend fun getImage(format: Image.Format, size: Image.Size): Image? =
getUrl(format, size)?.let { Image.fromUrl(kord.resources.httpClient, it) }

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

class EmojiIcon(kord: Kord, data: EmojiData) : Icon(
kord,
data.animated.discordBoolean,
"$CDN_BASE_URL/emojis/${data.id.asString}"
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved
)

class DefaultUserAvatar(kord: Kord, data: UserData) :
Icon(kord, false, "$CDN_BASE_URL/embed/avatars/${data.discriminator.toInt() % 5}")

class UserAvatar(kord: Kord, data: UserData) :
Icon(
kord,
data.avatar!!.startsWith("a_"),
"$CDN_BASE_URL/avatars/${data.id.asString}/${data.avatar}"
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved
)

class MemberAvatar(kord: Kord, data: MemberData) :
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved
Icon(
kord,
data.avatar.value!!.startsWith("a_"),
"$CDN_BASE_URL/guilds/${data.guildId.asString}/users/${data.userId.asString}/avatars/${data.avatar.value!!}"
)

companion object {
private const val CDN_BASE_URL = "https://cdn.discordapp.com"
}
}
9 changes: 9 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,15 @@ class Member(
*/
val displayName: String get() = nickname ?: username

/**
* The members guild avatar as [Icon] object
*/
val memberAvatar: Icon?
get() = when {
memberData.avatar.value != null -> Icon.MemberAvatar(kord, memberData)
else -> null
}

/**
* When the user joined this [guild].
*/
Expand Down
111 changes: 8 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,14 @@ 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() = when {
data.avatar != null -> Icon.UserAvatar(kord, data)
ByteAlex marked this conversation as resolved.
Show resolved Hide resolved
else -> Icon.DefaultUserAvatar(kord, data)
}

/**
* The username of this user.
Expand Down Expand Up @@ -97,105 +103,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