Skip to content

Commit

Permalink
add support for xenos v0.2.0 (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
scrayos authored Mar 22, 2024
1 parent f1e5fad commit 2a6ee77
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 56 deletions.
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ ktlint = "1.1.1"
junit = "5.10.2"
kotest-core = "5.8.1"
kotest-testcontainers = "2.0.2"
protobuf = "3.25.3"
protobuf = "4.26.0"
protoc-java = "1.62.2"
protoc-kotlin = "1.4.1"
kotlin-coroutines = "1.8.0"
slf4j = "2.0.12"
log4j = "2.23.0"
mockk = "1.13.9"
log4j = "2.23.1"
mockk = "1.13.10"

[libraries]
protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf" }
Expand Down
36 changes: 33 additions & 3 deletions src/main/kotlin/net/scrayos/xenos/client/GrpcXenosClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import io.grpc.ManagedChannelBuilder
import io.grpc.Status.Code.NOT_FOUND
import io.grpc.Status.Code.UNAVAILABLE
import io.grpc.StatusException
import net.scrayos.xenos.client.data.CapeInfo
import net.scrayos.xenos.client.data.HeadInfo
import net.scrayos.xenos.client.data.ProfileInfo
import net.scrayos.xenos.client.data.SkinInfo
import net.scrayos.xenos.client.data.UuidInfo
import net.scrayos.xenos.client.data.toResult
import org.slf4j.LoggerFactory
import scrayosnet.xenos.ProfileGrpcKt
import scrayosnet.xenos.capeRequest
import scrayosnet.xenos.headRequest
import scrayosnet.xenos.profileRequest
import scrayosnet.xenos.skinRequest
import scrayosnet.xenos.uuidRequest
import scrayosnet.xenos.uuidsRequest
import java.lang.IllegalStateException
import java.time.Duration
import java.util.UUID
Expand Down Expand Up @@ -55,12 +58,23 @@ class GrpcXenosClient(
private val stub: ProfileGrpcKt.ProfileCoroutineStub = ProfileGrpcKt.ProfileCoroutineStub(channel)

override suspend fun getUuid(name: String): UuidInfo? {
val result = getUuids(listOf(name))
return result[name.lowercase()]
return try {
stub.getUuid(
uuidRequest {
username = name
},
).toResult()
} catch (ex: StatusException) {
when (ex.status.code) {
UNAVAILABLE -> throw IllegalStateException("Xenos could not fetch the requested uuid")
NOT_FOUND -> null
else -> throw ex
}
}
}

override suspend fun getUuids(names: Collection<String>): Map<String, UuidInfo?> = stub.getUuids(
uuidRequest {
uuidsRequest {
usernames.addAll(names)
},
)
Expand Down Expand Up @@ -99,6 +113,22 @@ class GrpcXenosClient(
}
}

override suspend fun getCape(userId: UUID): CapeInfo? {
return try {
stub.getCape(
capeRequest {
uuid = userId.toString()
},
).toResult()
} catch (ex: StatusException) {
when (ex.status.code) {
UNAVAILABLE -> throw IllegalStateException("Xenos could not fetch the requested cape")
NOT_FOUND -> null
else -> throw ex
}
}
}

override suspend fun getHead(userId: UUID, includeOverlay: Boolean): HeadInfo? {
return try {
stub.getHead(
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/net/scrayos/xenos/client/XenosClient.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.scrayos.xenos.client

import kotlinx.coroutines.flow.Flow
import net.scrayos.xenos.client.data.CapeInfo
import net.scrayos.xenos.client.data.HeadInfo
import net.scrayos.xenos.client.data.ProfileInfo
import net.scrayos.xenos.client.data.SkinInfo
Expand Down Expand Up @@ -62,6 +63,14 @@ interface XenosClient : AutoCloseable {
*/
suspend fun getSkin(userId: UUID): SkinInfo?

/**
* Retrieves the current [cape][CapeInfo] of the supplied [userId] from Xenos. The result includes the moment when
* the data was fetched from the Mojang API, to that stale data can be identified. If the player cannot be found or
* has no cape, `null` will be returned. The texture is guaranteed to be present within the memory and therefore
* easily accessible.
*/
suspend fun getCape(userId: UUID): CapeInfo?

/**
* Retrieves the current [head][HeadInfo] of the supplied [userId] from Xenos. The result includes the moment when
* the data was fetched from the Mojang API, to that stale data can be identified. The overlay skin layer may
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/net/scrayos/xenos/client/data/CapeInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.scrayos.xenos.client.data

import net.scrayos.xenos.client.utility.toImage
import scrayosnet.xenos.ProfileOuterClass.CapeResponse
import java.awt.image.BufferedImage
import java.time.Instant

/**
* A [CapeInfo] is the response to the request of the cape texture of a specific player. The result includes the
* moment when this information was freshly retrieved from Mojang and may be stale or outdated, if Xenos is configured
* to return those values. The texture is optimized for fast retrieval and all data is stored in memory.
*/
data class CapeInfo(
/** The loaded texture of the cape that was requested for the supplied player. */
val texture: BufferedImage,
/** The moment when this data was originally fetched from the MojangAPI and when it was considered fresh. */
val retrievedAt: Instant,
)

/**
* Converts the raw response from gRPC into a more user-friendly data class, that encapsulates the contained data in an
* easy-to-use format. No information is dropped or lost through the conversion. The wrapped response drops all
* information that is related to the gRPC origin and is therefore independent of the client implementation, that was
* used to retrieve the data from Xenos.
*/
internal fun CapeResponse.toResult(): CapeInfo {
return CapeInfo(
bytes.toImage(),
Instant.ofEpochSecond(timestamp),
)
}
3 changes: 3 additions & 0 deletions src/main/kotlin/net/scrayos/xenos/client/data/HeadInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ data class HeadInfo(
val texture: BufferedImage,
/** The moment when this data was originally fetched from the MojangAPI and when it was considered fresh. */
val retrievedAt: Instant,
/** Whether this includes the default skin, as the supplied player did not have a custom skin configured. */
val default: Boolean,
)

/**
Expand All @@ -27,5 +29,6 @@ internal fun HeadResponse.toResult(): HeadInfo {
return HeadInfo(
bytes.toImage(),
Instant.ofEpochSecond(timestamp),
default,
)
}
3 changes: 3 additions & 0 deletions src/main/kotlin/net/scrayos/xenos/client/data/SkinInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ data class SkinInfo(
val texture: BufferedImage,
/** The moment when this data was originally fetched from the MojangAPI and when it was considered fresh. */
val retrievedAt: Instant,
/** Whether this includes the default skin, as the supplied player did not have a custom skin configured. */
val default: Boolean,
)

/**
Expand All @@ -27,5 +29,6 @@ internal fun SkinResponse.toResult(): SkinInfo {
return SkinInfo(
bytes.toImage(),
Instant.ofEpochSecond(timestamp),
default,
)
}
28 changes: 17 additions & 11 deletions src/main/kotlin/net/scrayos/xenos/client/data/UuidInfo.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.scrayos.xenos.client.data

import scrayosnet.xenos.ProfileOuterClass.UuidResponse
import scrayosnet.xenos.ProfileOuterClass.UuidsResponse
import java.time.Instant
import java.util.UUID

Expand All @@ -25,19 +26,24 @@ data class UuidInfo(
* information that is related to the gRPC origin and is therefore independent of the client implementation, that was
* used to retrieve the data from Xenos.
*/
internal fun UuidResponse.toResult(): Map<String, UuidInfo?> {
internal fun UuidResponse.toResult(): UuidInfo {
return UuidInfo(
UUID.fromString(uuid),
username,
Instant.ofEpochSecond(timestamp),
)
}

/**
* Converts the raw response from gRPC into a more user-friendly data class, that encapsulates the contained data in an
* easy-to-use format. No information is dropped or lost through the conversion. The wrapped response drops all
* information that is related to the gRPC origin and is therefore independent of the client implementation, that was
* used to retrieve the data from Xenos.
*/
internal fun UuidsResponse.toResult(): Map<String, UuidInfo?> {
return resolvedMap
.mapValues {
val value = it.value
if (!value.hasData()) {
null
} else {
UuidInfo(
UUID.fromString(value.data.uuid),
value.data.username,
Instant.ofEpochSecond(value.timestamp),
)
}
it.value.toResult()
}
.toMap()
}
55 changes: 41 additions & 14 deletions src/main/proto/profile.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,52 @@ package scrayosnet.xenos;
// Profile is the service responsible for profile information lookup. It also contains information about the player skin
// and other properties of the profile.
service Profile {
// Get the Minecraft UUIDs for a specific username.
rpc GetUuid(UuidRequest) returns (UuidResponse);

// Get the Minecraft UUIDs for specific usernames.
rpc GetUuids(UuidRequest) returns (UuidResponse);
rpc GetUuids(UuidsRequest) returns (UuidsResponse);

// Get the Minecraft Profile for a specific UUID.
rpc GetProfile(ProfileRequest) returns (ProfileResponse);

// Get the Minecraft Skin for a specific UUID.
rpc GetSkin(SkinRequest) returns (SkinResponse);

// Get the Minecraft Cape for a specific UUID.
rpc GetCape(CapeRequest) returns (CapeResponse);

// Get the Minecraft Head for a specific UUID.
rpc GetHead(HeadRequest) returns (HeadResponse);
}

// UuidRequest is a request of the Minecraft UUIDs of specific, case-insensitive usernames.
// UuidRequest is a request of the Minecraft UUID of a specific, case-insensitive username.
message UuidRequest {
// The individual, case-insensitive username whose UUID should be queried.
string username = 1;
}

// UuidsRequest is a request of the Minecraft UUIDs of specific, case-insensitive usernames.
message UuidsRequest {
// The individual, case-insensitive usernames whose UUIDs should be queried.
repeated string usernames = 1;
}

// UuidResult is an individual result of the Minecraft UUID resolution at a specific timestamp.
message UuidResult {
// UuidResponse is an individual result of the Minecraft UUID resolution at a specific timestamp.
message UuidResponse {
// The unix timestamp (in seconds) at which the returned data was last updated.
uint64 timestamp = 1;
// The resolved uuid data (username and uuid).
optional UuidData data = 2;
}

// UuidData is an individual result of the Minecraft UUID resolution.
message UuidData {
// The username with correct capitalization.
string username = 2;
// The UUID in hyphenated form.
string uuid = 3;
}

// UuidResponse is a response with the Minecraft UUIDs of the requested usernames.
message UuidResponse {
// The individual results of the requested usernames. The keys are the requested usernames in lowercase.
map<string, UuidResult> resolved = 1;
// UuidsResponse is a response with the Minecraft UUIDs of the requested usernames.
message UuidsResponse {
// The individual responses of the requested usernames. The keys are the requested usernames in lowercase.
// Usernames that weren't found, aren't included.
map<string, UuidResponse> resolved = 1;
}

// ProfileRequest is a request of the Minecraft Profile of a specific UUID.
Expand Down Expand Up @@ -88,6 +95,24 @@ message SkinResponse {
uint64 timestamp = 1;
// The binary data of the 64x64 PNG image of the player's Skin.
bytes bytes = 2;
// The model of the player's Skin (e.g. "slim").
string model = 3;
// Whether the skin is the player default skin.
bool default = 4;
}

// CapeRequest is a request of the Cape texture of a specific UUID.
message CapeRequest {
// The UUID in simple or hyphenated form whose Minecraft Cape should be queried.
string uuid = 1;
}

// CapeResponse is a response with the Cape texture of the requested UUID.
message CapeResponse {
// The unix timestamp (in seconds) at which the returned data was last updated.
uint64 timestamp = 1;
// The binary data of the PNG image of the player's Cape.
bytes bytes = 2;
}

// HeadRequest is a request of the Head texture of a specific UUID.
Expand All @@ -104,4 +129,6 @@ message HeadResponse {
uint64 timestamp = 1;
// The binary data of the 8x8 PNG image of the player's Head.
bytes bytes = 2;
// Whether the head was generated from the player default skin.
bool default = 3;
}
Loading

0 comments on commit 2a6ee77

Please sign in to comment.