add Gradle task to upload the mod to the portal, and some gradle fixes

This commit is contained in:
Adam 2022-05-18 19:32:52 +02:00
parent 0f08eee56f
commit b6b874d8f7
22 changed files with 723 additions and 273 deletions

View file

@ -4,7 +4,7 @@ plugins {
idea idea
`kotlin-dsl` `kotlin-dsl`
kotlin("jvm") version "1.6.21" kotlin("jvm") version "1.6.21"
`project-report` kotlin("plugin.serialization") version "1.6.21"
} }
val gradleJvmTarget = "11" val gradleJvmTarget = "11"
@ -19,9 +19,12 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable") implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:${libs.versions.kotlinx.serialization.get()}")) implementation(platform(libs.kotlinx.coroutines.bom))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") implementation(libs.kotlinx.coroutines.core)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
implementation(platform(libs.kotlinx.serialization.bom))
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
// implementation("com.charleskorn.kaml:kaml:0.37.0") // implementation("com.charleskorn.kaml:kaml:0.37.0")
implementation("com.github.node-gradle:gradle-node-plugin:${libs.versions.gradleNodePlugin.get()}") implementation("com.github.node-gradle:gradle-node-plugin:${libs.versions.gradleNodePlugin.get()}")
@ -32,13 +35,29 @@ dependencies {
implementation("net.swiftzer.semver:semver:${libs.versions.semver.get()}") implementation("net.swiftzer.semver:semver:${libs.versions.semver.get()}")
implementation(platform(libs.okio.bom))
implementation(libs.okio.core)
implementation(gradleKotlinDsl())
implementation(libs.kotlinx.cli)
implementation(platform(libs.ktor.bom))
implementation(libs.ktor.clientAuth)
implementation(libs.ktor.clientContentNegotiation)
implementation(libs.ktor.clientCore)
implementation(libs.ktor.clientEncoding)
implementation(libs.ktor.clientLogging)
implementation(libs.ktor.clientCIO)
implementation(libs.ktor.clientResources)
implementation(libs.ktor.serializationKotlinxJson)
// https://github.com/avast/gradle-docker-compose-plugin // https://github.com/avast/gradle-docker-compose-plugin
// implementation("com.avast.gradle:gradle-docker-compose-plugin:${Versions.gradleDockerComposePlugin}") // implementation("com.avast.gradle:gradle-docker-compose-plugin:${Versions.gradleDockerComposePlugin}")
implementation(platform("org.http4k:http4k-bom:${libs.versions.http4k.get()}")) // implementation(platform("org.http4k:http4k-bom:${libs.versions.http4k.get()}"))
implementation("org.http4k:http4k-core") // implementation("org.http4k:http4k-core")
implementation("org.http4k:http4k-client-okhttp") // implementation("org.http4k:http4k-client-okhttp")
// https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 // https://github.com/gradle/gradle/issues/15383#issuecomment-779893192
// https://youtrack.jetbrains.com/issue/IDEA-262280#focus=Comments-27-5397040.0-0 // https://youtrack.jetbrains.com/issue/IDEA-262280#focus=Comments-27-5397040.0-0
@ -63,14 +82,24 @@ tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
// "-Xcontext-receivers", // "-Xcontext-receivers",
"-opt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalStdlibApi", "-Xopt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlin.time.ExperimentalTime", "-Xopt-in=kotlin.time.ExperimentalTime",
// "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", // "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
) )
} }
afterEvaluate {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
apiVersion = gradleKotlinTarget
languageVersion = gradleKotlinTarget
}
}
}
kotlin { kotlin {
jvmToolchain { jvmToolchain {
(this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(gradleJvmTarget)) (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(gradleJvmTarget))

View file

@ -13,16 +13,10 @@ val jsonMapper = Json {
object Versions { object Versions {
const val kotlinLang = "1.6"
const val jvmTarget = "11"
const val node = "14.19.1" const val node = "14.19.1"
// const val kotlinWrappers = "0.0.1-pre.330-kotlin-1.6.20"
const val kotlinWrappers = "1.0.0-pre.336" const val kotlinWrappers = "1.0.0-pre.336"
const val junit = "5.8.2"
const val kotest = "5.2.3"
const val mockk = "1.12.1"
} }

View file

@ -0,0 +1,134 @@
package dev.adamko.kafkatorio.factoriomod
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.logging.SIMPLE
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.Parameters
import io.ktor.http.append
import io.ktor.http.isSuccess
import io.ktor.serialization.kotlinx.json.json
import java.io.File
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.gradle.api.internal.tasks.userinput.UserInputHandler
import org.gradle.kotlin.dsl.support.useToRun
import org.jetbrains.kotlin.cli.common.toBooleanLenient
class FactorioModPortalPublishClient(
private val userInputHandler: UserInputHandler,
private val distributionZip: File,
private val modName: String,
private val modVersion: String,
private val portalApiKey: String,
private val portalUploadEndpoint: String,
) {
private fun client() = HttpClient(CIO) {
install(Logging) {
logger = Logger.SIMPLE
level = LogLevel.ALL
}
install(ContentNegotiation) {
json(Json {
prettyPrint = false
isLenient = true
})
}
defaultRequest {
header(HttpHeaders.Authorization, "Bearer $portalApiKey")
port = 443
}
followRedirects = false
expectSuccess = false
}
fun uploadMod() = runBlocking {
client().useToRun {
val initUploadResponse = initUpload()
val confirmation = userInputHandler
.askQuestion("Are you sure you want to publish $modName:$modVersion?", "no")
.toBooleanLenient() ?: false
if (confirmation) {
upload(initUploadResponse)
} else {
println("aborting upload")
}
}
}
private suspend fun HttpClient.initUpload(): InitUploadResponse.Success {
val response = submitForm(
url = portalUploadEndpoint,
formParameters = Parameters.build {
append("mod", modName)
}
)
// val response = post(portalUploadEndpoint) {
// contentType(ContentType.MultiPart.FormData)
// setBody(Json.encodeToString(InitUploadRequest.serializer(), InitUploadRequest(modName)))
//// setBody(InitUploadRequest(modName))
// }
println(response)
val initUploadResponse: InitUploadResponse =
Json.decodeFromString(InitUploadResponse.serializer(), response.bodyAsText())
println(initUploadResponse)
require(response.status.isSuccess()) { "init upload request failed" }
return when (initUploadResponse) {
is Failure -> error(initUploadResponse)
is InitUploadResponse.Success -> initUploadResponse
}
}
private suspend fun HttpClient.upload(initUploadResponse: InitUploadResponse.Success) {
val requestForm = formData {
append(
"file",
distributionZip.readBytes(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Zip)
append(HttpHeaders.ContentDisposition, "filename=${distributionZip.name}")
}
)
}
val response = post(initUploadResponse.uploadUrl) {
setBody(MultiPartFormDataContent(requestForm))
}
// val response = submitFormWithBinaryData(initUploadResponse.uploadUrl, requestForm) {
// header(HttpHeaders.Authorization, "Bearer $portalApiKey")
// }
val submitUploadResponse =
Json.decodeFromString(SubmitUploadResponse.serializer(), response.bodyAsText())
println(response)
println(submitUploadResponse)
require(response.status.isSuccess() && submitUploadResponse is SubmitUploadResponse.Success) { "upload request failed" }
}
}

View file

@ -0,0 +1,101 @@
package dev.adamko.kafkatorio.factoriomod
import io.ktor.http.URLBuilder
import io.ktor.http.takeFrom
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.tasks.userinput.UserInputHandler
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.support.get
abstract class FactorioModPublishTask @Inject constructor(
objects: ObjectFactory,
providers: ProviderFactory,
) : DefaultTask() {
@get:InputFile
abstract val distributionZip: RegularFileProperty
@get:Input
abstract val modName: Property<String>
@get:Input
abstract val modVersion: Property<String>
// @get:Input
// abstract val factorioVersion: Property<String>
/** Bearer API key */
@get:Input
val portalApiKey: Property<String> = objects.property<String>().convention(
providers.gradleProperty("FACTORIO_MOD_PORTAL_API_KEY")
)
@get:Input
val portalApiUrl: Property<String> = objects.property<String>()
.convention("https://mods.factorio.com/api/v2/")
@get:Input
val portalUploadEndpoint: Property<String> = objects.property<String>()
.convention("mods/releases/init_upload")
init {
group = "factorio"
}
@TaskAction
fun deploy() = runBlocking {
val userInputHandler = services.get<UserInputHandler>()
val portalUploadEndpoint = URLBuilder(portalApiUrl.get())
.takeFrom(portalUploadEndpoint.get())
.buildString()
val client = FactorioModPortalPublishClient(
userInputHandler,
distributionZip.get().asFile,
modName.get(),
modVersion.get(),
portalApiKey.get(),
portalUploadEndpoint,
)
client.uploadMod()
}
// private fun validateInfoJson() {
// val modVersion = modVersion.get()
// val factorioVersion = factorioVersion.get()
//
// val distributionZip = distributionZip.get().asFile.toOkioPath()
//
// val zipContents = FileSystem.SYSTEM.openZip(distributionZip)
//
// val infoJson = zipContents.read("info.json".toPath()) {
// generateSequence { readUtf8Line() }
// }.joinToString("\n")
//
// val info: FactorioModInfo = Json.decodeFromString(FactorioModInfo.serializer(), infoJson)
//
// require(info.version == modVersion) {
// "mod version mismatch, ${info.version} vs $modVersion "
// }
//
// require(info.factorioVersion == factorioVersion) {
// "Factorio version mismatch, ${info.factorioVersion} vs $factorioVersion "
// }
// }
}

View file

@ -0,0 +1,91 @@
package dev.adamko.kafkatorio.factoriomod
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
//@Serializable
//data class InitUploadRequest(
// @SerialName("mod") val modName: String,
//)
@Serializable(with = InitUploadResponse.Serializer::class)
sealed interface InitUploadResponse {
/**
* @param[uploadUrl] URL the mod zip file should be uploaded to
*/
@Serializable
data class Success(
@SerialName("upload_url") val uploadUrl: String,
) : InitUploadResponse
object Serializer :
JsonContentPolymorphicSerializer<InitUploadResponse>(InitUploadResponse::class) {
override fun selectDeserializer(element: JsonElement) = when {
"upload_url" in element.jsonObject -> Success.serializer()
else -> Failure.serializer()
}
}
}
@Serializable(with = SubmitUploadResponse.Serializer::class)
sealed interface SubmitUploadResponse {
@Serializable
object Success : SubmitUploadResponse
object Serializer : JsonContentPolymorphicSerializer<SubmitUploadResponse>(
SubmitUploadResponse::class
) {
override fun selectDeserializer(element: JsonElement) = when {
"success" in element.jsonObject -> Success.serializer()
else -> Failure.serializer()
}
}
}
@Serializable
data class Failure(
val error: String? = null,
val message: String? = null,
) : InitUploadResponse, SubmitUploadResponse {
private val reason: String?
get() = reasons[error]
override fun toString() = "Failure(error=$error, message=$message, reason=$reason)"
companion object {
private val reasons: Map<String?, String> = mapOf(
//@formatter:off
"InvalidApiKey" to "Missing or invalid API key for the current endpoint",
"InvalidRequest" to "Invalid request.",
"InternalError" to "Internal error, please try again later.",
"Forbidden" to "Insufficient permission for current endpoint",
"Unknown" to "Unknown error, please try again later.",
"InvalidModRelease" to "Invalid release data in info.json",
"InvalidModUpload" to "Invalid mod data in zipfile",
"UnknownMod" to "Mod does not exist in mod portal",
//@formatter:on
)
}
}
@Serializable
data class FactorioModInfo(
val name: String,
val version: String,
val title: String,
val description: String,
val author: String,
@SerialName("factorio_version") val factorioVersion: String,
val dependencies: List<String>,
)

View file

@ -1,6 +1,5 @@
package dev.adamko.kafkatorio.lang package dev.adamko.kafkatorio.lang
import dev.adamko.kafkatorio.Versions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@ -8,33 +7,38 @@ plugins {
kotlin("jvm") kotlin("jvm")
} }
val projectKotlinTarget = "1.6"
val projectJvmTarget = "11"
dependencies { dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom")) // implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
testImplementation(platform("org.junit:junit-bom:${Versions.junit}")) // testImplementation(platform("org.junit:junit-bom:${Versions.junit}"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") { // testRuntimeOnly("org.junit.platform:junit-platform-launcher") {
because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions") // because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions")
} // }
testImplementation(platform("io.kotest:kotest-bom:${Versions.kotest}")) // testImplementation(platform("io.kotest:kotest-bom:${Versions.kotest}"))
testImplementation("io.kotest:kotest-runner-junit5") testImplementation("io.kotest:kotest-runner-junit5")
testImplementation("io.kotest:kotest-assertions-core") testImplementation("io.kotest:kotest-assertions-core")
testImplementation("io.kotest:kotest-property") testImplementation("io.kotest:kotest-property")
testImplementation("io.kotest:kotest-assertions-json") testImplementation("io.kotest:kotest-assertions-json")
testImplementation("io.mockk:mockk:${Versions.mockk}") testImplementation("io.mockk:mockk")
} }
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = Versions.jvmTarget jvmTarget = projectJvmTarget
apiVersion = Versions.kotlinLang apiVersion = projectKotlinTarget
languageVersion = Versions.kotlinLang languageVersion = projectKotlinTarget
freeCompilerArgs += listOf( }
kotlinOptions.freeCompilerArgs += listOf(
// "-Xcontext-receivers", // "-Xcontext-receivers",
"-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
@ -43,7 +47,6 @@ tasks.withType<KotlinCompile>().configureEach {
// "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", // "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
) )
} }
}
tasks.compileTestKotlin { tasks.compileTestKotlin {
kotlinOptions.freeCompilerArgs += "-opt-in=io.kotest.common.ExperimentalKotest" kotlinOptions.freeCompilerArgs += "-opt-in=io.kotest.common.ExperimentalKotest"
@ -51,10 +54,10 @@ tasks.compileTestKotlin {
kotlin { kotlin {
jvmToolchain { jvmToolchain {
(this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(Versions.jvmTarget)) (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(projectJvmTarget))
} }
} }
tasks.withType<Test> { tasks.withType<Test>().configureEach {
useJUnitPlatform() useJUnitPlatform()
} }

View file

@ -52,6 +52,6 @@ val updatePackageJson by tasks.registering(UpdatePackageJson::class) {
tasks.assemble { dependsOn(updatePackageJson) } tasks.assemble { dependsOn(updatePackageJson) }
tasks.withType<NpmInstallTask> { tasks.withType<NpmInstallTask>().configureEach {
dependsOn(updatePackageJson) dependsOn(updatePackageJson)
} }

View file

@ -1,23 +0,0 @@
package dev.adamko.kafkatorio.task
import org.gradle.api.DefaultTask
import org.http4k.client.OkHttp
import org.http4k.core.Method
import org.http4k.core.Request
abstract class FactorioModPortalDeploy : DefaultTask() {
init {
super.doFirst {
val client = OkHttp()
val request = Request(Method.GET, "https://factorio.com/login?mods=1")
println(client(request))
}
}
}

View file

@ -22,7 +22,7 @@ object JsonPropertiesUpToDateSpec : ExplainingSpec<Task> {
else -> { else -> {
val packageJsonContent = packageJsonFile.get().asFile.readText() val packageJsonContent = packageJsonFile2.get().asFile.readText()
val packageJson = jsonMapper.parseToJsonElement(packageJsonContent).jsonObject val packageJson = jsonMapper.parseToJsonElement(packageJsonContent).jsonObject
val outdatedProperties = propertiesToCheck.get().mapNotNull { (key, expectedVal) -> val outdatedProperties = propertiesToCheck.get().mapNotNull { (key, expectedVal) ->

View file

@ -2,6 +2,7 @@ package dev.adamko.kafkatorio.task
import com.github.gradle.node.npm.task.NpmTask import com.github.gradle.node.npm.task.NpmTask
import javax.inject.Inject import javax.inject.Inject
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.FileSystemOperations
import org.gradle.api.plugins.BasePlugin import org.gradle.api.plugins.BasePlugin

View file

@ -13,6 +13,7 @@ import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.ProviderFactory import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.support.useToRun import org.gradle.kotlin.dsl.support.useToRun
@ -20,14 +21,15 @@ import org.jetbrains.kotlin.util.suffixIfNot
@CacheableTask @CacheableTask
abstract class UpdatePackageJson @Inject constructor( abstract class UpdatePackageJson @Inject constructor(
private val providers: ProviderFactory private val providers: ProviderFactory,
) : DefaultTask() { ) : DefaultTask() {
@get:Input @get:Input
abstract val propertiesToCheck: MapProperty<String, String> abstract val propertiesToCheck: MapProperty<String, String>
@get:OutputFile // @get:OutputFile
abstract val packageJsonFile: RegularFileProperty @get:Internal
abstract val packageJsonFile2: RegularFileProperty
init { init {
group = NodePlugin.NODE_GROUP group = NodePlugin.NODE_GROUP
@ -36,26 +38,37 @@ abstract class UpdatePackageJson @Inject constructor(
outputs.upToDateWhen(JsonPropertiesUpToDateSpec) outputs.upToDateWhen(JsonPropertiesUpToDateSpec)
super.doFirst { // super.doFirst {
propertiesToCheck.disallowChanges() // propertiesToCheck.disallowChanges()
packageJsonFile.disallowChanges() // packageJsonFile.disallowChanges()
// }
}
init {
outputs.upToDateWhen { task ->
require(task is UpdatePackageJson) { "$task is not a UpdatePackageJson task" }
val currentPackageJson = task.currentPackageJson()
val updatedPackageJson = task.updatedPackageJson(currentPackageJson)
val status = currentPackageJson == updatedPackageJson
logger.lifecycle("package.json is ${if (status) "up to date" else "not up to date"}")
status
} }
} }
@TaskAction @TaskAction
fun exec() { fun exec() {
val packageJsonFile = packageJsonFile.asFile.get() val packageJsonFile = packageJsonFile2.asFile.get()
val propertiesToCheck = propertiesToCheck.get()
logger.info("updating package.json ${packageJsonFile.canonicalPath}") logger.lifecycle("updating package.json ${packageJsonFile.canonicalPath}")
val packageJson = jsonMapper.parseToJsonElement(packageJsonFile.readText()).jsonObject
val newJsonProps = propertiesToCheck.mapValues { (_, newVal) -> JsonPrimitive(newVal) } val currentPackageJson = currentPackageJson()
val updatedPackageJson = updatedPackageJson(currentPackageJson)
val packageJsonUpdate = JsonObject(packageJson + newJsonProps)
val packageJsonContentUpdated = val packageJsonContentUpdated =
jsonMapper jsonMapper
.encodeToString(packageJsonUpdate) .encodeToString(updatedPackageJson)
.suffixIfNot("\n") .suffixIfNot("\n")
logger.debug(packageJsonContentUpdated) logger.debug(packageJsonContentUpdated)
@ -64,4 +77,15 @@ abstract class UpdatePackageJson @Inject constructor(
write(packageJsonContentUpdated) write(packageJsonContentUpdated)
} }
} }
private fun currentPackageJson(): JsonObject {
val packageJsonFile = packageJsonFile2.asFile.get()
return jsonMapper.parseToJsonElement(packageJsonFile.readText()).jsonObject
}
private fun updatedPackageJson(currentPackageJson: JsonObject): JsonObject {
val propertiesToCheck = propertiesToCheck.get()
val newJsonProps = propertiesToCheck.mapValues { (_, newVal) -> JsonPrimitive(newVal) }
return JsonObject(currentPackageJson + newJsonProps)
}
} }

View file

@ -27,6 +27,7 @@ kotlinJsWrappers = "0.0.1-pre.334"
kotlinx-html = "0.7.5" kotlinx-html = "0.7.5"
kotlinx-nodejs = "0.0.7" kotlinx-nodejs = "0.0.7"
kotlinx-coroutines = "1.6.1" kotlinx-coroutines = "1.6.1"
kotlinx-cli = "0.3.4"
ktor = "2.0.1" ktor = "2.0.1"
@ -77,9 +78,12 @@ kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotl
kotlinx-html = { group = "org.jetbrains.kotlinx", name = "kotlinx-html", version.ref = "kotlinx-html" } kotlinx-html = { group = "org.jetbrains.kotlinx", name = "kotlinx-html", version.ref = "kotlinx-html" }
kotlinx-nodejs = { group = "org.jetbrains.kotlinx", name = "kotlinx-nodejs", version.ref = "kotlinx-nodejs" } kotlinx-nodejs = { group = "org.jetbrains.kotlinx", name = "kotlinx-nodejs", version.ref = "kotlinx-nodejs" }
kotlinx-cli = { group = "org.jetbrains.kotlinx", name = "kotlinx-cli", version.ref = "kotlinx-cli" }
kotlin-jsWrappers-bom = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-wrappers-bom", version.ref = "kotlinJsWrappers" } kotlin-jsWrappers-bom = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-wrappers-bom", version.ref = "kotlinJsWrappers" }
## http4k ## ## http4k ##
http4k-bom = { group = "org.http4k", name = "http4k-bom", version.ref = "http4k" } http4k-bom = { group = "org.http4k", name = "http4k-bom", version.ref = "http4k" }
http4k-core = { group = "org.http4k", name = "http4k-core" } http4k-core = { group = "org.http4k", name = "http4k-core" }
@ -88,9 +92,9 @@ http4k-client-okhttp = { group = "org.http4k", name = "http4k-client-okhttp"
http4k-client-ws = { group = "org.http4k", name = "http4k-client-websocket" } http4k-client-ws = { group = "org.http4k", name = "http4k-client-websocket" }
http4k-server = { group = "org.http4k", name = "http4k-server-undertow" } http4k-server = { group = "org.http4k", name = "http4k-server-undertow" }
http4k-cloudnative = { group = "org.http4k", name = "http4k-cloudnative" } http4k-cloudnative = { group = "org.http4k", name = "http4k-cloudnative" }
http4k-format-json = { group = "org.http4k", name = "http4k-format-jackson" } #http4k-format-jackson-json = { group = "org.http4k", name = "http4k-format-jackson" }
http4k-format-xml = { group = "org.http4k", name = "http4k-format-jackson-xml" } #http4k-format-jackson-xml = { group = "org.http4k", name = "http4k-format-jackson-xml" }
http4k-format-yaml = { group = "org.http4k", name = "http4k-format-jackson-yaml" } #http4k-format-jackson-yaml = { group = "org.http4k", name = "http4k-format-jackson-yaml" }
http4k-format-kotlinx = { group = "org.http4k", name = "http4k-format-kotlinx-serialization" } http4k-format-kotlinx = { group = "org.http4k", name = "http4k-format-kotlinx-serialization" }
http4k-graphql = { group = "org.http4k", name = "http4k-graphql" } http4k-graphql = { group = "org.http4k", name = "http4k-graphql" }
@ -98,6 +102,17 @@ http4k-graphql = { group = "org.http4k", name = "http4k-graphql"
## ktor ## ## ktor ##
ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" }
ktor-clientAuth = { group = "io.ktor", name = "ktor-client-auth" }
ktor-clientCIO = { group = "io.ktor", name = "ktor-client-cio" }
ktor-clientContentNegotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" }
ktor-clientCore = { group = "io.ktor", name = "ktor-client-core" }
ktor-clientEncoding = { group = "io.ktor", name = "ktor-client-encoding" }
ktor-clientLogging = { group = "io.ktor", name = "ktor-client-logging" }
ktor-clientResources = { group = "io.ktor", name = "ktor-client-resources" }
ktor-clientWebsockets = { group = "io.ktor", name = "ktor-client-websockets" }
ktor-serializationKotlinxJson = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" }
ktor-test-clientMock = { group = "io.ktor", name = "ktor-client-mock" }
### jackson ## ### jackson ##
#jackson-bom = { group = " com.fasterxml.jackson.module", name = ":jackson-module-kotlin", version = "2.13.+" } #jackson-bom = { group = " com.fasterxml.jackson.module", name = ":jackson-module-kotlin", version = "2.13.+" }
@ -129,6 +144,7 @@ skrapeit = { group = "it.skrape", name = "skrapeit", version = "1.2.0" }
scrimage-core = { group = "com.sksamuel.scrimage", name = "scrimage-core", version.ref = "scrimage" } scrimage-core = { group = "com.sksamuel.scrimage", name = "scrimage-core", version.ref = "scrimage" }
semver = { group = "net.swiftzer.semver", name = "semver", version.ref = "semver" }
## okio ## ## okio ##
okio-bom = { group = "com.squareup.okio", name = "okio-bom", version.ref = "okio" } okio-bom = { group = "com.squareup.okio", name = "okio-bom", version.ref = "okio" }
@ -194,9 +210,9 @@ http4k = [
"http4k-client-ws", "http4k-client-ws",
"http4k-server", "http4k-server",
"http4k-cloudnative", "http4k-cloudnative",
"http4k-format-xml", # "http4k-format-jackson-xml",
"http4k-format-json", # "http4k-format-jackson-json",
"http4k-format-yaml", # "http4k-format-jackson-yaml",
"http4k-format-kotlinx", "http4k-format-kotlinx",
] ]

View file

@ -1,3 +1,4 @@
import dev.adamko.kafkatorio.factoriomod.FactorioModPublishTask
import dev.adamko.kafkatorio.gradle.asConsumer import dev.adamko.kafkatorio.gradle.asConsumer
import dev.adamko.kafkatorio.gradle.asProvider import dev.adamko.kafkatorio.gradle.asProvider
import dev.adamko.kafkatorio.gradle.dropDirectories import dev.adamko.kafkatorio.gradle.dropDirectories
@ -50,7 +51,7 @@ val tsSrcDir: Directory = layout.projectDirectory.dir("src/main/typescript")
node { node {
nodeProjectDir.set(tsSrcDir) nodeProjectDir.set(layout.projectDirectory)
} }
@ -115,6 +116,15 @@ tasks.distZip {
} }
val publishMod by tasks.registering(FactorioModPublishTask::class) {
distributionZip.set(tasks.distZip.flatMap { it.archiveFile })
val projectModName = project.extra.get("modName") as String
modName.set(projectModName)
modVersion.set(project.version.toString())
}
distributions { distributions {
main { main {

View file

@ -15,7 +15,7 @@ description = """
val projectId: String by project.extra val projectId: String by project.extra
val kafkaStateDir = layout.projectDirectory.dir("kafka-state") val kafkaStateDir: Directory = layout.projectDirectory.dir("kafka-state")
dependencies { dependencies {
implementation(platform(projects.modules.versionsPlatform)) implementation(platform(projects.modules.versionsPlatform))
@ -64,7 +64,7 @@ application {
// ) // )
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",

View file

@ -3,32 +3,41 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
dev.adamko.kafkatorio.lang.`kotlin-jvm` dev.adamko.kafkatorio.lang.`kotlin-jvm`
kotlin("plugin.serialization") kotlin("plugin.serialization")
application `java-gradle-plugin`
} }
dependencies { dependencies {
implementation(platform(projects.modules.versionsPlatform)) implementation(platform(projects.modules.versionsPlatform))
implementation(projects.modules.eventsServer) // implementation(gradleKotlinDsl())
implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.cli)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.http4k) // implementation(libs.bundles.http4k)
implementation(libs.http4k.format.kotlinx) // implementation(libs.http4k.format.kotlinx)
implementation(libs.skrapeit) // implementation(libs.skrapeit)
implementation(libs.okio.core)
implementation(libs.semver)
implementation(libs.ktor.clientAuth)
implementation(libs.ktor.clientContentNegotiation)
implementation(libs.ktor.clientCore)
implementation(libs.ktor.clientEncoding)
implementation(libs.ktor.clientLogging)
implementation(libs.ktor.clientCIO)
implementation(libs.ktor.clientResources)
implementation(libs.ktor.serializationKotlinxJson)
} }
// tasks.withType<KotlinCompile>().configureEach {
//application {
// mainClass.set("dev.adamko.kafkatorio.processor.EventProcessorKt")
//}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",

View file

@ -1,48 +0,0 @@
package dev.adamko.kafkatorio
import dev.adamko.kafkatorio.processor.config.env
import dev.adamko.kafkatorio.processor.config.fromEnvVarPath
import dev.adamko.kafkatorio.processor.config.fromResourcesFile
import java.time.Clock
import org.http4k.cloudnative.env.Environment
import org.http4k.cloudnative.env.EnvironmentKey
import org.http4k.cloudnative.env.Secret
import org.http4k.lens.nonEmptyString
import org.http4k.lens.secret
/**
* Load properties, in descending priority order, from
*
* 1. Environment variables
* 2. This app's env file (as defined by the environment variable [Lenses.APPLICATION_PROPERTIES_FILE_PATH])
* # 3. A resources-dir yaml file
*
*/
internal class ApplicationProperties(
vararg environments: Environment = arrayOf(
Environment.env(),
Environment.fromEnvVarPath(),
Environment.fromResourcesFile(".secret.config.yml")
)
) {
private val appProps: Environment =
environments.reduce { acc, environment -> acc.overrides(environment) }
fun clock(): Clock = Clock.systemUTC()
val factorioModPortalUsername: String
get() = EnvironmentKey
.nonEmptyString()
.required("factorio-mod-portal.username")[appProps]
val factorioModPortalPassword: Secret
get() = EnvironmentKey
.secret()
.required("factorio-mod-portal.password")[appProps]
}

View file

@ -1,107 +1,107 @@
package dev.adamko.kafkatorio package dev.adamko.kafkatorio
import dev.adamko.kafkatorio.processor.serdes.KXS.asPrettyJsonString //import dev.adamko.kafkatorio.processor.serdes.KXS.asPrettyJsonString
import dev.adamko.kafkatorio.processor.serdes.jsonMapper //import dev.adamko.kafkatorio.processor.serdes.jsonMapper
import it.skrape.core.htmlDocument //import it.skrape.core.htmlDocument
import it.skrape.selects.html5.input //import it.skrape.selects.html5.input
import kotlinx.serialization.json.JsonObject //import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject //import kotlinx.serialization.json.jsonObject
import org.http4k.client.OkHttp //import org.http4k.client.OkHttp
import org.http4k.core.HttpHandler //import org.http4k.core.HttpHandler
import org.http4k.core.Method //import org.http4k.core.Method
import org.http4k.core.Request //import org.http4k.core.Request
import org.http4k.core.Response //import org.http4k.core.Response
import org.http4k.core.body.form //import org.http4k.core.body.form
import org.http4k.core.cookie.Cookie //import org.http4k.core.cookie.Cookie
import org.http4k.core.cookie.cookie //import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.cookies //import org.http4k.core.cookie.cookies
import org.http4k.core.then //import org.http4k.core.then
import org.http4k.filter.ClientFilters //import org.http4k.filter.ClientFilters
//
//
/** ///*
* * https://github.com/shanemadden/factorio-mod-portal-publish/blob/master/entrypoint.sh // * * https://github.com/shanemadden/factorio-mod-portal-publish/blob/master/entrypoint.sh
* * https://github.com/nicolas-lang/factorio-create-release-action/blob/master/entrypoint.py // * * https://github.com/nicolas-lang/factorio-create-release-action/blob/master/entrypoint.py
* * https://github.com/nicolas-lang/factorio-create-release-action/blob/master/factorioModPortal.py // * * https://github.com/nicolas-lang/factorio-create-release-action/blob/master/factorioModPortal.py
* * https://github.com/justarandomgeek/vscode-factoriomod-debug/blob/master/src/ModPackageProvider.ts#L530 // * * https://github.com/justarandomgeek/vscode-factoriomod-debug/blob/master/src/ModPackageProvider.ts#L530
* // *
* // *
*/ // */
internal class FactorioModPortalClient( //internal class FactorioModPortalClient(
private val appProps: ApplicationProperties = ApplicationProperties(), // private val appProps: ApplicationProperties = ApplicationProperties(),
private val loginFormUrl: String = "https://factorio.com/login?mods=1", // private val loginFormUrl: String = "https://factorio.com/login?mods=1",
private val loginUrl: String = "https://factorio.com/login", // private val loginUrl: String = "https://factorio.com/login",
) { //) {
//
private val client: HttpHandler = // private val client: HttpHandler =
ClientFilters.FollowRedirects() // ClientFilters.FollowRedirects()
.then(OkHttp()) // .then(OkHttp())
//
private val csrfCookie: Cookie by lazy { fetchCsrfToken() } // private val csrfCookie: Cookie by lazy { fetchCsrfToken() }
private val sessionCookie: Cookie by lazy { login() } // private val sessionCookie: Cookie by lazy { login() }
//
//
private fun fetchCsrfToken(): Cookie { // private fun fetchCsrfToken(): Cookie {
//
val request = Request(Method.GET, loginFormUrl) // val request = Request(Method.GET, loginFormUrl)
val response: Response = client(request) // val response: Response = client(request)
//
val csrfToken = htmlDocument(response.bodyString()) { // val csrfToken = htmlDocument(response.bodyString()) {
input { // input {
withId = csrfTokenName // withId = csrfTokenName
withAttribute = "name" to csrfTokenName // withAttribute = "name" to csrfTokenName
findFirst { attribute("value") } // findFirst { attribute("value") }
} // }
} // }
//
println("csrfToken: $csrfToken") // println("csrfToken: $csrfToken")
return Cookie(csrfTokenName, csrfToken) // return Cookie(csrfTokenName, csrfToken)
} // }
//
//
private fun login(): Cookie { // private fun login(): Cookie {
//
val loginRequest = Request(Method.POST, loginUrl) // val loginRequest = Request(Method.POST, loginUrl)
.header("Referer", loginFormUrl) // .header("Referer", loginFormUrl)
//// .cookie(csrfCookie)
// .form("csrf_token", csrfCookie.value)
// .form("username_or_email", appProps.factorioModPortalUsername)
// .form("password", appProps.factorioModPortalPassword.use { it })
// .form("next_url", "/profile")
//
// val loginResponse = client(loginRequest)
//
// println(loginResponse.bodyString())
//
// val sessionCookie = loginResponse.cookies()
// .first { it.name == "session" && it.domain == ".factorio.com" }
//
// println("sessionCookie: $sessionCookie")
//
// return sessionCookie
// }
//
// fun getModInfo(modName: String): JsonObject {
// val response = client(
// Request(Method.GET, "https://mods.factorio.com/api/mods/${modName}/full")
// .cookie(csrfCookie) // .cookie(csrfCookie)
.form("csrf_token", csrfCookie.value) // .cookie(sessionCookie)
.form("username_or_email", appProps.factorioModPortalUsername) // )
.form("password", appProps.factorioModPortalPassword.use { it }) // return jsonMapper.parseToJsonElement(response.bodyString()).jsonObject
.form("next_url", "/profile") // }
//
val loginResponse = client(loginRequest) // fun fetchModUploadToken(modName: String): String {
// val response = client(
println(loginResponse.bodyString()) // Request(Method.GET, "https://mods.factorio.com/mod/${modName}/edit")
// .cookie(csrfCookie)
val sessionCookie = loginResponse.cookies() // .cookie(sessionCookie)
.first { it.name == "session" && it.domain == ".factorio.com" } // )
//
println("sessionCookie: $sessionCookie") // return response.bodyString()
// }
return sessionCookie //
} // companion object {
// private const val csrfTokenName = "csrf_token"
fun getModInfo(modName: String): JsonObject { // }
val response = client( //
Request(Method.GET, "https://mods.factorio.com/api/mods/${modName}/full") //}
.cookie(csrfCookie)
.cookie(sessionCookie)
)
return jsonMapper.parseToJsonElement(response.bodyString()).jsonObject
}
fun fetchModUploadToken(modName: String): String {
val response = client(
Request(Method.GET, "https://mods.factorio.com/mod/${modName}/edit")
.cookie(csrfCookie)
.cookie(sessionCookie)
)
return response.bodyString()
}
companion object {
private const val csrfTokenName = "csrf_token"
}
}

View file

@ -0,0 +1,83 @@
package dev.adamko.kafkatorio
//import javax.inject.Inject
//import kotlinx.coroutines.runBlocking
//import kotlinx.serialization.json.Json
//import net.swiftzer.semver.SemVer
//import okio.FileSystem
//import okio.Path.Companion.toOkioPath
//import okio.Path.Companion.toPath
//import okio.openZip
//import org.gradle.api.DefaultTask
//import org.gradle.api.file.RegularFileProperty
//import org.gradle.api.model.ObjectFactory
//import org.gradle.api.provider.Property
//import org.gradle.api.provider.ProviderFactory
//import org.gradle.api.tasks.Input
//import org.gradle.api.tasks.InputFile
//import org.gradle.api.tasks.TaskAction
//import org.gradle.kotlin.dsl.property
//
//
//abstract class FactorioModPortalPublishTask @Inject constructor(
// objects: ObjectFactory,
// providers: ProviderFactory,
//) : DefaultTask() {
//
// @get:InputFile
// abstract val distributionZip: RegularFileProperty
//
// @get:Input
// abstract val modVersion: Property<String>
//
// @get:Input
// abstract val modName: Property<String>
//
// @get:Input
// abstract val factorioVersion: Property<String>
//
// /** Bearer API key */
// @get:Input
// val portalApiKey: Property<String> = objects.property<String>().convention(
// providers.gradleProperty("FACTORIO_MOD_PORTAL_API_KEY")
// )
//
// @get:Input
// val portalUploadEndpoint: Property<String> = objects.property<String>()
// .convention("https://mods.factorio.com/api/v2/mods/releases/init_upload")
//
//
// @TaskAction
// fun deploy() = runBlocking {
// val modVersion = SemVer.parse(modVersion.get())
//
// val portalUploadEndpoint = portalUploadEndpoint.get()
// val modName = modName.get()
// val portalApiKey = portalApiKey.get()
// val distributionZip = distributionZip.get().asFile
//
// }
//
// private fun validateInfoJson() {
// val modVersion = modVersion.get()
// val factorioVersion = factorioVersion.get()
//
// val distributionZip = distributionZip.get().asFile.toOkioPath()
//
// val zipContents = FileSystem.SYSTEM.openZip(distributionZip)
//
// val infoJson = zipContents.read("info.json".toPath()) {
// generateSequence { readUtf8Line() }
// }.joinToString("\n")
//
// val info: FactorioModInfo = Json.decodeFromString(FactorioModInfo.serializer(), infoJson)
//
// require(info.version == modVersion) {
// "mod version mismatch, ${info.version} vs $modVersion "
// }
//
// require(info.factorioVersion == factorioVersion) {
// "Factorio version mismatch, ${info.factorioVersion} vs $factorioVersion "
// }
// }
//}

View file

@ -1,14 +1,40 @@
package dev.adamko.kafkatorio package dev.adamko.kafkatorio
//
import dev.adamko.kafkatorio.processor.serdes.KXS.asPrettyJsonString //import java.io.File
//import kotlinx.cli.ArgParser
//import kotlinx.cli.ArgType
private const val modName = "kafkatorio-events" //import kotlinx.cli.required
//import kotlinx.coroutines.runBlocking
//
fun main() { //fun main(args: Array<String>) = runBlocking {
val client = FactorioModPortalClient() // val parser = ArgParser("example")
// val modInfo = client.getModInfo(modName) //
// println(modInfo.asPrettyJsonString()) // val distributionZip: File by parser.option(FileArg).required()
println(client.fetchModUploadToken(modName)) // val modName: String by parser.option(ArgType.String).required()
} // val portalApiKey: String by parser.option(ArgType.String).required()
// val portalUploadEndpoint: String by parser.option(ArgType.String).required()
//
// parser.parse(args)
//
// val client = FactorioModPortalPublishClient(
// distributionZip,
// modName,
// portalApiKey,
// portalUploadEndpoint,
// )
//
// client.uploadMod()
//}
//
//
//object FileArg : ArgType<File>(true) {
// override val description: kotlin.String
// get() = "{ File }"
//
// override fun convert(value: kotlin.String, name: kotlin.String): File {
// return File(value).also {
// require(it.exists()) { "${it.canonicalPath} does not exist" }
// require(it.isFile) { "${it.canonicalPath} is not a file" }
// }
// }
//}

View file

@ -24,18 +24,12 @@ dependencies {
constraints { constraints {
api(libs.skrapeit) api(libs.skrapeit)
api(libs.kotlinx.serialization.yaml) api(libs.kotlinx.serialization.yaml)
api(libs.kafka.kotkaStreams) api(libs.kafka.kotkaStreams)
api(libs.kafka.streams) api(libs.kafka.streams)
api(libs.okio.core) api(libs.okio.core)
api(libs.scrimage.core) api(libs.scrimage.core)
api(libs.kafka.streamsTestUtils) api(libs.kafka.streamsTestUtils)
api(libs.mockk)
} }
} }

View file

@ -36,7 +36,10 @@ kotlin {
port = 3000, port = 3000,
proxy = mutableMapOf( proxy = mutableMapOf(
"/kv/*" to "http://localhost:8080", "/kv/*" to "http://localhost:8080",
"/kvws/*" to mapOf("target" to "ws://localhost:8080", "ws" to true) "/kvws/*" to mapOf(
"target" to "ws://localhost:8080",
"ws" to true,
)
), ),
static = mutableListOf("$buildDir/processedResources/frontend/main") static = mutableListOf("$buildDir/processedResources/frontend/main")
) )
@ -160,3 +163,6 @@ fun KotlinDependencyHandler.kvision(
// resolution("node-forge", "1.3.0") // resolution("node-forge", "1.3.0")
// } // }
//} //}
evaluationDependsOn(rootProject.path)
evaluationDependsOn(projects.modules.eventsSchema.dependencyProject.path)

View file

@ -15,7 +15,7 @@ include(
":modules:kt-rcon", ":modules:kt-rcon",
":modules:web-map", ":modules:web-map",
":modules:factorio-mod-portal-client", // ":modules:factorio-mod-portal-client",
":modules:versions-platform", ":modules:versions-platform",
) )