Skip to content

Commit ec472e7

Browse files
committed
Add user lookup service with Minecraft and Minetools API integration
1 parent 5135bbb commit ec472e7

7 files changed

Lines changed: 265 additions & 1 deletion

File tree

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ dependencies {
1919
exclude("org.slf4j", "slf4j-api")
2020
}
2121
api(libs.hikari)
22+
23+
runtimeOnly("org.xerial:sqlite-jdbc:3.49.1.0")
24+
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.5.2")
2225
}
2326

2427
kotlin {

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kotlin.code.style=official
22
kotlin.stdlib.default.dependency=false
33
org.gradle.parallel=true
4-
version=1.0.2-SNAPSHOT
4+
version=1.0.3-SNAPSHOT
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package dev.slne.surf.database.user
2+
3+
import com.github.benmanes.caffeine.cache.Caffeine
4+
import com.sksamuel.aedile.core.expireAfterWrite
5+
import dev.slne.surf.database.user.minecraft.MinecraftApiClient
6+
import dev.slne.surf.database.user.minetools.MinetoolsApiClient
7+
import io.ktor.client.*
8+
import io.ktor.client.call.*
9+
import io.ktor.client.request.*
10+
import io.ktor.client.statement.*
11+
import kotlinx.coroutines.DelicateCoroutinesApi
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.GlobalScope
14+
import kotlinx.coroutines.future.await
15+
import kotlinx.coroutines.future.future
16+
import kotlinx.coroutines.withContext
17+
import kotlinx.serialization.json.Json
18+
import java.util.*
19+
import kotlin.coroutines.CoroutineContext
20+
import kotlin.time.Duration.Companion.hours
21+
22+
@OptIn(DelicateCoroutinesApi::class)
23+
object UserLookupService {
24+
25+
private val nameToUuidCache = Caffeine.newBuilder()
26+
.expireAfterWrite(1.hours)
27+
.buildAsync<String, UUID> { key, _ ->
28+
GlobalScope.future {
29+
try {
30+
MinecraftApiClient.getUuid(key)?.uuid
31+
} catch (_: Exception) {
32+
try {
33+
MinetoolsApiClient.getUuid(key)?.uuid
34+
} catch (_: Exception) {
35+
null
36+
}
37+
}
38+
}
39+
}
40+
41+
private val uuidToNameCache = Caffeine.newBuilder()
42+
.expireAfterWrite(1.hours)
43+
.buildAsync<UUID, String> { key, _ ->
44+
GlobalScope.future {
45+
try {
46+
MinecraftApiClient.getUsername(key)?.name
47+
} catch (_: Exception) {
48+
try {
49+
MinetoolsApiClient.getUsername(key)?.name
50+
} catch (_: Exception) {
51+
null
52+
}
53+
}
54+
}
55+
}
56+
57+
val client = HttpClient()
58+
val json = Json { ignoreUnknownKeys = true }
59+
60+
/**
61+
* Fetches data from an API and deserializes it into an object of type [T].
62+
*
63+
* @param url The URL of the API endpoint.
64+
*
65+
* @return The deserialized object, or null if the request failed.
66+
*/
67+
suspend inline fun <reified T> fetchFromApi(url: String): T? {
68+
val response: HttpResponse = client.get(url)
69+
70+
return if (response.status.value == 200) {
71+
json.decodeFromString<T>(response.body())
72+
} else {
73+
null
74+
}
75+
}
76+
77+
/**
78+
* Returns the UUID of a player by their username.
79+
*
80+
* @param username The username of the player.
81+
*
82+
* @return The UUID of the player, or null if the player does not exist.
83+
*/
84+
suspend fun getUuidByUsername(
85+
username: String,
86+
context: CoroutineContext = Dispatchers.IO
87+
): UUID? = withContext(context) { nameToUuidCache.get(username).await() }
88+
89+
/**
90+
* Returns the username of a player by their UUID.
91+
*
92+
* @param uuid The UUID of the player.
93+
*
94+
* @return The username of the player, or null if the player does not exist.
95+
*/
96+
suspend fun getUsernameByUuid(
97+
uuid: UUID,
98+
context: CoroutineContext = Dispatchers.IO
99+
): String? = withContext(context) { uuidToNameCache.get(uuid).await() }
100+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.slne.surf.database.user.minecraft
2+
3+
import dev.slne.surf.database.user.UserLookupService
4+
import java.util.*
5+
6+
object MinecraftApiClient {
7+
8+
private const val BASE_URL = "https://api.mojang.com"
9+
10+
/**
11+
* Get the username of a player by their UUID.
12+
*
13+
* @param uuid The UUID of the player.
14+
*
15+
* @return The username of the player, or null if the player does not exist.
16+
*/
17+
suspend fun getUsername(uuid: UUID): MinecraftApiResponse? =
18+
UserLookupService.fetchFromApi("$BASE_URL/user/profile/$uuid")
19+
20+
/**
21+
* Get the UUID of a player by their username.
22+
*
23+
* @param username The username of the player.
24+
*
25+
* @return The UUID of the player, or null if the player does not exist.
26+
*/
27+
suspend fun getUuid(username: String): MinecraftApiResponse? =
28+
UserLookupService.fetchFromApi("$BASE_URL/users/profiles/minecraft/$username")
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.slne.surf.database.user.minecraft
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.Transient
6+
import java.util.*
7+
8+
/**
9+
* {
10+
* "id": "5c63e51b82b14222af0f66a4c31e36ad",
11+
* "name": "NotAmmo"
12+
* }
13+
*/
14+
@Serializable
15+
data class MinecraftApiResponse(
16+
@SerialName("id")
17+
val id: String,
18+
19+
@SerialName("name")
20+
val name: String,
21+
) {
22+
23+
@Transient
24+
val uuid = UUID.fromString(
25+
id.substring(0, 8) + "-" +
26+
id.substring(8, 12) + "-" +
27+
id.substring(12, 16) + "-" +
28+
id.substring(16, 20) + "-" +
29+
id.substring(20)
30+
)
31+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.slne.surf.database.user.minetools
2+
3+
import dev.slne.surf.database.user.UserLookupService
4+
import java.util.*
5+
6+
object MinetoolsApiClient {
7+
8+
private const val BASE_URL = "https://api.minetools.eu"
9+
10+
/**
11+
* Get the username of a player by their UUID.
12+
*
13+
* @param uuid The UUID of the player.
14+
*
15+
* @return The username of the player, or null if the player does not exist.
16+
*/
17+
suspend fun getUsername(uuid: UUID): MinetoolsApiResponse? =
18+
UserLookupService.fetchFromApi("$BASE_URL/uuid/$uuid")
19+
20+
/**
21+
* Get the UUID of a player by their username.
22+
*
23+
* @param username The username of the player.
24+
*
25+
* @return The UUID of the player, or null if the player does not exist.
26+
*/
27+
suspend fun getUuid(username: String): MinetoolsApiResponse? =
28+
UserLookupService.fetchFromApi("$BASE_URL/uuid/$username")
29+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package dev.slne.surf.database.user.minetools
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.Transient
6+
import java.util.*
7+
8+
/**
9+
* {
10+
* "cache": {
11+
* "HIT": false,
12+
* "cache_time": 518400,
13+
* "cache_time_left": 518399,
14+
* "cached_at": 1736343742.7480717,
15+
* "cached_until": 1736862142.7480717
16+
* },
17+
* "id": "5c63e51b82b14222af0f66a4c31e36ad",
18+
* "name": "NotAmmo",
19+
* "status": "OK"
20+
* }
21+
*/
22+
@Serializable
23+
data class MinetoolsApiResponse(
24+
@SerialName("id")
25+
val id: String,
26+
27+
@SerialName("name")
28+
val name: String,
29+
30+
@SerialName("status")
31+
val status: String,
32+
33+
@SerialName("cache")
34+
val cache: MinetoolsApiResponseCache,
35+
) {
36+
37+
@Transient
38+
val uuid = UUID.fromString(
39+
id.substring(0, 8) + "-" +
40+
id.substring(8, 12) + "-" +
41+
id.substring(12, 16) + "-" +
42+
id.substring(16, 20) + "-" +
43+
id.substring(20)
44+
)
45+
46+
/**
47+
* "cache": {
48+
* "HIT": false,
49+
* "cache_time": 518400,
50+
* "cache_time_left": 518399,
51+
* "cached_at": 1736343742.7480717,
52+
* "cached_until": 1736862142.7480717
53+
* }
54+
*/
55+
@Serializable
56+
data class MinetoolsApiResponseCache(
57+
@SerialName("HIT")
58+
val hit: Boolean,
59+
60+
@SerialName("cache_time")
61+
val cacheTime: Long,
62+
63+
@SerialName("cache_time_left")
64+
val cacheTimeLeft: Long?,
65+
66+
@SerialName("cached_at")
67+
val cachedAt: Double,
68+
69+
@SerialName("cached_until")
70+
val cachedUntil: Double,
71+
)
72+
}

0 commit comments

Comments
 (0)