Files
Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt
Mitchell Syer 2d535b44d8 Extension API 1.6 (#2120)
* Non-Extension Index changes for 1.6

* Changelog

* Minor fixes

* Implement extension store

* Test build fix

* Docs

* Simplify fetching manga and chapters

* Use EMPTY JsonObject

* Update docs/Configuring-Suwayomi‐Server.md

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>

* Improve Fetch Extension Store

* Fixes

* Simplify deprecated isNsfw in SourceQuery

* Simplify ContentRating in Source.kt

* Simplify isNsfw in SourceType

* No magic numbers for ContentRating, improves safety for future versions of extension api

* Fix SearchTest

* Lint

* Lint

* Optimize imports and fix unchecked cast warning

* Proper extension store queries

* Optimize import fixes

* Add ContentRatingFilter

* Improve extension store sync

* fix: re-sync (#2121)

* Lint

* Add ExtenionStores to the fetchExtensions result since its possible for the stores to change.

* Use a single version of ContentRating

* Exclude ServerConfig.extensionStores from GraphQL

* Use syncDbToPrefs in ExtensionStoreMutation

* Optimize Imports

* Update server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>

* Remove replaceWith and add specific description for GQL APIs

* Include OkHttp ZSTD

* Update to latest Mihon extension lib

* Fix latest Mihon Extension Lib

* Lint

* Optimize imports

* Lint

* Review fixes

* Add a index to extesnion table store url

* Lint

---------

Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>
2026-06-27 13:39:28 -04:00

193 lines
6.7 KiB
Kotlin

package suwayomi.tachidesk.manga.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.HttpStatus
import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
import kotlin.time.Duration.Companion.days
object ExtensionController {
private val logger = KotlinLogging.logger {}
/** list all extensions */
val list =
handler(
documentWith = {
withOperation {
summary("Extension list")
description("List all extensions")
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
ExtensionsList.getExtensionList()
}.thenApply {
ctx.json(it)
}
}
},
withResults = {
json<Array<ExtensionDataClass>>(HttpStatus.OK)
},
)
/** install extension identified with "pkgName" */
val install =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension install")
description("install extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
Extension.installExtension(pkgName)
}.thenApply {
ctx.status(it)
}
}
},
withResults = {
httpCode(HttpStatus.CREATED)
httpCode(HttpStatus.FOUND)
httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
},
)
/** install the uploaded apk file */
val installFile =
handler(
documentWith = {
withOperation {
summary("Extension install apk")
description("Install the uploaded apk file")
}
uploadedFile("file") {
it.description("Extension apk")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename() }
ctx.future {
future {
Extension.installExternalExtension(
uploadedFile.content(),
uploadedFile.filename(),
)
}.thenApply {
ctx.status(it)
}
}
},
withResults = {
httpCode(HttpStatus.CREATED)
httpCode(HttpStatus.FOUND)
httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
},
)
/** update extension identified with "pkgName" */
val update =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension update")
description("Update extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
Extension.updateExtension(pkgName)
}.thenApply {
ctx.status(it)
}
}
},
withResults = {
httpCode(HttpStatus.CREATED)
httpCode(HttpStatus.FOUND)
httpCode(HttpStatus.NOT_FOUND)
httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
},
)
/** uninstall extension identified with "pkgName" */
val uninstall =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension uninstall")
description("Uninstall extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Extension.uninstallExtension(pkgName)
ctx.status(200)
},
withResults = {
httpCode(HttpStatus.CREATED)
httpCode(HttpStatus.FOUND)
httpCode(HttpStatus.NOT_FOUND)
httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
},
)
/** icon for extension named `apkName` */
val icon =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension icon")
description("Icon for extension named `apkName`")
}
},
behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future { Extension.getExtensionIcon(pkgName) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 365.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds, immutable")
ctx.result(it.first)
}
}
},
withResults = {
image(HttpStatus.OK)
httpCode(HttpStatus.NOT_FOUND)
},
)
}