Skip to content

Commit c2103b1

Browse files
committed
Basic websocket backend + a few api endpoints
1 parent 7086aac commit c2103b1

File tree

18 files changed

+322
-23
lines changed

18 files changed

+322
-23
lines changed

build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
44
import dev.deftu.gradle.utils.GameSide
5+
import dev.deftu.gradle.utils.includeOrShade
56

67
plugins {
78
java
@@ -13,6 +14,11 @@ plugins {
1314
id("dev.deftu.gradle.tools.shadow") // Applies the Shadow plugin, which allows us to shade our dependencies into our mod JAR. This is NOT recommended for Fabric mods, but we have an *additional* configuration for those!
1415
id("dev.deftu.gradle.tools.minecraft.loom") // Applies the Loom plugin, which automagically configures Essential's Architectury Loom plugin for you.
1516
id("dev.deftu.gradle.tools.minecraft.releases") // Applies the Minecraft auto-releasing plugin, which allows you to automatically release your mod to CurseForge and Modrinth.
17+
kotlin("plugin.serialization")
18+
}
19+
20+
repositories {
21+
mavenCentral()
1622
}
1723

1824
toolkitLoomHelper {
@@ -59,4 +65,19 @@ dependencies {
5965
}
6066
}
6167
modImplementation("com.github.JnCrMx:discord-game-sdk4j:v0.5.5")
68+
69+
implementation("io.ktor:ktor-client-core:3.3.1")
70+
shade("io.ktor:ktor-client-core:3.3.1")
71+
implementation("io.ktor:ktor-client-cio:3.3.1")
72+
shade("io.ktor:ktor-client-cio:3.3.1")
73+
implementation("io.ktor:ktor-client-content-negotiation:3.3.1")
74+
shade("io.ktor:ktor-client-content-negotiation:3.3.1")
75+
implementation("io.ktor:ktor-serialization-kotlinx-json:3.3.1")
76+
shade("io.ktor:ktor-serialization-kotlinx-json:3.3.1")
77+
}
78+
79+
tasks {
80+
named("build") {
81+
dependsOn("fatJar")
82+
}
6283
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ org.gradle.daemon=true
33
org.gradle.parallel=true
44
org.gradle.configureoncommand=true
55
org.gradle.parallel.threads=4
6-
org.gradle.jvmargs=-Xmx2G
6+
org.gradle.jvmargs=-Xmx5G
77
loom.ignoreDependencyLoomVersionValidation=true
88

99
# gradle.properties file -- CHANGE THE VALUES STARTING WITH `mod.*` AND REMOVE THIS COMMENT.

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pluginManagement {
2424

2525
plugins {
2626
kotlin("jvm") version("2.2.10")
27+
kotlin("plugin.serialization") version("2.2.10")
2728
id("dev.deftu.gradle.multiversion-root") version("2.51.0")
2829
}
2930
}

src/main/java/org/polyfrost/polyplus/client/mixin/Mixin_ReplaceCapeTexture.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public class Mixin_ReplaceCapeTexture {
2020

2121
@Inject(method = "getLocationCape", at = @org.spongepowered.asm.mixin.injection.At("HEAD"))
2222
private void polyplus$onGetCape(CallbackInfoReturnable<ResourceLocation> cir) {
23-
this.locationCape = new ResourceLocation(PolyPlus.ID, "randomcape2.png");
23+
this.locationCape = new ResourceLocation(PolyPlus.ID, "64px_poly.png");
2424
}
2525
}

src/main/kotlin/org/polyfrost/polyplus/PolyPlus.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package org.polyfrost.polyplus
22

3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.engine.cio.CIO
5+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
6+
import io.ktor.client.plugins.defaultRequest
7+
import io.ktor.http.userAgent
8+
import io.ktor.serialization.kotlinx.json.json
39
import kotlinx.coroutines.CoroutineScope
410
import kotlinx.coroutines.Dispatchers
511
import kotlinx.coroutines.SupervisorJob
6-
import net.minecraft.client.Minecraft
712
import org.polyfrost.oneconfig.api.commands.v1.CommandManager
813
import org.polyfrost.polyplus.client.ExampleCommand
914
import org.polyfrost.polyplus.client.Config
15+
import org.polyfrost.polyplus.discordrpc.RPC
16+
import java.time.Instant
17+
import java.util.logging.Logger
1018

1119
//#if FABRIC
1220
//$$ import net.fabricmc.api.ModInitializer
@@ -23,9 +31,6 @@ import org.polyfrost.polyplus.client.Config
2331
//#else
2432
import net.minecraftforge.fml.common.Mod
2533
import net.minecraftforge.fml.common.event.FMLInitializationEvent
26-
import org.polyfrost.polyplus.discordrpc.RPC
27-
import java.time.Instant
28-
2934
//#endif
3035
//#elseif NEOFORGE
3136
//$$ import net.neoforged.bus.api.IEventBus
@@ -37,7 +42,6 @@ import java.time.Instant
3742

3843

3944
//#if FORGE-LIKE
40-
//$$ import org.polyfrost.example.ExampleConstants
4145
//#if MC >= 1.16.5
4246
//$$ @Mod(PolyPlus.ID)
4347
//#else
@@ -91,7 +95,7 @@ class PolyPlus
9195
//#endif
9296
//#endif
9397
) {
94-
//#if MC <= 1.12.2
98+
//#if FORGE-LIKE && MC <= 1.12.2
9599
if (!event.side.isClient) return
96100
//#endif
97101

@@ -109,6 +113,17 @@ class PolyPlus
109113
//#endif
110114

111115
companion object {
116+
val logger = Logger.getLogger(NAME)
117+
118+
val client = HttpClient(CIO) {
119+
defaultRequest {
120+
userAgent("todo")
121+
}
122+
123+
install(ContentNegotiation) {
124+
json()
125+
}
126+
}
112127
var launch: Instant? = null
113128

114129
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

src/main/kotlin/org/polyfrost/polyplus/client/Config.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.polyfrost.oneconfig.api.config.v1.Config
44
import org.polyfrost.oneconfig.api.config.v1.annotations.Dropdown
55
import org.polyfrost.oneconfig.api.config.v1.annotations.Slider
66
import org.polyfrost.oneconfig.api.config.v1.annotations.Switch
7+
import org.polyfrost.oneconfig.api.config.v1.annotations.Text
78
import org.polyfrost.polyplus.PolyPlus
89

910
object Config : Config("${PolyPlus.ID}.json", PolyPlus.NAME, Category.OTHER) {
@@ -12,11 +13,13 @@ object Config : Config("${PolyPlus.ID}.json", PolyPlus.NAME, Category.OTHER) {
1213
@Switch(title = "Discord RPC")
1314
var rpcEnabled = true // The default value for the boolean Switch
1415

15-
@JvmStatic
16-
@Slider(title = "Example Slider", min = 0f, max = 100f, step = 10f)
17-
var exampleSlider = 50f // The default value for the float Slider
18-
19-
@Dropdown(title = "Example Dropdown", options = ["Option 1", "Option 2", "Option 3", "Option 4"])
20-
var exampleDropdown = 1 // Default option (in this case, "Option 2")
16+
// @JvmStatic
17+
// @Slider(title = "Example Slider", min = 0f, max = 100f, step = 10f)
18+
// var exampleSlider = 50f // The default value for the float Slider
19+
//
20+
// @Dropdown(title = "Example Dropdown", options = ["Option 1", "Option 2", "Option 3", "Option 4"])
21+
// var exampleDropdown = 1 // Default option (in this case, "Option 2")
2122

23+
@Text(title = "API URL", description = "The url for the polyplus api. Only change if you know what you're doing.", placeholder = "https://plus.polyfrost.org/")
24+
var apiUrl: String = "https://plus-staging.polyfrost.org/"
2225
}

src/main/kotlin/org/polyfrost/polyplus/discordrpc/DownloadSDK.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package org.polyfrost.polyplus.discordrpc
22

3+
import io.ktor.client.call.body
4+
import io.ktor.client.request.get
5+
import io.ktor.client.statement.bodyAsChannel
6+
import io.ktor.client.statement.request
7+
import io.ktor.utils.io.jvm.javaio.toInputStream
38
import kotlinx.coroutines.Dispatchers
49
import kotlinx.coroutines.withContext
510
import org.polyfrost.oneconfig.utils.v1.dsl.mc
11+
import org.polyfrost.polyplus.PolyPlus.Companion.client
612
import java.io.BufferedInputStream
713
import java.io.File
814
import java.net.URI
@@ -31,11 +37,9 @@ object DownloadSDK {
3137
val url = URI.create(SDK_URL).toURL()
3238

3339
suspend fun download(): File? = withContext(Dispatchers.IO) {
34-
val connection = url.openConnection().apply {
35-
setRequestProperty("User-Agent", "todo")
36-
}
40+
val test = client.get(url)
3741

38-
ZipInputStream(BufferedInputStream(connection.getInputStream())).use { stream ->
42+
ZipInputStream(BufferedInputStream(test.bodyAsChannel().toInputStream())).use { stream ->
3943
for (entry in generateSequence { stream.nextEntry }) {
4044
if (entry.name != path) continue
4145

src/main/kotlin/org/polyfrost/polyplus/discordrpc/RPC.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import de.jcm.discordgamesdk.activity.Activity
77
import kotlinx.coroutines.launch
88
import kotlinx.coroutines.time.delay
99
import org.polyfrost.polyplus.PolyPlus
10+
import org.polyfrost.polyplus.PolyPlus.Companion.logger
1011
import org.polyfrost.polyplus.client.Config.rpcEnabled
1112
import java.time.Duration
1213
import java.util.concurrent.atomic.AtomicBoolean
@@ -19,7 +20,7 @@ object RPC {
1920
fun start() {
2021
if (!rpcEnabled || running.getAndSet(true)) return
2122
PolyPlus.scope.launch { run().onFailure {
22-
println("[PolyPlus] Failed to start Discord RPC: ${it.message}")
23+
logger.warning("Failed to start Discord RPC: ${it.message}")
2324
running.set(false)
2425
} }
2526
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package org.polyfrost.polyplus.network.plus
2+
3+
import dev.deftu.omnicore.api.client.client
4+
import dev.deftu.omnicore.api.client.player.uuid
5+
import io.ktor.client.HttpClient
6+
import io.ktor.client.call.body
7+
import io.ktor.client.request.HttpRequestBuilder
8+
import io.ktor.client.request.bearerAuth
9+
import io.ktor.client.request.post
10+
import io.ktor.client.request.request
11+
import io.ktor.client.request.url
12+
import io.ktor.client.statement.HttpResponse
13+
import io.ktor.http.HttpMethod
14+
import io.ktor.http.HttpStatusCode
15+
import io.ktor.http.Url
16+
import kotlinx.coroutines.CoroutineStart
17+
import kotlinx.coroutines.Deferred
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.async
20+
import kotlinx.coroutines.sync.Mutex
21+
import kotlinx.coroutines.sync.withLock
22+
import kotlinx.coroutines.withContext
23+
import org.polyfrost.polyplus.PolyPlus
24+
import org.polyfrost.polyplus.PolyPlus.Companion.logger
25+
import org.polyfrost.polyplus.client.Config
26+
import org.polyfrost.polyplus.network.plus.responses.AuthResponse
27+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
28+
29+
@OptIn(ExperimentalAtomicApi::class)
30+
object Auth {
31+
val authLock = Mutex()
32+
var authRes: AuthResponse? = null
33+
var authJob: Deferred<AuthResponse>? = null
34+
35+
suspend fun getToken(): String {
36+
return authLock.withLock {
37+
authRes?.token ?: refreshToken()
38+
}
39+
}
40+
41+
suspend fun refreshToken(): String {
42+
authJob?.let { return it.await().token }
43+
44+
return authLock.withLock {
45+
authJob?.let { return it.await().token }
46+
47+
val job = PolyPlus.scope.async(start = CoroutineStart.LAZY) {
48+
runAuth().also {
49+
authLock.withLock {
50+
authRes = it
51+
authJob = null
52+
}
53+
}
54+
}
55+
56+
authJob = job
57+
job.start()
58+
job.await().token
59+
}
60+
}
61+
62+
suspend inline fun HttpClient.authRequest(method: HttpMethod, url: String, noinline builder: HttpRequestBuilder.() -> Unit): Result<HttpResponse> = runCatching {
63+
var response = this.request {
64+
apply(builder)
65+
url(url)
66+
this.method = method
67+
bearerAuth(getToken())
68+
}
69+
70+
if (response.status == HttpStatusCode.Unauthorized) {
71+
response = this.request {
72+
apply(builder)
73+
url(url)
74+
this.method = method
75+
bearerAuth(refreshToken())
76+
}
77+
}
78+
79+
return Result.success(response)
80+
}
81+
82+
suspend fun runAuth() = withContext(Dispatchers.IO) {
83+
val serverId = genServerId()
84+
mojangAuth(serverId)
85+
val auth: AuthResponse = PolyPlus.client.post(Url(Config.apiUrl + "account/login?server_id=$serverId&username=${client.session.username}")).body()
86+
logger.info("Successfully authenticated with PolyPlus")
87+
auth
88+
}
89+
90+
suspend inline fun <reified T> HttpClient.authRequest(method: HttpMethod, url: String) = runCatching {
91+
val response = this.authRequest(method, url) {}
92+
response.getOrElse { return Result.failure<T>(it) }.body<T>()
93+
}
94+
}
95+
96+
fun genServerId(): String {
97+
val chars = ('a'..'z') + ('A'..'Z')
98+
return (0..<32).joinToString("") { "${chars.random()}" }
99+
}
100+
101+
fun mojangAuth(serverId: String) {
102+
try {
103+
//#if MC >= 1.20.4
104+
//$$ val profile = client.uuid
105+
//#else
106+
val profile = client.session.profile
107+
//#endif
108+
client.sessionService.joinServer(profile, client.session.token, serverId)
109+
} catch (e: Exception) {
110+
logger.warning("Failed to authenticate with Mojang: ${e.message}")
111+
}
112+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.polyfrost.polyplus.network.plus
2+
3+
import io.ktor.client.call.body
4+
import io.ktor.client.request.get
5+
import io.ktor.client.request.request
6+
import io.ktor.client.request.setBody
7+
import io.ktor.client.statement.bodyAsText
8+
import io.ktor.http.ContentType
9+
import io.ktor.http.HttpMethod
10+
import io.ktor.http.HttpStatusCode
11+
import io.ktor.http.contentType
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.launch
14+
import org.polyfrost.polyplus.PolyPlus
15+
import org.polyfrost.polyplus.PolyPlus.Companion.logger
16+
import org.polyfrost.polyplus.client.Config
17+
import org.polyfrost.polyplus.network.plus.Auth.authRequest
18+
import org.polyfrost.polyplus.network.plus.responses.Cosmetic
19+
import org.polyfrost.polyplus.network.plus.responses.PlayerCosmetics
20+
import org.polyfrost.polyplus.network.plus.responses.PutCosmetics
21+
22+
object Cosmetics {
23+
fun getOwned() = PolyPlus.scope.async {
24+
val cosmetics: Result<PlayerCosmetics> = PolyPlus.client.authRequest(HttpMethod.Get, "${Config.apiUrl}/cosmetics/player")
25+
cosmetics.onFailure { logger.warning("Failed to fetch owned cosmetics: ${it.message}") }
26+
// put on player probably?
27+
}
28+
29+
fun setOwned(cosmetics: PutCosmetics) = PolyPlus.scope.launch {
30+
val response = PolyPlus.client.authRequest(HttpMethod.Put, "${Config.apiUrl}/cosmetics/player") {
31+
contentType(ContentType.Application.Json)
32+
setBody(cosmetics)
33+
}.onFailure { logger.warning("Failed to set owned cosmetic: ${it.message}") }.getOrNull() ?: return@launch
34+
35+
if (response.status != HttpStatusCode.OK) logger.warning("Failed to set owned cosmetic: ${response.status}, ${response.bodyAsText()}")
36+
}
37+
38+
fun getAll() = PolyPlus.scope.async {
39+
val cosmetics: Result<List<Cosmetic>> = runCatching { PolyPlus.client.get("${Config.apiUrl}/cosmetics") }.map { it.body() }
40+
cosmetics.onFailure { logger.warning("Failed to fetch all cosmetics: ${it.message}") }
41+
// store somewhere/do something with them
42+
}
43+
}

0 commit comments

Comments
 (0)