/* * This file is a part of the SteamWar software. * * Copyright (C) 2025 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package de.steamwar.routes import de.steamwar.config import de.steamwar.plugins.SWAuthPrincipal import de.steamwar.plugins.SWPermissionCheck import de.steamwar.sql.UserPerm import io.ktor.client.* import io.ktor.client.engine.java.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import java.time.Instant import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Base64 import java.util.Date val pathPageIdMap = mutableMapOf() var pageId = 1 @Serializable data class Identity(val name: String, val email: String) @Serializable data class PageResponseList( val path: String, val name: String, val sha: String, val downloadUrl: String, val id: Int ) { constructor(res: JsonObject, id: Int) : this( res["path"]?.jsonPrimitive?.content!!, res["name"]?.jsonPrimitive?.content!!, res["sha"]?.jsonPrimitive?.content!!, res["download_url"]?.jsonPrimitive?.content!!, id ) } @Serializable data class PageResponse( val path: String, val name: String, val sha: String, val downloadUrl: String, val content: String, val size: Int, val id: Int, ) { constructor(res: JsonObject, id: Int) : this( res["path"]?.jsonPrimitive?.content!!, res["name"]?.jsonPrimitive?.content!!, res["sha"]?.jsonPrimitive?.content!!, res["download_url"]?.jsonPrimitive?.content!!, res["content"]?.jsonPrimitive?.content!!, res["size"]?.jsonPrimitive?.int!!, id ) } @Serializable data class CreatePageRequest(val path: String, val slug: String?, val title: String?) @Serializable data class AddImageRequest(val name: String, val data: String) @Serializable data class CreateBranchRequest(val branch: String) @Serializable data class UpdatePageRequest(val content: String, val sha: String, val message: String) @Serializable data class MergeBranchRequest(val branch: String, val message: String) @Serializable data class DeletePageRequest(val sha: String, val message: String) @Serializable data class CreateGiteaPageRequest(val message: String, val content: String, val branch: String, val author: Identity) fun Route.configurePage() { val client = HttpClient(Java) { install(ContentNegotiation) { json() } defaultRequest { url("https://git.steamwar.de/api/v1/") header("Authorization", "token " + config.giteaToken) } } suspend fun filesInDirectory(path: String, branch: String = "master", fileFilter: (name: String) -> Boolean = { true }): List { val filesToCheck = mutableListOf(path) val files = mutableListOf() while (filesToCheck.isNotEmpty()) { val path = filesToCheck.removeAt(0) val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch") val fileJson = Json.parseToJsonElement(res.bodyAsText()) if (fileJson is JsonArray) { fileJson.forEach { val obj = it.jsonObject if (obj["type"]?.jsonPrimitive?.content == "dir") { filesToCheck.add(obj["path"]?.jsonPrimitive?.content!!) } else if (obj["type"]?.jsonPrimitive?.content == "file" && fileFilter(obj["name"]!!.jsonPrimitive.content)) { files.add(PageResponseList(obj, pathPageIdMap.computeIfAbsent(obj["path"]?.jsonPrimitive?.content!!) { pageId++ })) } } } else { files.add(PageResponseList(fileJson.jsonObject, pathPageIdMap.computeIfAbsent(fileJson.jsonObject["path"]?.jsonPrimitive?.content!!) { pageId++ })) } } return files } route("page") { install(SWPermissionCheck) { permission = UserPerm.MODERATION } get { val branch = call.request.queryParameters["branch"] ?: "master" call.respond(filesInDirectory("/src/content", branch) { it.endsWith(".md") || it.endsWith(".json") }) } post { val req = call.receive() if(req.path.startsWith("src/content/")) { call.respond(HttpStatusCode.BadRequest, "Invalid path") return@post } val res = client.post("repos/SteamWar/Website/contents/src/content/${req.path}") { contentType(ContentType.Application.Json) setBody(CreateGiteaPageRequest( "Create page ${req.path}", Base64.getEncoder().encodeToString(( if (req.path.endsWith(".md")) """ --- title: ${req.title?.removeSuffix(".md") ?: "Enter Title"} description: Enter Description key: ${req.slug?.lowercase()?.removeSuffix(".md") ?: "Enter Slug"} created: ${LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)} tags: - test --- # ${req.path} """ else "{}" ).trimIndent().toByteArray()), call.request.queryParameters["branch"] ?: "master", Identity(call.principal()!!.user.userName, "admin-tool@steamwar.de" ))) } call.respond(res.status) } route("branch") { get { val res = client.get("repos/SteamWar/Website/branches") call.respond(res.status, Json.parseToJsonElement(res.bodyAsText()).jsonArray.map { it.jsonObject["name"]?.jsonPrimitive?.content!! }) } post { @Serializable data class CreateGiteaBranchRequest(val new_branch_name: String, val old_branch_name: String) val branch = call.receive().branch val res = client.post("repos/SteamWar/Website/branches") { contentType(ContentType.Application.Json) setBody(CreateGiteaBranchRequest(branch, "master")) } @Serializable data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String) client.post("repos/SteamWar/Website/pulls") { contentType(ContentType.Application.Json) setBody(CreateGiteaMergeRequest("master", branch, "Merge branch $branch")) } call.respond(res.status) } delete { val branch = call.receive().branch val res = client.delete("repos/SteamWar/Website/branches/$branch") call.respond(res.status) } } route("{id}") { get { val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id") val path = pathPageIdMap.entries.find { it.value == id }?.key ?: return@get call.respond(HttpStatusCode.NotFound, "Page not found") val branch = call.request.queryParameters["branch"] ?: "master" val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch") val fileJson = Json.parseToJsonElement(res.bodyAsText()) if (fileJson is JsonArray) { return@get call.respond(HttpStatusCode.BadRequest, "Invalid id") } val file = PageResponse(fileJson.jsonObject, id) call.respond(file) } delete { val data = call.receive() val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@delete call.respond(HttpStatusCode.NotFound, "Page not found") val branch = call.request.queryParameters["branch"] ?: "master" @Serializable data class DeleteGiteaPageRequest(val sha: String, val message: String, val branch: String, val author: Identity) val res = client.delete("repos/SteamWar/Website/contents/$path") { contentType(ContentType.Application.Json) setBody(DeleteGiteaPageRequest(data.sha, data.message, branch, Identity(call.principal()!!.user.userName, "admin-tool@steamwar.de"))) } call.respond(res.status) } put { @Serializable data class UpdateGiteaPageRequest(val content: String, val sha: String, val message: String, val branch: String, val author: Identity) val data = call.receive() val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@put call.respond(HttpStatusCode.NotFound, "Page not found") val res = client.put("repos/SteamWar/Website/contents/$path") { contentType(ContentType.Application.Json) setBody(UpdateGiteaPageRequest(data.content, data.sha, data.message, (call.request.queryParameters["branch"] ?: "master"), Identity(call.principal()!!.user.userName, "admin-tool@steamwar.de"))) } call.respond(res.status) } } route("images") { get { val branch = call.request.queryParameters["branch"] ?: "master" call.respond(filesInDirectory("/src/images", branch)) } post { val req = call.receive() client.post("repos/SteamWar/Website/contents/src/images/${req.name}") { contentType(ContentType.Application.Json) setBody(CreateGiteaPageRequest( "Add Image ${req.name}", req.data, call.request.queryParameters["branch"] ?: "master", Identity(call.principal()!!.user.userName, "admin-tool@steamwar.de" ))) } call.respond(HttpStatusCode.Created) } } } }